首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[原创]KCTF 2026 第五题
发表于: 2026-8-17 20:55
710
[原创]KCTF 2026 第五题
TimeQvQ
2026-8-17 20:55
710
# 一条被 SEH 打散的 RSA Key 校验链 题目给出 `cm.exe`,规则很简单:输入 Key,看到 `verify success.` 就算通过。样本是 x86 PE, 对照一下SHA1是否题面一致: ``` D1C8124C5964AF1531C6E311328854A05CA40BCA ``` 最终Flag ``` 323C47184B0D3C44254B445842552F365C362C1144424B0D3C4416433B0DD6B12A0D3D95FA65B5E0ADE5E11B ``` 这题的难点不在爆破,而在于三件事:控制流被 SEH 和运行时跳板打散,输入实际上是 44 字节数据,最后 16 字节还经过了一次小模数 RSA。下面按真实逆向顺序展开。 ## 1. 不要从 `admin123` 开始 字符串窗口里有一组很刻意的提示: ```text admin123 TODO: exception handler contains real verification logic... NOTE: the int3 in __try is just obfuscation... ``` 如果跟着这些字符串做,会在几个自定义大整数对象里兜圈子。真正应该先盯住的是成功字符串。最终比较的目标位于 `.rdata:0x416380`: ```text Welcome to KCTF2026! Come and give it a try. ``` 长度恰好是 44 字节。交叉引用落在 `0x40454A`,关键比较代码非常直白: ``` 0040454A lea ecx, [ebp-88h] ; 程序算出的 44 字节 00404550 mov eax, 416380h ; Welcome... 目标串 00404555 mov dl, [eax] 00404557 cmp dl, [ecx] 00404559 jne 404575h ; fail ... 0040457C jne 40459Dh 0040457E jmp 404586h ; verify success. ``` 因此最稳的路线不是猜密码,而是把这 44 字节目标反推回输入。 ## 2. 88 个字符其实是 44 个字节 主函数 `0x403D90` 读取输入后,先进行格式检查: ``` 00404070 mov al, [ebp+ecx-858h] 0040407B cmp al, 30h 0040407D jl fail 0040407F cmp al, 39h 00404081 jle ok 00404083 sub al, 41h 00404085 cmp al, 19h ; 允许 A-Z 00404087 ja fail ... 00404092 cmp edx, 58h ; 0x58 = 88 00404095 jne fail ``` 注意这里的字符集表面上允许 `0-9A-Z`,但后续实际解法使用的是大写十六进制字符。`0x402100` 把输入按 16 进制解析成 11 个 32 位 limb,随后主函数将 limb 反序导出为连续的 44 字节。 对最终 Key 下断点,`0x404112` 处的缓冲区内容为: ```text 32 3C 47 18 4B 0D 3C 44 25 4B 44 58 42 55 2F 36 5C 36 2C 11 44 42 4B 0D 3C 44 16 43 3B 0D D6 B1 2A 0D 3D 95 FA 65 B5 E0 AD E5 E1 1B ``` 随后有两道前置校验。它们都在 `0x403D90` 中,静态和动态结果一致: ```python def precheck(raw: bytes) -> None: assert len(raw) == 44 x = 0 for byte in raw: x ^= byte assert x == 0x8F # 0x4041BC acc = 0 for byte in raw: acc += byte * ((acc & 0x7F) + 1) assert (acc & 0xFFFF) == 0xBEFF # 0x40425F ``` 最终数据的实际结果是: ```text xor = 0x8F acc = 0x6BEFF acc & 0xFFFF = 0xBEFF ``` 只满足这两项仍然会失败。真正的校验从 `0x404271` 调用 `0x403500` 后才开始。 ## 3. 为什么线性反汇编看起来不通 `0x403500` 一开始就建立 SEH 链: ``` push 4059CEh mov eax, fs:[0] push eax ... lea eax, [ebp-0Ch] mov fs:[0], eax ``` 函数体里反复出现这种布局: ```text EB 06 jmp short +6 C3 ret 8B FF mov edi, edi EB EB jmp short backward F9 stc ``` 再加上进入输入路径前的 `sub_4033A0`,直接让 IDA 按线性控制流读,很容易把大量 `ret` 当成函数结束。实际运行时这些块由异常处理和跳板串起来,所以这题不能只盯一份伪代码。 我使用的做法是:静态分析只负责确定输入格式、常量和最终比较;运行时在 `0x404112`、`0x4041BC`、`0x40425F`、`0x403500`、`0x404586` 下点确认数据确实按预期流动。这样可以避开假控制流,同时不需要 patch 样本。 ## 4. 查表索引与 RSA 块 运行到 `0x403500` 后,程序会先截取 Key 的后 32 个字符: ```text 3B0DD6B12A0D3D95FA65B5E0ADE5E11B ``` 它会被解析为 4 个 32 位 limb,也就是 16 字节 RSA 密文块。函数中的查表逻辑最终需要 44 个索引;前 28 个索引直接来自输入,后 16 个索引由 RSA 运算恢复。 把目标 Welcome 串逆查程序的表,得到的索引序列为: ```text 32 3C 47 18 4B 0D 3C 44 25 4B 44 58 42 55 2F 36 5C 36 2C 11 44 42 4B 0D 3C 44 16 43 0F 44 39 37 4E 3C 44 37 25 44 16 44 25 15 1D 1B ``` 程序对候选密文 `c` 做的是: ```python m = pow(c, 65537, n) ``` 其中: ```text n = 0x8C91CB79EC693F3ED6519C945DD67371 e = 65537 ``` 末 16 个索引正是 `m`: ```text m = 0x0F4439374E3C44372544164425151D1B ``` 为了反推 `c`,分解这个 128 位模数即可: ```text p = 0xBD3D59FAC7CC547B q = 0xBE28EF96C8318203 d = e^(-1) mod ((p-1)(q-1)) = 0x721A022D849DE5CC5AD9BBB701F9FB65 ``` 因此: ```python c = pow(m, d, n) ``` 得到: ```text c = 0x3B0DD6B12A0D3D95FA65B5E0ADE5E11B ``` 把前 28 个索引和这个密文拼接,就是最终的 44 字节输入数据。 ## 5. 运行复现 完整脚本: ``` TARGET = b"Welcome to KCTF2026! Come and give it a try." # Entries used by TARGET, recovered from the program's lookup table. CHAR_TO_INDEX = { " ": 0x44, "!": 0x11, ".": 0x1B, "0": 0x5C, "2": 0x36, "6": 0x2C, "C": 0x42, "F": 0x2F, "K": 0x58, "T": 0x55, "W": 0x32, "a": 0x16, "c": 0x18, "d": 0x0F, "e": 0x3C, "g": 0x39, "i": 0x37, "l": 0x47, "m": 0x0D, "n": 0x43, "o": 0x4B, "r": 0x15, "t": 0x25, "v": 0x4E, "y": 0x1D, } P = 0xBD3D59FAC7CC547B Q = 0xBE28EF96C8318203 N = P * Q E = 65537 D = pow(E, -1, (P - 1) * (Q - 1)) def checksum(data: bytes) -> tuple[int, int]: xor_value = 0 acc = 0 for byte in data: xor_value ^= byte acc += byte * ((acc & 0x7F) + 1) return xor_value, acc def solve() -> str: indices = bytes(CHAR_TO_INDEX[chr(byte)] for byte in TARGET) # The program applies c^E mod N to the final 16-byte block. cipher = pow(int.from_bytes(indices[28:], "big"), D, N).to_bytes(16, "big") raw = indices[:28] + cipher xor_value, acc = checksum(raw) assert len(TARGET) == len(indices) == len(raw) == 44 assert xor_value == 0x8F assert (acc & 0xFFFF) == 0xBEFF assert pow(int.from_bytes(cipher, "big"), E, N) == int.from_bytes(indices[28:], "big") return raw.hex().upper() if __name__ == "__main__": key = solve() print(f"d = 0x{D:X}") print(f"key = {key}") ``` 它包含从目标串使用到的查表项、`p/q/d` 推导、RSA 逆运算和两个前置校验 输出: ```text d = 0x721A022D849DE5CC5AD9BBB701F9FB65 key = 323C47184B0D3C44254B445842552F365C362C1144424B0D3C4416433B0DD6B12A0D3D95FA65B5E0ADE5E11B ``` 最后用原程序验证: ```text Enter your key: verify success. ``` 最后一位从 `B` 改成 `C` 后,输出会变成 `verify fail.retry it...`,可以确认这不是长度、换行或偶然路径导致的成功。 ## 6. 小结 这题的烟雾弹很多:假密码字符串、SEH、`int3`、大量看似会结束函数的 `ret`。但真正的链条并不长: ```text 88 字符输入 -> 44 字节 -> XOR / 滚动加权和 -> 前 28 个查表索引 + 后 16 字节 RSA 解密索引 -> Welcome 明文比较 -> verify success. ``` 关键不是顺着假提示往下猜,而是先抓住最终明文比较,再把数据流逐段向前推。对带异常跳板的 CrackMe,这通常比硬啃线性伪代码更可靠。 ## 证据链 - **Evidence**:`0x40454A` 将栈上结果与 `.rdata:0x416380` 的 44 字节 Welcome 串逐字节比较;动态 trace 确认最终 Key 到达 `0x404586`。 - **Finding**:输入前 28 字节是查表索引,后 16 字节是满足 `c^65537 mod n = m` 的 RSA 密文;两个前置校验只是筛选条件。 - **Path**:Welcome 串反查索引 → 分解 `n` 并计算 `d` → 生成后 16 字节密文 → 拼接、校验、运行原程序。
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于
2026-8-17 21:05 被TimeQvQ编辑 ,原因:
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
TimeQvQ
1
发帖
15
回帖
10
RANK
关注
私信
他的文章
[原创]KCTF 2026 第五题
710
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部