首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[原创] KCTF 2026 第八题《亥子合辰·塔影迷楼》题解
发表于: 2026-8-22 03:29
38
[原创] KCTF 2026 第八题《亥子合辰·塔影迷楼》题解
wx_吴_369
2026-8-22 03:29
38
# [原创] KCTF 2026 第八题《亥子合辰·塔影迷楼》题解 > 题目类型:Reverse 出题战队:心学 > 解题耗时:1 小时 02 分 25 秒 > 最终序列号:`kanxue@2o26o8!@#` ## 0. 题目 附件 `kctf2026_CrackMe08.rar` 解压出一个 27KB 的 x64 console 程序,判胜条件是输入序列号回车显示 `correct`。 拿到手先看"地面真相"而不是题面故事: ``` 架构: x64 CUI | 无 ASLR | 导入表只有 8 个函数: GetStdHandle, GetCurrentProcess, FlushInstructionCache, VirtualProtect, GetModuleHandleA, GetProcAddress, GetCurrentThread, GetContext 节区: .text(熵 7.90!) .rdata .data .pdata .tgt(非标准名) strings 残影: 'Inpuf' 'faulf' 'corr' 'ZZZZ...' ntdll.dll/NtReadFile/NtWriteFile... ``` 三个信号叠在一起基本定型:**VirtualProtect+FlushInstructionCache = 自修改代码(SMC)**;`.text` 熵 7.9 = 大半代码是密文;ntdll 动态解析 + 无 ReadFile/WriteFile 导入 = 内联 syscall(反 hook)。后面还发现出题人在节区表里自己写了名字 `.text$smca` / `.text$smcb` —— 节区名直接自曝了 SMC。 题面故事里的"十六层加密、密钥分散在十六个节点、按正确顺序叠加、任何节点失败全盘皆输",读完全文你会承认这不是装饰,是精确的剧透。 ## 1. 第一层:EP 就是一个解密器 入口 0x12c0 的 stub 逻辑干净利落: ``` GetModuleHandleA(NULL) → base rbx = 0x510, rdi = base+0x1800 ← 参数来自 .tgt 节 +0x20/+0x28 VirtualProtect(base+0x1800, 0x510, RWX) XOR 解密: SSE 循环 16 字节异或 key@[0x1400070d0],尾部单字节 xor 0x5a FlushInstructionCache → 跳入解密后的代码 ``` key 读出来是 16 个 `0x5a` —— strings 里那堆 'ZZZ' 残影就是它。解密 0x510 字节,得到第一层明文代码。 非标准节 `.tgt`(target?)是个参数表,交错存放 `(len, off)` 对和一个 64 字节的目标值区。这个节是全题的"地图"。 ## 2. 第二层:代码的哈希是下一层的密钥 第一层代码里有个函数(0x1400018d0)先对自己动刀: ``` 双 FNV-1a 哈希第一层代码(0x1800..0x2310): h1 = FNV 基 0xcbf29ce484222325 h2 = FNV 基 0x9e3779b97f4a7c15 ← 黄金比例!题面"Fibonacci"的第一个实体 VirtualProtect 段B (0x1d10, 0x4962) 用 (h1, h2) 派生密钥流解密段B → call 段B 首函数(输入) → 用同一密钥流把段B 加密回去 ``` 密钥流是逐字节的:对每个字节偏移 off, ``` t = off × φ; t ^= h1; t ^= h2; t = (t>>33)^t; t *= 0xff51afd7ed558ccd; key = t >> 56 ``` (murmur3 fmix64 尾混淆取最高字节;SSE 双 lane 的 `paddq (0,1)` 说明相邻字节各有独立流。) 这就是"密钥分散在节点、按顺序叠加"的真身:**每一层代码的哈希是下一层的解密密钥**——你 patch 任何一层,后续所有层全部解废。"任何节点失败,全盘皆输"是字面意思。 ## 3. frida 全军覆没与 RPM 一击 想抓"段B 解密态"的内存,frida hook `NtReadFile`/`NtWriteFile` —— 全部静默失效,程序照常跑完输出 `Input:fault`。原因:导入表那 8 个函数 + GetProcAddress 只是为了**读 ntdll 函数头提取 syscall number,然后内联 syscall**(Hell's Gate),用户态 hook 根本拦不到。 换最朴素的姿势反而一击必中:**subprocess 起进程、stdin 用 PIPE 保持打开** —— 程序阻塞在"等输入"上,进程活着;再用 ctypes `OpenProcess+ReadProcessMemory` 从外部读内存(不是调试器,DebugPort=0,也不碰 DR 寄存器)。等输入时 dump 全映像,diff 磁盘文件:只有第一层(0x510)被解密,段B 还是密文 —— 它要到验证流程里才解密。 ## 4. 验证器:16 字节、sum=1280、五重"毁钥式"反调试 主流程很快读完:读输入 → 剥 `\r\n` → `cmp ecx, 0x10` —— **输入必须恰好 16 字节**;然后 `sum(16字节) == 1280`(`.tgt+0x10` 的 0x500);首尾字节算 `selector = (input[0]×φ) ^ (input[15]<<33)` 查一张 8 项函数指针表 —— 8 个指针全指向同一个地址,烟雾弹。真验证器在 0x140004380。 验证器开头是五重反调试:PEB BeingDebugged、NtGlobalFlag、CONTEXT 的 DR0-DR3+DR7、ProcessDebugPort/ObjectHandle(还先扫 ntdll 找 syscall 指令防 hook)、rdtsc 双读差值 < 50M cycles。**最阴的是检测命中不返回失败,而是把检测位图 xor 进密钥种子** —— 调试器在场,程序一切正常,只是密钥流悄悄变错,你在错误的解密结果里打转。这直接抬高了 oracle 的卫生要求。 对策(unicorn 全程模拟,15ms/次): - 假 PEB 全零(BeingDebugged=0,NtGlobalFlag=0) - `GetProcAddress` stub 返回 `xor eax,eax; ret` —— 里面没有 `B8 xx 0F 05` 的 syscall 模式,程序的扫描逻辑失败后直接跳过整个 syscall 直调检查 - GetThreadContext stub 把 CONTEXT 清零返回 1 - rdtsc 两处 hook 注入小时间戳 ## 5. 从黑盒差分到代数结构 oracle 在手,开始差分。先说两个自己给自己挖的坑(都是真事): - 构造差分样本时 `b'P'*7+b'QO'+b'P'*6` 少写了一个字节 —— 15 字节死于长度检查,我却当成"位置 7/8 有特殊检查"分析了一轮。**差分样本先 `len()` 自检**。 - unicorn 逐指令 python hook 太慢导致模拟超时,探针点没执行,`vals.get` 的默认值让我把"没跑到"误读成"值归零"。**hook 必须限定地址范围**(`hook_add(begin, end)`),大循环裸跑。 排除假象后,黑盒结论收敛得非常干净: 1. 全部输入依赖收敛到栈上**一个 u64**:`(input[0]×φ) ^ (input[15]<<33)` —— 就是 selector,它直接作为密码量参与运算; 2. 16 字节逐字节 `-0x21` 后按 **base-94 Horner** 折叠成大数:`N = Σ(bᵢ−0x21)·94^(15−i)`(105 位)。数值验证三连中:`47×94=0x1142`、`(0x1142+47)×94=0x6677e`、再乘一步 `0x25a1186`,逐位吻合; 3. `94 = 0x5e` 是可打印 ASCII 的数量,`sum=1280` 恰好等价于 base-94 数位和为 752。 ## 6. 破题三连:5 次多项式 mod 2¹²⁷−39 验证器中段是一大坨软乘法:4 个 64×64 部分积拼 128×128 乘法,再跟"乘 78 / 加 39 / 条件减法"的软除法归约。看到 `78` 和 `39` 这种小 magic 数字,第一反射该是 **Mersenne 型素数** —— 试一下: ``` p = 2^127 − 39 2^128 mod p = 2·2^127 mod p = 2·39 mod p = 78 ✓ ``` "除以 78 加 39"就是 mod p 的高位折叠。题面里无处不在的 39/78 至此有了着落。 在 p 的语言下,整条链是: ``` V = h(N) = ((((N·(N+M1) + C1)·N + C2)·N + C3)·N + C4) mod p ← 5 次 Horner 多项式 m68, L = V>>64, V&M64 ← 双 lane 初值 20 轮 ARX(sᵢ = C[i%2] ^ T[i],T 为 20 qword 轮密钥表): D(x,s): t = X·((x<<64|s) mod p) mod p v = ror(t_hi,47) ^ s ^ t_lo return ror(v,51) ^ S(v) ← S = 256B S-box m68' = D(m68, sᵢ) ^ L L' = D(m68', sᵢ^φ) ^ m68 之后 6 条 FNV/S-box 混合管线,与 6 个 64 位目标比对 ``` 6 个目标值 = 密钥流水线值 xor `.tgt` 里那 7 个常量中的 6 个(第 7 个 `0x6666666666666666` 是 padding;7 个目标对应题面"她尝试七次,前六次都失败")。 ## 7. 反解:不走哈希的路 6 路比对里 5 路经过 FNV 单向压缩,逆不动。但 trace 发现 **r10 路和 r12 路从 20 轮结束到比对原封未动** —— 它们就是 `m68₂₀` 和 `L₂₀`,不经任何哈希。于是: 1. `T1 = m68₂₀`、`T0 = L₂₀`,两个已知目标正好凑齐双 lane 终态; 2. 每轮是三角结构,精确可逆:`m68ᵢ = L_{i+1} ^ D(m68_{i+1}, sᵢ^φ)`,`Lᵢ = m68_{i+1} ^ D(m68ᵢ, sᵢ)`,逐轮倒退 20 步得 V; 3. V 定了,解 5 次方程 `h(N) = V (mod p)` —— p 是素数,**Cantor-Zassenhaus** 求全部根;过滤 `[0, 94¹⁶)`,**根唯一**; 4. base-94 反编码 N → 16 字节。 ``` N = 0x174a5e1d4cf4f146e72f70092ac → kanxue@2o26o8!@# ``` sum 校验 1280 ✓,语义自洽(kanxue + @2o26 = 2026 的 leet + o8 = 08 月)。unicorn oracle `eax=1`,实机: ``` $ echo 'kanxue@2o26o8!@#' | ./kctf2026_CrackMe08.exe Input:correct ``` 一次提交,平台锁定。 ## 8. 完整解题脚本(solve_final.py,自包含一键复现) ```python # -*- coding: utf-8 -*- # KCTF 2026 第八题《亥子合辰·塔影迷楼》最终求解脚本(自包含,无外部依赖) # 输入: 两个可逆路目标 T0/T1 -> 输出: kanxue@2o26o8!@# # 方法: 逆20轮双lane ARX -> V=h(N) -> Cantor-Zassenhaus 解 5 次多项式 mod (2^127-39) # -> base-94 反编码 import random M64 = (1 << 64) - 1 P = (1 << 127) - 39 # 素数;程序里 ÷78/mod39 软归约的真身 GR = 0x9E3779B97F4A7C15 # 黄金比例 φ(题面 "Fibonacci") M1 = 0x21B3BDE1ACF9ADFA471FB28461A53BB5 # ---- 运行时导出的固定常量(unicorn 快照,输入无关已验证)---- SBOX = bytes.fromhex( '4ed835172d8e916bab6530f28adeda186f5cf09e608981629046fd613daa949f' '88e22fc430b64f4fde66b80ade4067d76d6551e656e1e540232e61488b476f4c' '9996650cfff1f87593c03fd152ae048c388dcbf88159a1104c80a10331e6195c' 'd8be465021e378d8f386d0c7fb358fac7974d4426b84f57e49d5266688b3784' '1182b9230bd9500caedf4df297b70e3d98ac1baa1795b5a8da5927489c7a402b' 'a08db3e554db27324da17ac9c1ba512e1d1151e090e57da9e74ded1365352d4e' '966536e03cd266e1bcb2d5776460932554c37cc9f00faddcd2e92fe3d0e71ac8' '9dc183d87d5fa40efa98c65da9d7dc32fc092638af4e673fc81a5ad390563c349') T = [0x454ab6cf8f0fe386, 0x715664a40e9f5f5, 0x9306305b57fba9a1, 0xe98c2c52e5ebb7db, 0xfb3fd083f20738c3, 0xccb6f8a75c3fde1b, 0x2131dd38af5d4e80, 0xa08b202d0096f0cd, 0xd910717024bb4898, 0x8e30f948a63f886, 0x3f28976422a1b7ea, 0x509fd4dac702028b, 0x4ed3c0879a56c01f, 0x5c804f2b54da01e2, 0xb04119cd47a9c4e0, 0xef0de2ad3c4820ed, 0x9e75101fa5267b0a, 0xa384473bb6e5fdd3, 0x2d2fb4022ad6e75c, 0xc5e6b576cdfde33a] C = [0x553d3c5ef6ee11ff, 0x4aeee52b5738eb8f] X = (0x4D8A9E9343D415CB << 64) | 0x83A0A488EC8523FE CS = [0x191A3870AD3D9411_05F15A9C38AAE874, 0x40C190E211322CAA_2F01797348F02C97, 0x1DF732140B8A3EF7_58FD09BDE0058A37, 0x3F0CB8512C6F88E4_7E5F164FB478AAB5] # ---- 验证器最终 6 个比对目标中两个可逆路的值 ---- T0_r12 = 0xb8fd0694401d3d03 # r12 路 = L_20 T1_r10 = 0x0c14db1c20dd97a8 # r10 路 = m68_20 def ror(x, n): return ((x >> n) | (x << (64 - n))) & M64 def S64(v): return int.from_bytes(bytes(SBOX[b] for b in v.to_bytes(8, 'little')), 'little') def D(x, s): t = (X * ((x << 64 | s) % P)) % P v = ror(t >> 64, 47) ^ s ^ (t & M64) return ror(v, 51) ^ S64(v) def h(N): w = (N * (N + M1)) % P for i, c in enumerate(CS): w = (w + c) % P if i < 3: w = (w * N) % P return w def chain_forward(V): m68, L = V >> 64, V & M64 for i in range(20): s = C[i % 2] ^ T[i] mn = D(m68, s) ^ L L = D(mn, s ^ GR) ^ m68 m68 = mn return m68, L def invert_m68(m68_final, L_final): m68, L = m68_final, L_final for i in range(19, -1, -1): s = C[i % 2] ^ T[i] m68_prev = L ^ D(m68, s ^ GR) L_prev = m68 ^ D(m68_prev, s) m68, L = m68_prev, L_prev return (m68 << 64) | L # ---------------- 多项式求根 mod P(Cantor-Zassenhaus)---------------- def pmul(a, b): r = [0] * (len(a) + len(b) - 1) for i, x in enumerate(a): if x: for j, y in enumerate(b): r[i + j] = (r[i + j] + x * y) % P while len(r) > 1 and r[-1] == 0: r.pop() return r def pmod(a, b): a = a[:]; db = len(b) - 1; inv = pow(b[-1], -1, P) for i in range(len(a) - 1, db - 1, -1): c = a[i] * inv % P if c: for j in range(db + 1): a[i - db + j] = (a[i - db + j] - c * b[j]) % P while len(a) > 1 and a[-1] == 0: a.pop() return a def pgcd(a, b): while len(b) > 1 or b[0]: a, b = b, pmod(a, b) if len(a) > 1: inv = pow(a[-1], -1, P); a = [c * inv % P for c in a] return a def ppowmod(base, e, m): r = [1]; base = pmod(base, m) while e: if e & 1: r = pmod(pmul(r, base), m) base = pmod(pmul(base, base), m); e >>= 1 return r def psub(a, b): n = max(len(a), len(b)); r = [0] * n for i in range(n): r[i] = ((a[i] if i < len(a) else 0) - (b[i] if i < len(b) else 0)) % P while len(r) > 1 and r[-1] == 0: r.pop() return r def pdiv(a, b): A = a[:]; db = len(b) - 1; inv = pow(b[-1], -1, P) q = [0] * max(len(A) - db, 1) for i in range(len(A) - 1, db - 1, -1): c = A[i] * inv % P; q[i - db] = c if c: for j in range(db + 1): A[i - db + j] = (A[i - db + j] - c * b[j]) % P while len(q) > 1 and q[-1] == 0: q.pop() return q def poly_roots(f, tries=100): pder = lambda a: [(i * a[i]) % P for i in range(1, len(a))] or [0] if len(f) < 2: return [] g = pgcd(f, pder(f)) hq = pdiv(f, g) if len(g) > 1 else f xp = ppowmod([0, 1], P, hq) hq = pgcd(hq, pmod(psub(xp, [0, 1]), hq)) roots, stack = [], [hq] if len(hq) > 1 else [] while stack: g = stack.pop() if len(g) == 2: roots.append((-g[0]) * pow(g[1], -1, P) % P); continue for _ in range(tries): u = ppowmod([random.randrange(P), 1], (P - 1) // 2, g) u[0] = (u[0] - 1) % P w = pgcd(g, u) if 1 < len(w) < len(g): stack.append(w); stack.append(pdiv(g, w)); break else: raise RuntimeError('CZ split failed') return roots def _poly_f(Vt): f = [0, M1 % P, 1] for i, c in enumerate(CS): f[0] = (f[0] + c) % P if i < 3: f = pmul(f, [0, 1]) f[0] = (f[0] - Vt) % P return f def serial_of(N): out = [] for _ in range(16): N, d = divmod(N, 94); out.append(d) if N: raise ValueError('N too big') return bytes(b + 0x21 for b in reversed(out)) def solve(): Vt = invert_m68(T1_r10, T0_r12) print(f'V = h(N) = {Vt:#034x}') roots = poly_roots(_poly_f(Vt)) cands = [n for n in roots if 0 <= n < 94 ** 16] print(f'多项式全部根: {[hex(r) for r in roots]}') for n in cands: s = serial_of(n) ok = chain_forward(h(n)) == (T1_r10, T0_r12) # 正向闭环自验 print(f'N={n:#x} -> {s.decode()} 正向复验: {"PASS" if ok else "FAIL"} sum={sum(s)}') return [serial_of(n).decode() for n in cands] if __name__ == '__main__': print('FLAG:', solve()[0]) ``` **运行输出:** ``` V = h(N) = 0x4be831b0ad3a2d361489375bba3fb8de 多项式全部根: ['0x174a5e1d4cf4f146e72f70092ac'] N=0x174a5e1d4cf4f146e72f70092ac -> kanxue@2o26o8!@# 正向复验: PASS sum=1280 FLAG: kanxue@2o26o8!@# ``` (完整工程产物同目录:unicorn oracle `oracle.py`、中段指令级解释器 `mid_stage.py`、逆推器 `invert_mid.py`、段B 解密器 `peel.py`、RPM dumper `rpm_dump.py`、提交客户端 `submit.py`。) --- 感谢出题战队心学。这题把 SMC 自校验洋葱、毁钥式反调试、Mersenne 型模算术、ARX 轮函数和 base-94 编码缝成一个整体,每层都对应题面一句话——"十六层加密"是 SMC 分层、"密钥按顺序叠加"是哈希链、"Fibonacci"是 φ 常数、"尝试七次"是 7 个目标值、"十二次失败第十三个团队"是模数 2¹²⁷−39 里的 39 与循环叙事的呼应。出题人在叙事和技术之间做了一比一映射,是攻防赛里少见完成度。快的话 10 分钟能破是因为构件全是反射弧级的标准件——而这恰恰是对题目工程质量的另一种夸奖。
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
wx_吴_369
2
发帖
0
回帖
0
RANK
关注
私信
他的文章
[原创] KCTF 2026 第八题《亥子合辰·塔影迷楼》题解
38
[原创]KCTF 2026 第七题《戌时·暗能潜流》题解
25
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部