堆溢出利用
堆溢出攻击
堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数,因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块。
与栈溢出所不同的是,堆上并不存在返回地址等可以让攻击者直接控制执行流程的数据,因此我们一般无法直接通过堆溢出来控制 EIP 。一般来说,我们利用堆溢出的策略是:
一、 从“数据溢出”到“控制流劫持”
所有堆利用策略的终极目标,几乎都是实现以下两种“原语”之一,并最终劫持 EIP/RIP(指令指针寄存器):
- 任意地址写 (Arbitrary Write / Write-What-Where): 能够将一个可控的值写入到一个可控的地址。
- 任意地址分配 (Arbitrary Alloc): 能够让 malloc 返回一个指向可控地址的指针。
一旦获得了这些能力,攻击者就可以覆写关键的程序数据,例如:
- 函数指针(如 C++ 虚函数表指针、回调函数)。
- 全局偏移表 (Global Offset Table, GOT) 中的函数地址。
- 关键的钩子函数(如 Glibc 中的 __malloc_hook, __free_hook)。
- 栈上的返回地址(如果能找到通往栈的路径)。
二、 unlink 宏利用
主要针对早期 Glibc 的 ptmalloc 分配器。
背景:当 free() 一个不属于 fastbin 的 Chunk 时,如果其前后 Chunk 也是空闲的,分配器会进行合并(consolidate),将它们从空闲双向链表(bin)中“解链(unlink)”。
攻击思路:
- 攻击者通过堆溢出,覆盖一个空闲 Chunk A 的元数据。
- 精心构造 Chunk A 的 fd 和 bk 指针。比如,将 fd 设置为 target_addr - 12,将 bk 设置为一个可控的地址 attacker_controlled_addr。
- 当程序 free() 另一个与 Chunk A 相邻的 Chunk 时,会触发合并,进而对 Chunk A 执行 unlink 操作。
- 此时 FD->bk = BK 这行代码,就会变成 *( (target_addr - 12) + 12 ) = attacker_controlled_addr,即 *target_addr = attacker_controlled_addr。
- 这就实现了一个任意地址写!攻击者可以将一个函数在 GOT 表中的地址(target_addr)修改为 shellcode 的地址(attacker_controlled_addr)。
防御:Glibc 后来加入了 “unlink check”,验证 P->fd->bk == P && P->bk->fd == P,确保双向链表的完整性,使得原始的 unlink 利用失效。但这促使攻击者转向了更复杂的技巧。
三、UAF(use after free)
内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现问题。
利用流程:
- 触发 UAF: 找到一个漏洞,使得程序 free 了一个对象,但保留了指向它的悬挂指针 ptrA。假设这个对象大小为 0x50 字节。
- 释放对象 A: free(ptrA)。现在 ptrA 成为了悬挂指针,大小为 0x50 的内存块被放回了某个 bin 中(例如 Tcache)。
- 分配恶意对象 B: 攻击者立即申请一个同样大小 (0x50) 的新内存块 ptrB,并向其中填入恶意数据。由于分配器会优先复用刚被释放的内存,ptrB 和 ptrA 会指向完全相同的内存地址。
- 恶意数据是什么? 比如,如果原来的对象 A 是一个包含函数指针的 C++ 对象,攻击者就可以在 B 的对应位置填上 system() 函数的地址。
- 触发使用: 程序中的正常逻辑再次通过悬挂指针 ptrA 调用函数。它以为在调用对象 A 的合法函数,实际上却调用了攻击者在对象 B 中布置的 system() 函数,从而劫持了控制流。
四、Fastbin Attack (针对 fastbin)
fastbin 是为小内存分配设计的高速缓存,它是一个单向链表,且 free 时检查很少,这使其成为主要的攻击目标。
Fastbin Dup (Double Free)攻击:
- 申请三个大小相同的 Chunk:A, B, C。
- free(A),A 进入 fastbin。
- free(B),B 进入 fastbin,此时 B 的 fd 指向 A。
- free(A) 再次释放,这在旧版 Glibc 是允许的。现在 fastbin 链表变成 A -> B -> A。
- 连续 malloc 三次相同大小的内存,会依次返回 A, B, A。
- 这意味着攻击者可以同时持有对 A 的两次引用。第一次 malloc 后可以向 A 写入数据,第二次 malloc 后可以再次修改 A 的数据,这本身就是一种强大的能力。
- 升级版: 修改第一次释放的 A 的 fd 指针,让它指向一个目标地址。那么 malloc 两次后,第三次 malloc 就会返回这个目标地址。这就实现了任意地址分配。
House of Spirit 攻击:
- 在栈或 .bss 段等已知地址伪造一个 fake chunk,包括其大小元数据。
- 通过溢出等手段,让一个指针指向这个 fake chunk。
- free() 这个指针,分配器会误以为这是一个合法的 heap chunk,并将其加入 fastbin。
- 下次 malloc 同样大小时,就会返回这个位于栈或 .bss 的地址,攻击者便可直接写入数据到栈上或关键数据区。
五、Tcache Poisoning (Glibc 2.26+ 的主流技术)
Tcache (Thread-Local Cache) 是 Glibc 引入的更新、更快的缓存机制,每个线程私有。为了极致的速度,它的安全检查比 fastbin 更少。
攻击思路 (Tcache Dup / Double Free):
- Tcache 和 fastbin 一样是单向链表,并且对 Double Free 没有任何检查。
- free(A),A 进入 Tcache。
- free(A) 再次释放,A 再次被放入 Tcache 头部,形成 A -> A 的循环。
- malloc 第一次,返回 A。
- 此时,可以修改 A 的 next 指针(因为 A 还在 Tcache 链表中),将其指向任意目标地址,例如 __free_hook 的地址。
- malloc 第二次,会直接返回 __free_hook 的地址!
- 攻击者拿到这个指针后,就可以向其中写入 system() 函数的地址。
- 之后任何 free(“/bin/sh”) 的调用都会變成 system(“/bin/sh”),成功 Get Shell。
Tcache Poisoning 因其简单、稳定、高效,已成为当前堆利用的首选策略。
六、Unsorted Bin Attack
Unsorted Bin 是一个临时的 bin,刚被 free 的非 fastbin Chunk 会先被放入这里。利用它,攻击者可以泄露 Glibc 的基地址,从而绕过 ASLR。
攻击思路:
- 一个被 free 进 Unsorted Bin 的 Chunk,其 fd 和 bk 指针会指向 Glibc 中 main_arena 里的地址。
- 如果攻击者能通过某种方式(如溢出读、格式化字符串漏洞)打印出这个 Chunk 的内容,就能读到 fd 或 bk 的值。
- 用这个泄露的地址减去一个固定的偏移,就能计算出 libc.so.6 在内存中的基地址,从而定位所有 Glibc 函数和变量。
七、The House of X 系列
这是一系列复杂、精巧的利用技术的总称,通常用于特定场景或绕过特定防御,例如:
House of Force: 通过溢出修改 Top Chunk 的大小,使其变为一个超大值(-1),从而让 malloc 可以分配到任意地址。
House of Orange: 一种更现代的技术,通过滥用 Top Chunk 和 abort() 流程来实现利用。
完整的漏洞利用链:
在现代有完整保护(ASLR/PIE, NX, Canary)的程序中,一次成功的堆溢出利用通常是组合拳:
- 寻找漏洞: 定位一个堆溢出、Use-After-Free 或 Double Free 漏洞点。
- 信息泄露 (Info Leak): 利用 Unsorted Bin Attack 或其他漏洞泄露 libc 基地址和堆基地址,从而绕过 ASLR/PIE。
- 构建原语: 使用 Tcache Poisoning 或 Fastbin Attack 等技术,实现任意地址分配或任意地址写。
- 劫持控制流:
- 将 __free_hook 的值修改为 system 地址。
- 分配一个内容为 /bin/sh 的 chunk,然后 free 它来触发 system(“/bin/sh”)。
- 或者,将 __malloc_hook 修改为 ROP 链(One-gadget ROP)的地址,下次 malloc 时直接 Get Shell。
- Get Shell: 成功执行 system(“/bin/sh”) 或其他 shellcode,获得服务器控制权。
什么是堆风水
其核心思想是:在触发内存破坏漏洞(如堆溢出)之前,通过一系列精心设计的内存申请和释放操作,来精确地塑造(或“排布”)堆内存的结构,使得目标对象恰好位于我们能够破坏的内存区域旁边,从而将一个看似不可控的内存破坏漏洞,转化为一次精准、可靠的攻击。
让特定的两个对象相邻,或者让一个对象重用另一个被释放的对象的内存,这些技巧被称为堆风水。