.NET 安全之 JIT 保护技术深度解析

发布于:2025-07-09 ⋅ 阅读:(20) ⋅ 点赞:(0)

一、JIT 编译器概述

1.1 什么是 JIT

Just-In-Time(JIT)编译器是 .NET 公共语言运行时(Common Language Runtime, CLR)的核心组件之一。CLR 负责管理和执行所有 .NET 程序,不管这些程序是用 C#、VB.NET 还是 F# 等 .NET 编程语言开发的。

程序编译分为两个主要阶段:

  • 语言编译器阶段:特定于编程语言的编译器(如 Roslyn for C#)将源代码编译为与平台无关的中间语言,即 Microsoft 中间语言(MSIL)或通用中间语言(CIL)。
  • JIT 编译阶段:程序实际运行时,JIT 编译器将中间语言(MSIL/CIL)即时编译为目标计算机 CPU 能直接执行的本地机器代码,大大提升了 .NET 应用程序的整体运行效率。

1.2 JIT 编译器工作原理

JIT 编译采用 “按需” 编译机制:

  • 并非在程序启动时就把所有 MSIL/CIL 一次性编译成机器代码。
  • 当一个方法首次被调用时,JIT 编译器才介入,将该方法对应的 MSIL/CIL 指令编译为高效的本地机器代码。
  • 编译生成的机器代码会被缓存,后续对该方法的调用将直接执行缓存中的本地代码,无需再次编译,提高了执行效率。

1.3 JIT 编译器的优缺点

优点
  • 内存效率高:只编译当前执行路径所需的方法,避免将整个程序集的本地代码一次性加载到内存中,显著减少了内存占用。
  • 运行时优化:JIT 编译器能在运行时收集实际执行数据(如分支频率、类型信息),并基于此进行高级代码优化,如方法内联、循环优化、寄存器分配优化等,这是静态编译通常难以做到的,使得最终生成的机器代码能更好地适应当前运行环境和数据特征。
  • 平台适应性:相同的 MSIL/CIL 可在不同的 CPU 架构(如 x86, x64, ARM)上,由相应平台的 JIT 编译器生成本地代码,实现 “一次编写,到处运行”。
缺点
  • 启动延迟:程序启动初期,首次执行的方法需经历 JIT 编译过程,会带来一定性能开销,导致启动时间相对变长,即 “JIT 预热” 阶段。
  • 运行时开销:虽然缓存机制缓解了后续调用的开销,但编译过程本身需消耗 CPU 和内存资源。对于执行路径复杂或包含大量首次调用方法的场景,累积的 JIT 开销可能影响性能。
  • 缓存占用:JIT 编译器需要内存来存储编译生成的本地机器代码缓存。

二、JIT 加密技术

2.1 什么是 JIT 加密

JIT 加密是一种保护 .NET 程序集的方法,它通过在 JIT 编译过程中对 IL(中间语言)代码进行加密,然后在运行时动态解密,以此防止静态反编译和内存转储。这种方法能在一定程度上提高 .NET 应用程序的安全性,同时尽可能减少对性能的影响。

2.2 JIT 加密实现方式

在程序运行时通过劫持 JIT 编译相关函数(如 CompileMethod),在 CLR 对函数进行 JIT 编译时,解密方法体的相关信息(如方法体、异常块、元数据的 token 等数据),并将其传给真正的 JIT 编译函数生成正确的本地代码。

2.3 推荐 .NET 保护工具

在众多 .NET 保护工具中,Virbox Protector 因其成熟稳定的保护方案和良好的兼容性而被广泛推荐,尤其在其 JIT 加密实现方面表现突出:

  • 成熟方案:提供经过市场验证的、可靠的 JIT 加密保护。
  • 高框架兼容:兼容大多数主流 .NET Framework 和 .NET Core 运行时环境。
  • 跨平台支持:JIT 加密功能同时支持 Windows 和 Linux 操作系统平台。
  • 综合保护:除了 JIT 加密,还提供强大的函数级代码虚拟化(Code Virtualization)保护,两者可结合使用,提供更深层次的安全防御。

2.4 JIT 加密效果

通过对比保护前后的程序,可明显看到 JIT 加密对程序的保护作用。保护前的程序可能容易被反编译,而保护后的程序在一定程度上防止了静态反编译和内存转储。

三、实验验证

3.1 实验目的

通过劫持 CLR 中的 CompileMethod 函数,dump 被 JIT 加密的 IL 代码。

3.2 实验环境

  • 平台:Windows x64
  • .NET 版本:.NET Framework 4.7.2
  • 依赖库:dnlib、MinHook

3.3 实验效果

实验成功 dump 出了相关方法的 IL 代码,如下所示:

System.Void ConsoleApp1.Program::Main(System.String[])Body:
IL_0000: ldstr "Error, XXX Runtime library not loaded!"
IL_0005: newobj System.Void System.Exception::.ctor(System.String)
IL_000A: throw
---------dump Main---------
System.Void ConsoleApp1.Program::Main(System.String[])Body:
IL_0000: nop
IL_0001: ldstr "Hello World"
IL_0006: call System.Void System.Console::WriteLine(System.String)
IL_000B: nop
IL_000C: call System.ConsoleKeyInfo System.Console::ReadKey()
IL_0011: pop
IL_0012: ret
press any key to continue...

3.4 代码实现

以下是实验的代码实现,分为 Clr.csProgram.cs 两个文件:

Clr.cs
using System;
using System.Runtime.InteropServices;

namespace test_dnlib
{
    internal static unsafe class Clr
    {
        private static void* _clrModuleHandle;
        private static int METHODDESC_RESET_RVA = 0x4254B4;
        private static int METHODDESC_DOPRESTUB_RVA = 0x5A0A0;
        private static ResetDelegate _MethodDesc_Reset;
        private static DoPrestubDelegate _MethodDesc_DoPrestub;

        public enum CorInfoOptions
        {
            CORINFO_OPT_INIT_LOCALS = 0x00000010,                // zero initialize all variables
            CORINFO_GENERICS_CTXT_FROM_THIS = 0x00000020,        // is this shared generic code that access the generic context from the this pointer?  If so, then if the method has SEH then the 'this' pointer must always be reported and kept alive.
            CORINFO_GENERICS_CTXT_FROM_METHODDESC = 0x00000040,  // is this shared generic code that access the generic context from the ParamTypeArg(that is a MethodDesc)?  If so, then if the method has SEH then the 'ParamTypeArg' must always be reported and kept alive. Same as CORINFO_CALLCONV_PARAMTYPE
            CORINFO_GENERICS_CTXT_FROM_METHODTABLE = 0x00000080, // is this shared generic code that access the generic context from the ParamTypeArg(that is a MethodTable)?  If so, then if the method has SEH then the 'ParamTypeArg' must always be reported and kept alive. Same as CORINFO_CALLCONV_PARAMTYPE
            CORINFO_GENERICS_CTXT_MASK = CORINFO_GENERICS_CTXT_FROM_THIS | CORINFO_GENERICS_CTXT_FROM_METHODDESC | CORINFO_GENERICS_CTXT_FROM_METHODTABLE,
            CORINFO_GENERICS_CTXT_KEEP_ALIVE = 0x00000100,       //  Keep the generics context alive throughout the method even if there is no explicit use, and report its location to the CLR
        };
        public enum CORINFO_EH_CLAUSE_FLAGS
        {
            CORINFO_EH_CLAUSE_NONE = 0,
            CORINFO_EH_CLAUSE_FILTER = 0x0001,
            CORINFO_EH_CLAUSE_FINALLY = 0x0002,
            CORINFO_EH_CLAUSE_FAULT = 0x0004,
            CORINFO_EH_CLAUSE_DUPLICATE = 0x0008,
            CORINFO_EH_CLAUSE_SAMETRY = 0x0010,
        };

        [StructLayout(LayoutKind.Sequential)]
        public struct CORINFO_SIG_INST
        {
            public uint classInstCount;
            public void** classInst; // CORINFO_CLASS_HANDLE*
            public uint methInstCount;
            public void** methInst; // CORINFO_CLASS_HANDLE*
        }

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        public struct CORINFO_SIG_INFO_40
        {
            public uint callConv; // CorInfoCallConv
            public void* retTypeClass; // CORINFO_CLASS_HANDLE
            public void* retTypeSigClass; // CORINFO_CLASS_HANDLE
            public byte retType; // CorInfoType
            public byte flags;
            public ushort numArgs;
            public CORINFO_SIG_INST sigInst;
            public void* args; // CORINFO_ARG_LIST_HANDLE
            public void* pSig; // PCCOR_SIGNATURE
            public uint cbSig;
            public void* scope; // CORINFO_MODULE_HANDLE
            public uint token;
        };

        [StructLayout(LayoutKind.Sequential)]
        public struct CORINFO_METHOD_INFO_40
        {
            public void* ftn;               // CORINFO_METHOD_HANDLE
            public void* scope;             // CORINFO_MODULE_HANDLE
            public byte* ILCode;
            public uint ILCodeSize;
            public uint maxStack;
            public uint EHcount;
            public CorInfoOptions options;
            public uint regionKind;         // CorInfoRegionKind
            public CORINFO_SIG_INFO_40 args;
            public CORINFO_SIG_INFO_40 locals;
        };

        [StructLayout(LayoutKind.Sequential)]
        public struct CORINFO_EH_CLAUSE
        {
            public CORINFO_EH_CLAUSE_FLAGS Flags;
            public uint TryOffset;
            public uint TryLength;
            public uint HandlerOffset;
            public uint HandlerLength;
            public uint ClassTokenOrFilterOffset;
        }

        [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
        public delegate void ResetDelegate(void* pMethodDesc);

        [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
        public delegate void* DoPrestubDelegate(void* pMethodDesc, void* pDispatchingMT);

        [DllImport("clrjit.dll", BestFitMapping = false, CharSet = CharSet.Unicode, EntryPoint = "getJit", SetLastError = true)]
        public static extern void* getJit();

        [DllImport("kernel32.dll", BestFitMapping = false, CharSet = CharSet.Unicode, SetLastError = true)]
        public static extern void* GetModuleHandle(string lpModuleName);

        public static void MethodDesc_Reset(void* pMethodDesc)
        {
            if (_MethodDesc_Reset != null)
            {
                _MethodDesc_Reset.Invoke(pMethodDesc);
                return;
            }

            if (_clrModuleHandle == null)
            {
                _clrModuleHandle = GetModuleHandle("clr.dll");
            }
            IntPtr ptr = IntPtr.Add((IntPtr)_clrModuleHandle, METHODDESC_RESET_RVA);
            _MethodDesc_Reset = Marshal.GetDelegateForFunctionPointer<ResetDelegate>(ptr);
            _MethodDesc_Reset.Invoke(pMethodDesc);
        }

        public static void MethodDesc_doPrestub(void* pMethodDesc)
        {
            if (_MethodDesc_DoPrestub != null)
            {
                _MethodDesc_DoPrestub.Invoke(pMethodDesc, null);
                return;
            }

            if (_clrModuleHandle == null)
            {
                _clrModuleHandle = GetModuleHandle("clr.dll");
            }
            IntPtr ptr = IntPtr.Add((IntPtr)_clrModuleHandle, METHODDESC_DOPRESTUB_RVA);
            _MethodDesc_DoPrestub = Marshal.GetDelegateForFunctionPointer<DoPrestubDelegate>(ptr);
            _MethodDesc_DoPrestub.Invoke(pMethodDesc, null);
        }
    }
}
Program.cs
using dnlib.DotNet;
using dnlib.DotNet.Emit;
using System;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static test_dnlib.Clr;
using static test_dnlib.MinHook;

namespace test_dnlib
{
    internal sealed unsafe class Program
    {
        private unsafe delegate int CompileMethodDelegate(void* pThis, void* pICorJitInfo, void* pMethodInfo, uint flags, byte** ppEntryAddress, uint* pNativeSizeOfCode);
        static private IntPtr origingalCompileMethod = IntPtr.Zero;
        static private CompileMethodDelegate ClrJitCompileMethodDelegate;

        static private int mainMethodIndex = 0;
        static private void* moduleHandle = null;
        static private ModuleDefMD moduleDef;
        static private void*[] methodHandles;
        private static bool Initialize()
        {
            MH_Initialize();
            return true;
        }
        private static void Uninitialize()
        {
            MH_Uninitialize();
        }

        private static void*[] GetAllMethodHandles(Module module, ModuleDefMD moduleDef)
        {
            void*[] methodHandles;
            ModuleHandle moduleHandle;

            methodHandles = new void*[moduleDef.TablesStream.MethodTable.Rows];
            moduleHandle = module.ModuleHandle;
            for (int i = 0; i < methodHandles.Length; i++)
            {
                MethodDef methodDef;

                methodDef = moduleDef.ResolveMethod((uint)i + 1);
                if (!methodDef.HasBody)
                    continue;

                methodDef.Body.KeepOldMaxStack = true;
                methodHandles[i] = (void*)moduleHandle.ResolveMethodHandle(0x06000001 + i).Value;

                if (methodDef.Name == "Main")
                    mainMethodIndex = i;
            }
            return methodHandles;
        }

        private static bool HookcompileMethod()
        {
            void** CILJitVTable = *(void***)Clr.getJit();
            void* compileMethod = CILJitVTable[0];

            // Get the function pointer for the delegate
            IntPtr functionPointer = Marshal.GetFunctionPointerForDelegate(new CompileMethodDelegate(CompileMethodDetours)); ;

            if (MH_CreateHook((IntPtr)compileMethod, functionPointer, out origingalCompileMethod) != MH_STATUS.MH_OK)
            {
                return false;
            }
            ClrJitCompileMethodDelegate = Marshal.GetDelegateForFunctionPointer<CompileMethodDelegate>(origingalCompileMethod);
            if (MH_EnableHook((IntPtr)compileMethod) != MH_STATUS.MH_OK)
            {
                return false;
            }
            return true;
        }

        private static void PrepareAllMethods()
        {
            // 预先触发当前程序集JIT
            const BindingFlags BindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;

            foreach (MethodBase methodBase in
            Assembly.GetExecutingAssembly().ManifestModule.GetTypes()
            .SelectMany(t => Enumerable.Concat<MethodBase>(t.GetMethods(BindingFlags), t.GetConstructors(BindingFlags)))
            .Where(m => !m.IsAbstract && !m.ContainsGenericParameters))
            {
                try
                {
                    RuntimeHelpers.PrepareMethod(methodBase.MethodHandle);
                }
                catch
                {
                }
            }
        }
        private static void DoJitComileMethod(void* methodHandle)
        {
            MethodDesc_Reset(methodHandle);
            MethodDesc_doPrestub(methodHandle);
            MethodDesc_Reset(methodHandle);
        }

        private static void PrintMainIL()
        {
            MethodDef mainMethod = moduleDef.ResolveMethod((uint)mainMethodIndex + 1);
            Console.WriteLine(mainMethod.FullName + "Body:");
            foreach (var instruction in mainMethod.Body.Instructions)
            {
                Console.WriteLine(instruction);
            }
        }
        private static unsafe int CompileMethodDetours(void* pThis, void* pICorJitInfo, void* pMethodInfo, uint flags, byte** ppEntryAddress, uint* pNativeSizeOfCode)
        {

            CORINFO_METHOD_INFO_40* methodInfo = (CORINFO_METHOD_INFO_40*)(pMethodInfo);


            if (methodInfo->scope != moduleHandle)
            {

                return ClrJitCompileMethodDelegate(pThis, pICorJitInfo, pMethodInfo, flags, ppEntryAddress, pNativeSizeOfCode);
            }
            int methodIndex = -1;
            for (int i = 0; i < methodHandles.Length; ++i)
            {
                if (methodHandles[i] == methodInfo->ftn)
                {
                    methodIndex = i;
                    break;
                }
            }

            if (methodIndex == -1)
            {
                return ClrJitCompileMethodDelegate(pThis, pICorJitInfo, pMethodInfo, flags, ppEntryAddress, pNativeSizeOfCode);
            }
            MethodDef methodDef = moduleDef.ResolveMethod((uint)methodIndex + 1);


            if (methodDef.Name == "Main")
            {
                byte[] code = new byte[methodInfo->ILCodeSize];
                Marshal.Copy((IntPtr)methodInfo->ILCode, code, 0, code.Length);

                Debug.Assert(methodInfo->locals.numArgs == 0);
                Debug.Assert(methodInfo->EHcount == 0);
                methodDef.FreeMethodBody();
                methodDef.Body = MethodBodyReader.CreateCilBody(moduleDef, code, null, methodDef.Parameters, 0, (ushort)methodInfo->maxStack, (uint)code.Length, 0); ;
            }

            return ClrJitCompileMethodDelegate(pThis, pICorJitInfo, pMethodInfo, flags, ppEntryAddress, pNativeSizeOfCode);
        }
        static unsafe void Main(string[] args)
        {
            try
            {
                if (args.Length != 1)
                {
                    Console.WriteLine("invalid argments");
                    return;
                }
                string moduleFullPath = args[0];
                Initialize();
                PrepareAllMethods();
                Module module = Assembly.LoadFile(moduleFullPath).ManifestModule;

                // 获取模块句柄
                moduleHandle = (void*)(IntPtr)module.GetType().GetField("m_pData", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(module);

                // 获取模块中所有函数句柄
                moduleDef = ModuleDefMD.Load(moduleFullPath);
                methodHandles = GetAllMethodHandles(module, moduleDef);

                if (!HookcompileMethod())
                {
                    Console.WriteLine("[FATAL] Failed to Hook CompileMethod");
                    return;
                }

                PrintMainIL();

                // 调用 <Module>.cctor
                MethodDef module_cctor = moduleDef.GlobalType.FindStaticConstructor();
                module.ResolveMethod(module_cctor.MDToken.ToInt32()).Invoke(null, null);

                // 主动触发Jit
                for (int i = 0; i < methodHandles.Length; i++)
                {
                    if (methodHandles[i] == null)
                        continue;

                    MethodDef methodDef = moduleDef.ResolveMethod((uint)i + 1);
                    // a函数jit会崩溃,先不深究
                    if (!methodDef.HasBody || methodDef.Name == "a")
                        continue;

                    try
                    {
                        DoJitComileMethod(methodHandles[i]);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine("[ERROR] " + "Exception: 0x" + (0x06000001 + i).ToString("X8") + " " + methodDef.ToString());
                    }
                }

                // 转储
                //TODO 
                Console.WriteLine("---------dump Main---------");
                PrintMainIL();
            }
            catch (Exception e)
            {
                Console.WriteLine("Unsupported Programs");
            }
            finally
            {
                Console.WriteLine("press any key to continue...");
                Console.ReadKey();
                Uninitialize();
            }
        }
    }
}

需要注意的是,上述代码仅用于实验验证,不可用于实际生产用途。通过本次实验,我们深入了解了 .NET 安全之 JIT 保护技术的原理和实现方式,同时也验证了通过劫持 CompileMethod 函数 dump 被 JIT 加密的 IL 代码的可行性。


网站公告

今日签到

点亮在社区的每一天
去签到