星期一, 九月 15, 2008

hook compileMethod的几种常见形式


compileMethod算是.NET中的万能断点,对付一般的.NET加密壳均可采用hook compileMethod的方式得到IL代码和相关信息,保护层次较深的.NET壳除外。

我们自己在编写程序hook该方法时,一般有以下几种形式。

第一种形式是:my_compileMethod(既需要替换的方法)采用compileMethod的原型。这种形式代码漂亮,结构工整,编程方便,推荐使用。通常代码可以如下定义:

int __stdcall my_compileMethod(ULONG_PTR classthis,

                                                     ICorJitInfo *comp,

                                                     CORINFO_METHOD_INFO *info,

                                                      unsigned flags,BYTE **nativeEntry,

                                                     ULONG   *nativeSizeOfCode)

{

//你的代码

//调用原始的compileMethod

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

return nRet;

}

上面的代码还有一个优点,就是可直接兼容x64位

    第二种形式:naked。既调用约定不采用__stdcall,而是用naked。代码如下:

void __declspec(naked) mycompileMethod()

{

//你的代码

}

这种形式明显低级一些,许多工作要自己做,比如取CORINFO_METHOD_INFO *时,就要从堆栈中取值:

__asm

{

pushad

pushfd

mov eax,DWORD PTR[esp+48]

push eax

pop pmethodinfo

}

还有就是,直接使用x86汇编代码则无法兼容x64,需编写两套程序。当然,也不是一点好处没有,比如很多hook程序在开始喜欢保存全部的寄存器和状态,这里就可以使用pushad和pushfd了。

上面两种方法,在具体hook时,通常采取替换CILJit::'vtable'的第一项: dd offset CILJit::compileMethod的地址实现。该值可以通过getJit()函数获得。

第三种方法就更直接了,在JIT的路径中找到合适的位置后,直接Jmp。这种naked+jmp的方式,最底层,也最灵活,在对于某些hook位置较深的壳来说,这也是唯一的方法。不过代码编写时,工作量就比前两种要稍大一些了。

Btw:在编写.NET内核hook程序时,C++/CLI是不二之选。真得很爽!


    compileMethod方法在各类.NET内核的文章中出现频率相当高,因为它是JIT引擎工作的关键函数。其原型如下(参考sscli代码):

代码:
CorJitResult __stdcall FJitCompiler::compileMethod (                ICorJitInfo*                  compHnd,               /* IN */                CORINFO_METHOD_INFO*           info,                  /* IN */                unsigned                   flags,                 /* IN */                BYTE **                    entryAddress,          /* OUT */                ULONG     *                   nativeSizeOfCode       /* OUT */                )
     一般的用法是通过该函数的第二个参数COFINFO_METHOD_INFO取得代码的IL和大小。但其实还可以更加深入,注意第一个传入参数很有意思,指向了ICorJitInfo接口。该接口定义如下:
代码:
/*********************************************************************************  * a ICorJitInfo is the main interface that the JIT uses to call back to the EE and  *      get information  *********************************************************************************/ class ICorJitInfo : public virtual ICorDynamicInfo {//省略}
     这说明ICorJitInfo继承了ICorDynamicInfo接口。而后者的定义如下:
代码:
/*****************************************************************************  * ICorDynamicInfo contains EE interface methods which return values that may  * change from invocation to invocation.     They cannot be embedded in persisted  * data; they must be requeried each time the EE is run.  *****************************************************************************/  class ICorDynamicInfo : public virtual ICorStaticInfo {//省略}
     这说明ICorDynamicInfo又继承了ICorStaticInfo。接着来,继续看ICorStaticInfo的代码:

代码:
/*****************************************************************************  * ICorStaticInfo contains EE interface methods which return values that are  * constant from invocation to invocation.     Thus they may be embedded in  * persisted information like statically generated code. (This is of course  * assuming that all code versions are identical each time.)  *****************************************************************************/ class ICorStaticInfo : public virtual ICorMethodInfo, public virtual ICorModuleInfo,                           public virtual ICorClassInfo,     public virtual ICorFieldInfo,                           public virtual ICorDebugInfo,     public virtual ICorArgInfo,                           public virtual ICorLinkInfo,      public virtual ICorErrorInfo
     这一次牛了,ICorStaticInfo继承了8个接口的方法,其中第一个为ICorMethodInfo。该接口定义了许多与方法相关的函数,如果能调用它,在hook中是非常爽的。那可不可以调用呢?当然可以,compileMethod的第一个参数就是我们需要的。
     比如我们需要取得当前hook方法的方法名,可以调用ICorMethodInfo中的如下方法:
代码:
       virtual const char* __stdcall getMethodName (                CORINFO_METHOD_HANDLE          ftn,           /* IN */                const char                   **moduleName     /* OUT */                ) = 0;
     这里需要传入CORINFO_METHOD_HANDLE这个参数,该参数是.NET内核中表示方法的核心结构,在sscli中也未公开。不过它的值是可以取得的,从compileMethod第二个参数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; };
     同样,在调用ICorModuleInfor接口中的许多方法时,也需要传入CORINFO_MODULE_HANDLE作为参数,同样可以从CORINFO_METHOD_INFO结构中获得该值。比如调用findClass函数,以取得CORINFO_CLASS_HANDLE,定义如下:
代码:
       virtual CORINFO_CLASS_HANDLE __stdcall findClass (                CORINFO_MODULE_HANDLE          module,        /* IN     */                unsigned                       metaTOK,       /* IN     */                CORINFO_CONTEXT_HANDLE         context,       /* IN     */                CorInfoTokenKind               tokenKind = CORINFO_TOKENKIND_Default /* IN     */                ) = 0;
     其中出现了CORINFO_CONTEXT_HANDLE。该结构也非常容易取得,具体参考sscli,就不详述了。
     具体在VS中编程时,可以添加corinfo.h和corjit.h,并在同一目录下添加corhdr.h,便可顺利编译通过。
     最后还有一个问题,sscli毕竟是早期框架了,还是精简版,现在还能直接使用吗?不妨分析一下。随便运行一个.NET程序,用WinDbg调试并中断在compileMethod处,查看ICorJitInfo值所指的内存:
代码:
0012ea38 79f10654 mscorwks!CEEJitInfo::`vbtable' 0012ea3c 00174d18  0012ea40 00997850  0012ea44 00107210  0012ea48 00000000  0012ea4c 0018a610  0012ea50 00000000  0012ea54 00000000  0012ea58 00000000  0012ea5c 0012ea40  0012ea60 00000000  0012ea64 00000000  0012ea68 00000000  0012ea6c 79f105b8 mscorwks!CEEJitInfo::`vftable' 0012ea70 00000000  0012ea74 79f10584 mscorwks!CEEJitInfo::`vftable' 0012ea78 00000000  0012ea7c 79f104e0 mscorwks!CEEJitInfo::`vftable' 0012ea80 00000000  0012ea84 79f104bc mscorwks!CEEJitInfo::`vftable' 0012ea88 00000000  0012ea8c 79f104a4 mscorwks!CEEJitInfo::`vftable' 0012ea90 00000000  0012ea94 79f10498 mscorwks!CEEJitInfo::`vftable' 0012ea98 00000000  0012ea9c 79f10494 mscorwks!CEEJitInfo::`vftable' ...//下略
     这里又涉及到VC编译器对类的vftable和vbtable在内存中的布局问题了,最先两项是类自身定义的虚方法表和虚基址表,相关资料请自己查阅。我们跟进vftable:
代码:
79f10624 79f106ac mscorwks!CEEJitInfo::getMemoryManager 79f10628 79f11d82 mscorwks!CEEJitInfo::allocMem 79f1062c 79f11f39 mscorwks!CEEJitInfo::allocGCInfo 79f10630 7a12b8cb mscorwks!CEEJitInfo::getEHInfo 79f10634 7a12b6cf mscorwks!CEEJitInfo::yieldExecution 79f10638 79f16373 mscorwks!CEEJitInfo::setEHcount 79f1063c 79f16491 mscorwks!CEEJitInfo::setEHinfo 79f10640 7a12ed41 mscorwks!CEEJitInfo::logMsg 79f10644 7a27ffcc mscorwks!ZapperModule::doAssert 79f10648 7a12eeb6 mscorwks!CEEJitInfo::allocBBProfileBuffer 79f1064c 7a2c2fa1 mscorwks!MDInternalRO::ConvertTextSigToComSig 79f10650 79f0efee mscorwks!CEEJitInfo::isVerifyOnly 79f10654 fffffffc
       这些地址在静态编译时就已经确定了,因此如果用IDA反编译mscorwks.dll,同样会得到这些值。这便是最新的.NET框架内核中ICorJitInfo定义的方法。和sscli对比下(在corjit.h文件中),完全一样!(也可以再对比其它关键接口的方法,应该也是一样,我没有详细对比了。)
     这样,我们就得到如下的结论:.NET内核框架从2.0开始,内核变化不大,包括sscli的内核代码,这些可以从内部函数的定义看出来;通过hook compileMethod,可以得到ICorJitInfo等关键接口,并调用其中的许多内部方法;具体编写时,可以在VS中引入sscli的corinfo.h、corjit.h和corhdr.h等文件,之后便可以直接调用。因此,通过compileMethod,我们可以做的事很多很多,远不限于仅获得某个方法的IL。

没有评论: