首页
社区
课程
招聘
[原创]KCTF 第8题解析
发表于: 2026-8-21 13:27 49

[原创]KCTF 第8题解析

2026-8-21 13:27
49

KCTF2026 CrackMe08 Writeup

结果

kanxue@2o26o8!@#

在未修改的解包程序 extract/kctf2026_CrackMe08.exe 中输入该字符串,输出为:

Input:correct

1. 样本与解密层

原始压缩包:kctf2026_CrackMe08.rar。分析均在副本目录 work/CrackMe08 完成,原始 RAR 未修改。

PE 的有效校验逻辑有两段运行时解密:

阶段 RVA 长度 还原方式
stage 1 0x1800 0x510 每字节 XOR 0x5a
stage 2 0x1d10 0x4962 FNV 派生的字节流 XOR

解密后,主校验函数位于 0x140004380。没有在原程序上 patch 校验分支;解密副本只用于反汇编和 Unicorn 单函数复现。

2. 输入格式

程序要求输入恰好 16 个可打印字符。每个字符映射为:

digit = ord(ch) - 0x21      # 0 <= digit < 94

随后按大端 base-94 将 16 个数字编码成一个 128 位附近的整数 A:

A = 0
for ch in text:
    A = A * 94 + (ord(ch) - 0x21)

第一个廉价约束是:

sum(digit) = 752

等价地,原始 ASCII 字节和必须是 1280。这一步只能缩小候选空间,不能直接爆破,因为总空间仍是 94^16。

3. 校验模型

3.1 有限域与多项式

核心数值域为:

P = 2**127 - 39

这是一个素数模数。程序使用 Horner 形式的五次多项式,从 A 生成 128 位种子:

x = (A + c0) % P
x = (x * A + c1) % P
x = (x * A + c2) % P
x = (x * A + c3) % P
x = (x * A + c4) % P
seed = x

其中 c0..c4 均为样本中提取的 128 位常量,完整常量表保存在可运行求解器 solve_crackme08.py 的 POLY_C 中。

3.2 两个 20 轮可逆 phase

seed 被拆成 (high, low) 两个 64 位字。程序用两组不同轮常量,分别执行 20 轮变换;两个 phase 的最终状态都必须命中硬编码目标。

每轮先派生 key,再按下式更新:

high_next = phi(high, key) ^ low
low_next  = phi(high_next, key ^ GOLDEN) ^ high
high, low = high_next & MASK64, low_next & MASK64

其中:

def phi(high, key):
    x = ((high << 64) | key) % P
    y = (MUL_C * x) % P
    lo = y & MASK64
    hi = y >> 64
    t = ror64(hi, 47) ^ key ^ lo
    return ror64(t, 51) ^ subbytes64(t)

subbytes64 是对每个字节查 256 项 S-box。phase 1 与 phase 2 的区别是轮常量数组和交替使用 H_A/H_B 的顺序不同,均已在 solver 中逐字恢复。

目标状态是:

phase 1: high = 0x0c14db1c20dd97a8, low = 0xb8fd0694401d3d03
phase 2: high = 0x9a2789b6de31c2dc, low = 0xaca5240a53fb78a6

关键点是这里没有单向压缩。给定一轮输出 (new_high, new_low) 和该轮 key,原输入唯一确定:

old_high = new_low ^ phi(new_high, key ^ GOLDEN)
old_low  = new_high ^ phi(old_high, key)

因此从第 20 轮向第 1 轮倒推即可,不需要符号执行 40 轮。

对两个目标分别逆推后,得到完全相同的 seed:

seed_hi = 0x4be831b0ad3a2d36
seed_lo = 0x1489375bba3fb8de
seed    = 0x4be831b0ad3a2d361489375bba3fb8de

两个独立 phase 汇聚到同一个 seed,是模型和端序正确的重要交叉验证。

3.3 FNV 约束

最后还有一个 FNV-1a 64 位链。字节序是小端:

FNV64_LE(
    A.to_bytes(16, "little") ||
    target0_low || target0_high || target1_low || target1_high
) == 0x6cd601b3049aa32c

尾部 32 字节都已知,因此从终值反向执行 FNV(乘法逆元存在)可得到对 A 的独立要求:

FNV64_LE(A.to_bytes(16, "little")) == 0xc8a68671c59dd0ad

4. 求解

现在多项式的输出 seed 已知,只需求:

f(A) - seed = 0  (mod P)

这是一元五次方程,而不是 16 个字符的爆破问题。使用 SymPy 在 GF(P) 中因式分解,取一次因子的根,再依次应用三个格式约束:

  1. 根可表示为 16 位 base-94;
  2. 16 个 digit 的和为 752;
  3. 满足反推的 FNV-1a 值。

唯一保留的整数为:

A = 0x00000174a5e1d4cf4f146e72f70092ac

将它连续除以 94,得到的 base-94 digits 为:

[74, 64, 77, 87, 84, 68, 31, 17, 78, 17, 21, 78, 23, 0, 31, 2]

每项加上 0x21 后即为:

kanxue@2o26o8!@#

5. 可复现验证

完整常量、S-box、两组轮常量和逆推实现见:

python D:\kctf\CTF6\work\CrackMe08\solve_crackme08.py --emu

预期输出:

flag: kanxue@2o26o8!@#
A: 0x00000174a5e1d4cf4f146e72f70092ac
seed_hi: 0x4be831b0ad3a2d36
seed_lo: 0x1489375bba3fb8de
unicorn_ret: 1

最后必须回到未 patch 的原版解包 exe 闭环:

"kanxue@2o26o8!@#" | & D:\kctf\CTF6\work\CrackMe08\extract\kctf2026_CrackMe08.exe

输出:

Input:correct

6. 要点

  • 先解密并定位真实 checker,避免对外层垃圾代码建模。
  • 两段 20 轮结构都可逆,应从目标倒推 seed,而非对字符作 SMT 或穷举。
  • 已知 FNV 尾部可以逆推,FNV 在这里是筛选器,不是障碍。
  • 将字符空间映射为单个 base-94 整数后,核心问题化成有限域上的低次多项式求根。
  • 最终答案由两个 phase、有限域多项式、字符集/字符和和 FNV 链共同闭环验证。

冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 2026-8-21 21:08 被kanxue编辑 ,原因:
收藏
点赞 ・0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回