首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[原创] 第八题:亥子合辰·塔影迷楼 wp by lzq2000
发表于: 2026-8-22 05:50
57
[原创] 第八题:亥子合辰·塔影迷楼 wp by lzq2000
lzq2000
2026-8-22 05:50
57
题目文件就一个 `kctf2026_CrackMe08.exe`,x64 PE,体积不大。跑起来会打 `Input:`,从 stdin 读一行。长度对了才往下走,对了输出 `correct`,错了 `fault`。flag 没有 `flag{}` 这种壳,16 个可打印字符。 具体解题思路及脚本如下: ## 1、 入口那两层加密 程序几乎不走正常导入表,入口 `start` 在 `0x1400012C0`。`.text` 里一大段是加密的。 第一层好认:RVA `0x1800`,长度 `0x510`,单字节 XOR `0x5A`。解开之后是真正的读输入、校验长度、调后面那坨逻辑。 第二层在 `0x140001D10`,长度 `0x4962`。keystream 跟已经解开的 stub 的 FNV-1a / golden-FNV 有关: ``` x = (i * 0x9E3779B97F4A7C15) ^ hgold ^ hfnv x ^= x >> 33 x *= 0xFF51AFD7ED558CCD buf[i] ^= (x >> 56) & 0xFF ``` 静态把两层都解开之后,跳表最终进 `0x140004380`。这才是校验本体。前面 `sub_140001070` 那个 FNV+ROR64 对输入做了个哈希,对不上会搅后面的时间反调试全局量,主逻辑还是 4380。 输入这边:`NtReadFile` 读 stdin,去掉 `\r\n` 之后必须正好 16 字节。另外 16 个字节的算术和要等于 `0x500`(1280),平均每个字节是 `'P'`。字符范围实际是 `0x21`~`0x7E`,也就是 `'!'` 到 `'~'`,94 个可打印字符,没有空格。 ## 2、 16 个字符先被当成 94 进制 4380 里先把输入收成一个 128 位数: ``` n = 0 for c in input: n = n * 94 + (c - 0x21) ``` 后面所有运算都吃这个 `n`。`'P'*16` 对应 `0xed048f469eec3fb8af41531605`,拿 Unicorn 对着寄存器对过,没跑偏。 ## 3、 反调试掺进密钥里 4380 开头一堆经典货:PEB.BeingDebugged、NtGlobalFlag、DR0–DR3/DR7、`NtQueryInformationProcess` 的 DebugPort / DebugObjectHandle、rdtsc 超时。检测结果不是简单 abort,而是混进后面 splitmix64 的种子。 所以这题动态调的时候,密钥全都会漂。我这边是 Unicorn 造了个干净 PEB + 假 ntdll/syscall,从 `0x140004380` 跑到返回,静态 dump S 盒和 round key。 splitmix 吐出来的东西跟输入无关(干净环境下是常量): - 256 字节 S 盒(不是置换,大概 160 多个不同取值) - 两组各 20 个 qword 的 round key(rk1 / rk2) - 若干 128-bit 常数,后面多项式和 Feistel 都用得到 比较目标也是算好放在栈上的: ``` e0 = 0xB8FD0694401D3D03 # 跟第一组 Feistel 的 r12 比 e1 = 0x0C14DB1C20DD97A8 # 跟第一组 Feistel 的 r9 比 e2 = 0xACA5240A53FB78A6 # 第二组 e3 = 0x9A2789B6DE31C2DC ``` 后面还有两个 FNV 校验,第一组对上的话这两个会跟着对。 ## 4、 预变换:模 2^128-78 的五次多项式 输入 `a` 不会直接进 Feistel。`0x140004EE9` 到 `0x140005910` 这段重复了四次「128×128 乘 + 用 0x4E 做 Barrett 式归约」。模数是: ``` M = 2^128 - 78 ``` 0x4E 就是 78。另外还有个循环 4 次的修正,我这边叫它 `add39x4`:高半部分 `>= 0x7FFFFFFFFFFFFFFF` 的时候,低半 `+= 39`,高半再加 `0x8000000000000001` 并处理借位。直观效果是值一旦摸到 `2^127` 附近,就减掉 `P = 2^127-39`,把数按回高位为 0 的区间。 所以它**并不严格保持 mod M 的剩余类**,只保证 mod P 不变(借位走常见那条路径的时候)。这点后面解方程会踩到。 抛开 add39x4 的毛刺,这段代数结构很干净。设 ``` C1 = 0x21B3BDE1ACF9ADFA471FB28461A53BB5 C2 = 0x191A3870AD3D941105F15A9C38AAE874 C3 = 0x40C190E211322CAA2F01797348F02C97 C4 = 0x1DF732140B8A3EF758FD09BDE0058A37 C5 = 0x3F0CB8512C6F88E47E5F164FB478AAB5 ``` (C1/C2 在 `rbp-0x20`、`rbp-0x10`,C3/C4/C5 在 `rbp+0`、`+0x10`、`+0x20`,都是 splitmix 吐的。) ``` x1 = a * (a + C1) x2 = a * (x1 + C2) x3 = a * (x2 + C3) x4 = a * (x3 + C4) T = x4 + C5 ``` 也就是 ``` T = a^5 + C1 a^4 + C2 a^3 + C3 a^2 + C4 a + C5 ``` 用 `'P'*16` 对过,5910 处的 `(r9<<64)|r12` 跟这个式子一致。 ## 5、 20 轮 Feistel 5910 之后是 20 轮,结构是: ``` new_r9 = SB(F(old_r9, keyxor)) XOR old_r12 new_r12 = SB(F(new_r9, keyxor^GOLDEN)) XOR old_r9 ``` `keyxor` 偶数轮用 FNV-1a 常量 `0x553D3C5EF6EE11FF` 再异或 rk1[i],奇数轮换 golden-FNV `0x4AEEE52B5738EB8F`。 `F` 本身是:两个 128 位数先 add39x4,做 128×128 乘,再 `% M`,再 add39x4。其中一个操作数是常量 ``` A = 0x4D8A9E9343D415CB83A0A488EC8523FE ``` 另一个是 `(r9, keyxor)`。 `SB` 是 `ror(x,47) ^ keyxor ^ R_lo`,然后 `ror(t,51) ^ SboxPerm(t)`。S 盒按字节 7,6,5,...,0 的顺序抽。 这是标准 Feistel,反一轮只需要正向再算一遍 F: ``` old_r9 = SB(F(new_r9, keyxor^GOLDEN)) XOR new_r12 old_r12 = SB(F(old_r9, keyxor)) XOR new_r9 ``` 从 `(r9, r12) = (e1, e0)` 往回拆 20 轮,得到进入 Feistel 的初态: ``` r9 = 0x4BE831B0AD3A2D36 r12 = 0x1489375BBA3FB8DE T = 0x4BE831B0AD3A2D361489375BBA3FB8DE ``` 5EB8 起还有第二组 20 轮,换 rk2,初态还是同一份 T。第一组对上,第二组的 e2/e3 会一起对上,当交叉验证就行。 ## 6、 解五次方程 现在变成: ``` a^5 + C1 a^4 + C2 a^3 + C3 a^2 + C4 a + C5 ≡ T (某种归约下) ``` `M = 2 * P`,`P = 2^127 - 39` 是素数。一开始我按 mod M 去解,多项式在 GF(2) 上恒为 1,**模 2 无根,模 M 当然也无根**。T 是偶数,而这个五次式对任意 a 都是奇数——除非中间 add39x4 翻了奇偶性。 回头看 add39x4:高位到了 `2^127` 就减 P,剩余类从 mod M 的一个陪集跳到另一个,但 **mod P 不变**。所以方程应该放在 `GF(P)` 上解。 Sage / sympy 分解五次多项式,只有一个一次因子: ``` a ≡ 0x174A5E1D4CF4F146E72F70092AC (mod P) ``` `a` 还得能编成 16 位 94 进制,上界 `94^16` 远小于 P,所以这个根就是唯一候选。按大端 94 进制展开,字节和正好 0x500: ``` kanxue@2o26o8!@# ``` ## 7、 验证 运行:kctf2026_CrackMe08.exe Input:kanxue@2o26o8!@# correct 完整脚本如下: ``` # -*- coding: utf-8 -*- """kctf2026 CrackMe08 solver — invert Feistel then solve the quintic mod P=2^127-39.""" from sympy import Poly, symbols MASK64 = (1 << 64) - 1 M = (1 << 128) - 78 P = (1 << 127) - 39 GOLDEN = 0x9E3779B97F4A7C15 C1 = 0x21B3BDE1ACF9ADFA471FB28461A53BB5 C2 = 0x191A3870AD3D941105F15A9C38AAE874 C3 = 0x40C190E211322CAA2F01797348F02C97 C4 = 0x1DF732140B8A3EF758FD09BDE0058A37 C5 = 0x3F0CB8512C6F88E47E5F164FB478AAB5 E0 = 0xB8FD0694401D3D03 # after 20 rounds, compared with r12 E1 = 0x0C14DB1C20DD97A8 # after 20 rounds, compared with r9 M30 = 0x553D3C5EF6EE11FF M38 = 0x4AEEE52B5738EB8F A_HI, A_LO = 0x4D8A9E9343D415CB, 0x83A0A488EC8523FE SBOX = bytes.fromhex( "4ed835172d8e916bab6530f28adeda186f5cf09e608981629046fd613daa949f" "88e22fc430b64f4fde66b80ade4067d76d6551e656e1e540232e61488b476f4c" "9996650cfff1f87593c03fd152ae048c388dcbf88159a1104c80a10331e6195c" "d8be465021e378d8f386d0c7fb358fac7974d4426b84f57e49d5266688b37841" "182b9230bd9500caedf4df297b70e3d98ac1baa1795b5a8da5927489c7a402ba" "08db3e554db27324da17ac9c1ba512e1d1151e090e57da9e74ded1365352d4e9" "66536e03cd266e1bcb2d5776460932554c37cc9f00faddcd2e92fe3d0e71ac89" "dc183d87d5fa40efa98c65da9d7dc32fc092638af4e673fc81a5ad390563c349" ) RK1 = [ int.from_bytes(bytes.fromhex( "86e30f8fcfb64a45f5f5e9404a661507a1a9fb575b300693dbb7ebe5522c8ce9" "c33807f283d03ffb1bde3f5ca7f8b6cc804e5daf38dd3121cdf096002d208ba0" "9848bb24707110d986f8638a940fe308eab7a1226497283f8b0202c7dad49f50" "1fc0569a87c0d34ee201da542b4f805ce0c4a947cd1941b0ed20483cade20def" "0a7b26a51f10759ed3fde5b63b4784a35ce7d62a02b42f2d3ae3fdcd76b5e6c5" )[i * 8:(i + 1) * 8], "little") for i in range(20) ] def ror64(x, n): n &= 63 return ((x >> n) | (x << (64 - n))) & MASK64 def add39x4(hi, lo, add_hi=0x8000000000000001): rdi = 0x7FFFFFFFFFFFFFFF special = MASK64 - 0x26 for _ in range(4): do = True if not (hi == rdi and lo == special): if hi < rdi: do = False if do: borrow = 1 if lo < special else 0 hi = (hi - borrow + add_hi) & MASK64 lo = (lo + 0x27) & MASK64 return hi, lo def sbox_apply(x): r8 = SBOX[(x >> 0x30) & 0xFF] rdx = (SBOX[(x >> 0x38) & 0xFF] << 8) & MASK64 r8 |= rdx r8 = (r8 << 8) & MASK64 for sh in (0x28, 0x20, 0x18, 0x10, 8): r8 |= SBOX[(x >> sh) & 0xFF] r8 = (r8 << 8) & MASK64 r8 |= SBOX[x & 0xFF] return ror64(x, 0x33) ^ r8 def F_half(hi_state, keyxor): B_hi, B_lo = add39x4(hi_state, keyxor) A_hi, A_lo = add39x4(A_HI, A_LO) R = ((A_hi << 64) | A_lo) * ((B_hi << 64) | B_lo) % M return add39x4(R >> 64, R & MASK64) def SB_from_R(R_hi, R_lo, keyxor, xor_other): t = ror64(R_hi, 0x2F) ^ keyxor ^ R_lo return sbox_apply(t) ^ xor_other def round_inv(new_r9, new_r12, rnd): keyxor = (M30 if rnd & 1 == 0 else M38) ^ RK1[rnd] keyxor2 = keyxor ^ GOLDEN R2_hi, R2_lo = F_half(new_r9, keyxor2) old_r9 = SB_from_R(R2_hi, R2_lo, keyxor2, new_r12) R_hi, R_lo = F_half(old_r9, keyxor) old_r12 = SB_from_R(R_hi, R_lo, keyxor, new_r9) return old_r9, old_r12 def to_b94(n, length=16): out = [] for _ in range(length): out.append(0x21 + (n % 94)) n //= 94 if n: return None return bytes(reversed(out)) def main(): r9, r12 = E1, E0 for i in range(19, -1, -1): r9, r12 = round_inv(r9, r12, i) T = (r9 << 64) | r12 x = symbols("x") f = Poly( x ** 5 + C1 * x ** 4 + C2 * x ** 3 + C3 * x ** 2 + C4 * x + (C5 - T), x, modulus=P, ) roots = [int(r) for r in f.ground_roots()] B = 94 ** 16 for a in roots: if 0 <= a < B: flag = to_b94(a) if flag and sum(flag) == 0x500: print(flag.decode("ascii")) return raise SystemExit("no flag") if __name__ == "__main__": main() ``` S 盒和 rk1 是干净环境下 dump 出来的常量,跑一下直接出 flag 如下: ``` kanxue@2o26o8!@# ```
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
lzq2000
6
发帖
6
回帖
47
RANK
关注
私信
他的文章
[原创] 第十题:卯时·曦光初现 wp by lzq2000
1038
[原创] 第九题:丑寅同墟·星海抉择 wp by lzq2000
41
[原创] 第八题:亥子合辰·塔影迷楼 wp by lzq2000
57
第七题:戌时·暗能潜流
21
看雪 CTF 2026 第二题「巳时·绿光幽语」Writeup by lzq2000
1399
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部