ccsssc-2026-决赛
ccsssc-2026-决赛
Attention! AI写的wp
DungeonLover
题目外面套了一个 proxy,但真正被打的是后面的 pwn。exp 里本地加载的也是 ./pwn,远程才通过 SOCKS5 连到代理转发出来的服务。
保护和入口
主程序保护比较直接:no PIE、no canary、NX 开。没有栈 canary,且程序基址固定,所以只要有栈上写原语,就可以直接往返回地址处铺 ROP。
开局需要先走一段剧情输入:
1 | sla(b'> ', b'5201314') |
之后菜单 6 对应 Desperate Strike,会把 puts 地址以十六进制打印出来:
1 | sla(b'> ', b'6') |
这里拿到 puts 泄露以后,直接减 libc.sym.puts 得到 libc_base,后续 system、/bin/sh、gadget 都从同一份 libc 里取。
漏洞点
关键点在 do_use_item 一类的使用道具逻辑。菜单 5 读入 Select a Fate Engraving 和 Soul Weight to Offer,下标检查只限制在 0..0x16 这种范围内,但实际写入时是按 base + idx * 4 往栈上写 4 字节。
exp 里把写接口封成:
1 | def use(off, byte): |
再从偏移 0x38 开始分 4 字节写 payload:
1 | def wr(payload): |
也就是说,0x38 正好打到保存返回地址附近,之后每次 idx * 4 写一个 dword,就能拼完整 ROP 链。
利用链
泄露 libc 后,链子非常短:
1 | wr(flat([CG.ret(), CG.pop_rdi_ret(), CG.bin_sh(), libc.sym.system])) |
ret 用来对齐栈,pop rdi; ret 把 "/bin/sh" 放进 rdi,最后跳 system。菜单 0 退出当前流程触发函数返回,执行栈上的 ROP。
完整 exp
1 | #!/usr/bin/env python3 |
StudentManagement
学生管理系统是典型的结构体加链表题。外层有注册、登录、删除;登录后能看 bio、改 bio、退出登录。核心利用点不是单个越界写,而是删除用户时释放 student 和 bio,后续通过重新分配 bio 控制堆布局,最终把可控写转到 libc 上的 _IO_2_1_stdout_,再用 FSOP 组织一次 ORW。
结构和功能
从交互和 exp 可以看出,学生节点大致包含这些字段:
idnamepasswordbio指针bio_size- 链表指针
注册 reg 会创建学生节点并挂进链表:
1 | def reg(id, usr=b'inkey', pwd=b'inkey'): |
登录后可以 View 或 EditBio。edit 会重新设置 bio 大小并写入内容:
1 | def edit(id, size, data, pass1=1): |
删除 dele 会把对应学生删掉,释放 student 结构和 bio。漏洞利用依赖这些释放后的堆块被后续 bio 申请复用,从而把结构体字段和 bio 内容布到同一片可控区域附近。
堆布局和 libc 泄露
第一阶段先批量注册学生,制造稳定的堆布局:
1 | stg1 = 10 |
接着删除前 10 个学生,让对应 student/bio 进入 tcache 或 unsorted 相关布局,再对后面的学生批量申请 0xD0、0x198、0x400 等不同尺寸的 bio,构造可控重叠和可泄露块:
1 | for i in range(stg1): |
后面注册 0x100..0x106,再注册 115,通过 view(115) 读出 libc 指针:
1 | for i in range(7): |
这里泄露值落在 libc 内部,减固定偏移得到 libc_base。
改 bio 指针到 stdout
拿到 libc 后,利用堆布局把某个学生的 bio 指针改到 _IO_2_1_stdout_:
1 | edit(0x101, 0x88, b'\x00' * 0x70 + p64(libc.sym._IO_2_1_stdout_) + p64(0x108)) |
这样之后编辑 116 的 bio,实际就是往 stdout 的 _IO_FILE 结构写。题目没有直接给栈写,但 stdout FSOP 可以把一次普通输入/输出路径变成受控读写,再把 ORW ROP 链布到合适位置。
最后打house of some2
完整 exp
1 | #!/usr/bin/env python3 |
fix
通防
traditional
这题表面是普通菜单堆题,但所有菜单输入都要先过自定义 base64 编码。exp 里没有直接发送 1/2/3/4/5/7,而是统一通过 b64en 转换后再发。
漏洞在free,触发特殊free会uaf(特殊free会把删除后的槽位后面的chunk记录都往前搬,但不清除最后一个)
自定义 base64
编码逻辑是标准 base64 之后换表。标准表:
1 | old_table = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' |
题目表:
1 | new_table = b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=' |
所以所有菜单、index、size、content 都要这样编码:
1 | def b64en(s): |
菜单和隐藏 copy
程序用 chunk_manage_array 管理 chunk 指针和 size,常规菜单有 add/edit/show/delete/exit。隐藏菜单 7.copy 能从一个 chunk 拷贝到另一个 chunk:
1 | def ccopy(idx1, idx2, size): |
最终 exp 没有大量依赖 copy,但这个功能能辅助把已有内容搬到目标 chunk,是题面里明显给利用准备的隐藏能力。
heap 泄露和 safe-linking
先申请一个小块和一批 0x280,再用异常 index 删除:
1 | add(0, 0x20) |
这里 show(0xF) 读到的是 safe-linking 保护后的堆指针片段,按 glibc safe-linking 的形态还原出 heap_base。后面 tcache poisoning 写 fd 时都用 protect_ptr 重新编码目标地址。
tcache poisoning 到 tcache_perthread_struct
泄露 heap 后,把某个 0x280 tcache bin 的 fd 改到 heap_base + 0x10,也就是 tcache_perthread_struct 附近:
1 | add(0xE, 0x280) |
随后编辑拿到的伪造 chunk,直接构造 tcache_perthread_struct 的 counts 和 entries,让指定 size 的申请返回任意地址:
1 | edit(0x1, p16(0x6) * 39 + p16(0x0) + p16(0x6) * 36 + p64(0) * 0x28 + p64(heap_base + 0x10) + p64(heap_base + 0xD80)) |
libc 和 stack 泄露
先让 0x2A0 申请落到可泄露的 libc 链表指针上:
1 | dele(0x4) |
再把 tcache_perthread_struct 里的 entry 改到 libc_base + 0x2346E0,也就是能拿到 environ 一类栈指针的位置:
1 | add(0x3, 0x290) |
拿到栈地址后,再次伪造 tcache entry 到 stack - 8。
覆盖返回地址
最后让 add(0x6, 0x2A0) 返回到栈上返回地址附近,写入普通 ret2libc:
1 | edit(0x6, flat([0, ret, rdi, sh, libc.sym.system])) |
退出菜单时函数返回,执行 ret; pop rdi; "/bin/sh"; system,拿 shell。
完整 exp
1 | #!/usr/bin/env python3 |
- 标题: ccsssc-2026-决赛
- 作者: InkeyP
- 创建于 : 2026-05-26 08:16:10
- 更新于 : 2026-05-26 09:39:28
- 链接: https://blog.inkey.top/202605/26/ccsssc-2026-决赛/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。