.NET 程序保护实战系列 07 · JIT Hook 保护:在编译时刻解密

07 · JIT Hook 保护:在编译时刻解密

目录

  1. JIT 编译:从 IL 到机器码的最后一步
  2. Hook 原理:替换 clrjit.dll 的 compileMethod
  3. Trampoline:在不破坏 vtable 的前提下跳转
  4. 哨兵 IL:用 7 个字节传递方法索引
  5. 方法数据存储与加密
  6. x86 vs x64:两种架构的 trampoline 实现
  7. 限制与兼容性
  8. 结语

平台限制: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 接口,其 vtable[0] 就是 compileMethod 函数指针。每个需要执行的方法在首次调用时都会经过 compileMethod。

JIT Hook 的思路是:在 compileMethod 被调用时,把加密的 IL 替换回原始 IL,让 JIT 编译器正常编译。


2. Hook 原理:替换 clrjit.dll 的 compileMethod

// 伪代码流程
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

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 字节):

mov rax, 0x123456789ABCDEF0   ; 48 B8 [8 bytes addr]
jmp rax                       ; FF E0

x86 trampoline(7 字节):

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 驱动。完整源码和工具请访问项目主页。

posted @ 2026-07-09 08:40  翼帆  阅读(163)  评论(3)    收藏  举报