首页
社区
课程
招聘
[分享]KCTF 2026 · cm.exe 逆向分析
发表于: 2026-8-17 13:22 29

[分享]KCTF 2026 · cm.exe 逆向分析

2026-8-17 13:22
29

一个把"误导"当作主要防线的 crackme。假注释、花指令、假容器、假计时器层层堆叠,
而真正的算法只有三件事:一张双射查表、两个校验和、一次 128 位 RSA。

序列号

验证逻辑(剔除全部诱饵后):

由于第 6 步的表是双射,目标字符串唯一确定了那 44 个字节;再反解 RSA 即可得到唯一的 key。
第 3、4 步的两个校验和无需参与求解——解出 key 之后回代,它们自动成立。

难点不在算法,在于三层遮蔽:花指令让线性反汇编错位;假注释把分析引向不存在的 strcmp
置换存储让数据段的静态 dump 完全失真。

节表:

注意 .text 只有 19 KB,而 .rdata 有 152 KB —— 代码很少,数据很多。这个比例本身就提示
"核心是查表"。

残留的 PDB 路径是全程唯一一条没有说谎的提示:

rsa_tf 点出了 RSA。

.text 通篇是同一个模式——一条短跳转越过若干合法但不可达的字节:

至少有三种变体交替出现:

objdump -d 会忠实地把这些垃圾字节解码成指令,导致后续几条真实指令的边界全错。

不需要去花指令,只要换一种遍历方式:从函数入口开始,遇到无条件 jmp 跟着立即数走,
遇到条件跳转把目标压栈,遇到 ret 停止。垃圾字节因为不可达,自然不会被解码。

约 40 行。main 从 6975 行噪声收敛到 609 条真实指令,之后的分析全部基于这份输出。

.rdata 里躺着三条措辞极其可信的"开发者备注":

三条全是假的。0x401234 甚至不在任何函数边界上;程序里不存在对 admin123strcmp
唯一带 __try/__except 的函数返回值被丢弃。

真正精巧的地方:这三条备注本身被当作数据消费。它们被送进 0x402100
的十六进制解析器转成三个大整数,而这三个大整数从头到尾没有被读过一次
最后在 main 尾部被直接释放。诱饵套着诱饵。

确认这些只需要看结果流向哪里

三条各花不到一分钟确认,却砍掉了大半个攻击面。

这一层针对的是谁

把权威口吻的注释埋进 .rdata,成本极低,却精准打击两类分析:一是习惯先 strings
再定位的人工流程,二是会把工具输出当作可信指令的自动化分析。正确的姿势是把二进制里
读到的一切都当作数据而不是指令——它们只描述作者想让你相信什么,
不描述程序做什么。

main 位于 0x403D90。剔除全部诱饵后,验证链只剩九步:

加粗的五步会直接跳到 verify fail

关键在于它不跳过非十六进制字符,而是把它们当成数字 0,同时照常执行 ×16
所以 GZ 在数值上完全等价于 0,却仍然能通过第 3 步的字符集检查。
这留下了一个可证伪的预测,见 §9

size 是有效 limb 数。size*8 == 88size == 11 limb ⟹ 352 位 ⟹ 88 位十六进制

一个状态相关的非线性累加,s 的低 7 位参与下一项的权重。注意最终只比较低 16 位

一个隐藏的坑movsx符号扩展。若 buf[i] >= 0x80idx 变成负数,
按无符号解释后远大于 sizeoperator[] 会退化成返回 data[0]
答案里所有字节都 ≤ 95,不触发这条路径,但爆破时很容易在这里踩空。

这是本题真正的机关。大数类和查表类共用一个容器,构造函数(0x402BD0)暴露了全部设计:

洗牌用的随机源是 xorshift32(0x401160),外层 rand(n)0x4011A0
用拒绝采样消除取模偏差。不过对求解而言这些都不重要——真正用到的两张
perm[] 是静态烘焙在文件里的
,静态对象绕过了构造函数,洗牌从未对它们执行过。

还原成 C:

也就是说,内存里看到的 limb 顺序和逻辑顺序完全无关。直接 dump .data
只会得到一片乱序噪声。我第一次 dump 那几个数组时看到的是:

一堆全部小于 0x800 的小数值——当时以为是垃圾,其实那正是 perm[] 本身。

更狠的是,RSA 参数和查表都不走构造函数,而是把结构体逐字节拼进 .data
的一块暂存区,再整体拷进目标对象。逐字节写指针可以躲开静态分析工具的交叉引用识别:

拼装完成后的两个 RSA 对象:

.data 里一共 6 个 0x2000 字节的数组,两两配对成 (data, perm)。

main 在使用查表前先 malloc 了一个 size=1 的假容器,再整体覆盖:

最后那次还原是必须的——否则 free 会拿到 .rdata 里的静态地址而崩溃。
作者把这个细节处理得很干净。

0x4033D0 拷进来的 20 字节(同样先逐字节打补丁)就是真正的查表容器:

注意这个容器存的是字节,所以取值公式不乘 4:

i = 0…126 全部展开(table[i] = data[perm[i]]):

字节 1..95 与 95 个可打印 ASCII 字符构成双射idx = 字节值 - 1),
96..127 全部映射到 \0

既然是双射,目标字符串就唯一确定了那 44 个字节。把
Welcome to KCTF2026! Come and give it a try.(正好 44 字符)逐字符反查:

一个需要确认的边界条件:第 8 步写回的字节数是 BN.size * 4
若 RSA 结果的最高 limb 为 0(size < 4),写回的就不是 16 字节,
后续拷贝会发生错位。这里密文最高字节是 0x0F ≠ 0 ⟹ 最高 limb
0x0F443937 ≠ 0size == 4 ⟹ 恰好 16 字节,不触发错位。

必须按 data[perm[i]] 取,顺序读取会得到完全错误的模数:

模幂在 0x402510,调用形式 powmod(this = 输入, exp = e, mod = N)
结果经 0x4022D0 转成大写十六进制字符串(字符表 "0123456789ABCDEF"0x4061B4),
再被 main 重新解析成大数——一次无意义的往返,但不影响数值。

128 位半素数,两个 64 位因子。试除到 2×10⁶ 无果,Fermat 30 万步无果,
Pollard p−1(B = 2×10⁵)无果。纯 Python 的 Pollard rho 需要约 2³² 次迭代,太慢。

于是用 gcc 写了个 128 位 Montgomery 乘法的 Pollard-Brent

唯一需要小心的是:N 的最高位是 10x8C… > 2¹²⁷),
REDC 的中间结果可能溢出 128 位,必须显式接住进位。

正确性依据:REDC 的真值 < 2N < 2¹²⁹。若发生溢出,真值 = res + 2¹²⁸ > N
减一次 N= res + 2¹²⁸ − N < N < 2¹²⁸,而 res - Nu128
下的回绕结果恰好等于它。

gcc -O3 -march=native,几十秒出结果:

解密:

X 即输入的后 32 个字符。

把前 28 字节和解出的 16 字节拼起来,得到 RSA 覆写之前的缓冲区,
也就是 key 本身的字节表示:

这两个值全程没有参与求解,是解完之后回代验证的:

这是整个过程里最有说服力的一步。它们不是约束条件,是验算——
如果对 perm、对字节序、对覆写顺序中任何一处理解有偏差,
这两个独立的校验和不可能同时对上。

分析还顺带推出两个副作用,都在真机上验证通过。能预测"哪些错误的 key 会成功",
比"正确的 key 会成功"更能说明模型是对的:

因此严格来说 key 不唯一:所有值为 0 的十六进制位都可以替换成 GZ 中任意字符,
共 21 种选择。纯十六进制形式是其中的规范解。

连续 30 次运行全部 verify success.,无抖动。结合 §3
中确认的"GetTickCount 返回值被全部丢弃",可以确定不存在时间或环境依赖

pq 两个因子来自离线分解外,其余参数全部在运行时从 cm.exe 现读,
不含任何硬编码中间值。

输出:

cm.exe 的算法强度其实很有限:一张固定查表、两个线性校验和、一个 128 位 RSA,
每一项单拎出来都不难。它的难度几乎全部押在误导上,而且分层很清楚:

最省力的判据是:跟着数据流走,不跟着叙述走。 任何字符串、注释、
看起来很关键的 API 调用,只要它的结果没有流向最终比较,就可以直接划掉。

反过来,这题最值得学的设计是置换存储:它不增加算法复杂度,
不引入任何反调试,却让"dump 数据段找常量"这个最常用的静态手段彻底失效。
N 就明明白白躺在 .data 里,但你必须先读懂 operator[] 才能把它拼出来。

配套文件:

序列号

文件 cm.exe,224 256 字节
SHA1 d1c8124c5964af1531c6e311328854a05ca40bca
格式 PE32 控制台程序,Intel i386,5 节
编译器 MSVC 14.24(Visual Studio 2019)
ImageBase 0x400000(带 DYNAMIC_BASE
入口 0x404D00
判胜条件 输入序列号后回显 verify success.
VMA 大小 文件偏移
.text 0x401000 0x4A80 0x400
.rdata 0x406000 0x252BE 0x5000
.data 0x42C000 0xC200 0x2A400
.rsrc 0x439000 0x1E0 0x36600
.reloc 0x43A000 0x35C 0x36800
位置 看上去是什么 真相
0x406208 0x406214 0x406220 admin123 / r3v3rs3! / password 诱饵:三个字符串从未参与任何比较
0x4061F0 n0_4i_c4n_r34d_th1s! 诱饵:同上,纯装饰
0x406268 0x4062C8 0x406320 三条"开发者备注" 诱饵:被解析成三个大整数,但从未被索引
0x403C60 __try/__except 的校验函数 诱饵main 调用后丢弃 eax0x403D05–0x403D66 的处理器主体被 jmp 跳过,是死代码
0x402114 解析器入口调用 GetTickCount 诱饵:结果原样 return,四个调用点全部丢弃 eax,不存在时间依赖
0x4043E2 malloc 出一个 size=1 的查表容器,种子 0xA5A5A5A5 诱饵:随即被 0x4033D0 用 20 字节静态结构体整体覆盖
0x438018+0x0C 0xDEADBEEF 诱饵:落在 seed 字段,构造后不再使用
0x416380 Welcome to KCTF2026! Come and give it a try. :44 字符,就是最终 strcmp 的目标
# 步骤 位置 说明
1 读取输入 0x403F81 gets_s(buf, 0x3E8),缓冲区在 ebp-0x858
2 解析为大整数 0x404040 逐字符 ×16,非十六进制字符按 0 计入
3 字符集检查 0x404070 每个字符必须 ∈ [0-9A-Z]
4 长度检查 0x404092 BN.size * 8 == 0x58
5 取大端字节 0x4040C0 buf[44] = BE(BN),存放在 ebp-0x470
6 异或校验 0x4041BC XOR(buf) == 0x8F
7 加权和校验 0x40425F (s & 0xFFFF) == 0xBEFF
8 RSA 覆写 0x403500 buf[28..43] = BE16(X^65537 mod N)
9 查表 + 比对 0x404550 strcmp(out, "Welcome to KCTF2026! …")
偏移 字段 模数 N 指数 e
+0x00 size 4(128 位) 1(32 位)
+0x04 capacity 0x800 0x800
+0x08 data 0x436018 0x430018
+0x0C seed 0xDEADBEEF(诱饵) 0xDEADBEEF
+0x10 perm 0x42E018 0x434018
+0x14 extra 0x432018 0x42C018
手段 防的是 破解成本
花指令 线性反汇编 低——换递归下降即可
假注释 / 假字符串 "先 strings 再定位"的人工流程;把观察到的文本当可信指令的自动化分析 低——但会浪费大量时间
假计时器 / 假异常处理 动态跟踪时的注意力 低——看返回值流向即可排除
置换存储 静态 dump 数据段 高——唯一真正拖慢进度的设计
假容器(先 malloc 再覆盖) 调试器里的第一现场
逐字节拼指针 静态交叉引用识别
地址 作用
0x401160 xorshift32:x ^= x<<13; x ^= x>>17; x ^= x<<5
0x4011A0 rand(n):在 xorshift32 上做拒绝采样,消除取模偏差
0x402100 字符串 → 大数(radix 16,非法字符按 0)
0x4022D0 大数 → 大写十六进制字符串
0x402510 模幂 powmod(base, exp, mod)
0x402A70 Vec::operator[](dword,data + 4*perm[i]
0x402BD0 容器构造函数(capacity 0x800,seed 0xCAFEBABE
0x402D70 return perm[i]
0x402DA0 Fisher-Yates 洗牌 perm[]
0x402EA0 std::string::assign
0x4033A0 printf 包装
0x4033D0 0x438030 的 20 字节静态结构体拷进目标(先打补丁)
0x403500 RSA 阶段:取 input+56,做 X^e mod N,返回十六进制串
0x403C60 诱饵:对三条假注释和输入求和,返回值被丢弃
0x403D90 main
0x404870 ByteVec::operator[](byte,data + perm[i]
0x406268 / 0x4062C8 / 0x406320 三条假注释(被解析成三个未使用的大数)
0x4061B4 "0123456789ABCDEF"
0x416380 "Welcome to KCTF2026! Come and give it a try."(44 字符)
0x4163B0 / 0x4263B0 查表的 perm[] / data[]
0x438018 RSA 对象暂存区(24 字节,运行时打补丁)
0x438030 查表容器暂存区(20 字节,运行时打补丁)
0x42C0180x436018 6 个 0x2000 字节数组,两两配对成 (data, perm)

传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

收藏
免费 0
打赏
分享
最新回复 (1)
雪    币: 3
活跃值: (165)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
大佬我我这边有很多棋牌只有我希望可以认识你合作共赢共同发展 看到回复越秀区真心求合作一天上百个w 
3天前
0
游客
登录 | 注册 方可回帖
返回