以下内容来自Gemini:
Crackme-Nexus 深度逆向分析 WriteUp:跨越 .NET 混淆与 JNI 虚拟机的降维打击
0x01 背景与概览
本次分析的目标是名为 MinHook.exe 的挑战程序(源自 crackme-nexus 仓库)。该程序在表层包裹了多重现代加固与混淆手段,其核心验证逻辑被深埋于一个底层依赖 Java Native Interface (JNI) 的动态链接库中,并被一套 C# 深度混淆机制包裹。
本文将详细还原该程序的保护流、破解思路、以及核心校验算法的提取与数学求解过程。
0x02 保护流还原:外壳剥离与运行机制
第一层:C# 混淆外壳与动态释放
直接对 MinHook.exe (原名或主程序名) 查壳或使用 .NET 反编译工具(如 dnSpy / ILSpy)分析,会发现它是一个被深度混淆的 .NET 程序(包含控制流平坦化、字符串加密等机制),代码可读性极低。
通过动态监控与反编译推演,我们发现 C# 仅仅是一个“外壳程序” (Wrapper/Loader):
- 它负责接收用户终端输入的 Flag 字符串。
- 它会剥离掉格式前缀和后缀(即剥离
NEXUS{ 与 }),仅提取中间的 16 字节内容。
- 运行时,程序会在内存中解压/解密,并向系统的
%TEMP% 临时目录动态释放一个名为 ProtectedByLivelyXuan_xxxx.ycvm (本质上是一个原生 DLL) 的底层模块。
- C# 将 16 字节的 Flag 通过动态调用的方式传入该 DLL 中进行黑盒验证。
第二层:JNI 环境与 Native 验证核心
剥离出 ycvm.dll 后,使用 IDA 或 Capstone 反汇编,发现该 DLL 导出了诸如 Java_CoreValidator__00024jnicLoader 的标准 JNI 函数符号。这说明底层代码是用 Java 编写的,但被某种类似 Excelsior JET 或高强度混淆器编译/转译成了原生 C++ DLL。
它的核心机制如下:
- 动态注册拦截:代码通过调用
RegisterNatives 动态注册了一整套校验函数(如 cascade, scatter, drift, mirror, fold, diff, probe 等)。
- 初始化数组混淆:在初始化函数(如
$jnicClinit)中,调用了大量的 SetByteArrayRegion,在堆内动态生成并隐蔽存放了数十个加密校验数组(比如 MUL_TARGET, XOR_KEYS, ADD_TARGET 等)。
这种架构导致极难通过静态分析找出正确的散列或比对常量。
0x03 突破口:构建高仿真 Python JNI 沙盒
面对经过深度混淆和环境检测的 .NET 外壳,如果我们强行使用 Frida 去 Hook 上层 C# 或者与内存加固搏斗,会陷入无尽的崩溃中。
降维打击思路:既然核心在 ycvm.dll 且依赖 JNI,我们完全可以放弃原有的运行环境,自己用 Python 写一个 JNI 虚拟机(沙盒)喂给它!
- 自建 JNIEnv:利用 Python 的
ctypes,我们构造了一个拥有 230 个函数指针的 JNINativeInterface 结构体,将其中的 SetByteArrayRegion、GetByteArrayRegion 等关键方法映射到我们自己的 Python 回调函数中。
- 接管与数据转储:强行加载
ycvm.dll 并主动调用 $jnicClinit。此时 DLL 会以为自己在 JVM 中运行,乖乖地将所有加密 Target 数组写入了我们 Python 定义的字典中。
- 黑盒探针:我们主动调用它的注册函数
probe,得出 Flag 长度必须为严格的 16 字节。
0x04 核心算法逆向与数学推导
通过在 Python 沙盒中转储出校验数组并结合 IDA 反汇编追踪,我们定位到了决定胜负的关键校验函数之一:scatter 阶段。
Scatter 的汇编逻辑还原
对 scatter 函数的 x64 汇编进行切片分析:
0x25d7: movzx eax, r13b ; eax = INPUT_FLAG[i]
0x25db: mov ecx, eax
0x25dd: shl ecx, 5 ; ecx = Flag[i] * 32
0x25e0: sub ecx, eax ; ecx = Flag[i] * 32 - Flag[i] = Flag[i] * 31
0x25e2: movzx eax, cl ; eax = (Flag[i] * 31) & 0xFF
0x25e5: xor eax, r15d ; r15d 是随循环递增的索引相关量,等于 (i * 13) & 0xFF
0x25e8: cmp eax, ebp ; ebp 是目标数组 MUL_TARGET[i]
0x25ea: jne 0x2616 ; 不相等则验证失败
此外,程序中间还调用了一次迷惑性的原生方法 CoreValidator.u(byte) 来处理 MUL_TARGET。但通过我们的 JNI 沙盒测试该方法,发现 u(x) 的返回值永远等于 x,纯粹是干扰分析的无用调用(Obfuscation)。
算法代数模型
由此,我们得出了非常清晰的单个字节校验数学方程:
$$((Flag[i] \times 31) \pmod{256}) \oplus (i \times 13) == MUL\_TARGET[i]$$
这里 $MUL\_TARGET$ 是我们之前从内存里截获的 16 字节常量数组:
6b 03 8a 56 e2 f6 75 00 3a a3 f3 28 92 79 fa 0b
逆向求解:乘法逆元 (Modular Multiplicative Inverse)
方程中含有异或和模 256 下的乘法。这是一个仿射密码 (Affine Cipher) 变种。为了求出 $Flag[i]$,我们可以:
- 将两边异或 $(i \times 13)$,消除异或运算。
- 为了消除 $\times 31$,需要求出 $31$ 在模 $256$ 下的乘法逆元。
由于 $\gcd(31, 256) = 1$,逆元必然存在。
求得:$31^{-1} \equiv 249 \pmod{256}$ (即 $(31 \times 249) \pmod{256} == 1$)
由此得到终极解密公式:
$$Flag[i] = \Big( \big( MUL\_TARGET[i] \oplus (i \times 13) \big) \times 249 \Big) \pmod{256}$$
0x05 一键解密与结果
将上述思路用极简的 Python 脚本实现:
mul_target = bytes.fromhex('6b038a56e2f675003aa3f3289279fa0b')
# 31 在模 256 的乘法逆元
inv_31 = 249
flag = bytearray(16)
for i in range(16):
rhs = mul_target[i] ^ ((i * 13) & 0xFF)
flag[i] = (rhs * inv_31) % 256
print(f"NEXUS{{{flag.decode('ascii')}}}")
运行输出:
NEXUS{52pojieEnjoy2048}
将该密文输入原 C# 程序,验证通过,破解圆满成功。
0x06 总结与防破解建议
该 Crackme 运用了令人惊艳的多语言混合与底层化加固技术(.NET 深度混淆外壳 + JNI 虚拟化核心)。但其防线崩溃的核心原因有二:
- 运行环境不设防:Native 验证函数与主程序进程缺乏强绑定,轻易就被隔离出来放进自定义的假沙盒中任意揉捏,导致黑盒测试和内存常量提取如同探囊取物。
- 线性数学漏洞:乘法与异或虽然掩盖了字符特征,但这属于标准的线性/仿射变换。没有使用非线性的密码学哈希(如 SHA-256、AES),导致攻击者能够利用乘法逆元轻易实现 $O(1)$ 的直接代数反推,而无需暴力破解。
修复建议:
- 增加 JNIEnv 内部堆栈来源与代码段 Hash 校验(反沙盒隔离)。
- 放弃可逆的线性代数算法验证,采用非对称加密或高强度单向散列。
- 避免验证失败时立即
return 的短路操作,改用恒定时间 (Constant-Time) 运算,防止单字节旁路爆破。