首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
0
3
[原创]KCTF 2026 第八题:亥子合辰·塔影迷楼 writeup
发表于: 2026-8-22 10:43
218
[原创]KCTF 2026 第八题:亥子合辰·塔影迷楼 writeup
neilwu
1
2026-8-22 10:43
218
# KCTF 2026 第八题:亥子合辰·塔影迷楼 题目信息 - **kctf2026_CrackMe08.ex** - **Flag / Input**:16 字符 - **核心考点**:两级 SMC 解密、动态反调试环境过检、Feistel 混淆器可逆性分析、$GF(2^{127}-39)$ 域上多项式求根 --- ## 一. 静态侦察与两级 SMC 还原 查看导入表与节区特征: 1. 导入表仅 8 个 `KERNEL32` 基础 API,无 I/O 相关函数(采用 Direct Syscall)。 2. 包含 `VirtualProtect`、`FlushInstructionCache` 以及自定义的 `.tgt` 节,存在典型的自修改代码(SMC)。 3. `.tgt` 节内包含两组偏移与长度参数:`(0x1800, 0x510)` 与 `(0x1d10, 0x4962)`。 ### 解密机制 - **Stage 1 (`0x140001800` ~ `0x140001D10`)**:入口点循环做 `XOR 0x5A`。 - **Stage 2 (`0x140001D10` ~ `0x140006672`)**:对 Stage 1 明文计算两组 64 位 FNV-1a Hash(初始种子分别为 `0xcbf29ce484222325` 和 `0x9e3779b97f4a7c15`),随后通过类似 Murmur3 `fmix64` 的算法生成密钥流进行异或解密。 > 存在级联校验,动态调试下软断点(`0xCC`)会破坏 Stage 2 密钥流,导致后续代码解密失败。 ### 静态解密脚本 (`rev.py`) ```python import sys, pefile from capstone import Cs, CS_ARCH_X86, CS_MODE_64 PATH = "kctf2026_CrackMe08.exe" pe = pefile.PE(PATH) IB = pe.OPTIONAL_HEADER.ImageBase raw = bytearray(open(PATH, "rb").read()) def va2off(va): for s in pe.sections: start = IB + s.VirtualAddress if start <= va < start + max(s.Misc_VirtualSize, s.SizeOfRawData): return s.PointerToRawData + (va - start) return None # 解 Stage 1 o1 = va2off(0x140001800) for i in range(0x510): raw[o1 + i] ^= 0x5A # 计算 FNV-1a 并解 Stage 2 M, PRIME, GOLD = (1 << 64) - 1, 0x100000001b3, 0x9e3779b97f4a7c15 H1, H2 = 0xcbf29ce484222325, GOLD for b in raw[o1:o1 + 0x510]: H1 = ((H1 ^ b) * PRIME) & M H2 = ((H2 ^ b) * PRIME) & M o2 = va2off(0x140001D10) for p in range(0x4962): t = (p * GOLD) & M t = (t ^ H2 ^ H1) ^ ((t ^ H2 ^ H1) >> 33) t = (t * 0xff51afd7ed558ccd) & M raw[o2 + p] ^= (t >> 56) & 0xff open("dec.bin", "wb").write(raw) ``` 解出的明文可以直接 patch 进 IDA 中分析(起始地址 `0x140001800`,长度 `0x4E72`)。 --- ## 二. 主流程与反调试绕过 解密后分析校验函数 `0x140004380`: 1. **字节和过滤(Gate)**: 校验输入 16 字节求和必须为 `0x500`(1280)。可用 `"P" * 16`(ASCII 80 * 16 = 1280)作为测试输入通过第一道门。 2. **反调试累加器机制**: 程序检测多处环境状态(`PEB.BeingDebugged`、`NtGlobalFlag`、`Dr0-Dr7`、`ProcessDebugPort`、`ProcessDebugObjectHandle`、4 处 `rdtsc` 时钟差)。 检测结果累加到 `rbx` 并混入 PRNG 种子中,若触发反调试不会报错退出,而是**静默使所有运行时常量与 S-Box 变为脏数据**。 *注意*:`NtQueryInformationProcess` 的 Class `0x1e` 为 `ProcessDebugObjectHandle`,未被调试时应返回 `STATUS_PORT_NOT_SET (0xC0000353)` 且 buffer 为 0(避免误作为 `ProcessDebugFlags 0x1f` 返回 1)。 使用 Unicorn 构建模拟运行环境,Hook 掉 `rdtsc`,模拟 PEB/TEB 和 syscall 桩,确保 `0x1400046ed` 处的 `rbx` 累加器为 0。 --- ## 三. 算法逆向分析 主流程逻辑分两个阶段: ``` 输入 (16 字节) │ ▼ [Phase 1] Base-94 编码 -> 128 位大整数 X -> 模多项式 P1 │ ▼ [Phase 2] 两路独立的 Feistel 混淆器 (各 20 轮) -> 输出 (A, B) 与目标 (T1, T2) 比较 ``` ### Phase 1:大数编码与模多项式 1. **Base-94 编码**: 将 16 字节可见字符(ASCII 33~126)按 Horner 算法转成 128 位大整数 $X$: $$X = \sum_{i=0}^{15} ( ext{input}[i] - 33) imes 94^{15-i}$$ 因为 $94^{16} pprox 2^{104.87} < 2^{127}$,转换过程无溢出,可完全双向还原。 2. **模素数多项式**: 在 $GF(M)$(模数 $M = 2^{127} - 39$,为素数)下计算五次多项式: $$P_1 = X^5 + FE_0 X^4 + FE_1 X^3 + FE_2 X^2 + FE_3 X + FE_4 \pmod M$$ ($FE_0 \sim FE_4$ 为运行时 PRNG 导出的常量)。 ### Phase 2:Feistel 混淆器 $P_1$ 被拆为高低 64 位 `(P, Q)`,分别输入两组各 20 轮的双子轮结构(Loop 1 输出 $A$,Loop 2 输出 $B$): - **轮函数**: $$ egin{aligned} F(hi, lo) &= ((((hi \ll 64) \mid lo) mod M) imes C mod M) ext{ 异或折叠} \ mix(u) &= ext{SBox8}(u) \oplus ext{ror}(u, 51) \end{aligned}$$ - **状态更新**: $$ egin{aligned} P_{ ext{new}} &= Q \oplus mix(F(P, lo_1)) \ Q_{ ext{new}} &= P_{ ext{old}} \oplus mix(F(P_{ ext{new}}, lo_2)) \end{aligned}$$ --- ## 四. 逆向求解与还原 ### 步骤 1:混淆器逆推还原 $P_1$ 利用终点比较的已知目标值 $T_1, T_2$(由 `.tgt` 掩码解密得到),分别从 Loop 1 和 Loop 2 逆推回 $P_1$: ```python def bwd_round(P, Q, lo1, lo2): P_prev = Q ^ mix(F(P, lo2)) Q_prev = P ^ mix(F(P_prev, lo1)) return P_prev, Q_prev ``` 两路逆推均得到一致的中间值: $$P_{1 ext{-target}} = ext{0x4be831b0ad3a2d361489375bba3fb8de}$$ ### 步骤 2:Cantor–Zassenhaus 解五次方程 在有限域 $GF(2^{127}-39)$ 上求解多项式方程: $$X^5 + FE_0 X^4 + FE_1 X^3 + FE_2 X^2 + FE_3 X + (FE_4 - P_{1 ext{-target}}) \equiv 0 \pmod M$$ 1. 计算 $g(x) = \gcd(f(x), x^M - x)$ 滤出域内一次因式。 2. 使用 Cantor–Zassenhaus 算法随机切分因式求出根 $X$。 ### 步骤 3:还原 Flag 得到唯一有效实根: $$X = ext{0x00000174a5e1d4cf4f146e72f70092ac}$$ 将 $X$ 连续模 94 展开为 16 位字符($+33$),得到字符串:`kanxue@2o26o8!@#`。其 ASCII 和刚好为 1280,满足初始 Gate 约束。 ```python # solve.py import random import model M = (1 << 127) - 39 # ---------- GF(M) 上的多项式:系数列表,下标 = 次数 ---------- def trim(a): while a and a[-1] == 0: a.pop() return a def pmul(a, b): if not a or not b: return [] r = [0] * (len(a) + len(b) - 1) for i, x in enumerate(a): if x: for j, y in enumerate(b): if y: r[i+j] = (r[i+j] + x*y) % M return trim(r) def pdivmod(a, b): a = a[:]; db = len(b) - 1; invb = pow(b[-1], M-2, M) q = [0] * max(0, len(a) - db) while len(a) - 1 >= db and a: d = len(a) - 1 - db c = a[-1] * invb % M q[d] = c for i, y in enumerate(b): a[i+d] = (a[i+d] - c*y) % M trim(a) return trim(q), a def pmod(a, b): return pdivmod(a, b)[1] def pgcd(a, b): a, b = trim(a[:]), trim(b[:]) while b: a, b = b, pmod(a, b) if a: inv = pow(a[-1], M-2, M) a = [c*inv % M for c in a] return a def ppowmod(base, e, f): r = [1]; b = pmod(base[:], f) while e: if e & 1: r = pmod(pmul(r, b), f) b = pmod(pmul(b, b), f); e >>= 1 return r def psub(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)) % M for i in range(n)]) def roots(f): f = trim(f[:]) inv = pow(f[-1], M-2, M); f = [c*inv % M for c in f] g = pgcd(f, psub(ppowmod([0, 1], M, f), [0, 1])) # gcd(f, x^M - x) out = [] def split(p): deg = len(p) - 1 if deg <= 0: return if deg == 1: out.append((-p[0] * pow(p[1], M-2, M)) % M); return while True: a = random.randrange(M) t = psub(ppowmod([a, 1], (M-1)//2, p), [1]) d = pgcd(p, t) if 0 < len(d)-1 < deg: split(d); split(pdivmod(p, d)[0]); return split(g) return sorted(set(out)) if __name__ == "__main__": MASK = (1 << 64) - 1 cfg = model.dump() m = model.Mix(cfg) a = m.bwd(cfg['T1'] >> 64, cfg['T1'] & MASK, cfg['rc1'], False) b = m.bwd(cfg['T2'] >> 64, cfg['T2'] & MASK, cfg['rc2'], True) assert a == b, "loop1/loop2 不一致,模型有错" P1T = (a[0] << 64) | a[1] print("P1_target = %032x" % P1T) FE = cfg['FE'] f = [(FE[4] - P1T) % M, FE[3], FE[2], FE[1], FE[0], 1] # 自检:已知的 (X, P1) 对必须满足未打靶的多项式 chk = 0 for c in [1] + FE: chk = (chk*cfg['X'] + c) % M assert chk == cfg['P1'], "多项式形式不对" rs = roots(f) print("五次方程的根个数:", len(rs)) LIM = 94 ** 16 for X in rs: print(" X = %032x < 94^16 : %s" % (X, X < LIM)) if X >= LIM: continue d, v = [], X for _ in range(16): d.append(v % 94); v //= 94 d.reverse() s = "".join(chr(c + 33) for c in d) print(" 字节和 = %d (需要 1280) 全可打印=%s" % (sum(d) + 528, all(0 <= c <= 93 for c in d))) print(" CANDIDATE = %r" % s) ``` 运行: ```bash $ python solve.py P1_target = 4be831b0ad3a2d361489375bba3fb8de 五次方程的根个数: 1 X = 00000174a5e1d4cf4f146e72f70092ac < 94^16 : True 字节和 = 1280 (需要 1280) 全可打印=True CANDIDATE = 'kanxue@2o26o8!@#' real 0m0.454s ```
回复或点赞可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
收藏
・
0
点赞
・
3
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
git_51951meggadf3df
非常支持你的观点!
2026-9-11 14:17
mb_lthgjpwj
为你点赞!
2026-8-27 15:28
huangyalei
这个讨论对我很有帮助,谢谢!
2026-8-23 00:57
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
1
)
manyuegong33
雪 币:
427
活跃值:
(340)
能力值:
( LV2,RANK:10 )
在线值:
发帖
4
回帖
59
粉丝
10
关注
私信
manyuegong33
2
楼
111
2026-9-8 17:18
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
neilwu
1
23
发帖
170
回帖
224
RANK
关注
私信
他的文章
[原创]KCTF 2026 第九题:丑寅同墟·星海抉择 WriteUp
203
[原创]KCTF 2026 第八题:亥子合辰·塔影迷楼 writeup
218
[原创]对ollvm的算法进行逆向分析和还原
27497
[原创]使用trace进行算法还原
28369
[原创]记一道Android算法逆向题
14708
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部