07 · JIT Hook 保护:在编译时刻解密
目录
- [JIT 编译:从 IL 到机器码的最后一步](#JIT 编译:从 IL 到机器码的最后一步)
- [Hook 原理:替换 clrjit.dll 的 compileMethod](#Hook 原理:替换 clrjit.dll 的 compileMethod)
- [Trampoline:在不破坏 vtable 的前提下跳转](#Trampoline:在不破坏 vtable 的前提下跳转)
- [哨兵 IL:用 7 个字节传递方法索引](#哨兵 IL:用 7 个字节传递方法索引)
- 方法数据存储与加密
- [x86 vs x64:两种架构的 trampoline 实现](#x86 vs x64:两种架构的 trampoline 实现)
- 限制与兼容性
- 结语
平台限制 :JIT Hook 仅支持 .NET Framework 4.5+ 。在 .NET Core / .NET 5+ 上,JIT 接口已完全重构(
ICorJitCompiler不再通过getJit导出),此方案不可用。跨平台保护请使用方法体加密或代码虚拟化。
1. JIT 编译:从 IL 到机器码的最后一步
.NET 程序的执行流程:
IL 程序集 → CLR 加载 → JIT 编译器 (clrjit.dll)
→ 逐方法编译 IL 为 x86/x64 机器码 → 执行
clrjit.dll 导出的 getJit() 函数返回 ICorJitCompiler 接口,其 vtable0 就是 compileMethod 函数指针。每个需要执行的方法在首次调用时都会经过 compileMethod。
JIT Hook 的思路是:在 compileMethod 被调用时,把加密的 IL 替换回原始 IL,让 JIT 编译器正常编译。
2. Hook 原理:替换 clrjit.dll 的 compileMethod
csharp
// 伪代码流程
Hook()
{
// 1. 加载 clrjit.dll
var jitModule = LoadLibrary("clrjit.dll");
// 2. 获取 getJit 导出
var getJit = GetProcAddress(jitModule, "getJit");
// 3. 调用 getJit() → 获取 ICorJitCompiler*
var compiler = ((delegate*)getJit)();
// 4. 读取 vtable[0] = compileMethod 原始指针
var originalCompileMethod = compiler[0];
// 5. 创建 trampoline
var trampoline = CreateTrampoline(HookHandler, originalCompileMethod);
// 6. 修改内存保护 → 写入 trampoline → 替换 vtable[0]
VirtualProtect(compiler, PAGE_EXECUTE_READWRITE);
compiler[0] = trampoline;
}
3. Trampoline:在不破坏 vtable 的前提下跳转
直接修改 vtable 在现代 Windows 上会被 DEP(数据执行保护)阻止。我们通过"trampoline"(跳板)实现:
Trampoline 结构(x64):
mov rax, <HookHandler 地址> ; 48 B8 [8 bytes addr]
jmp rax ; FF E0
共 12 字节
trampoline 通过 VirtualAlloc(MEM_COMMIT, PAGE_EXECUTE_READWRITE) 分配在可执行内存中。
4. 哨兵 IL:用 7 个字节传递方法索引
保护时为每个方法生成一条"哨兵 IL"------JIT Hook 可以检测到它并替换为真实 IL:
哨兵 IL(7 字节):
ldc.i4 <methodIndex> ; 20 [4 bytes index] (实际上 index 嵌入在 IL 字节 1-4)
pop ; 26
ret ; 2A
检测条件:ILCodeSize == 7 && ILCode[0] == 0x20 && ILCode[5] == 0x26 && ILCode[6] == 0x2A
csharp
int HookHandler(ICorJitCompiler* compiler, CORINFO_METHOD_INFO* methodInfo, ...)
{
// 检测哨兵 IL
if (methodInfo->ILCodeSize == 7 &&
methodInfo->ILCode[0] == 0x20 &&
methodInfo->ILCode[5] == 0x26 &&
methodInfo->ILCode[6] == 0x2A)
{
// 从 IL 字节 1-4 读取方法索引
uint methodIndex = *(uint*)(methodInfo->ILCode + 1);
// 从加密数据中解密真实 IL
byte[] realIL = DecryptMethodData(methodIndex);
// 替换 ILCode 指针和大小
methodInfo->ILCode = realIL;
methodInfo->ILCodeSize = realIL.Length;
}
// 调用原始 compileMethod
return originalCompileMethod(compiler, methodInfo, ...);
}
5. 方法数据存储与加密
与虚拟化和方法体加密类似,使用魔术头扫描定位:
[4 bytes] Magic: 0x4A495448 ("JITH")
[4 bytes] MethodCount
[foreach method]:
[4 bytes] MethodToken
[4 bytes] ILCodeLength
[N bytes] ILCode (加密)
[4 bytes] LocalSigLength
[N bytes] LocalSig (加密)
6. x86 vs x64:两种架构的 trampoline 实现
x64 trampoline(12 字节):
nasm
mov rax, 0x123456789ABCDEF0 ; 48 B8 [8 bytes addr]
jmp rax ; FF E0
x86 trampoline(7 字节):
nasm
mov eax, 0x12345678 ; B8 [4 bytes addr]
jmp eax ; FF E0
两种架构的 trampoline 大小必须完整保存,以便内存对齐和后续切换。
7. 限制与兼容性
| 限制 | 说明 |
|---|---|
| 仅 .NET Framework 4.5+ | ICorJitCompiler 在 .NET Core 中不再是导出接口 |
| 仅支持 Windows | clrjit.dll 是 Windows 专属 |
| v1 不支持异常处理器 | 含 EH 的方法自动跳过 |
| 不支持泛型方法 | 类型参数在编译时才确定 |
| 每个方法的 IL 大小限制 | 必须 < 哨兵 IL 大小的理论极限 |
8. 结语
JIT Hook 是一种巧妙的保护手段------在"最后一道门"(JIT 编译)拦截并替换 IL,绕过了所有静态分析工具。但它仅适用于 .NET Framework 平台。对于跨平台需求,建议使用方法体加密(第 06 篇)或代码虚拟化(第 05 篇)。
下一篇将讲解防篡改校验------如何确保保护后的程序没有被修改过。
本文由 TWSoft.AssemblyProtector 驱动。完整源码和工具请访问项目主页。