CrackMe.net V4 专业完整步骤分析
文档类型:逆向分析、动态调试、算法恢复与验证复盘
案件编号:crackme_net_20260817
原始目标:C:\Users\LKROG\Downloads\VMP\Crackme_net\CrackMe.exe
原始 SHA-256:181B7AF7932D472654DA074A26ED8577D4875B8205F0D581DB94F34E68E57C45
目标运行时:.NET Framework 4.8 / Windows x64
工具链:Detect It Easy、ILDasm、IDA 9.4、x64dbg、Frida 17.17.0、PowerShell、Python、.NET SDK
分析原则:不修改原始 EXE,不使用 x64dbg 反反调试插件伪造结果,以静态证据、运行时证据和原始程序验收组成闭环。
1. 结论先行
1.1 最终密码
~TWPro~2608v6~
密码长度为 14 个字符,UTF-8 编码后也是 14 字节。程序通过 Array.Resize(ref input, 16) 把输入扩展为 16 字节,因此进入核心校验的实际数据为:
7E 54 57 50 72 6F 7E 32 36 30 38 76 36 7E 00 00
1.2 原始程序验收
在未修改的原始文件上输入正确密码,实际显示:
恭喜,密码正确!
当前 .NET V4 样本的成功字符串是中文,不是旧样本要求中的字面量 Correct!。报告以实际目标输出为准。
1.3 完成情况
| 项目 |
结果 |
| 密码求解 |
PASS |
| Main 明文 IL 恢复 |
PASS,796 字节 |
| Verify 明文 IL 恢复 |
PASS,1,525 字节 |
| 完整密码校验算法还原 |
PASS |
| .NET Framework 4.8 源码编译 |
PASS,0 warning / 0 error |
| x64dbg + Frida 无插件反调试 |
PASS |
| 原始程序正例验证 |
PASS |
| 两个原始程序反例验证 |
PASS |
| 原始文件运行前后完整性 |
PASS |
2. 分析范围与真实边界
本次“完整算法源码”指与密码输入、长度约束、字节变换、反调试分支、数组置换、结果比较和密码逆求解有关的完整业务算法。
下列内容不冒充已经恢复:
- TWProtector 的完整商业源码;
- 整个 C++ VM 工程的逐字源代码;
- 作者原始变量名、注释、项目结构和编译参数;
- 未被密码路径调用的全部保护器方法。
交付的 Program.cs 是从受保护方法明文 IL 恢复的语义等价实现。它能够编译、求解并正向验证密码,但不声称是作者逐字源码。
3. 证据分层
为了避免把推测写成事实,本案把证据分为六级:
| 层级 |
定义 |
本案内容 |
| L0 |
用户提供的说明或截图 |
DiE、IDA、x64dbg 截图 |
| L1 |
文件直接事实 |
哈希、PE 头、CLR 头、节、资源、导入 |
| L2 |
静态逆向事实 |
IDA 函数、RVA、结构偏移、交叉引用 |
| L3 |
动态运行事实 |
API 调用、VM 映射地址、运行时日志、明文 IL |
| L4 |
独立算法复现 |
Python/C# 正向与逆向变换一致 |
| L5 |
原始程序验收 |
正确密码显示成功,错误输入显示失败 |
只有 L4 和 L5 同时成立时,密码才被标记为最终结果。
4. 整体工作流
flowchart TD
A["原始 CrackMe.exe 基线"] --> B["备份与只读工作副本"]
B --> C["PE / CLR / 资源静态分析"]
C --> D["拆出 VMNX / VMBC / JITH"]
D --> E["提取 x86 / x64 Native VM"]
E --> F["IDA 恢复 VM_Initialize / VM_Invoke / vm_interpret"]
F --> G["x64dbg 硬件断点验证运行环境"]
G --> H["Frida 无插件反调试与手工映射监控"]
H --> I["抓取 Main / Verify 明文 IL"]
I --> J["ECMA-335 IL 解析"]
J --> K["状态机与不透明谓词去除"]
K --> L["恢复校验常量和完整算法"]
L --> M["逆向求解密码"]
M --> N["正向算法自验证"]
N --> O["原始 EXE 正例与反例验收"]
O --> P["报告、源码、脚本与回滚记录"]
5. 第一步:建立文件基线
5.1 基线命令
$target = 'C:\Users\LKROG\Downloads\VMP\Crackme_net\CrackMe.exe'
Get-Item -LiteralPath $target | Select-Object FullName, Length, LastWriteTimeUtc
Get-FileHash -LiteralPath $target -Algorithm SHA256
基线结果:
| 属性 |
值 |
| 文件大小 |
841,728 bytes |
| SHA-256 |
181B7AF7932D472654DA074A26ED8577D4875B8205F0D581DB94F34E68E57C45 |
| 最后写入 UTC |
2026-08-14T02:04:36.0727636Z |
5.2 备份策略
备份位置:
C:\Users\LKROG\Documents\ChatGPT\VMP\case_crackme_net_20260817\backup\source_snapshot_20260817_115658\CrackMe.exe
备份文件与原文件 SHA-256 一致。活动 IDA 数据库的 .id0、.id1、.nam 当时被 IDA 锁定,没有强制终止 IDA 或复制半写入数据库;.til 成功备份。
完整基线账本:
evidence\runtime\baseline.json
6. 第二步:Detect It Easy 与 PE/CLR 初筛
Detect It Easy 给出的方向性判断:
PE32
MSIL / C#
.NET Framework 4.8 / CLR 4.0.30319
Obfuscation
Virtualization
Anti-debug
Strange sections
截图只能说明工具识别结果,不能直接证明算法。随后用脚本和 CLR 工具重新确认。
6.1 CorFlags
ILONLY = 1
32BITREQ = 0
32BITPREF = 0
这代表 PE 外壳是 PE32,但程序集为 AnyCPU。目标在 x64 Windows 上进入 x64 CLR,因此保护器最终选用内嵌 x64 VM。用户的 x64dbg 进程也确认实际调试对象是 x64。
6.2 PE 关键字段
| 字段 |
值 |
| ImageBase |
0x00400000 |
| EntryPoint RVA |
0x000CEC7E |
| SizeOfImage |
0x000D4000 |
| CLR Header RVA |
0x00002008 |
| Metadata RVA |
0x000C1794 |
| Metadata size |
0x0000D494 |
| CLR Entry token |
0x06000004 |
6.3 节表
| 节名 |
RVA |
Raw size |
特征 |
.YYPM |
0x2000 |
0xCCE00 |
高熵主节,熵约 6.71373 |
.SWSL |
0xD0000 |
保护附加数据 |
异常命名 |
.URQOK |
0xD2000 |
保护附加数据 |
异常命名 |
6.4 导入
PE 导入几乎被压缩为:
mscoree.dll!_CorExeMain
这符合托管入口、保护逻辑位于 CLR 元数据与嵌入资源中的特征。
7. 第三步:CLR 元数据与磁盘方法体分析
7.1 元数据流
有效签名:
42 53 4A 42 = BSJB
元数据流:
#-
#Strings
#US
#GUID
#Blob
7.2 Main 调用链
Program.Main token 为 0x06000004,磁盘 IL 为:
call 0x0600000C // 保护运行时初始化
ldarg.0
call 0x0600000B // 受保护 Main 业务逻辑
ret
7.3 Verify 磁盘诱饵体
Program.Verify(byte[]) token 为 0x06000005,返回类型是 bool,但磁盘方法体只有:
ldc.i4 0x243a5ccf
pop
ret
ret 前没有 bool 返回值,因此这不是实际业务体。保护器在运行时通过 JIT Hook 或 Native VM 恢复真实实现。
7.4 ILDasm 注意事项
ILDasm 报告:
class 02000008 nested in missing class 02000001
这是混淆元数据造成的异常嵌套提示,不影响完整 IL 输出。完整磁盘 IL 已保存为案件分析文件,不能把这类 warning 误判成提取失败。
8. 第四步:资源拆分
8.1 原始资源
| 文件 |
魔数 |
大小 |
SHA-256 |
base.resources |
VMNX |
413,708 |
4BEAF1AFDC489709E64E1B009949826721A19DD339A8480589600A8B76E5A7B3 |
global.bin |
VMBC |
13,372 |
18D71BB3ECDA66DBBBC8AB7248578297E3C810D61834434AF8EAF5992187A034 |
gqzumbstkrxi |
JITH |
273 |
68A85B86265EE0B87E12A7E70E25D7DF46AF71CB85C652AA59E8FA4178FEB66B |
8.2 VMNX 结构
VMNX 结构可以简化为:
"VMNX"
uint32 x86Length
byte[x86Length] x86 PE
uint32 x64Length
byte[x64Length] x64 PE
容器被两个长度字段精确消费,没有未知尾部。
提取结果:
| 文件 |
架构 |
大小 |
SHA-256 |
native_vm_x86.dll |
x86 |
186,368 |
3284E82BBE4856590095FAA324CD163765B534A44D0A158D412E8D101C033487 |
native_vm_x64.dll |
x64 |
227,328 |
3857AEE2DF5D0BE7DA80F4F8DE9C115550BDA4B31AE536A59AB241D7F1253E9A |
复现命令:
python .\scripts\extract_vm_resources.py `
--vmnx .\evidence\runtime\base.resources `
--vmbc .\evidence\runtime\global.bin `
--jith .\evidence\runtime\gqzumbstkrxi `
--out .\analysis\extracted
交付目录不包含重复的大型原始资源,但保留了拆分清单:
evidence\runtime\resource_manifest.json
9. 第五步:IDA 分析原生 x64 VM
9.1 为什么单独加载 Native VM
原始程序集只显示托管加载器和混淆后的运行时。如果只在原始 IDA 数据库中追踪,会混合 CLR 元数据、手工加载器和 VM 指令。把 native_vm_x64.dll 单独载入第二个 IDA 实例,可以直接恢复:
- PE 导出;
- C++ VM 诊断字符串;
- 解释器主循环;
- 方法数据结构;
- S-box 解密位置;
- VM 到 CLR 的 bridge 调用。
9.2 关键导出和函数
IDA 默认 ImageBase:0x180000000。
| 名称 |
VA |
RVA |
VM_Initialize |
0x18000F470 |
0xF470 |
VM_Invoke |
0x18000F850 |
0xF850 |
VM_SetLogEnabled |
0x18000FD40 |
0xFD40 |
VM_Shutdown |
0x18000FD50 |
0xFD50 |
vm_data_load |
0x1800032A0 |
0x32A0 |
vm_do_call |
0x180007040 |
0x7040 |
vm_do_newobj |
0x180007A10 |
0x7A10 |
vm_interpret |
0x180007DD0 |
0x7DD0 |
9.3 原生导入暴露的保护面
IsDebuggerPresent
CheckRemoteDebuggerPresent
QueryPerformanceCounter
QueryPerformanceFrequency
VirtualAlloc
VirtualProtect
VirtualFree
这些导入表明保护不仅存在托管层的 Program.IsDebuggerPresent,Native VM 自身也进行调试器和时间环境检测。
9.4 VMBC 诊断字符串
IDA 中发现:
vm_data_load: rawLen=%d
master key decrypted
HMAC passed
encryptedLen=%d
methodCount=%d
per-method S-box derived
fullByteSbox applied
vm_interpret
同时发现 SHA-256 K 常量的完整引用,说明 HMAC/master-key 管线不是伪字符串。
9.5 ParsedMethod 结构
IDA 伪代码和 Frida 动态值共同确认:
struct ParsedMethod {
uint32_t token; // +0x00
uint32_t maxStack; // +0x04
uint32_t localCount; // +0x08
...
uint32_t exHandlerLen; // +0x18
...
uint8_t *il; // +0x20
uint32_t ilLen; // +0x28
};
对应伪代码位于:
evidence\ida_pseudocode\vm_interpret_0x180007DD0.c
10. 第六步:不破坏完整性校验的运行测试
10.1 启用保护器自带诊断
运行副本设置:
$env:TW_VM_DEBUG = '1'
.\CrackMe.exe
VM 自带日志确认:
VMBC magic = 0x43424D56
version = 3
flags = 127
master key decrypted
HMAC passed
encryptedLen = 13288
methodCount = 5
TokenInfo encrypted len = 9895
TokenInfo count = 23
method 0x0600000B offset = 12286
method 0x0600000B ilLen = 796
关键判断:不要直接修改 VMBC 或跳过 HMAC。正确方法是在完整性校验已经通过、方法已经解密后抓取内存明文。
10.2 为什么重定向输入后有异常
程序最终执行 Console.ReadKey()。当 stdin 被管道重定向时,.NET 会在结果输出之后抛出异常。因此自动化日志中出现异常不等于密码判断失败。
验收顺序必须是:
- 检查是否已经输出成功或失败标记;
- 再判断后续异常是否仅来自
ReadKey 重定向限制;
- 不使用进程退出码替代决定性输出。
11. 第七步:x64dbg 动态基线
11.1 初始状态
用户 x64dbg 截图和 MCP 基线显示:
PID = 5620
RIP = ntdll system breakpoint
main base = 0x276805C0000
main entry = 0x2768068EC7E
thread = 38896
11.2 使用硬件执行断点
设置:
kernel32!IsDebuggerPresent
kernel32!CheckRemoteDebuggerPresent
ntdll!NtQueryInformationProcess
选择硬件执行断点的原因:
- 不改 API 原始代码;
- 不依赖 ScyllaHide、TitanHide、VoidDbg 等插件;
- 可以记录真实 API 入参和调用时机;
- 便于与 Frida 消息做时间和地址对齐。
11.3 纯 x64dbg 路径的局限
只运行 x64dbg 时,目标在早期保护阶段退出。x64dbg MCP 当时禁用了寄存器/内存写和脚本执行权限,因此不能通过 MCP 强制改写返回寄存器。
这不是失败,而是边界:
- x64dbg 用于断点、寄存器、内存映射和反汇编证据;
- Frida 用于无插件 API 返回值标准化;
- IDA 用于静态函数和 RVA 解释。
12. 第八步:Frida 无插件反调试
12.1 Windows 本机模式
本案使用:
device = frida.get_local_device()
没有启动 Android frida-server。Android-oriented MCP 返回的 running=false 只说明 Android server 未启动,不能据此判断 Windows local-device 不可用。
12.2 Hook 行为
| API |
原始语义 |
标准化结果 |
IsDebuggerPresent() |
调试中返回非零 |
返回 0 |
CheckRemoteDebuggerPresent |
写出调试标志 |
写 0,函数返回 TRUE |
NtQueryInformationProcess, class 7 |
ProcessDebugPort |
写 0 |
NtQueryInformationProcess, class 0x1E |
DebugObjectHandle |
写 0 |
NtQueryInformationProcess, class 0x1F |
DebugFlags |
写 1 |
核心逻辑:
Interceptor.attach(Module.getGlobalExportByName('IsDebuggerPresent'), {
onLeave(retval) {
const original = retval.toInt32();
retval.replace(0);
send({ type: 'anti_debug', api: 'IsDebuggerPresent', original, forced: 0 });
}
});
完整代理:
scripts\frida_vm_agent.js
12.3 与 x64dbg 联合验证
x64dbg 占用调试端口后,把 Frida agent 附加到同一 PID。实际消息:
IsDebuggerPresent: original=1, forced=0
IsDebuggerPresent: original=1, forced=0
这里的 original=1 是关键证据:它证明目标确实处于 x64dbg 调试环境,而 Frida 在没有 x64dbg 隐藏插件的条件下把返回值改成 0。
13. 第九步:定位手工映射的 Native VM
13.1 第一次失败原因
最初使用 Process.attachModuleObserver() 等待包含 VM_Invoke 导出的模块,但没有收到 VM 模块事件。
原因:保护器没有用常规 LoadLibrary 注册 Native VM,而是手工映射 PE。手工映射的映像不会可靠出现在 Process.enumerateModules() 中。
13.2 唯一函数签名
从提取的 x64 VM 得到 vm_interpret RVA 0x7DD0 的入口签名:
48 89 4C 24 08 55 53 56 57 41 54 41 55 41 56 41 57 B8 08 20 00 00
13.3 把扫描前移到 VirtualProtect
如果使用定时扫描,第一次 VM 调用可能已经进入解释器。最终策略是在 VirtualProtect 返回时同步扫描刚设为可执行的范围:
VirtualProtect onEnter:
保存 base / size / newProtect
VirtualProtect onLeave:
如果成功且目标区域可执行:
在该范围扫描 vm_interpret 签名
vmBase = match - 0x7DD0
验证 vmBase[0:2] == "MZ"
立即安装 VM Hook
正确密码运行时的一个实例:
manual VM base = 0x1D23A3D0000
vm_interpret = base + 0x7DD0
VM_Invoke = base + 0xF850
VM_Initialize = base + 0xF470
ASLR 每次运行会改变基址,但 RVA 固定。
13.4 x64dbg 交叉验证
x64dbg + Frida 联合会话中另一个映射实例:
base = 0x1CF313D0000
vm_interpret = 0x1CF313D7DD0
VM_Invoke = 0x1CF313DF850
x64dbg 从基址读到:
4D 5A 90 00 ...
说明该地址确实是手工映射 PE。RVA 与 IDA 静态数据库一致。
14. 第十步:选择正确的明文 IL 抓取点
14.1 为什么入口处还是密文
vm_interpret 入口接收 ParsedMethod,但方法 IL 仍经过 per-method full-byte S-box。入口抓取只会得到密文。
14.2 S-box 解密循环
IDA 反汇编:
0x1800081D0 movzx ecx, byte ptr [rdx]
0x1800081D3 mov rax, [rdi+28h]
0x1800081D7 movzx ecx, byte ptr [rcx+rax]
0x1800081DB mov [rdx], cl
0x1800081DD lea rdx, [rdx+1]
0x1800081E1 sub r8, 1
0x1800081E5 jnz 0x1800081D0
循环结束后:
0x1800081FB mov r8d, r14d
因此模块 RVA 0x81FB 是完整 S-box 解密完成、解释器主循环开始前的稳定抓取点。
14.3 为什么不能只用 onLeave
Main 最后调用 Console.ReadKey。重定向环境中的异常会使解释器不能正常返回,onLeave 不会执行。
最终采用双点抓取:
vm_interpret 入口:保存 ParsedMethod 指针并抓取密文;
base + 0x81FB:抓取明文;
- 正常返回时再抓一份,校验明文没有变化。
15. 第十一步:恢复两个核心方法
15.1 Main 业务方法
token = 0x0600000B
ilLen = 796
maxStack = 7
localCount = 3
SHA-256 = 13B4A26DCB96615E2C9E8C41ED1D2F5109AE0090B24B7D8E6ADC8235B97340E9
15.2 Verify 方法
token = 0x06000005
ilLen = 1525
maxStack = 10
localCount = 11
SHA-256 = 5166122AE8F3182A33E2E4CB76EB0733D24A6B8E1965FBB5B41BA14079B03BF9
15.3 正确密码运行中的调用顺序
Frida 修正 VM_Invoke 原型后记录:
VM_Invoke token=0x0600000B argc=1
VM_Invoke token=0x06000005 argc=1
这与 Main 调用 Verify 的静态 token 关系一致。
明文文件:
evidence\recovered_il\method_0600000B.decrypted.ilbin
evidence\recovered_il\method_06000005.decrypted.ilbin
16. 第十二步:解析原始 ECMA-335 IL
裸 IL 缓冲没有方法头,不能直接交给普通反编译器。为此编写 RawIlDisasm:
- 通过
System.Reflection.Emit.OpCodes 建立单字节和 0xFE 双字节 opcode 表;
- 解析
InlineI、InlineI8、branch、switch、token、local、argument 等操作数;
- 通过原程序集
ManifestModule.ResolveMethod/Field/Type/String 解析 metadata token;
- 同时输出人类可读文本和结构化 JSON。
使用:
dotnet build .\scripts\RawIlDisasm.csproj -c Release
dotnet .\scripts\bin\Release\net8.0\RawIlDisasm.dll `
<original-assembly> `
.\evidence\recovered_il\method_06000005.decrypted.ilbin `
.\analysis\method_06000005
交付结果:
evidence\recovered_il\method_0600000B.il.txt
evidence\recovered_il\method_0600000B.il.json
evidence\recovered_il\method_06000005.il.txt
evidence\recovered_il\method_06000005.il.json
17. 第十三步:Main 控制流去混淆
17.1 分发器结构
Main 使用:
state % 15
switch (... 15 targets ...)
每个业务块结束时把下一个伪随机大整数留在栈上,再跳回:
dup
stloc.2
ldc.i4.s 15
rem.un
switch
17.2 初始状态
115683235 % 15 = 10
沿状态转换和实际条件折叠后得到:
string password = Console.ReadLine();
if (password == null)
Fail();
if (password.Length != 14)
Fail();
byte[] bytes = Encoding.UTF8.GetBytes(password);
Array.Resize(ref bytes, 16);
if (Verify(bytes))
Console.WriteLine("恭喜,密码正确!");
else
Console.WriteLine("FAILED");
Console.ReadKey();
17.3 长度常量恢复
混淆 IL 使用:
615708912 XOR 615708926
计算结果:
14
因此候选密码必须恰好 14 个 .NET 字符。
18. 第十四步:Verify 控制流去混淆
18.1 分发器
Verify 使用:
state % 31
switch (... 31 targets ...)
初始值:
999431186 % 31 = 21
18.2 局部变量语义
根据 opcode、数组类型和读写方式恢复:
| Local |
语义 |
V_0 |
第二轮 XOR key,byte[16] |
V_1 |
permutation,int[16] |
V_2 |
第一轮 XOR key,byte[16] |
V_3 |
permuted/transformed,byte[16] |
V_4 |
expected,byte[16] |
V_5 |
第二 XOR 循环索引 |
V_6 |
最终比较循环索引 |
V_7 |
第一 XOR 循环索引 |
V_8 |
ROL 循环索引 |
V_9 |
permutation 循环索引 |
V_10 |
dispatcher state |
18.3 三个返回块
| 结果 |
IL block |
| false |
IL_00E0 |
| false |
IL_029E |
| true |
IL_05E9 |
18.4 完整业务流程
去除垃圾常量、恒真条件、恒假条件和状态变量后:
bool Verify(byte[] input)
{
if (IsDebuggerPresent() != 0)
return false;
for (int i = 0; i < 16; i++)
input[i] ^= XorKey1[i];
for (int i = 0; i < 16; i++)
input[i] = Rol8(input[i], 3);
byte[] transformed = new byte[16];
for (int i = 0; i < 16; i++)
transformed[i] = input[Permutation[i]];
for (int i = 0; i < 16; i++)
transformed[i] ^= XorKey2[i];
for (int i = 0; i < 16; i++)
if (transformed[i] != Expected[i])
return false;
return true;
}
结构化控制流记录:
evidence\runtime\control_flow_reconstruction.json
19. 第十五步:恢复四个 RVA 数组
受保护 IL 使用:
ldtoken <field>
RuntimeHelpers.InitializeArray
通过 ManifestModule.ResolveField(token) 和 RuntimeHelpers.InitializeArray 直接读取字段数据。
19.1 Expected — 0x04000002
41 F7 25 EE 25 81 9C 93 47 21 E3 5B 88 12 59 28
十进制:
65, 247, 37, 238, 37, 129, 156, 147,
71, 33, 227, 91, 136, 18, 89, 40
19.2 Permutation — 0x04000003
3, 11, 7, 14, 1, 9, 4, 12,
0, 6, 10, 2, 15, 5, 13, 8
19.3 XorKey1 — 0x04000004
A5 5A A5 5A 0F F0 0F F0 33 CC 33 CC 55 AA 55 AA
19.4 XorKey2 — 0x04000005
11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF 00
复现:
.\scripts\extract_verify_constants.ps1 `
-AssemblyPath <CrackMe.exe> `
-OutputJson .\analysis\verify_constants.json
已保存结果:
evidence\runtime\verify_constants.json
20. 第十六步:逆向求解密码
正向算法:
PaddedInput
XOR XorKey1
ROL8 3
Permutation
XOR XorKey2
== Expected
逆向顺序:
Expected
XOR XorKey2
InversePermutation
ROR8 3
XOR XorKey1
= PaddedInput
20.1 第一步:撤销第二轮 XOR
50 D5 16 AA 70 E7 EB 1B DE 8B 58 97 55 FC A6 28
20.2 第二步:逆置换
DE 70 97 50 EB FC 8B 16 28 E7 58 D5 1B A6 AA 55
20.3 第三步:ROR8(3)
DB 0E F2 0A 7D 9F 71 C2 05 FC 0B BA 63 D4 55 AA
20.4 第四步:撤销第一轮 XOR
7E 54 57 50 72 6F 7E 32 36 30 38 76 36 7E 00 00
ASCII/UTF-8:
~TWPro~2608v6~\0\0
去掉 Main 扩容产生的两个尾零:
~TWPro~2608v6~
20.5 正向回代
求解脚本把结果重新执行完整正向算法,最终得到:
41 F7 25 EE 25 81 9C 93 47 21 E3 5B 88 12 59 28
与 Expected 逐字节完全一致,verification=true。
执行:
python .\scripts\solve_password.py
21. 第十七步:还原并编译 .NET 4.8 源码
源码:
source\Program.cs
source\CrackMe.Restored.csproj
目标框架:
<TargetFramework>net48</TargetFramework>
编译:
dotnet build .\source\CrackMe.Restored.csproj -c Release
结果:
0 warnings
0 errors
求解模式:
.\source\CrackMe.Restored.exe --solve
输出:
~TWPro~2608v6~
自检:
.\source\CrackMe.Restored.exe --self-test
输出:
SELFTEST PASS
22. 第十八步:原始程序正例与反例验收
22.1 正例
input = ~TWPro~2608v6~
output = 恭喜,密码正确!
result = PASS
22.2 错误长度
input = test
output = FAILED
result = PASS
22.3 错误内容、正确长度
input = AAAAAAAAAAAAAA
output = FAILED
result = PASS
22.4 完整性
三次运行后的 SHA-256:
181B7AF7932D472654DA074A26ED8577D4875B8205F0D581DB94F34E68E57C45
与基线一致。
完整输出:
evidence\runtime\runtime_regression.json
23. 完整性校验的处理方式
本案没有通过以下方式“解决”完整性校验:
- 没有 NOP HMAC 分支;
- 没有改写 VMBC;
- 没有替换 Expected;
- 没有强制 Verify 返回 true;
- 没有修改原始 PE checksum 或节内容。
实际策略:
- 允许 VMBC magic、version、master key 和 HMAC 正常验证;
- 等待方法密文被合法解析;
- 等待 per-method inverse S-box 正常完成;
- 在解释器执行前读取明文;
- 从真实算法逆向求解输入。
因此完整性链本身没有被破坏,结果也不依赖补丁。
24. 关键失败、原因与修正
24.1 ModuleObserver 看不到 VM
现象: API Hook 生效,但没有 vm_module 事件。
原因: Native VM 是手工映射 PE,不在常规模块列表。
修正: 在 VirtualProtect 返回时扫描 vm_interpret 唯一签名。
24.2 Hook 安装后没有明文 IL
现象: 找到 VM,但第一次只拿到密文。
原因: 定时扫描太晚,Main 已经进入解释器并阻塞在 Console.ReadLine。
修正: 扫描前移到保护属性切换瞬间,在手工映射器调用 VM 前安装 Hook。
24.3 onLeave 丢失 Main
现象: Verify 能在返回时抓取,Main 没有 onLeave。
原因: 重定向 stdin 后 Console.ReadKey 抛异常,解释器异常离开。
修正: 在 RVA 0x81FB 的解密完成点立即抓取。
24.4 VM_Invoke token 一度记录错误
现象: 日志 token 像随机指针低位。
原因: 初版 Frida 把第二参数当成 token;IDA 原型显示第二参数是 VM 指针,第三参数才是 token。
修正: args[2].toUInt32(),最终记录 0x0600000B 和 0x06000005。
24.5 x64dbg 中目标早退
现象: 纯 x64dbg 继续运行后目标停止。
原因: 反调试检查得到真实调试状态;MCP 写权限关闭,不能修改寄存器。
修正: x64dbg 保持观察角色,Frida 完成返回值标准化。联合证据显示 original=1 -> forced=0。
25. 可复现命令顺序
以下命令假设当前目录为:
C:\Users\LKROG\Downloads\VMP\Crackme_net\Recovered
25.1 直接查看密码与报告
Get-Content .\PASSWORD.txt
Get-Content .\CrackMe_net_V4_专业完整步骤分析.md
25.2 独立求解
python .\scripts\solve_password.py
25.3 还原程序自检
.\source\CrackMe.Restored.exe --solve
.\source\CrackMe.Restored.exe --self-test
25.4 重新编译源码
dotnet build .\source\CrackMe.Restored.csproj -c Release
25.5 Frida 隔离运行
$python = 'C:\Users\LKROG\frida-mcp\.venv\Scripts\python.exe'
& $python .\scripts\frida_local_runner.py `
--exe <CrackMe.exe工作副本> `
--agent .\scripts\frida_vm_agent.js `
--out .\analysis\frida_recheck `
--input '~TWPro~2608v6~' `
--timeout 12
必须使用工作副本或隔离副本,避免把运行时日志写入原始样本目录。
26. 交付文件说明
26.1 结论与报告
PASSWORD.txt
CrackMe_net_V4_full_reverse_report.md
CrackMe_net_V4_专业完整步骤分析.md
26.2 源码
source\Program.cs
source\CrackMe.Restored.csproj
source\CrackMe.Restored.exe
source\ALGORITHM.md
26.3 核心脚本
scripts\frida_vm_agent.js
scripts\frida_local_runner.py
scripts\frida_attach_collector.py
scripts\RawIlDisasm.Program.cs
scripts\RawIlDisasm.csproj
scripts\extract_verify_constants.ps1
scripts\solve_password.py
scripts\extract_vm_resources.py
scripts\triage_pe.py
scripts\triage_native.py
scripts\mcp_http_client.py
26.4 关键证据
evidence\recovered_il\
evidence\ida_pseudocode\
evidence\runtime\evidence_summary.json
evidence\runtime\runtime_regression.json
evidence\runtime\password_solution.json
evidence\runtime\verify_constants.json
evidence\runtime\final_test_results.json
evidence\runtime\rollback_record.json
27. 回滚与恢复
原始 EXE 在分析和验收阶段没有被修改,因此没有二进制补丁需要回滚。
分析备份:
C:\Users\LKROG\Documents\ChatGPT\VMP\case_crackme_net_20260817\backup\source_snapshot_20260817_115658\CrackMe.exe
SHA-256:
181B7AF7932D472654DA074A26ED8577D4875B8205F0D581DB94F34E68E57C45
报告扩展前的清单和旧报告另有备份:
C:\Users\LKROG\Documents\ChatGPT\VMP\case_crackme_net_20260817\backup\report_extension_20260817_124904
28. 当前状态补充
本报告扩展时,C:\Users\LKROG\Downloads\VMP\Crackme_net 根目录中只观察到 Recovered 和 CrackMe.exe.i64,原始 CrackMe.exe 当时不在该根目录。原始文件成功与哈希结论来自此前已经保存的运行验收、基线、回归 JSON 和备份文件;本文没有把当前缺失状态冒充成实时复验。
这是一个外部文件状态变化,不影响已经保存的算法、密码和历史验收结果。如果需要重新现场运行,应先从合法原件或上述备份恢复 CrackMe.exe,并再次核对 SHA-256。
29. 最终审计
| 检查项 |
结果 |
| 密码是否由算法逆求得 |
PASS |
| 是否完成正向回代 |
PASS |
| 是否恢复 Main 输入流程 |
PASS |
| 是否恢复 Verify 完整业务流程 |
PASS |
| 是否恢复全部校验数组 |
PASS |
| 是否提供 net48 可编译源码 |
PASS |
| 是否在真实 x64dbg 条件下验证反调试返回值修正 |
PASS |
| 是否避免 x64dbg 隐藏插件 |
PASS |
| 是否保存 IDA、x64dbg、Frida 分层证据 |
PASS |
| 是否通过原始程序正例与反例 |
PASS(历史验收已固化) |
| 是否声明源码真实性边界 |
PASS |
| 是否记录回滚位置 |
PASS |
最终结论:密码 ~TWPro~2608v6~ 已通过完整静态、动态、算法回代和原始程序验收链确认;密码校验源码已经语义等价恢复,保护完整性链未被破坏。