與JIT 跳隻舞

在幾年前,我寫過一篇 Dynamic Proxies in C#,內容是透過Interface、Proxy的方式置換虛擬函式,這是許多Mock Library用的手法,事實上除了這個手段外,還有另一個可以達到類似效果的技巧,那就是透過JIT 引擎,用更粗暴的方式來置換IL Code,跟Dynamic Proxies手法不同的是這個方式是針對函式本身,可以影響到靜態與非虛擬函式。

CLR 中的 JIT運作流程

   .NET 是個 Managed 平台,除了預先NGEN的函式之外,所有的函式都是在呼叫的當下進行IL Code轉換Native Code,負責這個工作的就是JIT引擎,因此如果可以取得這個JIT 引擎的物件進行重導向,那麼就能夠接手JIT 的工作,在IL Code 轉換 Native Code之前置換 IL Code,這樣就能夠以偷天換日的手法把一個函式的內容變成另一個函式。

取得JIT 引擎物件

  首先要解決的是如何取得JIT 引擎的物件,ClrJit.dll export了一個getJIT()函式,呼叫這個函式便可以取得JIT 引擎物件,這個物件有一個成員函式: compileMethod,當CLR 需要JIT 編譯IL Code時就會呼叫這個函式進行編譯,我們可以透過以下的定義來呼叫getJIT函式:

[DllImport("Clrjit.dll", CallingConvention = CallingConvention.StdCall, PreserveSig = true)]
private static extern IntPtr getJit();

接下來的問題是如何對compileMethod這個函式進行重導向,getJIT()函式回傳的其實是一個介面:ICorJitCompiler,只要是介面,就會有VTable,只要有VTable,那麼就可以置換。

public static IntPtr VTableAddr
{
     get
     {
         IntPtr pVTable = getJit();
         if (pVTable == IntPtr.Zero)
             throw new Exception("Could not retrieve address for getJit");
         return pVTable;
     }
}

public static unsafe bool Initialize()
{
     NewCompileMethod = HookedCompileMethod;
     IntPtr pCompileMethod = Marshal.ReadIntPtr(VTableAddr);
     uint old;
     if (!JITNative.VirtualProtect(pCompileMethod, (uint)IntPtr.Size,
                    JITNative.Protection.PAGE_EXECUTE_READWRITE, out old))
         return false;
     OriginalCompileMethod =
                (JITNative.CompileMethodDelegate)
                    Marshal.GetDelegateForFunctionPointer(Marshal.ReadIntPtr(pCompileMethod), typeof(JITNative.CompileMethodDelegate));

     RuntimeHelpers.PrepareDelegate(NewCompileMethod);
     RuntimeHelpers.PrepareDelegate(OriginalCompileMethod);
     RuntimeHelpers.PrepareMethod(typeof(JITInterceptor).GetMethod("Initialize", BindingFlags.Static | BindingFlags.Public).MethodHandle);
     RuntimeHelpers.PrepareMethod(typeof(JITInterceptor).GetMethod("FindModule", BindingFlags.Static | BindingFlags.NonPublic).MethodHandle);
     Marshal.WriteIntPtr(pCompileMethod, Marshal.GetFunctionPointerForDelegate(NewCompileMethod));
     return JITNative.VirtualProtect(pCompileMethod, (uint)IntPtr.Size,
             (JITNative.Protection)old, out old);
}

這裡先取得VTable的指標,compileMethod是這個介面的第一個成員函式,因此只要置換第一個slot就可以了。

 

置換之後呢?

  Hook函式執行之後所有的JIT編譯動作都會被重導向到我們的HookedCompileMethod,這裡會收到IL Code及相關資訊,此時就可以進行IL Code的置換,不過置換IL Code的過程有點複雜,這裡只做到把函式名稱列出來。

private static unsafe int HookedCompileMethod(IntPtr thisPtr, [In] IntPtr corJitInfo,
            [In] JITNative.CorMethodInfo* methodInfo, int flags,
            [Out] IntPtr nativeEntry, [Out] IntPtr nativeSizeOfCode)
{
     int token;
     var module = FindModule(methodInfo->moduleHandle);
     if (module != null)
     {
         token = (0x06000000 + *(ushort*)methodInfo->methodHandle);
         Console.WriteLine("\r\n");
         Console.WriteLine("Name: " + module.ResolveMethod(token).Name);
     }
     return OriginalCompileMethod(thisPtr, corJitInfo, methodInfo, flags, nativeEntry, nativeSizeOfCode);
}

 

測試的程式如下:

class Program
{

      class MyTest
      {

            public static void TestStatic()
            {
                Console.WriteLine("Static");
            }


            public void TestInstance()
            {
                Console.WriteLine("Instance");
            }
       }


        static void Main(string[] args)
        {
            JITInterceptor.RegisterModule(typeof(MyTest).Module);           
            JITInterceptor.Initialize();
            MyTest.TestStatic();
            var c = new MyTest();
            c.TestInstance();
            JITInterceptor.UnInitialize();
            Console.ReadLine();
        }
}

 

執行結果:

縱然這個手法看起來威力相當強大,但其脆弱程度也很高,任何狀況都有可能發生,也就是說進入一個未知領域了,作為研究題材可以,實務上就要特別小心了。

GitHub Sample