随机数预测与任意写:ctf-tasks Nim 挑战的完整利用链拆解
2026/8/18 17:57:33 网站建设 项目流程

随机数预测与任意写:ctf-tasks Nim 挑战的完整利用链拆解

【免费下载链接】ctf-tasksAn archive of low-level CTF challenges developed over the years项目地址: https://gitcode.com/gh_mirrors/ct/ctf-tasks

ctf-tasks 是一个收录多年 CTF 低层攻防题目的开源存档库,其中「Dragon CTF 2021」的Nim 挑战堪称利用链设计的典范:它把随机数预测任意写(write-what-where)两条漏洞链严丝合缝地串在一起,在 NX、PIE、全 RELRO、栈金丝雀全部开启的 Linux x64 程序上完成了一次优雅的 getshell。整场只有 5/247 支队伍解出这道 408 分的题,本文就带你从零拆解这条完整利用链。

挑战档案:一场只有 5 支队伍解出的 CTF 漏洞利用题

项目内容
题目名称Nim
比赛Dragon CTF 2021
分类漏洞利用(Exploitation)
平台Linux x64
分值 / 难度408 分(中等)
解题队伍5 / 247
题目文件Dragon CTF 2021/Nim/task/

程序实现了一个「玩家 vs 庄家」的 Nim(尼姆)取石子游戏,现代缓解措施全开(NX、PIE、Full RELRO、Canary),却藏着两处隐蔽漏洞:随机数种子泄露连续栈缓冲区溢出。题目说明与官方解法摘要见 README。

规则速览:Nim 游戏与「尼姆和」必胜策略

Nim 的规则很简单:若干堆石子,双方轮流从任意一堆取走至少 1 颗,取走最后一颗者获胜。判断局势的关键是尼姆和(nim-sum),即所有堆大小的异或值:

  • 尼姆和 ≠ 0 → 当前行动方处于必胜局面
  • 尼姆和 = 0 → 当前行动方必败(对手按必胜策略走)

必胜策略:找到一堆,使其满足堆大小 ^ 尼姆和 < 堆大小,把它减到堆大小 ^ 尼姆和,即可把尼姆和归零交给对手。这是后文 exploit 中WinGame的核心逻辑,也是把庄家"安排"进必败局面的理论基础。

随机数预测:两个堆大小如何泄露 libc 地址

用地址做种子:PRNG 形同虚设

程序用 libc 中rand函数的地址作为内部 PRNG 的种子,而该 PRNG 是标准的线性同余生成器(LCG):

state' = (state × 561860773102413563) mod 1152921504606846883 输出值 = state & 0x7fffffff

游戏里生成的堆大小正是 PRNG 的输出。只要拿到连续两个输出,理论上就能反推出种子——因为种子本身就是我们要泄露的秘密。

反推 rand:2^10 次枚举还原完整地址

开局输入 4 堆石子,程序用 PRNG 再生成 4 堆,庄家先走一步。根据庄家第一步拿走的数量,可以还原第一、二堆的原始大小

  1. 第一堆原始大小 =rand地址的低 31 位(输出值取state & 0x7fffffff
  2. 高位地址按页对齐,只需枚举2^10个候选补齐中间 10 位
  3. 用候选地址跑一次 LCG,若输出等于第二堆大小,候选即命中

至此,随机数预测完成:不仅拿到rand的完整 64 位地址,还恢复了 PRNG 内部状态。由于rand在 libc 中的偏移固定(0x4AE90),libc 基址 = rand 地址 - 0x4AE90,之后每一局的所有堆大小全部尽在掌握。💡

栈溢出升级:如何构造 32 位任意写原语

Nim 堆大小的初始化存在连续的栈缓冲区溢出:用户输入的堆大小以 32 位为单位写入栈上数组,输入越多,溢出越深。又因为现代 clang 在 x64 下不会把第 7 个及以后的函数参数(经栈传递)复制到局部缓冲区下方,栈上的参数指针完全暴露在溢出范围内

两条线索叠加出本挑战最精妙的设计:

  • 每次开局,玩家的高分(score)会写入第 8 个函数参数所指向的地址
  • 溢出恰好可以改写这个指针

于是得到32 位 write-what-where原语:想写的值(what)= 玩家分数,想写的位置(where)= 被溢出的指针。分数本身既是赌注、又是写入值,可以靠下棋慢慢"赌"出来。

完整利用链:随机数预测 × 任意写 × ROP 四步拿 shell

第一步:泄露 libc 基址,掌握全部堆大小

按上文方法反推rand地址与 libc 基址,同时恢复 PRNG 状态。注意一个鲁棒性细节:如果 libc 基址的第 31 位为 1,32 位写入会跨边界产生进位,exploit 会检测到并自动重连重试(约 50% 概率命中)。

第二步:加倍下注,把分数刷成写入值

任意写只写 32 位,而写入值就是玩家分数。目标是把分数刷到栈迁移 gadget地址的低 32 位。技巧是加倍下注:每局下注min(当前分数, 目标分数 - 当前分数),赢了分数近乎翻倍,从 9999 分到目标只需约 6 局。同时利用预测出的庄家堆大小,通过[1,2,3,xor]等巧妙开局让庄家永远处于必败局面,保证局局必胜。

第三步:溢出改写 strlen 的 GOT 表项

最后一局故意下注 1 分并认输,把分数停在0x55040(栈迁移 gadget 的低 32 位)。随后输入超长堆大小序列触发溢出:

  • 前两个 32 位值把第 8 个参数指针改写为strlen在 libc 中的 GOT 地址
  • 程序把分数写入该地址,strlen的 GOT 表项改写成add rsp, 0x398; ret迁移 gadget

紧接着,程序在函数返回时检测到金丝雀被破坏,调用__stack_chk_fail__fortify_fail__libc_message。这条报错路径内部会调用strlen,而它用的正是一组未被保护、可写的 GOT 指针——控制流就此被接管。全 RELRO 也挡不住,这正是本题最大的看点。🎯

第四步:栈迁移 + 经典三段 ROP

栈迁移 gadget 把 RSP 加上 0x398,恰好落进溢出数据区中预先铺好的 ROP 链(pop rdi; ret/bin/sh字符串 →system),干净利落地弹出 shell,cat flag.txt收工。

关键偏移速查表(相对 libc 基址):

目标偏移
rand0x4AE90
system0x55410
"/bin/sh" 字符串0x1B75AA
pop rdi; ret0x26B72
栈迁移 gadget(add rsp, 0x398; ret)0x55040
strlen 的 GOT 表项0x1EB0A8

一键复现:exploit.py 的完整使用流程

整套利用已封装进官方脚本Dragon CTF 2021/Nim/solution/exploit.py,注释详尽、开箱即用:

  1. 获取项目文件:git clone https://gitcode.com/gh_mirrors/ct/ctf-tasks
  2. 在 Linux x64 上把Dragon CTF 2021/Nim/task/nim绑定到端口(如 4141),可用 socat 或 nc 转发 stdin/stdout
  3. 运行python2 exploit.py 127.0.0.1 4141,脚本自动完成泄露 → 刷分 → 溢出 → ROP 全流程
  4. 拿到 shell 后读取同目录的flag.txt;注意必须使用题目自带的Dragon CTF 2021/Nim/task/libc.so,偏移才完全匹配 ✅

总结与延伸:全防护之下利用链依然成立

  • 随机数预测的根源是"用地址当种子"——算法与模数公开时,输出即泄露内部状态
  • 任意写未必来自数组越界写,栈参数被溢出劫持同样致命;32 位写配合精心挑选的地址也能改写 GOT
  • 全 RELRO 不代表 GOT 绝对安全:glibc 内部报错路径__libc_message使用的部分 GOT 指针依旧可写
  • 金丝雀、NX、PIE 全开时利用链依然完整——防护永远只是提高门槛

这道题把两个"小漏洞"拧成一条自洽的利用链,是学习 pwn 的绝佳素材。完整题目、二进制与注释版 exploit 都在Dragon CTF 2021/Nim/目录下,强烈建议亲手跑一遍、逐行读一遍,收获会比看任何教程都大。🚀

【免费下载链接】ctf-tasksAn archive of low-level CTF challenges developed over the years项目地址: https://gitcode.com/gh_mirrors/ct/ctf-tasks

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询