unlink

unlink

glibc2.23 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
32
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else {
FD->bk = BK;
BK->fd = FD;
if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
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);
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;
}
}
}
}

glibc 2.31unlink源码

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)))//多了这个检测,要求当前的chunk_size == next_chunk的prev_size
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;
}
}
}

2.31比2.23多了chunk_size == next_chunk的prev_size的检测,prev_size位正常情况下都是能修改的,更不用说能修改prev_inuse位的情况,所以影响不大,下面以2.23为例

malloc
从恰好大小合适的largebin中获取chunk,发生unlink
从比malloc要求大的largebin中取chunk,发生unlink
Free
后向合并,合并物理相邻低物理地址空闲chunk时
前向合并,合并物理相邻高物理地址空闲chunk时(top chunk除外)

合并时对被合并的chunk的大小无要求,前向合并时,在低地址的chunk中构造一个被合并的chunk,同时将高地址chunk的prev_size位设置成被合并chunk的大小(包括chunk_header),size位的最低地址设置为0

malloc_consolidate
后向合并,合并物理相邻低地址空闲chunk时。
前向合并,合并物理相邻高地址空闲 chunk时(top chunk除外)
realloc

前向扩展,合并物理相邻高地址空闲chunk(除top chunk)

glibc malloc和free源码解析

事实上small bin和unsorted bin是在malloc函数中进行脱链的,而不是通过unlink函数,当malloc的chunk需要从small bin和unsorted bin中取出时,进行类似unlink中的操作

下面是malloc函数中small bin的分配源码,关键是这几句,victim指向双向链表末尾的small chunk(small bin采用先进先出策略),该chunk的fd指向链表头,即代码中的bin,bk指向前一个chunk

if (__glibc_unlikely (bck->fd != victim))

bck = victim->bk;

bin->bk = bck;
bck->fd = bin;

这个过程其实和unlink很像,但问题在于bin是不可控的,如果按照unlink的利用思路,bin->bk = bck;不仅无法通过fd来指定地址,还会破坏链表,bck->fd = bin;可以由bk来指定要修改的地址,但是内容固定为bin

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
#define NBINS             128
#define NSMALLBINS 64
#define SMALLBIN_WIDTH MALLOC_ALIGNMENT
#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > 2 * SIZE_SZ)
#define MIN_LARGE_SIZE ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH)

#define in_smallbin_range(sz) \
((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)

#define smallbin_index(sz) \
((SMALLBIN_WIDTH == 16 ? (((unsigned) (sz)) >> 4) : (((unsigned) (sz)) >> 3))\
+ SMALLBIN_CORRECTION)
#define bin_at(m, i) \
(mbinptr) (((char *) &((m)->bins[((i) - 1) * 2])) \
- offsetof (struct malloc_chunk, fd))
#define first(b) ((b)->fd)
#define last(b) ((b)->bk)

/*
If a small request, check regular bin. Since these "smallbins"
hold one size each, no searching within bins is necessary.
(For a large request, we need to wait until unsorted chunks are
processed to find best fit. But for small ones, fits are exact
anyway, so we can check now, which is faster.)
*/

if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);

if ((victim = last (bin)) != bin)
{
if (victim == 0) /* initialization check */
malloc_consolidate (av);
else
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
{
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;

if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
}

2.unlink时的漏洞及利用

默认64位

关键代码

1
2
3
4
5
6
7
FD = P->fd;								      
BK = P->bk;

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))

FD->bk = BK;
BK->fd = FD;

绕过检测要求:

1
2
3
4
5
6
7
8
p->fd->bk = p     
p->bk->fd = p
等价于
*(*(P+0X10)+0X18) = P
*(*(P+0X18)+0X10) = P
等价于
FD = &p - 0x18 #&p为heap_array中当前chunk(被合并chunk)的指针的存放地址
BK = &p - 0x10

也就是说把被unlink的chunk的fd和bk分别设置成&p - 0x18和&p - 0x10就能绕过检测,这里需要题目中存在一个存放p指针的数据结构

p是heap_array中存放的指针(指向低地址的chunk)

p是heap_array中存放的指针(指向低地址的chunk)

p是heap_array中存放的指针(指向低地址的chunk)

重要的话说三遍,高地址的chunk被合并后会被清空

绕过检测后执行

1
2
3
4
5
FD->bk = BK;							      
BK->fd = FD;
等价于
p = &p - 0x10
p = &p - 0x18

p是原本指向被unlink的chunk的chunk头的指针,此时已经指向了&p-0x18,假设p原本被存放在bss段的一个数组(heap_array)中,那么此时修改p原本指向的chunk的内容,就变成了修改bss段chunk头首地址在&p-0x18的fake chunk的内容,通常就可以修改存放chunk指针的数组了

总结:

条件:

bss段存在chunk指针数组heap_arr(经典的用法)

第一种情况:存在uaf,使被合并chunk在被free后还能修改chunk中的fd和bk指针

第二种情况:存在堆溢出,将被合并的chunk伪装成释放状态

构造:一般是后向合并,即free掉高地址的chunk,去合并低地址的chunk。在被合并chunk的usr_data中再伪造一个chunk,构造该chunk的fd和bk(fake_fd = &p - 0x18 fake_bk = &p - 0x10),同时将高地址chunk的prev_size位设置成伪造的chunk的大小(包括chunk_header),size位的最低地址设置为0。

效果:

p = &p - 0x18

p是heap_array中存放的指针(指向低地址的chunk),&p是该指针在heap_array中的地址,unlink后再去修改该指针指向的chunk的内容就变成了修改heap_array的内容

3.例题:

[ZJCTF 2019]EasyHeap

hitcon2014_stkof


unlink
https://lkliki.github.io/2023/05/18/unlink/
作者
0P1N
发布于
2023年5月18日
许可协议