首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
7
[原创] KCTF 2026 第八题:亥子合辰·塔影迷楼 Writeup
发表于: 2026-8-22 11:52
199
[原创] KCTF 2026 第八题:亥子合辰·塔影迷楼 Writeup
bananaships
2026-8-22 11:52
199
## Summary 程序先做两层运行时解密,再进入真正的 checker。checker 将 16 字节输入映射到有限域 `2^127 - 39`,经过五次多项式和两组 Feistel 网络后与目标值比较。反向恢复目标多项式值后,解有限域方程即可得到 flag。 ## 基本信息 ```text file: PE32+ executable (console) x86-64, for MS Windows sha256: b0dae7a21b059805a35fc5490f84a607d00afb28ad6b5b9f9ebc31574c382ea1 solve: Python 3, no external packages ``` ## 关键分析 ### 1. 脱壳流程 样本不是常见压缩壳,而是自定义两阶段运行时解密。先按 PE section 信息把文件映射成内存镜像,后续直接在镜像中还原代码即可,不需要修复导入表或重建完整 PE。 ```text .text raw 0x0400 -> RVA 0x1000, size 0x5800 .rdata raw 0x5c00 -> RVA 0x7000, size 0x0800 .data raw 0x6400 -> RVA 0x8000, size 0x0200 .tgt raw 0x6800 -> RVA 0xa000, size 0x0200 ``` `.tgt` 保存了解密配置和最终比较目标。入口首先读取 `.tgt+0x20` 的长度,对 `RVA 0x1800` 开始的 stage1 做固定异或: ```text stage1_rva = 0x1800 stage1_len = qword[0xa020] = 0x510 stage1_key = 0x5a stage1[i] ^= 0x5a ``` stage1 解开后,`0x1400018d0` 附近出现下一层解密逻辑。程序对 stage1 明文分别用 `FNV_OFFSET` 和 `GOLDEN` 作为 seed 计算 FNV64,结果作为第二层 keystream 的种子: ```text stage1 fnv = 0x73fb4f498aab364f stage1 golden = 0xb157c7e044b966df ``` 第二层目标由 `.tgt` 给出: ```text stage2_rva = qword[0xa008] = 0x1d10 stage2_len = qword[0xa000] = 0x4962 ``` 每个字节的 keystream 由 `state`、`stage1_fnv` 和 `stage1_golden` 计算,然后异或回 stage2: ```text x = state ^ stage1_golden ^ stage1_fnv state += 0x9e3779b97f4a7c15 x = (((x >> 0x21) ^ x) * 0xff51afd7ed558ccd) & 0xffffffffffffffff stage2[i] ^= x >> 0x38 ``` 解密完成后,`RVA 0x1d10..0x6671` 变成有效 x64 代码;真正 checker 位于 `0x140004380`,外层 wrapper 位于 `0x140001d10`。再对 stage2 明文计算 hash,后续常量、S-box、Feistel round key 都由这两个值派生: ```text stage2 fnv = 0x553d3c5ef6ee11ff stage2 golden = 0x4aeee52b5738eb8f ``` 完整脚本中的前半部分就是上述脱壳逻辑:把 section 映射到 `img`,依次解 stage1 和 stage2,然后直接在解密后的内存镜像上恢复 checker 常量。 ### 2. checker 结构 真正校验函数在 `0x140004380`。前置约束: ```text len(input) == 16 sum(input) == 0x500 ``` 随后把输入按 base94 编成有限域元素: ```text P = 2^127 - 39 N = (((b0 - 0x21) * 94 + (b1 - 0x21)) ... ) mod P ``` 再计算: ```text Y = (((((N + C0) * N + C1) * N + C2) * N + C3) * N + C4) mod P ``` `Y` 分别进入 A/B 两套 20 轮 Feistel。目标 qword 逆回去后,两套网络得到同一个多项式目标: ```text Y = 0x4be831b0ad3a2d361489375bba3fb8de ``` 于是只需在 GF(P) 上解五次方程: ```text ((((N + C0) * N + C1) * N + C2) * N + C3) * N + C4 - Y == 0 mod P ``` 常量生成时要注意顺序:第二个 `splitmix64` 的输入需要在 `r9 ^= a` 前取出,否则 A/B 两套 Feistel 逆回去不会收敛到同一个 `Y`。 ## Solve Script 下面是完整精简求解脚本,默认当前目录存在 `kctf2026_CrackMe08.exe`。无需 PE 解析库,section 偏移按样本固定值写入,执行后直接输出 flag。 ```python from pathlib import Path MASK = (1 << 64) - 1 P = (1 << 127) - 39 FNV_OFFSET = 0xCBF29CE484222325 FNV_PRIME = 0x100000001B3 GOLDEN = 0x9E3779B97F4A7C15 MIX = 0xFF51AFD7ED558CCD raw = Path("kctf2026_CrackMe08.exe").read_bytes() img = bytearray(0xB000) for rva, off, size in [ (0x1000, 0x400, 0x5800), (0x7000, 0x5C00, 0x800), (0x8000, 0x6400, 0x200), (0xA000, 0x6800, 0x200), ]: img[rva : rva + size] = raw[off : off + size] def q(rva): return int.from_bytes(img[rva : rva + 8], "little") def fnv(seed, data): h = seed for b in data: h = ((h ^ b) * FNV_PRIME) & MASK return h def ror(x, n): return ((x >> n) | (x << (64 - n))) & MASK def splitmix64(x): x &= MASK x ^= x >> 30 x = (x * 0xBF58476D1CE4E5B9) & MASK x ^= x >> 27 x = (x * 0x94D049BB133111EB) & MASK x ^= x >> 31 return x & MASK for i in range(q(0xA020)): img[0x1800 + i] ^= 0x5A stage1 = img[0x1800 : 0x1800 + q(0xA020)] s1_fnv = fnv(FNV_OFFSET, stage1) s1_golden = fnv(GOLDEN, stage1) s2_rva = q(0xA008) s2_len = q(0xA000) state = 0 for i in range(s2_len): x = (state ^ s1_golden ^ s1_fnv) & MASK state = (state + GOLDEN) & MASK x = (((x >> 0x21) ^ x) * MIX) & MASK img[s2_rva + i] ^= (x >> 0x38) & 0xFF stage2 = img[s2_rva : s2_rva + s2_len] s2_fnv = fnv(FNV_OFFSET, stage2) s2_golden = fnv(GOLDEN, stage2) pairs = [] r9 = (s2_golden ^ FNV_OFFSET) & MASK r10 = s2_fnv for _ in range(0x2B): r10 = (r10 - 0x61C8864680B583EB) & MASK r9 = (r9 + 0xBF58476D1CE4E5B9) & MASK a = splitmix64((r10 + GOLDEN) & MASK) b_seed = (r9 + GOLDEN) & MASK r9 ^= a b = splitmix64(b_seed) r10 ^= b pairs.append((a, b)) def pval(lo, hi): return ((hi << 64) | lo) % P def norm(lo, hi): x = pval(lo, hi) return x & MASK, x >> 64 C = [pval(*norm(*pairs[i])) for i in range(5)] D = pval(*norm(*pairs[5])) S = [] for lo, hi in pairs[6:22]: for k in range(8): S.append((lo >> (8 * k)) & 0xFF) S.append((hi >> (8 * k)) & 0xFF) A = [] B = [] for lo, hi in pairs[22:32]: A += [lo, hi] for lo, hi in pairs[32:42]: B += [lo, hi] def sbox64(x): y = 0 for i in range(8): y |= S[(x >> (8 * i)) & 0xFF] << (8 * i) return y & MASK def G(x, key): t = (D * ((((x & MASK) << 64) | (key & MASK)) % P)) % P lo, hi = t & MASK, t >> 64 v = (ror(hi, 0x2F) ^ key ^ lo) & MASK return (ror(v, 0x33) ^ sbox64(v)) & MASK def round_keys(arr, swap=False): out = [] for i, k in enumerate(arr): seed = s2_golden if (i & 1) ^ swap else s2_fnv out.append((seed ^ k) & MASK) return out def inv_feistel(l, r, keys): for key in reversed(keys): old_r = (l ^ G(r, key ^ GOLDEN)) & MASK old_l = (r ^ G(old_r, key)) & MASK l, r = old_l, old_r return l, r T = [q(0xA030 + i * 8) for i in range(7)] last_lo, last_hi = pairs[42] target = [ last_lo ^ T[4], last_hi ^ T[5], last_lo ^ T[2], last_hi ^ T[3], last_hi ^ T[0], last_lo ^ T[6], ] y_a = inv_feistel(target[0], target[1], round_keys(A, False)) y_b = inv_feistel(target[2], target[3], round_keys(B, True)) assert y_a == y_b Y = (y_a[1] << 64) | y_a[0] def trim(a): a = [x % P for x in a] while len(a) > 1 and a[-1] == 0: a.pop() return a def sub(a, b): n = max(len(a), len(b)) return trim([(a[i] if i < len(a) else 0) - (b[i] if i < len(b) else 0) for i in range(n)]) def mul(a, b): out = [0] * (len(a) + len(b) - 1) for i, x in enumerate(a): for j, y in enumerate(b): out[i + j] = (out[i + j] + x * y) % P return trim(out) def divmod_poly(a, b): a, b = trim(a), trim(b) qout = [0] * max(1, len(a) - len(b) + 1) inv = pow(b[-1], -1, P) while len(a) >= len(b) and a != [0]: c = a[-1] * inv % P k = len(a) - len(b) qout[k] = c for i in range(len(b)): a[k + i] = (a[k + i] - c * b[i]) % P a = trim(a) return trim(qout), trim(a) def mod(a, m): return divmod_poly(a, m)[1] def gcd(a, b): a, b = trim(a), trim(b) while b != [0]: a, b = b, mod(a, b) inv = pow(a[-1], -1, P) return trim([x * inv % P for x in a]) def modmul(a, b, m): return mod(mul(a, b), m) def modpow(a, e, m): r = [1] a = mod(a, m) while e: if e & 1: r = modmul(r, a, m) a = modmul(a, a, m) e //= 2 return r f = [(C[4] - Y) % P, C[3], C[2], C[1], C[0], 1] root_poly = gcd(f, sub(modpow([0, 1], P, f), [0, 1])) assert len(root_poly) == 2 N = (-root_poly[0] * pow(root_poly[1], -1, P)) % P out = [] for _ in range(16): N, d = divmod(N, 94) out.append(d + 0x21) assert N == 0 flag = bytes(reversed(out)) assert sum(flag) == 0x500 print(flag.decode()) ``` 运行输出: ```text kanxue@2o26o8!@# ```
回复或点赞可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
0
点赞
・
7
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
git_51951meggadf3df
非常支持你的观点!
2026-9-11 14:10
mb_lthgjpwj
为你点赞!
2026-8-27 15:27
ntdll
谢谢你的细致分析,受益匪浅!
2026-8-25 22:23
gailium
这个讨论对我很有帮助,谢谢!
2026-8-23 10:54
huangyalei
你的分享对大家帮助很大,非常感谢!
2026-8-23 00:58
mb_fxwmighi
这个讨论对我很有帮助,谢谢!
2026-8-23 00:40
梧桐生
期待更多优质内容的分享,论坛有你更精彩!
2026-8-22 15:56
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
1
)
PanPup1002
雪 币:
39
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
10
粉丝
0
关注
私信
PanPup1002
2
楼
2026-9-10 18:00
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
bananaships
4
发帖
4
回帖
10
RANK
关注
私信
他的文章
[原创] KCTF 2026 第八题:亥子合辰·塔影迷楼 Writeup
197
[原创] KCTF 2026 第五题:申时·忆海倒带 Writeup
1082
[原创]对某移动安全sdk的深入探究
6378
SIMD指令学习
5456
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部