.NET 安全之 JIT 保护技术

什么是JIT

Just-In-Time 编译器 (JIT) 是 .NET 公共语言运行时 (Common Language Runtime, CLR) 的核心组件之一。CLR 负责管理和执行所有 .NET 程序,无论其使用何种 .NET 编程语言(如 C#, VB.NET, F#)开发。程序编译过程分为两个主要阶段:

语言编译器阶段: 特定于编程语言的编译器(如 Roslyn for C#)将源代码编译为一种与平台无关的中间语言,即 Microsoft 中间语言 (MSIL) 或 通用中间语言 (CIL)。

JIT 编译阶段: 当程序实际运行时,JIT 编译器再将这个中间语言 (MSIL/CIL) 即时编译为目标计算机 CPU 能够直接执行的本地机器代码 (Native Machine Code),极大地提升了 .NET 应用程序的整体运行效率。。

JIT编译器的内部工作原理图:

JIT 编译的关键特性在于其“按需”编译机制:

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

JIT编译器的优缺点:

优点

  • 内存效率高: 只编译当前执行路径所需的方法,避免了将整个程序集的本地代码一次性加载到内存中,显著减少了内存占用。
  • 运行时优化: JIT 编译器能够在运行时收集实际的执行数据(如分支频率、类型信息),并基于此进行高级代码优化。这些优化包括方法内联 (Inlining)、循环优化 (Loop Unrolling, Hoisting)、寄存器分配优化等,这些是静态编译通常难以做到的。这使得最终生成的机器代码能更好地适应当前的运行环境和数据特征。
  • 平台适应性: 相同的 MSIL/CIL 可以在不同的 CPU 架构(如 x86, x64, ARM)上,由相应平台的 JIT 编译器生成本地代码,实现“一次编写,到处运行”。

缺点

  • 启动延迟: 程序启动初期,首次执行的方法需要经历 JIT 编译过程,这会带来一定的性能开销,导致启动时间相对变长,即所谓的“JIT 预热”阶段。
  • 运行时开销: 虽然缓存机制缓解了后续调用的开销,但编译过程本身需要消耗 CPU 和内存资源。对于执行路径非常复杂或包含大量首次调用方法的场景,累积的 JIT 开销可能影响性能。
  • 缓存占用: JIT 编译器需要内存来存储编译生成的本地机器代码缓存。

JIT加密

  • JIT 加密是一种保护.NET程序集的方法,它通过在JIT编译过程中对IL(中间语言)代码进行加密,然后在运行时动态解密,以此来防止静态反编译和内存转储。这种方法可以在一定程度上提高.NET应用程序的安全性,同时尽可能减少对性能的影响。
  • 在程序运行时通过劫持JIT编译相关函数比如CompileMethod,在CLR对函数进行JIT编译时,解密方法体的相关信息比如方法体,异常块,元数据的token等数据。并就将其传给真正的JIT编译函数生成正确的本地代码的保护技术。

推荐.NET保护工具

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

成熟方案: 提供了经过市场验证的、可靠的 JIT 加密保护。

高框架兼容: 兼容大多数主流 .NET Framework和 .NET Core 运行时环境。

跨平台支持: JIT 加密功能同时支持 Windows 和 Linux 操作系统平台。

综合保护: 除了 JIT 加密,还提供强大的函数级代码虚拟化 (Code Virtualization) 保护,两者可结合使用,提供更深层次的安全防御。

JIT加密效果

保护前

保护后

实验

目的

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

环境

平台: Windows x64

.net版本: .NET Framework 4.7.2

依赖库:

  1. dnlib
  2. MinHook

效果

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...

代码实现

注:下面代码仅用于实验验证,不可使用实际生产用途。

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();
    }
  }
}
}
滚动至顶部
售前客服
周末值班
电话

电话

13910187371