首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[原创]KCTF 2026 第八题 "塔影迷楼" WriteUp
发表于: 2026-8-21 14:15
31
[原创]KCTF 2026 第八题 "塔影迷楼" WriteUp
BenBenWen
1
2026-8-21 14:15
31
## 1. 程序入口 IDA 打开后函数很少,入口 `start` 的 F5 伪代码能直接看出基本交互流程: ```c qmemcpy(v19, "Input:", 6); sub_1400017B0(NtWriteFile, ..., v19, 6, ...); sub_1400017B0(NtReadFile, ..., v23, 63, ...); // 去掉换行后检查长度 if ( v12 != 16 ) { qmemcpy(v19, "fault\n", 6); sub_140001000(1); } sub_140001070(v23); if ( ((int (__fastcall *)(_BYTE *))loc_1400018D0)(v23) == 1 ) puts("correct"); else puts("fault"); ``` 关键点: - 输入长度必须是 16。 - `sub_140001070` 不是最终校验函数,只是额外状态扰动/反分析逻辑。 - 真正校验在运行时解密出来的 `loc_1400018D0`,后面被 IDA 建成 `sub_140001D10`。 ## 2. 第一层代码解密 入口最前面有一段自修改代码: ```c ModuleHandleA = GetModuleHandleA(0); v2 = (char *)ModuleHandleA + qword_14000A028; VirtualProtect(v2, dwSize, 0x40, &flOldProtect); for (i = 0; i < dwSize; i++) v2[i] ^= 0x5A; FlushInstructionCache(...); VirtualProtect(v2, dwSize, flOldProtect, &flOldProtect); ``` `.tgt` 里给出解密范围: ```text qword_14000A028 = 0x1800 dwSize = 0x510 ``` 所以第一层明文代码是: ```python stage1 = image[0x1800:0x1800+0x510] stage1 = bytes(b ^ 0x5A for b in stage1) ``` 解完后 `0x140001D10` 就能正常反汇编/反编译。 ## 3. 第二层 dispatcher `sub_140001D10` 的 F5 伪代码很短: ```c v2 = funcs_140001D50[(21 * *(_BYTE *)a1) & 7](a1); // 16 字节求和 v4 = sum(input[0..15]); if ( (v4 & 0xFFFFFF) != (dword_14000A010 ^ 1) ) v3 = v2; return v3 & 1; ``` 这里 `dword_14000A010 ^ 1 == 0x501`,所以最终正确时要求: ```text sum(input[0..15]) != 0x501 ``` 真实核心分支由输入首字节决定: ```text (21 * input[0]) & 7 ``` 正确答案首字节是 `k`,进入的分支是 `sub_140004380`。 ## 4. 核心函数的入口约束 `sub_140004380` 开头先做一次 16 字节求和: ```c v1 = _mm_add_epi32(... input[0..15] ...); v2 = _mm_add_epi32(v1, _mm_srli_si128(v1, 8)); if ( (_mm_cvtsi128_si32(_mm_add_epi32(v2, _mm_srli_si128(v2, 4))) & 0xFFFFFF) != dword_14000A010 ) return 0; ``` `dword_14000A010 == 0x500`,所以核心分支要求: ```text sum(input[0..15]) == 0x500 ``` 最终答案 `kanxue@2o26o8!@#` 的 ASCII 和正好是 `1280 == 0x500`。 ## 5. 反调试状态 v11 核心函数中间生成一组运行时参数,种子受反调试状态 `v11` 影响: ```c v10 = NtCurrentPeb(); v11 = 0; if ( v10->BeingDebugged ) v11 |= 1; if ( ((__int64)v10->ApiSetMap & 0x70) == 0x70 ) v11 |= 2; if ( GetThreadContext(...) && (Dr0 || Dr1 || Dr2 || Dr3 || Dr7) ) v11 |= 4; // NtQueryInformationProcess 相关检测 ... v20 = __rdtsc(); for (...) j += image_byte; v24 = __rdtsc(); if ( v24 - v20 > 0x2FAF080 ) v11 |= 0x20; ``` 这里有一个坑:如果用 Unicorn 单步模拟,`rdtsc` 差值很容易触发 `v11 |= 0x20`,会得到错误参数表。 正常运行环境下没有调试标记,也不会触发 timing 位,预期状态是: ```text v11 = 0 ``` 这是后面能否反推出正确注册码的关键。 ## 6. 参数表生成 F5 里这段循环生成 43 对 64 位数: ```c v26 = v11 ^ v7 ^ (0x9E3779B97F4A7C15 * qword_14000A018); v27 = v6 ^ (qword_14000A018 << 33) ^ 0xCBF29CE484222325; for (i = 0; i < 43; i++) { v29 = v26 - 0x61C8864680B583EB; v30 = v27 - 0x40A7B892E31B1A47; v31 = splitmix64(v29 - 0x61C8864680B583EB); v35 = splitmix64(v30 - 0x61C8864680B583EB); table[2*i] = v31; table[2*i + 1] = v35; v27 = v31 ^ v30; v26 = v35 ^ v29; } ``` 其中 `splitmix64` 是标准结构: ```python def splitmix64(x): x &= 0xffffffffffffffff x = (0xbf58476d1ce4e5b9 * (x ^ (x >> 30))) & 0xffffffffffffffff x = (0x94d049bb133111eb * (x ^ (x >> 27))) & 0xffffffffffffffff return (x ^ (x >> 31)) & 0xffffffffffffffff ``` 参数表用途: ```text table[0..9] -> 5 个 128-bit 多项式系数 table[10..11] -> 128-bit 模乘常量 mul table[12..43] -> 256 字节 S-box table[44..63] -> 第一组 20 轮 key table[64..83] -> 第二组 20 轮 key table[84..85] -> 用于 XOR 解目标常量 ``` 所有 128-bit 运算都在素数域: ```text P = 2^127 - 39 ``` F5 里反复出现的: ```c 0x7FFFFFFFFFFFFFFF : 0xFFFFFFFFFFFFFFD9 ``` 就是 `2^127 - 39` 的高低 64 位。 ## 7. 输入编码为 base94 大整数 核心函数把 16 字节输入按 printable 字符集转成一个大整数: ```c for (i = 0; i < 16; i++) { N = N * 94 + input[i] - 33; N %= P; } ``` 对应 Python: ```python P = (1 << 127) - 39 def base94(s): n = 0 for b in s: n = (n * 94 + b - 33) % P return n ``` ## 8. 多项式阶段 F5 里 `v409..v413` 是 5 个 128-bit 系数。整理后得到: ```python x = N x = (x + c0) % P x = (x * N + c1) % P x = (x * N + c2) % P x = (x * N + c3) % P x = (x * N + c4) % P ``` 这一步输出的 `x` 会作为后面两组 20 轮变换的初始状态。 ## 9. 两组 20 轮可逆变换 每组 20 轮本质是 Feistel 结构,因此可以反推。 轮函数抽象如下: ```python def ror(x, n): return ((x >> n) | (x << (64 - n))) & 0xffffffffffffffff def sbox64(sb, x): out = 0 for sh in (56, 48, 40, 32, 24, 16, 8, 0): out = (out << 8) | sb[(x >> sh) & 0xff] return out def F(xhi, key, mul, sb): t = (((xhi & MASK) << 64) | (key & MASK)) % P t = (t * mul) % P lo = t & MASK hi = t >> 64 a = lo ^ key ^ ror(hi, 47) return sbox64(sb, a) ^ ror(a, 51) ``` 逆 20 轮: ```python def invert_rounds(target_l, target_r, keys): l, r = target_l, target_r for i in range(19, -1, -1): key = keys[i] old_r = l ^ F(r, key ^ 0x9E3779B97F4A7C15, mul, sb) old_l = r ^ F(old_r, key, mul, sb) l, r = old_l & MASK, old_r & MASK return l, r ``` `v11 = 0` 时,两组目标反推到同一个多项式结果: ```text x = 0x4be831b0ad3a2d361489375bba3fb8de ``` 如果误用 Unicorn 慢速环境里的 `v11 = 0x20`,两组目标反推结果不一致,这是定位错误的重要依据。 ## 10. 解多项式根 现在只剩: ```text poly(N) == 0x4be831b0ad3a2d361489375bba3fb8de (mod 2^127 - 39) ``` 用 Sympy 在有限域中分解: ```python x = sp.symbols("x") poly = x + c0 for c in [c1, c2, c3, c4]: poly = poly * x + c poly = sp.Poly(poly - target, x, modulus=P) print(sp.factor_list(poly, modulus=P)) ``` 分解结果里有一个一次因子: ```text x - 29524214495202137505193489633964 ``` 因此: ```text N = 29524214495202137505193489633964 = 0x174a5e1d4cf4f146e72f70092ac ``` 把 `N` 反转成 16 位 base94 字符: ```python def digits_from_n(n): ds = [] for _ in range(16): ds.append(n % 94) n //= 94 return bytes(d + 33 for d in reversed(ds)) ``` 得到: ```text kanxue@2o26o8!@# ``` ## 11. 验证 最终用原程序直接验证: ```python import subprocess s = b"kanxue@2o26o8!@#\n" p = subprocess.run( ["kctf2026_CrackMe08.exe"], input=s, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=3, ) print(p.stdout) ``` 输出: ```text Input:correct ``` ## 12. 总结 这题的主要难点不在长度判断,而在三层结构: 1. 入口自解密,先把 `0x140001800..0x140001D10` 还原出来。 2. `sub_140001D10` 是 dispatcher,会按输入首字节选择真实分支。 3. `sub_140004380` 内部用反调试状态生成参数表,再把输入当成 base94 大整数,经过有限域多项式和两组可逆 Feistel 轮校验。 最容易踩坑的是动态模拟环境。Unicorn 里 `rdtsc` 会触发 timing 位 `v11 |= 0x20`,这会生成另一套参数表,导致后续目标无法一致反推。按正常运行环境使用 `v11 = 0` 后,两组 20 轮校验能反推出同一个多项式目标,随后有限域求根即可得到注册码。
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
BenBenWen
1
13
发帖
48
回帖
110
RANK
关注
私信
他的文章
[原创]KCTF 2026 第九题 "星海抉择" WriteUp
58
[原创]KCTF 2026 第八题 "塔影迷楼" WriteUp
31
[原创]KCTF 2026 第五题 "忆海倒带" WriteUp
23
[原创]KCTF 2026 第二题 "绿光幽语" WriteUp
135
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部