首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[原创] 看雪 KCTF 2026 第五题《申时·忆海倒带》Writeup
发表于: 2026-8-17 13:53
22
[原创] 看雪 KCTF 2026 第五题《申时·忆海倒带》Writeup
yuzhouheike
2026-8-17 13:53
22
# 看雪 KCTF 2026 第五题《申时·忆海倒带》Writeup ## 结论先行 - 文件:`cm.exe`(PE32 / x86,内部名 `ca_tf6.exe`),`main = 0x403D90` - 判胜:输入 Key,回车,打印 `verify success.` - **Key:** ``` 323C47184B0D3C44254B445842552F365C362C1144424B0D3C4416433B0DD6B12A0D3D95FA65B5E0ADE5E11B ``` **一句话:** 花指令 + 假注释 + 反调试全是障眼法;真正的锁 = **一张静态可逆的查表变换**(直接吐出 Key 高 28 字节)+ **一个 128 位 RSA**(锁住 Key 低 16 字节)。128 位模数是可分解的,分解后反解即可。 --- ## 一、最短路径(≈5 分钟版) 1. IDA `F5` 反编译 `main` / 模幂,看穿花指令,还原校验管线。 2. 目标串 `Welcome to KCTF2026! Come and give it a try.` 逆着查表 → 44 个 `forced` 字节。 3. `forced[0:28]` **就是** Key 高 28 字节(查表是双射,直接出)。 4. `forced[28:44]` = 需要的 RSA 输出;从二进制读出 `n`(128 位)、`e=65537`。 5. **factordb / yafu / msieve 秒分解 128 位 n** → p、q,RSA 解密得 Key 低 16 字节。 6. 高 28 + 低 16 拼成 44 字节 → 88 位 hex,即 Key。 后面是细节。 --- ## 二、反分析(先剥壳) - **花指令**:满屏 `eb06 / c3 / 8bff / ebeb / f9`。`eb06` = `jmp +6`,跳过后面的垃圾字节,执行流线性,但线性反汇编被带偏。**IDA 反编译器能直接还原**。 - **假注释(误导)**——注意:这三条根本不是注释,而是被 `sub_402100` 当 **16 进制大数**解析的参数(伪装): - `// FIXME: author confirmed password is 'admin123' ... 0x401234`(该地址是垃圾) - `TODO: exception handler contains real verification logic ...` - `NOTE: the int3 in __try is just obfuscation ...` - `n0_4i_c4n_r34d_th1s!`(“no AI can read this”,作者挑衅)。 - 解析函数里的 `GetTickCount`:返回值被丢弃,纯干扰。 - **反自动化**:程序脱离真实交互控制台就不出逻辑(重定向/管道/WinRM 全无输出),WOW64 下 cdb 断点不稳、进程提前退出。→ **本题纯静态求解,不依赖动态 oracle。** --- ## 三、校验管线(`main` 还原) ```c puts("Enter your key:"); gets_s(Buffer, 1000); key = parse(Buffer, 16); // 按 16 进制解析成大数 // 格式:每字符 ∈ [0-9A-Z],且解析出正好 11 limb(88 个 hex) if (charset_bad || 8*limbCount != 88) fail(); serialize_be(key, ArgList); // 大端序列化成 44 字节 ArgList[0..43] // 闸门1:44 字节异或 == 0x8F if (xor_all(ArgList,44) != 0x8F) fail(); // 闸门2:带反馈校验和 == 0xBEFF for (b in ArgList) acc += ((acc & 0x7F) + 1) * b; if ((uint16)acc != 0xBEFF) fail(); // RSA:base = Key 低 16 字节(input[56:88]),覆盖 ArgList[28..43] sub_403500(out, &Buffer[56]); // result = base^e mod n memcpy(&ArgList[28], serialize_be(result), 16); // 固定查表变换后逐字节比较 for (i=0;i<44;i++) v88[i] = T3[ T2[ ArgList[i]-1 ] ]; if (!strcmp(v88, "Welcome to KCTF2026! Come and give it a try.")) puts("verify success."); ``` 拆开看: - `ArgList[0..27]` = **Key 高 28 字节**(序列化后未被覆盖) - `ArgList[28..43]` = **RSA 输出**(被覆盖) - `base` = **Key 低 16 字节** --- ## 四、突破一:查表可逆 → 高 28 字节直接到手 变换 `out = T3[T2[x-1]]`,`T2@0x4163B0`(置换表)、`T3@0x4263B0`(字节表)全在 `.rdata`,**静态**。逆着查: ```python inv = {} for idx in range(255): inv.setdefault(T3[T2[idx]], []).append(idx) forced = bytes(inv[c][0] + 1 for c in target) # 每个目标字符恰好唯一原像 ``` 双射 → 44 个 `forced` 字节唯一确定。其中 `forced[0:28]` 就是 Key 高 28 字节。 --- ## 五、突破二:128 位 RSA,可分解 → 低 16 字节 `sub_403500 / sub_402510` 是 base-2³² 大数的平方-乘模幂;参数用"置换存储"混淆(`limb[i] = data[perm[i]]`)。从 IDA 读出: - **n**(4 limb,`data@0x436018`,`perm@0x42E018`): `n = 186848929443298675924877642916120916849`(= `0x8C91CB79EC693F3ED6519C945DD67371`,**128 位**) - **e**(1 limb):`0x10001 = 65537` 因 n 只有 128 位,`result = base^e mod n < 2^128`,恰好 16 字节 → `forced[28:44]` 即完整 RSA 输出。**128 位模数是可分解的**(factordb 直接命中 / yafu / msieve 秒级;纯 Python ECM 约 1 分半): - p = `13636154180376482939` - q = `13702465297157554691` 于是 `d = 65537⁻¹ mod λ(n)`,`base = result^d mod n` = Key 低 16 字节。 --- ## 六、求解脚本 ```python import struct d = open('cm.exe','rb').read() rd = lambda va: d[va-0x401000] # .rdata 取字节 w = lambda va: struct.unpack('<I', d[va-0x401000:va-0x401000+4])[0] target = b"Welcome to KCTF2026! Come and give it a try." # 1) 逆查表 → forced t2 = [w(0x4163B0+4*i) for i in range(256)] inv = {} for i in range(255): inv.setdefault(rd(0x4263B0+t2[i]), []).append(i) forced = bytes(inv[c][0]+1 for c in target) # 2) RSA(n/e 读自二进制;p,q 由 factordb/yafu 分解) n = 186848929443298675924877642916120916849 e = 0x10001 p, q = 13636154180376482939, 13702465297157554691 # 3) 反解低 16 字节 from math import gcd lam = (p-1)*(q-1)//gcd(p-1,q-1) dexp = pow(e, -1, lam) res = int.from_bytes(forced[28:44], 'big') low16 = pow(res, dexp, n).to_bytes(16, 'big') # 4) 拼 Key + 复核两道闸门 keyb = forced[0:28] + low16 x = 0 for b in keyb: x ^= b acc = 0 for b in keyb: acc = (acc + (((acc&0x7F)+1)*b)) & 0xFFFFFFFF assert x == 0x8F and (acc & 0xFFFF) == 0xBEFF print(format(int.from_bytes(keyb,'big'), '088X')) ``` --- ## 七、验证 - Key 高 28 字节:`323C47184B0D3C44254B445842552F365C362C1144424B0D3C441643` - Key 低 16 字节:`3B0DD6B12A0D3D95FA65B5E0ADE5E11B` | 校验 | 期望 | 结果 | |---|---|---| | 长度 88 / [0-9A-Z] / 11 limb | ✔ | ✔ | | 异或闸门 | `0x8F` | ✔ | | 反馈校验和 | `0xBEFF` | ✔ | | `base^65537 mod n` | `forced[28:44]` | ✔ | | 查表变换 == 目标串 | ✔ | ✔ | ``` 323C47184B0D3C44254B445842552F365C362C1144424B0D3C4416433B0DD6B12A0D3D95FA65B5E0ADE5E11B ``` --- ## 八、小结 - 障眼法:`admin123` / `__except` / `int3` / `GetTickCount` / “no AI can read this” 全是误导;三条“注释”才是伪装的 16 进制 RSA 参数。 - 真锁:静态可逆查表(高 28 字节)+ 128 位 RSA(低 16 字节)。 - 快点:别硬啃混淆汇编——`F5` 还原管线 + 逆查表 + **128 位模数直接 factordb/yafu 秒分解**,一条龙下来就是几分钟的事。作者赌你不敢分解、不肯静态复算,可这两步恰恰是最省事的。
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
yuzhouheike
9
发帖
38
回帖
60
RANK
关注
私信
他的文章
KCTF 第十题:卯时·曦光初现 - PWN Writeup
3201
[原创] 第九题:丑寅同墟·星海抉择
55
[原创] 看雪 KCTF 2026 第五题《申时·忆海倒带》Writeup
22
[原创] 看雪·2026 KCTF —— 第二题:巳时·绿光幽语 Writeup
58
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部