程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> .Net程序集基於方法的保護原理(HookJIT篇)

.Net程序集基於方法的保護原理(HookJIT篇)

編輯:關於.NET

引言

DOTNET程序集的保護由混淆、整體加密、基於方法保護到參與偽IL指令本地化,逐步由純.NET領域走向傳統WIN32加密領域。相應,解密的主體工作也由過去的 IL代碼分析走向了ASM代碼分析。我們留戀過去的“開源盛世”,但不得不正視現實。基於方法的保護是這一過渡中關鍵的一環,可見的實例代碼太少了,本文將通過手動實踐學習它的基本原理。

JIT及相關內容簡介

JIT Compiler(Just-in-time Compiler) 即時編譯。.net中當一個方法第一次被調用時,虛擬機會調用JIT來編譯生成本地代碼。也就是說.net的即時編譯是基於每個方法的,它的具體實現由MSCORJIT.DLL提供。當方法第一次被調用時,調用方從MethodTable中讀取指向一個代碼塊的地址,也就是方法的描述(MethodDesc),然後調用這個塊,塊接著調用JIT。當JIT完成了編譯後,將改變MethodTable,使其直接指向已經被JIT編譯過的代碼,也就是說無論代碼是否被JIT編譯,對方法的調用都是通過調用MethodTable中方法地址來實現的。

這正是我們要關注的兩個地方,MSCORJIT.DLL中的編譯函數CILJit::compileMethod以及PE格式文件中的MethodTable。MethodTable以後再提及,先讓我們看看JIT的方法調用流程:

1.MSCORJIT.DLL只提供了唯一一個導出函數getJit(),它返回一個虛表指針,而這個虛表的第一項就是CILJit::compileMethod函數指針。

2.CILJit::compileMethod函數不做任何工作,直接調用了jitNativeCode方法。而jitNativeCode則會調用Compiler::compCompile完成實質工作。

3.需要注意的是這和SSCLI並不完全相同,但上述方法中使用的接口和結構SSCLI已經給出:corinfo.h 和 corjit.h,掛勾時需要它們。

下面我們來看看getJit()方法在MSCORJIT.DLL和SSCLI中的實現,因為我們要調用getJit()得到CILJit::compileMethod函數指針,並通過替換它完成我們的掛勾,這是個穩妥的方法,不需要根據不同操作系統和運行時版本去找內存地址,加密程序需要穩定:

MSCORJIT中:

int *__cdecl getJit()
{
int *result; // eax@1
result = (int *)dword_790B7260;
if ( !dword_790B7260 )
{
result = &dword_790B7268;
dword_790B7268 = (int)&CILJit___vftable_;
dword_790B7260 = (int)&dword_790B7268;
}
return result;
}

SSCLI中:

extern "C"
ICorJitCompiler* __stdcall getJit()
{
     static char FJitBuff[sizeof(FJitCompiler)];
     if (ILJitter == 0)
     {
         // no need to check for out of memory, since caller checks for return value of NULL
         ILJitter = new(FJitBuff) FJitCompiler();
         _ASSERTE(ILJitter != NULL);
     }
     return(ILJitter);
}
class FJitCompiler : public ICorJitCompiler
{

public:

/* the jitting function */
     CorJitResult __stdcall compileMethod (
             ICorJitInfo*            comp,               /* IN */
             CORINFO_METHOD_INFO*    info,               /* IN */
             unsigned                flags,              /* IN */
             BYTE **                 nativeEntry,        /* OUT */
             ULONG *                nativeSizeOfCode    /* OUT */
             );

     /* notification from VM to clear caches */
     void __stdcall clearCache();
     BOOL __stdcall isCacheCleanupRequired();

     static BOOL Init();
     static void Terminate();

private:

/* grab and remember the jitInterface helper addresses that we need at runtime */
     BOOL GetJitHelpers(ICorJitInfo* jitInfo);
};

現在我們給出MSCORJIT.DLL中將要掛勾的CILJit::compileMethod的聲明:

int __stdcall CILJit::compileMethod(ULONG_PTR classthis, ICorJitInfo *comp,
                            CORINFO_METHOD_INFO *info, unsigned flags,
                            BYTE **nativeEntry, ULONG *nativeSizeOfCode)

(這和上面FjitCompiler:: compileMethod的聲明幾乎一樣)

在CILJit::compileMethod的入參中讓我們盯住一個讓人眼饞的結構CORINFO_METHOD_INFO:

CORINFO_METHOD_INFO 結構:

struct CORINFO_METHOD_INFO
{
     CORINFO_METHOD_HANDLE       ftn;
     CORINFO_MODULE_HANDLE       scope;
     BYTE *                      ILCode;
     unsigned                    ILCodeSize;
     unsigned short              maxStack;
     unsigned short              EHcount;
     CorInfoOptions              options;
     CORINFO_SIG_INFO            args;
     CORINFO_SIG_INFO            locals;
};

ILCode:指向方法的IL代碼體;  ILCodeSize:方法IL代碼體的大小(字節為單位);

maxStack:方法最大堆棧數;scope:使用另一個入參IcorJitInfo中的方法是要用到的MODULE句柄。

Ftn:使用另一個入參IcorJitInfo中的方法是要用到的METHOD句柄。

正是這個CORINFO_METHOD_INFO 結構中的ILCODE,當我們加密時可以將真正的IL字節碼賦給它交由JIT編譯本地碼,當我們解密時通過它得到正確的IL字節碼填回MethodTable。

保護樣例的實現及說明

首先,我們需要一個C#的樣例程序,我仍用前幾篇文章中用過的APPCALLDLL.exe.它非常簡單,只有一個按鈕,按鈕中調用了兩個方法:doAddFun()、doDllTwoFun();

MessageBox.Show(doAddFun());

MessageBox.Show(doDllTwoFun());

這兩個方法則調用了兩個DLL中的相應方法。我們的任務是對這兩個方法進行保護,並在運行時解密。(樣例程序見附件)

一、獲得控制權

為了方便地啟動解碼程序,也即HookDll,加密程序通常會在<Module>中加入靜態構造函數.cctor();並在其中調用HookDll的某個方法,作為實驗和慣例我們采用先期在Progrom類和Main方法中加入如下代碼:

//加入Program類
[DllImport("HookJitPrj.dll", CallingConvention = CallingConvention.Cdecl)]
  private static extern void HookJIT();
//加入Main()方法
try{HookJIT();}catch { }//保證有沒有hookdll都正常運行
//注:代碼中的HookJitPrj.dll和HookJIT()就是我們將要編寫的hookdll和啟動方法。

當然,這一步你可以通過Mono.Cecil或ildasm/ilasm來完成。

二、提取要保護方法的方法體並用亂碼填充原方法體

現在我們用CFF Explore打開我們的樣例程序APPCALLDLL.exe並提取出上述兩個方法的方法體來,然後用NOP填充,也即0。這步工作在程序中可用 Mono.Cecil求出方法RVA和CodeSize後通過我們自己的PE讀寫完成。(需要注意的是:別偷賴讓Mono代為完成寫NOP操作,因為它會重構PE,之後很多資源索引都可能變化,我們提取出的代碼體就失效了.別外:方法的CodeSize屬性只有在Mono.Cecil.0.6.9.0後才有,之前的只好用RVA指出的方法頭算一下了),這裡我們用CFF Explore手動完成:

1.打開MetaDataStreams->Tables->Method(15)->doAddFun,反鍵Disassamble Method.如下圖:

2.記下Opcode列的字節碼,然後每行都用反鍵NOP Instruction替換。

3.同樣另一個方法也同樣記下後替換,然後保存。現在看看吧:

4.如果用reflector.exe察看你會發現方法為空,如果你只替換了一部分,則更有意想不到的結果。

三、HOOKjit並還原保護的方法體

關鍵代碼:

extern "C" __declspec(dllexport) void HookJIT()

{

if (bHooked) return;

      LoadLibrary(_T("mscoree.dll"));

      HMODULE hJitMod = LoadLibrary(_T("mscorjit.dll"));

      if (!hJitMod)
          return;

      p_getJit = (ULONG_PTR *(__stdcall *)()) GetProcAddress(hJitMod, "getJit");

      if (p_getJit)
      {
          JIT *pJit = (JIT *) *((ULONG_PTR *) p_getJit());

          if (pJit)
          {
               DWORD OldProtect;
               VirtualProtect(pJit, sizeof (ULONG_PTR), PAGE_READWRITE, &OldProtect);
               compileMethod = pJit->compileMethod;
               pJit->compileMethod = &my_compileMethod;
               VirtualProtect(pJit, sizeof (ULONG_PTR), OldProtect, &OldProtect);
               bHooked = TRUE;
          }
      }
}

說明:

compileMethod = pJit->compileMethod;//保存虛表指針指向的原函數指針;

pJit->compileMethod = &my_compileMethod;//虛表指針指向我的的替換函數;

我們的代換函數裡做了什麼?很簡單,判斷方法名並替換方法體為我們剛才記下來的字節碼,然後調用原函數返回:

int __stdcall my_compileMethod(ULONG_PTR classthis, ICorJitInfo *comp,
                                     CORINFO_METHOD_INFO *info, unsigned flags,
                                     BYTE **nativeEntry, ULONG *nativeSizeOfCode)
{
    const char *szMethodName = NULL;
    const char *szClassName = NULL;
    szMethodName = comp->getMethodName(info->ftn, &szClassName);
     if (strcmp(szMethodName, "doAddFun") == 0)
      {
          info->ILCode=doAddFunCode;
      }
       if (strcmp(szMethodName, "doDllTwoFun") == 0)
      {
          info->ILCode=doDllTwoFunCode;
      }

      // call original method

      int nRet = compileMethod(classthis, comp, info, flags, nativeEntry, nativeSizeOfCode);
      return nRet;
}

請參見隨文檔提供的代碼及項目文件。

運行情況

現在把編譯好的HookJitPrj.dll和改造完成的AppCallDll.exe放在一起,對了,還那兩個沒什麼用的dll(方法裡要調用,原是上篇文章用來做整體打包實驗的),運行良好。而刪除HookJitPrj.dll後試試,出錯了,如果你在剛才的替換中把最後一個RET也即字節碼2A 留下,雖然沒了功能,但依然不會有運行錯誤。

現在核心在HookJitPrj.dll中了,你可以用win32的任何方式加密它。而改造過的AppCallDll.exe中已經沒有真的方法體了。

結語

這僅僅是個簡單的原理性實驗,你可以通過程序的方式實現所有的步驟,並把真正的字節碼加密保存在一個新的節區裡。通過自定義的結構描述每個方法所使用的加密方式等,甚至在自己的替換函數中多次調用原函數傳遞虛假值並攔截錯誤和只有你才知道的時候傳遞真值,但這一切仍逃不過掛勾,所以IL指令的替換和取代是更進一步的保護,因為當它的解碼不再依賴MSJIT時,我們要分析的一切都會是未知的!

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved