1. 从一道经典赛题看堆溢出漏洞的攻防本质
如果你刚开始接触CTF中的PWN方向,尤其是堆利用,那么 babyheap_0ctf_2017 这道题绝对是一个绕不开的里程碑。它不像一些简单的栈溢出题目那样直白,也不涉及过于复杂的堆风水技巧,而是恰到好处地展示了堆溢出漏洞最核心的破坏力—— 通过改写堆块的管理元数据,最终劫持程序控制流 。这道题之所以经典,是因为它剥离了繁杂的干扰,将攻击者的目光精准地引向了 malloc 和 free 这两个最基础函数背后的复杂世界。我们今天要做的,就是亲手揭开这层帷幕,看看一个看似简单的“越界写”漏洞,如何一步步演变成夺取shell的利器。
很多新手在初次面对堆题目时会有一种无力感,面对一堆 malloc 和 free 调用不知从何下手。这很正常,因为堆利用的本质是对 glibc 内存管理器的“规则”进行攻击。你需要先理解它的规则(如 ptmalloc2 的bin机制、chunk结构),然后找到程序违反规则的地方(漏洞点),最后精心构造输入,让程序在遵循错误数据时,帮你完成攻击。 babyheap_0ctf_2017 就是一个完美的教学样本:程序逻辑清晰,漏洞点明显,利用链完整。通过它,你将掌握堆利用中最基础也最关键的技巧之一—— unlink 攻击。我会带你从零开始,分析程序、定位漏洞、设计利用思路,并给出每一行EXP代码的详细解释,确保你能看懂、能复现、更能理解背后的原理。
2. 程序逻辑分析与漏洞定位
首先,我们需要知道程序在做什么。通常,我们会用 checksec 查看保护机制,用反汇编工具(如 IDA Pro 或 Ghidra )分析逻辑。
2.1 安全机制与功能菜单
用 checksec 检查,大概率会发现只开启了 NX (栈不可执行)和 ASLR (地址空间布局随机化)。没有 Canary 和 PIE ,这意味着栈溢出不好利用,但堆上的地址(如 libc 的基址)是随机的,我们需要想办法泄露。
用 IDA 打开程序,可以看到一个典型的菜单式逻辑:
1. Allocate
2. Fill
3. Free
4. Dump
5. Exit
这对应一个简单的堆管理器:
- Allocate : 调用
malloc申请指定大小的堆块。程序会用一个全局数组记录每个堆块的指针和大小,索引从0开始。 - Fill : 根据索引,向已分配的堆块中填充数据。 这里就是漏洞所在 。
- Free : 根据索引,调用
free释放堆块,并将指针置零,防止了Use-After-Free。 - Dump : 根据索引,打印堆块中的内容。这是我们 泄露地址信息 的关键。
程序的漏洞点非常清晰:在 Fill 函数中,它读取用户输入的长度,然后向对应堆块写入数据, 但没有检查输入长度是否小于或等于堆块当初申请的大小 。这就导致了堆溢出。
2.2 关键数据结构与堆布局规划
为了利用,我们必须理解程序如何管理堆块。通常,它会有一个结构体数组,比如:
struct heap_chunk {
char *ptr;
size_t size;
} chunks[16];
Allocate 时, ptr = malloc(size) ,并记录 size 。 Fill 时,使用 read(0, chunks[idx].ptr, user_input_size) ,这里的 user_input_size 是我们可控的,可以大于 chunks[idx].size 。
假设我们申请了三个堆块:
- Chunk 0: 大小 0x80
- Chunk 1: 大小 0x80
- Chunk 2: 大小 0x80
在内存中,它们可能是连续排列的(取决于当时堆的布局)。我们的目标是利用Chunk 0的溢出,去覆盖与Chunk 1相关的关键数据。
注意 :现代
glibc的malloc实现中,size字段位于chunk头,而用户使用的指针ptr指向的是chunk的数据区。一个malloc(0x80)得到的chunk,其实际大小(含元数据)是0x90(0x80用户区 + 0x10 chunk头)。chunk头包含前一个chunk的大小(prev_size)和当前chunk的大小及标志位(size)。free操作会检查这些元数据的合法性。
3. 利用思路:Unlink攻击详解
面对堆溢出,一个经典的利用手法是 unlink 。 unlink 是 glibc 在合并空闲堆块时的一个宏操作,它会将某个空闲块从其所在的双向链表(如 small bin 或 unsorted bin )中摘除。如果我们可以伪造一个空闲块,并诱使 glibc 对其执行 unlink ,就能实现一次任意地址写。
3.1 Unlink宏的传统利用原理
在旧版本 glibc (约2.26之前)中, unlink 宏类似如下:
#define unlink(P, BK, FD) {
FD = P->fd;
BK = P->bk;
FD->bk = BK; // *(P->fd + 12) = P->bk
BK->fd = FD; // *(P->bk + 8) = P->fd
}
其中 P 指向要脱链的假chunk。如果我们能控制 P->fd 和 P->bk 的值,就能向 *(P->fd + 12) 和 *(P->bk + 8) 写入任意数据。这被称为“ unlink 写”。
攻击通常分几步:
- 布局堆块 :申请多个相邻堆块(如A, B, C)。
- 溢出伪造 :利用A的溢出,覆盖B的chunk头,将B伪造成一个“已释放”状态的空闲块。同时,在B的用户数据区(即我们可控的区域)提前布置好伪造的
fd和
1万+



被折叠的 条评论
为什么被折叠?



