-
-
[原创]KCTF2026 第5题 解析 Writeup5
-
发表于: 5天前 70
-
KCTF 2026 — cm.exe CrackMe 逆向 Writeup
题目信息
项目 内容
目标文件 cm.exe(32 位 Windows 控制台程序)
编译环境 MSVC / MT(静态链接 UCRT),ASLR 开启
题目要求 输入正确的 key,使程序输出 verify success.
目录 E:\retoolkit\ctf\cm
程序导入 gets_s、getchar、memset 等;PDB 路径含 rsa_tf,暗示存在 RSA 相关变换。
字符串区埋有大量假开发者注释(// FIXME: author confirmed password is 'admin123'...、
// TODO: exception handler contains real verification logic、// NOTE: the int3 in __try is just obfuscation 等),
全部是干扰项,与真实校验无关(0x403c60 只是个把字符串长度相加的诱饵函数,返回值从未被使用)。反混淆与主函数还原
程序使用 jmp + 垃圾字节 控制流混淆(如 EB 06; <垃圾>; <真实代码>),线性反汇编会在垃圾字节处错位。
编写递归下降反汇编器:对 jmp 只跟踪目标、不继续线性扫描(天然跳过垃圾);对 jcc/call 同时跟踪目标与顺序流。
还原后的主函数(0x403D90)流程:
gets_s(key, 1000) ; [ebp-0x858]
│
▼
① hex 字符校验(0x404055~0x404092)
- 长度:解码后向量 size × 8 == 0x58(88 个字符)
- 只接受 '0'-'9' 和『大写』'A'-'F'(小写直接 verify fail!)
│
▼
② hex 解码(0x402100)→ keybuf[ebp-0x470](44 字节)
= 大数进制转换:value = Σ nibble_k × 16^(87-k),32 位 limb 小端存储
0x401a90 做 ×16,0x401510 做 +nibble(带进位传播)
│
▼
③ XOR 校验(0x4041BC):XOR(keybuf[0..44)) == 0x8F
④ 反馈式加权和(0x40426B):
sum = 0
for b in keybuf: sum += ((sum & 0x7F) + 1) * b
(sum & 0xFFFF) == 0xBEFF
│
▼
⑤ RSA 变换(0x403500,PDB 名 rsa_tf):
取输入最后 32 字符 inputbuf[56..88) → 解码为 16 字节 msg
result = msg^e mod n,转 hex 字符串后解码
16 字节结果覆盖 keybuf[28..44) ; 即 [ebp-0x470]+0x1C
│
▼
⑥ 查表(0x404870 / 0x402d70):
out[i] = byte[ 0x4263B0 + buffer2[keybuf[i]-1] ]
buffer2 = 0x4163B0:143 项 dword 偏移表(size = 0x8F)
keybuf[i]-1 >= 143 或为负 → 返回 byte[0x4263B0](恒为 0)
│
▼
⑦ strcmp(0x40454A):
out == "Welcome to KCTF2026! Come and give it a try."(44 字节)
关键点:
0x402bd0 是自定义大数 vector 类({size, data, perm, inv},data 为 0x800-limb 数组,
limb 经排列数组 perm 间接访问)。构造函数用 seed 0xcafebabe 洗牌,但写读对称,排列相互抵消,
因此 keybuf 就是 hex 解码的自然序字节。
校验③④在 RSA ⑤之前执行,作用于用户输入的 44 字节;查表⑥在⑤之后,后 16 字节已被 RSA 结果覆盖。
3. RSA 参数提取
0x403500 运行时在全局 0x438018 构造两个大数描述符,同样使用 {size, data, perm} 布局:
描述符 size data 数组 perm 数组
模数 n 4 0x436018 0x42E018
指数 e 1 0x430018 0x434018
真实 limb 值位于 data[perm[i]],读取排列指向的位置:
perm(n) = {0x91, 0x06, 0x65, 0x7F5}
n = 0x8C91CB79 EC693F3E D6519C94 5DD67371 ; 128 bit
perm(e) = {0x55E}
e = 0x10001 (65537)
即程序执行 RSA 加密:result = msg^65537 mod n,msg 为 key 最后 32 字符解码出的 16 字节。
- 求解
4.1 查表候选(唯一)
对每个目标字符 C[i],在 143 项查表中找所有 value[j] == C[i] 的 j:
每个位置恰好一个候选,所以 keybuf[i] = j+1 唯一确定。
keybuf[0..27] = 50 60 71 24 75 13 60 68 37 75 68 88 66 85 47 54 92 54 44 17 68 66 75 13 60 68 22 67
R_bytes[0..16) = 0F 44 39 37 4E 3C 44 37 25 44 16 44 25 15 1D 1B ; 查表所需 RSA 密文
4.2 分解 n 并解密
n = 13636154180376482939 × 13702465297157554691 ; sympy factorint
φ(n) = (p-1)(q-1)
d = e⁻¹ mod φ(n) = 0x721A022D849DE5CC5AD9BBB701F9FB65
B[28..43] = R_bytes^d mod n = 3B0DD6B12A0D3D95FA65B5E0ADE5E11B
验证:pow(B[28..43], e, n) == R_bytes ✓
4.3 组装并校验
B = B[0..27] || B[28..43]
= 323C47184B0D3C44254B445842552F365C362C1144424B0D3C441643
3B0DD6B12A0D3D95FA65B5E0ADE5E11B
XOR(B) == 0x8F ✓
加权和低16位 == 0xBEFF ✓
查表 out == 目标串 ✓(用 B[0..28) + R_bytes 查表)
4.4 踩坑:大小写
hex 校验循环 sub al,0x41; cmp al,0x19; ja fail 只认大写 A-F。
最初用小写 key 运行始终 verify fail,转大写后立即通过。
- 最终 Key 与验证
323C47184B0D3C44254B445842552F365C362C1144424B0D3C4416433B0DD6B12A0D3D95FA65B5E0ADE5E11B
$ echo 323C47184B0D3C44254B445842552F365C362C1144424B0D3C4416433B0DD6B12A0D3D95FA65B5E0ADE5E11B | ./cm.exe
Enter your key:
verify success. - 工具与复现
求解脚本:solve_cm.py(依赖 sympy 分解 n)
反混淆方法:递归下降反汇编器(jmp 只跟目标、跳过垃圾字节)
关键地址速查:
地址 作用
0x403D90 主函数
0x402100 hex→大数解码(×16 与 +nibble 交替)
0x403500 RSA 变换
0x402510 模幂核心(平方-乘)
0x404870 查表入口
0x402d70 查表偏移 = buffer2[index]
0x4163B0 buffer2:143 项偏移表
0x4263B0 buffer1:字节池
0x416380 目标字符串 "Welcome to KCTF2026!..."
0x42E018/0x436018 RSA 模数 perm/data
0x434018/0x430018 RSA 指数 perm/data