首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
0
[原创]第八题:亥子合辰·塔影迷楼 WP
发表于: 2026-8-21 13:05
44
[原创]第八题:亥子合辰·塔影迷楼 WP
mxym_
2
2026-8-21 13:05
44
## 0. 结论 最终输入: ``` kanxue@2o26o8!@# ``` 原始样本验证结果: ``` Input:correct ``` 该字符串满足: ``` len = 16 sum = 0x500 ``` ------ ## 1. 样本信息 附件为 RAR5 压缩包,解压后得到一个 Windows x64 console PE。 | 项目 | 内容 | | ---------- | ------------------------------------------------------------ | | 附件 | `kctf2026_CrackMe08.rar` | | RAR SHA256 | `65e4ae3b9702e219711d6e8c630ed1b355174ddc1f9629aa00f8890f81896a3b` | | 样本 | `kctf2026_CrackMe08.exe` | | EXE SHA256 | `b0dae7a21b059805a35fc5490f84a607d00afb28ad6b5b9f9ebc31574c382ea1` | | 文件类型 | `PE32+ executable (console) x86-64, for MS Windows` | | ImageBase | `0x140000000` | | EntryPoint | `0x1400012c0` | 节区如下: ``` .text RVA 0x1000 VSZ 0x5672 RAW 0x0400 RSZ 0x5800 .rdata RVA 0x7000 VSZ 0x06e2 RAW 0x5c00 RSZ 0x0800 .data RVA 0x8000 VSZ 0x0024 RAW 0x6400 RSZ 0x0200 .pdata RVA 0x9000 VSZ 0x01bc RAW 0x6600 RSZ 0x0200 .tgt RVA 0xa000 VSZ 0x0070 RAW 0x6800 RSZ 0x0200 ``` `.tgt` 是关键数据区: ``` 0xa000: 6249000000000000 101d000000000000 0xa010: 0005000000000000 0000000000000000 0xa020: 1005000000000000 0018000000000000 0xa030: 023eb96d9cef070e 2e9d23692feed162 0xa040: 430220a36458b6a1 f25f12b79967f6f8 0xa050: e647c6b0fa7aeeb5 860afe493335c56e 0xa060: 31ba95eed0220db1 6666666666666666 ``` 其中: ``` .tgt + 0x00: stage2 size = 0x4962 .tgt + 0x08: stage2 RVA = 0x1d10 .tgt + 0x10: input byte sum target = 0x500 .tgt + 0x18: PRNG extra seed = 0 .tgt + 0x20: stage1 size = 0x510 .tgt + 0x28: stage1 RVA = 0x1800 .tgt + 0x30 后面是最终比较常量 ``` ------ ## 2. 第一层自修改:XOR 0x5A 入口 `0x1400012c0` 会先从 `.tgt+0x20` 和 `.tgt+0x28` 取出一段代码的位置和长度: ``` RVA = 0x1800 size = 0x510 ``` 随后对该区域逐字节异或 `0x5A`,恢复 stage1 代码。 逻辑可以表示为: ``` for i in range(0x510): image[rva_to_off(0x1800) + i] ^= 0x5A ``` 恢复后,`0x1400018d0` 成为有效函数。该函数继续负责第二层代码解密。 ------ ## 3. 第二层自解密:FNV 派生流密钥 stage1 的 `0x1400018d0` 会先对第一层解密出的 `[RVA 0x1800, size 0x510]` 做两路 FNV-1a 风格哈希。 常量: ``` FNV prime = 0x100000001b3 seed1 = 0xcbf29ce484222325 seed2 = 0x9e3779b97f4a7c15 ``` 哈希结果: ``` h1 = 0x73fb4f498aab364f h2 = 0xb157c7e044b966df ``` 之后根据 `.tgt+0x00` 和 `.tgt+0x08` 解密第二段: ``` RVA = 0x1d10 size = 0x4962 ``` 密钥流公式: ``` MASK = (1 << 64) - 1 PHI = 0x9e3779b97f4a7c15 MIX = 0xff51afd7ed558ccd for i in range(0x4962): x = ((i * PHI) & MASK) ^ h2 ^ h1 y = ((x ^ (x >> 33)) * MIX) & MASK key = (y >> 56) & 0xff image[rva_to_off(0x1d10) + i] ^= key ``` 解密后得到完整 stage2。stage2 中: ``` 0x140001d10: 包装校验函数 0x140004380: 真正核心校验函数 ``` `0x140007100` 处存在一个 8 项跳表,但 8 项全部指向 `0x140004380`,所以表面上的动态选择不会改变最终校验函数。 ------ ## 4. 主流程与输入约束 主流程读取输入后会去掉末尾 `\n` / `\r`,要求长度必须为 16。 关键逻辑: ``` 0x140001531: cmp ecx, 0x10 0x1400015d2: call 0x140001070 0x1400015df: call 0x1400018d0 ``` 如果长度不是 16,直接输出: ``` fault ``` 核心函数 `0x140004380` 入口处还有一个硬约束: ``` sum(input[0..15]) == 0x500 ``` 包装函数 `0x140001d10` 中也有一层字节和检查,但比较的是 `0x500 ^ 1 = 0x501`。对真正满足核心条件 `0x500` 的输入,包装层不会抹掉核心返回值。 最终 flag: ``` s = b"kanxue@2o26o8!@#" len(s) == 16 sum(s) == 0x500 ``` ------ ## 5. 核心校验整体结构 `0x140004380` 的主校验可以拆成以下阶段: 1. 检查 16 个输入字节和是否为 `0x500`。 2. 对 stage2 自身 `[RVA 0x1d10, size 0x4962]` 做两路 FNV。 3. 采集反调试状态,作为 PRNG 种子扰动。 4. 生成 86 个 64-bit qword,用作有限域多项式系数、S-box、Feistel 轮密钥和目标掩码。 5. 将输入按 base94 编码为 `p = 2^127 - 39` 上的域元素。 6. 对该域元素做五次 Horner 多项式。 7. 两路 20 轮可逆 Feistel/S-box 变换。 8. 与 `.tgt` 中经过 PRNG mask 处理后的目标值比较。 9. 使用 FNV4 / FNV6 做自校验,防止错误路径撞上部分比较。 ------ ## 6. 核心 PRNG 与反调试 `0x140004380` 会对已解密 stage2 区域重新计算两路 FNV: ``` h1 = FNV(seed=0xcbf29ce484222325, stage2[0x1d10:0x6672]) = 0x553d3c5ef6ee11ff h2 = FNV(seed=0x9e3779b97f4a7c15, stage2[0x1d10:0x6672]) = 0x4aeee52b5738eb8f ``` 反调试 flags: | bit | 来源 | | ------ | ------------------------------------------------------------ | | `0x01` | PEB.BeingDebugged | | `0x02` | NtGlobalFlag 中 `0x70` 相关位 | | `0x04` | `GetThreadContext` 检查 DR0/DR1/DR2/DR3/DR7 | | `0x08` | `NtQueryInformationProcess(ProcessDebugPort / ProcessDebugObjectHandle)` | | `0x20` | RDTSC 检查,核心区域耗时超过 50,000,000 cycles | 正常无调试快速运行时: ``` flags = 0 ``` PRNG 初始化: ``` a = h1 ^ flags b = h2 ^ 0xcbf29ce484222325 ``` PRNG 是 splitmix64 变体: ``` MASK = (1 << 64) - 1 PHI = 0x9e3779b97f4a7c15 FNV_SEED = 0xcbf29ce484222325 MIX1 = 0xbf58476d1ce4e5b9 MIX2 = 0x94d049bb133111eb ADD1 = 0x03c6ef372fe94f82a ADD2 = 0x05d8fc1269c2f61ce def mix64(x): x &= MASK x = ((x ^ (x >> 30)) * MIX1) & MASK x = ((x ^ (x >> 27)) * MIX2) & MASK return (x ^ (x >> 31)) & MASK def gen_qwords(h1, h2, flags=0): a = h1 ^ flags b = h2 ^ FNV_SEED q = [] for _ in range(43): o1 = mix64((a + ADD1) & MASK) o2_seed = (b + ADD2) & MASK b = ((b + MIX1) & MASK) ^ o1 o2 = mix64(o2_seed) a = ((a + PHI) & MASK) ^ o2 q += [o1, o2] return q ``` 生成的 `q` 布局: | qword 范围 | 用途 | | -------------- | ------------------------------------------------------------ | | `q[0]..q[9]` | 五个 `p=2^127-39` 上的系数,每两个 qword 组成一个 128-bit 小端数 | | `q[10], q[11]` | Feistel 轮函数中的域乘法常量 `B` | | `q[12]..` 部分 | 交织生成 256 字节 S-box | | `q[44]..q[63]` | 第一组 20 轮密钥 | | `q[64]..q[83]` | 第二组 20 轮密钥 | | `q[84], q[85]` | 末尾目标值 mask | ------ ## 7. 输入编码:base94 到有限域 核心将 16 字节输入当成 base94 数字串: ``` P = (1 << 127) - 39 S = 0 for ch in input_bytes: S = (S * 94 + (ch - 0x21)) % P ``` 也就是每个字符取值区间为 printable base94: ``` '!' -> 0 '"' -> 1 ... '~' -> 93 ``` 因为: ``` 94^16 < 2^127 - 39 ``` 所以 16 字节 base94 编码在该域上不会回绕,可以唯一反解。 ------ ## 8. 五次 Horner 多项式 设 PRNG 生成的五个域元素为 `C0..C4`,输入编码后的域元素为 `S`。 核心计算: ``` Y = S + C0 Y = C1 + S * Y Y = C2 + S * Y Y = C3 + S * Y Y = C4 + S * Y Y %= P ``` 等价于: ``` Y = S^5 + C0*S^4 + C1*S^3 + C2*S^2 + C3*S + C4 (mod P) ``` 这里的模数: ``` P = 2^127 - 39 ``` ------ ## 9. 两路 20 轮 Feistel/S-box 多项式结果 `Y` 被拆成两个 64-bit 小端 qword,进入两路 20 轮可逆变换。 轮函数结构: ``` def F(k, x): # k 是 64-bit round key # x 是当前 64-bit 半边 # B 是 PRNG 生成的 127-bit 域乘法常量 lo, hi = split((B * (k + (x << 64))) % P) t = rol64(hi, 17) ^ lo ^ k return rol64(t, 13) ^ sbox_sub64(t) ``` Feistel 加密方向: ``` for i in range(20): k = keys[i] ^ seed_selector(i) T = F(k, R) ^ L L = F(k ^ 0x9e3779b97f4a7c15, T) ^ R R = T ``` 由于每轮是异或结构,因此可逆。逆一轮: ``` R_old = F(k ^ PHI, R) ^ L L_old = F(k, R_old) ^ R ``` 两路输出的目标值来自 `.tgt`,但会先与 `q[84]` / `q[85]` 异或。正常态反推出的关键目标如下: ``` 第一组 Feistel 输出目标: 0xb8fd0694401d3d03 0x0c14db1c20dd97a8 第二组 Feistel 输出目标: 0xaca5240a53fb78a6 0x9a2789b6de31c2dc FNV4 校验目标: 0xbc1e5ebe1e4ec0d4 FNV6 校验目标: 0x6cd601b3049aa32c ``` 逆第一组和第二组 20 轮,二者必须得到同一个 `Y`。正常态 `flags=0` 时唯一满足: ``` Y = 0x4be831b0ad3a2d361489375bba3fb8de ``` 对应检查: ``` y_equal = True hash4_ok = True ``` 其它反调试 flags 会导致两路逆回的 `Y` 不一致,或者 FNV4 不匹配,因此可以排除。 ------ ## 10. 解五次多项式并还原输入 已知目标: ``` Y = 0x4be831b0ad3a2d361489375bba3fb8de ``` 要求解: ``` S^5 + C0*S^4 + C1*S^3 + C2*S^2 + C3*S + C4 - Y = 0 (mod P) ``` 在 `GF(P)` 上分解该多项式,得到一个一次因子和两个二次因子。唯一一次因子的根为: ``` S = 0x00000174a5e1d4cf4f146e72f70092ac ``` 将 `S` 按 16 位 base94 反解: ``` def decode_base94(n): digits = [] for _ in range(16): digits.append(n % 94) n //= 94 if n != 0: return None digits.reverse() return bytes(d + 0x21 for d in digits) ``` 得到: ``` kanxue@2o26o8!@# ``` 验证: ``` len = 16 sum = 0x500 poly_ok = True c1_ok = True c2_ok = True hash6 = 0x6cd601b3049aa32c hash6_ok = True ``` ------ ## 11. 精简求解脚本 下面是精简后的核心解法,用已还原常量直接反解。运行后输出最终输入。 ``` #!/usr/bin/env python3 0xb8fd0694401d3d03, 0x0c14db1c20dd97a8, ) def ror(x, n): return ((x >> n) | ((x << (64 - n)) & MASK)) & MASK def rol(x, n): return (((x << n) & MASK) | (x >> (64 - n))) & MASK def split(x): x %= P return x & MASK, x >> 64 def pair_to_int(pair): lo, hi = pair return ((lo & MASK) + ((hi & MASK) << 64)) % P def mul_pair(a, b): return split((pair_to_int(a) * pair_to_int(b)) % P) def subst64(t): out = 0 for shift in (56, 48, 40, 32, 24, 16, 8, 0): out = ((out << 8) | SBOX[(t >> shift) & 0xff]) & MASK return out def round_f(k, x): lo, hi = mul_pair(M, split(((k & MASK) + ((x & MASK) << 64)) % P)) t = (rol(hi, 17) ^ lo ^ k) & MASK return (rol(t, 13) ^ subst64(t)) & MASK def inv_phase1(out_pair): L, R = out_pair seed0, seed1 = CSEL for i in range(19, -1, -1): # 第一组变换中,奇偶轮分别选 seed0 / seed1 k = (KEYS1[i] ^ (seed0 if (i & 1) else seed1)) & MASK R_old = (round_f(k ^ GOLDEN, R) ^ L) & MASK L_old = (round_f(k, R_old) ^ R) & MASK L, R = L_old, R_old return L, R def decode_base94(n): digits = [] for _ in range(16): digits.append(n % 94) n //= 94 if n != 0: return None digits.reverse() return bytes(d + 33 for d in digits) def main(): target_y = pair_to_int(inv_phase1(EXPECTED1)) coeff = [pair_to_int(x) for x in C] x = sp.symbols("x") poly = sp.Poly( x**5 + coeff[0] * x**4 + coeff[1] * x**3 + coeff[2] * x**2 + coeff[3] * x + coeff[4] - target_y, x, modulus=P, ) candidates = [] for factor, _ in sp.factor_list(poly, modulus=P)[1]: if factor.degree() != 1: continue a, b = [int(v) % P for v in factor.all_coeffs()] root = (-b * pow(a, -1, P)) % P s = decode_base94(root) if s is None: continue if len(s) == 16 and sum(s) == 0x500: candidates.append(s) for s in candidates: print(s.decode("latin1")) if __name__ == "__main__": main() ``` 输出: ``` kanxue@2o26o8!@# ``` ------ ## 12. 验证 ### 12.1 静态脚本验证 候选: ``` kanxue@2o26o8!@# ``` 检查结果: ``` sum=0x500 x=0x00000174a5e1d4cf4f146e72f70092ac poly_ok=True c1_ok=True c2_ok=True hash6=0x6cd601b3049aa32c hash6_ok=True ``` ### 12.2 Unicorn 核心函数验证 直接对 `0x140004380` 做 harness,传入候选: ``` kanxue@2o26o8!@# ``` 返回: ``` ret 0x1 ``` 最终 6 个比较值全部命中: ``` 0xb8fd0694401d3d03 0x0c14db1c20dd97a8 0xaca5240a53fb78a6 0x9a2789b6de31c2dc 0x6cd601b3049aa32c 0xbc1e5ebe1e4ec0d4 ``` ### 12.3 原始样本验证 运行原始样本,输入: ``` kanxue@2o26o8!@# ``` 输出: ``` Input:correct ``` 退出码为 `0`。 ------ ## 13. Flag ``` kanxue@2o26o8!@# ```
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
mxym_
2
11
发帖
2
回帖
120
RANK
关注
私信
他的文章
[原创] 第十题:卯时·曦光初现 WP
1512
[原创]第九题:丑寅同墟·星海抉择 WP
63
[原创]第八题:亥子合辰·塔影迷楼 WP
44
[原创]第七题:戌时·暗能潜流
18
[原创]第六题:酉时·书院迷局 WP
1500
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部