首页
论坛
课程
招聘
[原创].Net程序集基于方法的保护原理(HookJIT篇)
2010-7-4 22:54 34774

[原创].Net程序集基于方法的保护原理(HookJIT篇)

2010-7-4 22:54
34774
.Net程序集基于方法的保护原理(HookJIT篇)

作者:RZH     网名:看雪_grassdrago   2010-7-4

引言

    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时,我们要分析的一切都会是未知的!
    下篇文章我们将通过挂勾jitNativeCode完成解密?或者用代码实现本文的内容?或者。。。现在唯一能决定的是:谢谢网络上提供技术资料的每位网友!

            
         代码及PDF: hookjit_rzh.rar

[2023春季班]《安卓高级研修班(网课)》月薪两万班招生中~

上传的附件:
收藏
点赞0
打赏
分享
最新回复 (30)
雪    币: 245
活跃值: 活跃值 (10)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
rufus 活跃值 2 2010-7-4 23:20
2
0
沙发,顶一下
雪    币: 291
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
leking 活跃值 2010-7-5 09:33
3
0
等下篇学习~
雪    币: 15
活跃值: 活跃值 (13)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
safeaz 活跃值 2010-7-5 17:29
4
0
网上遍地都是, 人都说烂了。 你发出来,居然加精?!!!
雪    币: 776
活跃值: 活跃值 (13)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
baoobao 活跃值 2010-7-5 18:37
5
0
太深奥了 再学一遍啊
雪    币: 312
活跃值: 活跃值 (29)
能力值: ( LV9,RANK:140 )
在线值:
发帖
回帖
粉丝
grassdrago 活跃值 3 2010-7-5 21:36
6
0
哈哈哈哈...safeaz气晕了.
其实啊,"发出来"就是关键了,高手们知道了不说,或不屑一说,泛谈多实例少,最深层的技术则藏着掩着大家难得一窥.技术本身是层窗户纸,也总会过时,无论含量多少,讲清楚,给出代码例子,让后来学习的人可以一目了然,节省时间,这方面国外的大师们做很棒.而我只是钻个空子罢了.

哈哈...想想还是让人乐.坛里.net气氛并不浓,前时也拜读过你关于DNGuard的文章,其实把你的分析和代码细致些贴出来就是精华.祝好!
雪    币: 15
活跃值: 活跃值 (13)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
safeaz 活跃值 2010-7-6 12:10
7
0
哈哈,过去的事就算了。 那是和Rick玩的。
雪    币: 213
活跃值: 活跃值 (63)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
jasonzhou 活跃值 2010-7-7 09:22
8
0
特意登陆赞精神
雪    币: 238
活跃值: 活跃值 (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
七夜血 活跃值 2010-7-7 14:46
9
0
保护后的APP运行不了
上传的附件:
雪    币: 312
活跃值: 活跃值 (29)
能力值: ( LV9,RANK:140 )
在线值:
发帖
回帖
粉丝
grassdrago 活跃值 3 2010-7-8 15:00
10
0
楼上,您好!

从给出的抓图来看,和删除掉HookJitPrj.dll的运行效果是一样的。也就是说:HookJitPrj.dll没有运行,挂勾没完成,程序跑的是换成了空的方法体,没能在解析时替换回正确的方法字节码,所以出错了。

问题可能出在这里:
//加入Program类
[DllImport("HookJitPrj.dll", CallingConvention = CallingConvention.Cdecl)]
 private static extern void HookJIT();


部分.net1.1和.net2.0混杂的机器环境下(更具体的环境不明),系统找不到指定的非托管DLL,导致HookJIT()方法不会被执行。

你可以试一下把HookJitPrj.dll直接放入System32系统目录中,如果错误消除,说明猜测正确,这种情况的最简单解决方法为:拷入系统路径或改为固定路径 [DllImport("C:\\HookJitPrj.dll",。。。。],智能一点的办法是PInvoke kernel32.dll中的API获取指针然后用委托调用了。

如果HookJitPrj.dll放入系统路径仍旧不能执行的话,猜想您写的任意[DllImport"非托管.dll",。。。。]都执行不了的。我不知道它怎么了,只好把环境搬来一起研究一下了。
雪    币: 4199
活跃值: 活跃值 (1003)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
chinarenjf 活跃值 2010-7-8 15:24
11
0
期待下文的jitNativeCode...
雪    币: 228
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ssyfzy 活跃值 2010-7-20 15:37
12
0
楼主看下:
http://hi.baidu.com/kingcham/blog/item/28fa43951b2b2112d31b70d9.html

好像你的HookJitPrj.dll编译有问题
雪    币: 228
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ssyfzy 活跃值 2010-7-20 15:39
13
0
异常:已引发:“无法加载 DLL“HookJitPrj.dll”: 应用程序无法启动,因为应用程序的并行配置不正确。有关详细信息,请参阅应用程序事件日志,或使用命令行 sxstrace.exe 工具。 (异常来自 HRESULT:0x800736B1)。”(System.DllNotFoundException)
引发了一个 System.DllNotFoundException:“无法加载 DLL“HookJitPrj.dll”: 应用程序无法启动,因为应用程序的并行配置不正确。有关详细信息,请参阅应用程序事件日志,或使用命令行 sxstrace.exe 工具。 (异常来自 HRESULT:0x800736B1)。”
雪    币: 228
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ssyfzy 活跃值 2010-7-20 15:49
14
0
我的机器上,不做任何更改,重新编译一下HookJitPrj.dll就行了
雪    币: 228
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ssyfzy 活跃值 2010-7-20 16:03
15
0
楼主,我有个问题,要得到一个方法的字节很简单,但是反过来,已知字节,怎么得到方法呢?

MethodInfo/MethodDefinition GetMethod(byte[]){}

想通过byte[]得到MethodInfo或者Mono.Cecil中的MethodDefinition,以便调用,该怎么做呢?
雪    币: 154
活跃值: 活跃值 (14)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
opensrc 活跃值 1 2010-7-21 16:38
16
0
[QUOTE=ssyfzy;837965]楼主,我有个问题,要得到一个方法的字节很简单,但是反过来,已知字节,怎么得到方法呢?

MethodInfo/MethodDefinition GetMethod(byte[]){}

想通过byte[]得到MethodInfo或者Mono.Cecil中的MethodDefinition,以...[/QUOTE]

有点风、马、牛!不过万物都有联系。

把字节码填回原方法体(只针对这个简单例子)然后Mono.Cecil。。 AssemblyDefinition asm = AssemblyFactory.GetAssembly(。。。; foreach (TypeDefinition type in asm.MainModule.Types)想改什么改什么了。
反射则得到classtype后:
MethodInfo mi = yourGettype.GetMethod("方法名");然后动态改什么是什么了。
雪    币: 228
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ssyfzy 活跃值 2010-7-23 11:15
17
0
把字节码填回原方法体

怎么填呢?要在程序中填,而不是手动
比如我的程序中有一个
void Run()
{
}
函数,反编译什么也看不到,运行时我怎么把byte[]填进去并调用?
雪    币: 152
活跃值: 活跃值 (16)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
sbjofjhh 活跃值 2010-7-23 15:16
18
0
学习学习下。。。这好东西啊
雪    币: 173
活跃值: 活跃值 (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
tdhao 活跃值 2010-7-25 02:35
19
0
lz共享精神可嘉,学习中
雪    币: 194
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
fitmaster 活跃值 2010-11-18 15:08
20
0
 我是写.NET程序的,希望保护好劳动成果!
雪    币: 90
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
误伤友军 活跃值 2010-12-20 04:20
21
0
一般只看贴不回贴,今天必须得回!绝大多数.NET破解的文章毫无疑义,能看到源码了还破解个屁,楼主的文章很有建设意义!赞!
雪    币: 5840
活跃值: 活跃值 (880)
能力值: ( LV13,RANK:283 )
在线值:
发帖
回帖
粉丝
littlewisp 活跃值 2 2011-1-22 18:06
22
0
学习了,正好用上
雪    币: 225
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wxhanshan 活跃值 2011-1-22 18:35
23
0
支持凤姐顶一下
雪    币: 90
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
误伤友军 活跃值 2011-1-24 16:47
24
0
请在静态库中使用 MFC,但我在执行到加载mscorjit.dll仍不成功,貌似找不到,把mscorjit.dllCOPY到当前目录下可以了,跟了下,没有发现执行my_compileMethod方法,怀疑楼主的例子真的与挂勾没有半毛钱关系
雪    币: 244
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
听雨前缘 活跃值 2011-5-31 11:26
25
0
收藏了,!~
游客
登录 | 注册 方可回帖
返回