65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 内部机制和代码注入

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (51投票s)

2008年5月14日

CPOL

24分钟阅读

viewsIcon

150232

downloadIcon

5346

一篇关于 .NET 内部机制和代码注入的文章

目录

引言

本文显然是之前编写 Rebel.NET 应用程序的努力的结晶,也是关于 .NET Framework 内部机制和 .NET 程序集可用保护的两篇文章系列中的第一篇。下一篇文章将讨论 .NET 本机编译。由于 JIT 的内部工作原理尚未分析,如今 .NET 保护相当幼稚。一旦逆向工程社区将注意力集中在这项技术上,这种情况将迅速改变。这两篇文章旨在提高人们对 .NET 保护现状以及可以实现但尚未实现的事情的认识。特别是,当前关于 .NET 代码注入的文章代表着,可以说,现在,而下一篇关于 .NET 本机编译的文章代表着未来。我在这两篇文章中介绍的内容在我撰写时是新的,但我预计它将在不到一年的时间内过时。当然,这是显而易见的,因为我正在迈出摆脱当前 .NET 保护,走向更好保护的第一步。但本文并非真正关于保护:探索 .NET Framework 内部机制可用于许多目的。因此,谈论保护只是达到目的的手段。

什么是 .NET 代码注入?

.NET 代码注入是 .NET 打包器(将整个程序集解包到内存中)的“强大”兄弟。 .NET 代码注入器所做的是钩子 JIT,当请求某个方法的 MSIL 代码时,它们会过滤请求并提供真实的 MSIL,而不是程序集中包含的 MSIL,后者通常只是一个 ret。通过一次注入一个(或几乎一个)方法,MSIL 代码将保持隐藏。即使有人设法转储代码,也不应期望保护在 .NET 程序集中为真实的 MSIL 代码留出必要的空间,尽管许多商业保护确实如此。从头开始重建程序集是普遍有效的方法。当然,这对于 Rebel.NET 来说不是问题。

读者应该明白,.NET 代码注入器并不是可靠的保护。它只是在与逆向工程师玩捉迷藏。但是,由于许多软件生产商正在将他们的知识产权置于此类保护之下,因此有必要对其进行全面分析。

.NET 代码注入是如何工作的?

有一点应该从一开始就明确:注入 MSIL 的方法不止一种。因此,要移除这种保护,您必须评估具体情况。

一种非常干净但尚未被使用的方法是通过 .NET 性能分析 API 注入 MSIL。MSDN 上有一篇 Aleksandr Mikunov 撰写的关于 .NET 性能分析 API 的深入文章。然而,正如我之前所说,这种方法并未被 .NET 保护所使用。我之所以称这种方法为干净,仅仅因为它使用了框架本身提供的 API。因此,无论如何,它都将在所有 .NET Framework 上运行。而 .NET 保护通常会钩子 JIT,尽管这可能同样有效,但并不能保证一定如此。

.NET Framework 的 JIT 包含在 *mscorjit.dll* 模块中。要识别被 .NET 保护钩子的 JIT 部分,有一个非常简单有效的方法:从保护进程中转储 *mscorjit.dll* 模块,并将其与磁盘上的原始模块进行比较。我编写了一个小的 CFF Explorer 脚本来比较 PE 节,该脚本在比较中排除了 IAT。这在比较两个可执行文件的 `.text` 节时特别有用。

这是个十分钟的工作,它对于识别应用于 JIT 的钩子类型极其有用。让我们看看这个脚本可能输出的一个例子

...与...之间第 0 节的比较

C:\...\mscorjit.dll

...和

C:\...\mscorjit_dumped.dll

在以下位置发现差异

RVA1     RVA2
000460A0 000460A0
000460A1 000460A1
000460A2 000460A2
000460A3 000460A3

发现的差异数量:4。

通过反汇编器查看打补丁的 `dword`,可以了解钩子的类型。

.text:790A60A0 ??_7CILJit@@6B@ dd offset 
    ?compileMethod@CILJit@@EAG?AW4CorJitResult
    @@PAVICorJitInfo@@PAUCORINFO_METHOD_INFO@@IPAPAEPAK@Z
.text:790A60A0 ; DATA XREF: getJit()+Eo.text:790A60A0 ; 
    CILJit::compileMethod(ICorJitInfo *,CORINFO_METHOD_INFO *,uint,uchar * *,ulong *)
.text:790A60A4 dd offset ?clearCache@CILJit@@EAGXXZ ; CILJit::clearCache(void)
.text:790A60A8 dd offset ?isCacheCleanupRequired@CILJit@@EAGHXZ ; 
    CILJit::isCacheCleanupRequired(void)

被修补的 `dword` 是 `CILJit` 类的 `compileMethod` 方法的偏移量。这引导我们进入下一段。

.NET 内部机制(第一部分:JIT)

在这一段中,我将向您介绍 `compileMethod` 函数,作为获取 JIT 完全控制权的一种方式。代码注入器不需要那么多控制,它们的功能实际上非常简单。

正如 `mscorjit` 的调试符号已经表明的那样,`compileMethod` 函数在其他参数中接受指向 `CORINFO_METHOD_INFO` 结构的指针。为了深入探讨 JIT 内部机制,我将依赖于 Microsoft Rotor Project,它基本上是 .NET Framework 的一个更小的开源版本。我想几乎所有人都知道这个项目的存在,但只有少数人知道可以从其内部机制中获取多少内容以用于官方框架的上下文中。我并不是指那些创建代码注入器的人,因为他们不需要那么多关于 JIT 内部工作原理的知识就能实现他们所做的事情。

我们来看一下 `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` 和 `ILCodeSize`,提供一个有效的 MSIL 指针和大小。代码注入器依靠 `ILCode` 指针来知道正在请求哪个方法。事实上,这个指针指向 .NET 程序集中的原始 MSIL 代码。许多代码注入器甚至不需要知道正在请求哪个方法,因为 `ILCode` 指向的数据只需要解密。

包含 `compileMethod` 地址的 `vftable` 的指针通过 `mscorjit` 唯一导出的 API:`getJit` 获取。

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);
}

这就是代码注入器完成工作所需了解的全部。但我们还要更进一步。`FJitCompiler` 类是这样的:

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);
};

`ICorJitCompiler` 只是一个接口,所以我们无需讨论它。`compileMethod` 当然是类的第一个成员。我很快就想到可以完全控制 JIT。这有点难以用语言解释,但在大约十分钟内,我注意到反汇编的 `mscorjit` 代码和 `Rotor` 代码之间的对应关系太多了。因此,我决定包含使用 `Rotor` 项目中的 `ICorJitInfo` 类所需的头文件。实际上,要使用这个类,只需要两个头文件:`corinfo.h` 和 `corjit.h`。所有包含文件都可以在 `Rotor` 项目的路径 `sscli20\clr\src\inc` 中找到。而 JIT 代码的路径是 `sscli20\clr\src\fjit`。以下是 JIT 路径中主要文件内容的简要摘要:

fjit.cpp 实际的 JIT。这是一个庞大的代码文件,因为它包含将 MSIL 转换为本机代码的所有代码,尽管本机代码是外部定义的。
fjitcompiler.cpp 包含 `getJit` 和 `compileMethod` 函数。它是 `mscorjit` 提供的与 JIT 通信的接口。

基本上,`ICorJitCompiler` 是执行引擎访问的接口,用于将 MSIL 转换为本机代码。`compileMethod` 的参数之一是 `ICorJitInfo`,这是一个 JIT 用于回调执行引擎以检索所需信息的类。完全访问 `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
{
public:
    // return memory manager that the JIT can use to allocate a regular memory
    virtual IEEMemoryManager* __stdcall getMemoryManager() = 0;

    // get a block of memory for the code, readonly data, and read-write data
    virtual void __stdcall allocMem (
            ULONG               hotCodeSize,    /* IN */
            ULONG               coldCodeSize,   /* IN */
            ULONG               roDataSize,     /* IN */
            ULONG               rwDataSize,     /* IN */
            ULONG               xcptnsCount,    /* IN */
            CorJitAllocMemFlag  flag,           /* IN */
            void **             hotCodeBlock,   /* OUT */
            void **             coldCodeBlock,  /* OUT */
            void **             roDataBlock,    /* OUT */
            void **             rwDataBlock     /* OUT */
            ) = 0;

        // Get a block of memory needed for the code manager information,
        // (the info for enumerating the GC pointers while crawling the
        // stack frame).
        // Note that allocMem must be called first
    virtual void * __stdcall allocGCInfo (
            ULONG                    size        /* IN */
            ) = 0;

    virtual void * __stdcall getEHInfo(
            ) = 0;

    virtual void __stdcall yieldExecution() = 0;

    // indicate how many exception handlers blocks are to be returned
    // this is guaranteed to be called before any 'setEHinfo' call.
    // Note that allocMem must be called before this method can be called
    virtual void __stdcall setEHcount (
            unsigned             cEH    /* IN */
            ) = 0;

    // set the values for one particular exception handler block
    //
    // Handler regions should be lexically contiguous.
    // This is because FinallyIsUnwinding() uses lexicality to
    // determine if a "finally" clause is executing
    virtual void __stdcall setEHinfo (
            unsigned             EHnumber,   /* IN  */
            const CORINFO_EH_CLAUSE *clause      /* IN */
            ) = 0;

    // Level -> fatalError, Level 2 -> Error, Level 3 -> Warning
    // Level 4 means happens 10 times in a run, level 5 means 100, 
    // level 6 means 1000 ...
    // returns non-zero if the logging succeeded
    virtual BOOL __cdecl logMsg(unsigned level, const char* fmt, va_list args) = 0;

    // do an assert.  will return true if the code should retry (DebugBreak)
    // returns false, if the assert should be ignored.
    virtual int __stdcall doAssert
        (const char* szFile, int iLine, const char* szExpr) = 0;

    struct ProfileBuffer
    {
        ULONG bbOffset;
        ULONG bbCount;
    };

    // allocate a basic block profile buffer where execution counts will be stored
    // for jitted basic blocks.
    virtual HRESULT __stdcall allocBBProfileBuffer (
            ULONG                 size,
            ProfileBuffer **      profileBuffer
            ) = 0;

    // get profile information to be used for optimizing the current method. The format
    // of the buffer is the same as the format the JIT passes to allocBBProfileBuffer.
    virtual HRESULT __stdcall getBBProfileData(
            CORINFO_METHOD_HANDLE ftnHnd,
            ULONG *               size,
            ProfileBuffer **      profileBuffer,
            ULONG *               numRuns
            ) = 0;
};

这看起来不多,但 `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 required each time the EE is run.
 *****************************************************************************/

class ICorDynamicInfo : public virtual ICorStaticInfo
{
public:

    //
    // These methods return values to the JIT which are not constant
    // from session to session.
    //
    // These methods take an extra parameter : void **ppIndirection.
    // If a JIT supports generation of prejit code (install-o-jit), it
    // must pass a non-null value for this parameter, and check the
    // resulting value.  If *ppIndirection is NULL, code should be
    // generated normally.  If non-null, then the value of
    // *ppIndirection is an address in the cookie table, and the code
    // generator needs to generate an indirection through the table to
    // get the resulting value.  In this case, the return result of the
    // function must NOT be directly embedded in the generated code.
    //
    // Note that if a JIT does not support prejit code generation, it
    // may ignore the extra parameter & pass the default of NULL - the
    // prejit ICorDynamicInfo implementation will see this & generate
    // an error if the jitter is used in a prejit scenario.
    //

    // Return details about EE internal data structures

    virtual DWORD __stdcall getThreadTLSIndex(
                    void                  **ppIndirection = NULL
                    ) = 0;

    virtual const void * __stdcall getInlinedCallFrameVptr(
                    void                  **ppIndirection = NULL
                    ) = 0;

    virtual LONG * __stdcall getAddrOfCaptureThreadGlobal(
                    void                  **ppIndirection = NULL
                    ) = 0;

    virtual SIZE_T* __stdcall       
            getAddrModuleDomainID(CORINFO_MODULE_HANDLE   module) = 0;

    // return the native entry point to an EE helper (see CorInfoHelpFunc)
    virtual void* __stdcall getHelperFtn (
                    CorInfoHelpFunc         ftnNum,
                    void                  **ppIndirection = NULL,
                    InfoAccessModule       *pAccessModule = NULL
                    ) = 0;

    // return a callable address of the function (native code). This function
    // may return a different value (depending on whether the method has
    // been JITed or not.  pAccessType is an in-out parameter.  The JIT
    // specifies what level of indirection it desires, and the EE sets it
    // to what it can provide (which may not be the same).
    virtual void __stdcall getFunctionEntryPoint(
                       CORINFO_METHOD_HANDLE   ftn,                 /* IN  */
                       InfoAccessType          requestedAccessType, /* IN */
                       CORINFO_CONST_LOOKUP *  pResult,             /* OUT */
                       CORINFO_ACCESS_FLAGS    accessFlags = CORINFO_ACCESS_ANY) = 0;

    // return a directly callable address. This can be used similarly to the
    // value returned by getFunctionEntryPoint() except that it is
    // guaranteed to be the same for a given function.
    // pAccessType is an in-out parameter.  The JIT
    // specifies what level of indirection it desires, and the EE sets it
    // to what it can provide (which may not be the same).
    virtual void __stdcall getFunctionFixedEntryPointInfo(
                              CORINFO_MODULE_HANDLE  scopeHnd,
                              unsigned               metaTOK,
                              CORINFO_CONTEXT_HANDLE context,
                              CORINFO_LOOKUP *       pResult) = 0;

    // get the synchronization handle that is passed to monXstatic function
    virtual void* __stdcall getMethodSync(
                    CORINFO_METHOD_HANDLE               ftn,
                    void                  **ppIndirection = NULL
                    ) = 0;

    // These entry points must be called if a handle is being embedded in
    // the code to be passed to a JIT helper function. (as opposed to just
    // being passed back into the ICorInfo interface.)

    // a module handle may not always be available. 
    // A call to embedModuleHandle should always
    // be preceded by a call to canEmbedModuleHandleForHelper. 
    // A dynamicMethod does not have a module
    virtual bool __stdcall canEmbedModuleHandleForHelper(
                    CORINFO_MODULE_HANDLE   handle
                    ) = 0;

    virtual CORINFO_MODULE_HANDLE __stdcall embedModuleHandle(
                    CORINFO_MODULE_HANDLE   handle,
                    void                  **ppIndirection = NULL
                    ) = 0;

    virtual CORINFO_CLASS_HANDLE __stdcall embedClassHandle(
                    CORINFO_CLASS_HANDLE    handle,
                    void                  **ppIndirection = NULL
                    ) = 0;

    virtual CORINFO_METHOD_HANDLE __stdcall embedMethodHandle(
                    CORINFO_METHOD_HANDLE   handle,
                    void                  **ppIndirection = NULL
                    ) = 0;

    virtual CORINFO_FIELD_HANDLE __stdcall embedFieldHandle(
                    CORINFO_FIELD_HANDLE    handle,
                    void                  **ppIndirection = NULL
                    ) = 0;

    // Given a module scope (module), a method handle (context) and
    // a metadata token (metaTOK), fetch the handle
    // (type, field or method) associated with the token.
    // If this is not possible at compile-time (because the current method's
    // code is shared and the token contains generic parameters)
    // then indicate how the handle should be looked up at run-time.
    //
    // Type tokens can be combined with CORINFO_ANNOT_MASK flags
    // to obtain array type handles. These are typically required by the 'newarr'
    // instruction which takes a token for the *element* type of the array.
    //
    // Similarly method tokens can be combined with CORINFO_ANNOT_MASK flags
    // method entry points. These are typically required by the 'call' and 'ldftn'
    // instructions.
    //
    // Byrefs or System.Void should only occur in method and local signatures, which
    // are accessed using ICorClassInfo and ICorClassInfo.getChildType. ldtoken is one
    // exception from this rule. allowAllTypes should be set to true only for 
    // ldtoken only!
    //
    virtual void __stdcall embedGenericHandle(
                        CORINFO_MODULE_HANDLE   module,
                        unsigned                metaTOK,
                        CORINFO_CONTEXT_HANDLE  context,
                        CorInfoTokenKind        tokenKind,
                        CORINFO_GENERICHANDLE_RESULT *pResult) = 0;

    // Return information used to locate the exact enclosing type of the current method.
    // Used only to invoke .cctor method from code shared across generic instantiations
    //   !needsRuntimeLookup       statically known (enclosing type of method itself)
    //   needsRuntimeLookup:
    //      CORINFO_LOOKUP_THISOBJ     use vtable pointer of 'this' param
    //      CORINFO_LOOKUP_CLASSPARAM  use vtable hidden param
    //      CORINFO_LOOKUP_METHODPARAM use enclosing type of method-desc hidden param
    virtual CORINFO_LOOKUP_KIND __stdcall getLocationOfThisType(
                    CORINFO_METHOD_HANDLE context
                    ) = 0;

    // return the unmanaged target *if method has already been prelinked.*
    virtual void* __stdcall getPInvokeUnmanagedTarget(
                    CORINFO_METHOD_HANDLE   method,
                    void                  **ppIndirection = NULL
                    ) = 0;

    // return address of fixup area for late-bound PInvoke calls.
    virtual void* __stdcall getAddressOfPInvokeFixup(
                    CORINFO_METHOD_HANDLE   method,
                    void                  **ppIndirection = NULL
                    ) = 0;

    // Generate a cookie based on the signature that would needs to be passed
    // to CORINFO_HELP_PINVOKE_CALLI
    virtual LPVOID GetCookieForPInvokeCalliSig(
            CORINFO_SIG_INFO* szMetaSig,
            void           ** ppIndirection = NULL
            ) = 0;

    // Gets a handle that is checked to see if the current method is
    // included in "JustMyCode"
    virtual CORINFO_JUST_MY_CODE_HANDLE __stdcall getJustMyCodeHandle(
                    CORINFO_METHOD_HANDLE       method,
                    CORINFO_JUST_MY_CODE_HANDLE**ppIndirection = NULL
                    ) = 0;

    // Gets a method handle that can be used to correlate profiling data.
    // This is the IP of a native method, or the address of the descriptor struct
    // for IL.  Always guaranteed to be unique per process, and not to move. */
    virtual void __stdcall GetProfilingHandle(
                    CORINFO_METHOD_HANDLE      method,
                    BOOL                      *pbHookFunction,
                    void                     **pEEHandle,
                    void                     **pProfilerHandle,
                    BOOL                      *pbIndirectedHandles
                    ) = 0;

    // returns the offset into the interface table
    virtual unsigned __stdcall getInterfaceTableOffset (
                    CORINFO_CLASS_HANDLE    cls,
                    void                  **ppIndirection = NULL
                    ) = 0;

    //return the address of a pointer to a callable stub that will do the virtual 
    //or interface call
    //
    // When inlining methodBeingCompiledHnd should be the originating caller 
    // in a sequence of nested
    // inlines, e.g. it is used to determine if the code being generated is domain
    // neutral or not.

    virtual void __stdcall getCallInfo(
                        CORINFO_METHOD_HANDLE   methodBeingCompiledHnd,
                        CORINFO_MODULE_HANDLE   tokenScope,
                        unsigned                methodToken,
                                          // the type token from a preceding constraint.
                        unsigned                constraintToken,       
                                          // prefix instruction (if any)
                        CORINFO_CONTEXT_HANDLE  tokenContext,
                        CORINFO_CALLINFO_FLAGS  flags,
                        CORINFO_CALL_INFO *pResult) = 0;

    // Returns TRUE if the Class Domain ID is the RID of the class 
    // (currently true for every class
    // except reflection emitted classes and generics)
    virtual BOOL __stdcall isRIDClassDomainID(CORINFO_CLASS_HANDLE cls) = 0;

    // returns the class's domain ID for accessing shared statics
    virtual unsigned __stdcall getClassDomainID (
                    CORINFO_CLASS_HANDLE    cls,
                    void                  **ppIndirection = NULL
                    ) = 0;


    virtual size_t __stdcall getModuleDomainID  (
                    CORINFO_MODULE_HANDLE    module,
                    void                  **ppIndirection = NULL
                    ) = 0;

    // return the data's address (for static fields only)
    virtual void* __stdcall getFieldAddress(
                    CORINFO_FIELD_HANDLE    field,
                    void                  **ppIndirection = NULL
                    ) = 0;

    // registers a vararg sig & returns a VM cookie for it
    //(which can contain other stuff)
    virtual CORINFO_VARARGS_HANDLE __stdcall getVarArgsHandle(
                    CORINFO_SIG_INFO       *pSig,
                    void                  **ppIndirection = NULL
                    ) = 0;

    // Allocate a string literal on the heap and return a handle to it
    virtual InfoAccessType __stdcall constructStringLiteral(
                    CORINFO_MODULE_HANDLE   module,
                    mdToken                 metaTok,
                    void                  **ppInfo
                    ) = 0;

    // (static fields only) given that 'field' refers to thread local store,
    // return the ID (TLS index), which is used to find the beginning of the
    // TLS data area for the particular DLL 'field' is associated with.
    virtual DWORD __stdcall getFieldThreadLocalStoreID (
                    CORINFO_FIELD_HANDLE    field,
                    void                  **ppIndirection = NULL
                    ) = 0;

    // returns the class typedesc given a methodTok (needed for arrays since
    // they share a common method table, so we can't use getMethodClass)
    virtual CORINFO_CLASS_HANDLE __stdcall findMethodClass(
                    CORINFO_MODULE_HANDLE   module,
                    mdToken                 methodTok,
                    CORINFO_METHOD_HANDLE   context
                    ) = 0;

    // Sets another object to intercept calls to "self"
    virtual void __stdcall setOverride(
                ICorDynamicInfo             *pOverride
                ) = 0;

    // Adds an active dependency from the context method's module to the given module
    virtual void __stdcall addActiveDependency(
               CORINFO_MODULE_HANDLE       moduleFrom,
               CORINFO_MODULE_HANDLE       moduleTo
                ) = 0;

    virtual CORINFO_METHOD_HANDLE __stdcall GetDelegateCtor(
            CORINFO_METHOD_HANDLE  methHnd,
            CORINFO_CLASS_HANDLE   clsHnd,
            CORINFO_METHOD_HANDLE  targetMethodHnd,
            DelegateCtorArgs *     pCtorData
            ) = 0;

    virtual void __stdcall MethodCompileComplete(
                CORINFO_METHOD_HANDLE methHnd
                ) = 0;
};

现在这看起来确实有趣多了。但我们还没完成,因为 `ICorDynamicInfo` 类继承自 `ICorStaticInfo`。`ICorStaticInfo` 类继承自许多类。

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
{
public:
    // Return details about EE internal data structures
    virtual void __stdcall getEEInfo(
                CORINFO_EE_INFO            *pEEInfoOut
                ) = 0;
};

我们只看其中一个(`ICorMethodInfo`)

class ICorMethodInfo
{
public:
    // this function is for debugging only.  It returns the method name
    // and if 'moduleName' is non-null, it sets it to something that will
    // says which method (a class name, or a module name)
    virtual const char* __stdcall getMethodName (
            CORINFO_METHOD_HANDLE       ftn,        /* IN */
            const char                **moduleName  /* OUT */
            ) = 0;

    // this function is for debugging only.  It returns a value that
    // is will always be the same for a given method.  It is used
    // to implement the 'jitRange' functionality
    virtual unsigned __stdcall getMethodHash (
            CORINFO_METHOD_HANDLE       ftn         /* IN */
            ) = 0;

    // return flags (defined above, CORINFO_FLG_PUBLIC ...)
    // The callerHnd can be either the methodBeingCompiled or the immediate
    // caller of an inlined function.
    virtual DWORD __stdcall getMethodAttribs (
            CORINFO_METHOD_HANDLE       calleeHnd,        /* IN */
            CORINFO_METHOD_HANDLE       callerHnd     /* IN */
            ) = 0;

    // sets private JIT flags, which can be, retrieved using getAttrib.
    virtual void __stdcall setMethodAttribs (
            CORINFO_METHOD_HANDLE       ftn,        /* IN */
            CorInfoMethodRuntimeFlags   attribs     /* IN */
            ) = 0;

    // Given a method descriptor ftnHnd, extract signature information into sigInfo
    //
    // 'memberParent' is typically only set when verifying.  It should be the
    // result of calling getMemberParent.
    virtual void __stdcall getMethodSig (
             CORINFO_METHOD_HANDLE      ftn,        /* IN  */
             CORINFO_SIG_INFO          *sig,        /* OUT */
             CORINFO_CLASS_HANDLE      memberParent = NULL /* IN */
             ) = 0;

    /*********************************************************************
     * Note the following methods can only be used on functions known
     * to be IL.  This includes the method being compiled and any method
     * that 'getMethodInfo' returns true for
     *********************************************************************/

    // return information about a method private to the implementation
    //      returns false if method is not IL, or is otherwise unavailable.
    //      This method is used to fetch data needed to inline functions
    virtual bool __stdcall getMethodInfo (
            CORINFO_METHOD_HANDLE   ftn,            /* IN  */
            CORINFO_METHOD_INFO*    info            /* OUT */
            ) = 0;

    // Decides if you have any limitations for inlining. If everything's OK, 
    // it will return INLINE_PASS and will fill out pRestrictions 
    // with a mask of restrictions the caller of this function must respect. 
    // If caller passes pRestrictions = NULL, if there are any restrictions
    // INLINE_FAIL will be returned
    //
    //
    // The inlined method need not be verified

    virtual CorInfoInline __stdcall canInline (
            CORINFO_METHOD_HANDLE       callerHnd,                  /* IN  */
            CORINFO_METHOD_HANDLE       calleeHnd,                  /* IN  */
            DWORD*                      pRestrictions              /* OUT */
            ) = 0;


    //  Returns false if the call is across assemblies thus we cannot tailcall

    virtual bool __stdcall canTailCall (
            CORINFO_METHOD_HANDLE   callerHnd,      /* IN  */
            CORINFO_METHOD_HANDLE   calleeHnd,      /* IN  */
            bool fIsTailPrefix                      /* IN */
            ) = 0;

    // Returns false if precompiled code must ensure that
    // the EE's DoPrestub function gets run before the
    // code for the method is used, i.e. if it returns false
    // then an indirect call must be made.
    //
    // Returning true does not guarantee that a direct call can be made:
    // there can be other reasons why the entry point cannot be embedded.
    //

    virtual bool __stdcall canSkipMethodPreparation (
            CORINFO_METHOD_HANDLE   callerHnd,      /* IN  */
            CORINFO_METHOD_HANDLE   calleeHnd,      /* IN  */
            bool                    fCheckCode,     /* IN */
            CorInfoIndirectCallReason *pReason = NULL,
            CORINFO_ACCESS_FLAGS    accessFlags = CORINFO_ACCESS_ANY) = 0;

    //  Returns true if a direct call can be made via the method entry point
    //
    virtual bool __stdcall canCallDirectViaEntryPointThunk (
            CORINFO_METHOD_HANDLE   calleeHnd,      /* IN  */
            void **                 pEntryPoint     /* OUT */
            ) = 0;

    // get individual exception handler
    virtual void __stdcall getEHinfo(
            CORINFO_METHOD_HANDLE ftn,              /* IN  */
            unsigned          EHnumber,             /* IN */
            CORINFO_EH_CLAUSE* clause               /* OUT */
            ) = 0;

    // return class it belongs to
    virtual CORINFO_CLASS_HANDLE __stdcall getMethodClass (
            CORINFO_METHOD_HANDLE       method
            ) = 0;

    // return module it belongs to
    virtual CORINFO_MODULE_HANDLE __stdcall getMethodModule (
            CORINFO_METHOD_HANDLE       method
            ) = 0;

    // This function returns the offset of the specified method in the
    // vtable of it's owning class or interface.
    virtual unsigned __stdcall getMethodVTableOffset (
            CORINFO_METHOD_HANDLE       method
            ) = 0;

    // If a method's attributes have (getMethodAttribs) CORINFO_FLG_INTRINSIC set,
    // getIntrinsicID() returns the intrinsic ID.
    virtual CorInfoIntrinsics __stdcall getIntrinsicID(
            CORINFO_METHOD_HANDLE       method
            ) = 0;

    // return the unmanaged calling convention for a PInvoke
    virtual CorInfoUnmanagedCallConv __stdcall getUnmanagedCallConv(
            CORINFO_METHOD_HANDLE       method
            ) = 0;

    // return if any marshaling is required for PInvoke methods.  Note that
    // method == 0 => calli.  The call site sig is only needed for the 
    // varargs or calli case
    virtual BOOL __stdcall pInvokeMarshalingRequired(
            CORINFO_METHOD_HANDLE       method,
            CORINFO_SIG_INFO*           callSiteSig
            ) = 0;

    // Check Visibility rules.
    // For Protected (family access) members, type of the instance is also
    // considered when checking visibility rules.
    virtual BOOL __stdcall canAccessMethod(
            CORINFO_METHOD_HANDLE       context,
            CORINFO_CLASS_HANDLE        parent,
            CORINFO_METHOD_HANDLE       target,
            CORINFO_CLASS_HANDLE        instance
            ) = 0;

    // Check constraints on method type arguments (only).
    // The parent class should be checked separately using 
    // satisfiesClassConstraints(parent).
    virtual BOOL __stdcall satisfiesMethodConstraints(
            CORINFO_CLASS_HANDLE        parent, // the exact parent of the method
            CORINFO_METHOD_HANDLE       method
            ) = 0;

    // Given a delegate target class, a target method parent class, a target method,
    // a delegate class, a scope, the target method ref, 
    // and the delegate constructor member ref
    // check if the method signature is compatible with the Invoke method of the delegate
    // (under the typical instantiation of any free type variables 
    // in the memberref signatures).
    // NB: arguments 2-4 could be inferred from 5-7, but are assumed to be available,
    // and thus passed in for efficiency.
    virtual BOOL __stdcall isCompatibleDelegate(
            CORINFO_CLASS_HANDLE        objCls,          /* type of the delegate target,
                                                          if any */
            CORINFO_CLASS_HANDLE        methodParentCls, /* exact parent of 
                                                          the target method, if any */
            CORINFO_METHOD_HANDLE       method,          /* (representative) target 
                                                          method, if any */
            CORINFO_CLASS_HANDLE        delegateCls,     /* exact type of the delegate */
            CORINFO_MODULE_HANDLE       moduleHnd,       /* scope of the 
                                                          following refs */
            unsigned        methodMemberRef,             /* memberref of the 
                                                          target method */
            unsigned        delegateConstructorMemberRef /* memberref of the 
                                                          delegate constructor */
            ) = 0;


    // Indicates if the method is an instance of the generic
    // method that passes (or has passed) verification
    virtual CorInfoInstantiationVerification __stdcall isInstantiationOfVerifiedGeneric (
            CORINFO_METHOD_HANDLE   method /* IN  */
            ) = 0;

    // Loads the constraints on a typical method definition, detecting cycles;
    // for use in verification.
    virtual void __stdcall initConstraintsForVerification(
            CORINFO_METHOD_HANDLE   method, /* IN */
            BOOL *pfHasCircularClassConstraints, /* OUT */
            BOOL *pfHasCircularMethodConstraint /* OUT */
            ) = 0;

    // Returns enum whether the method does not require verification
    // Also see ICorModuleInfo::canSkipVerification
    virtual CorInfoCanSkipVerificationResult __stdcall canSkipMethodVerification (
            CORINFO_METHOD_HANDLE       ftnHandle,     /* IN  */
            BOOL                        fQuickCheckOnly
            ) = 0;

    //  Determines whether a callout is allowed.
    virtual CorInfoIsCallAllowedResult __stdcall isCallAllowed (
            CORINFO_METHOD_HANDLE       callerHnd,                  // IN
            CORINFO_METHOD_HANDLE       calleeHnd,                  // IN
            CORINFO_CALL_ALLOWED_INFO * CallAllowedInfo             // OUT
            ) = 0;

    // load and restore the method
    virtual void __stdcall methodMustBeLoadedBeforeCodeIsRun(
            CORINFO_METHOD_HANDLE       method
            ) = 0;

    virtual CORINFO_METHOD_HANDLE __stdcall mapMethodDeclToMethodImpl(
            CORINFO_METHOD_HANDLE       method
            ) = 0;

    // Returns the global cookie for the /GS unsafe buffer checks
    // The cookie might be a constant value (JIT), or a handle to memory location (Ngen)
    virtual void __stdcall getGSCookie(
            GSCookie * pCookieVal,                     // OUT
            GSCookie ** ppCookieVal                    // OUT
            ) = 0;
};

如您所见,`ICorMethodInfo` 类包含许多接受一个或多个 `CORINFO_METHOD_HANDLE` 作为参数的方法。`ICorModuleInfo` 具有接受 `CORINFO_MODULE_HANDLE` 参数的方法。这些句柄应该讨论,因为所有 JIT 的内部工作都依赖于它们。以下是它们的声明:

// Cookie types consumed by the code generator (these are opaque values
// not inspected by the code generator):

typedef struct CORINFO_ASSEMBLY_STRUCT_*    CORINFO_ASSEMBLY_HANDLE;
typedef struct CORINFO_MODULE_STRUCT_*      CORINFO_MODULE_HANDLE;
typedef struct CORINFO_DEPENDENCY_STRUCT_*  CORINFO_DEPENDENCY_HANDLE;
typedef struct CORINFO_CLASS_STRUCT_*       CORINFO_CLASS_HANDLE;
typedef struct CORINFO_METHOD_STRUCT_*      CORINFO_METHOD_HANDLE;
typedef struct CORINFO_FIELD_STRUCT_*       CORINFO_FIELD_HANDLE;
// represents a list of argument types
typedef struct CORINFO_ARG_LIST_STRUCT_*    CORINFO_ARG_LIST_HANDLE;
// represents the whole list
typedef struct CORINFO_SIG_STRUCT_*         CORINFO_SIG_HANDLE;
typedef struct CORINFO_JUST_MY_CODE_HANDLE_*CORINFO_JUST_MY_CODE_HANDLE;
// a handle guaranteed to be unique per process
typedef struct CORINFO_PROFILING_STRUCT_*   CORINFO_PROFILING_HANDLE;
typedef DWORD*                              CORINFO_SHAREDMODULEID_HANDLE;
// a generic handle (could be any of the above)
typedef struct CORINFO_GENERIC_STRUCT_*     CORINFO_GENERIC_HANDLE; 

结构未在代码中定义:正如注释所述,它们是“不透明”的。实际上,这些句柄只是指针。但我们稍后会看到。现在应该理解的是,它们被 JIT 的方法用来识别事物。我粘贴了上面所有的声明,以便让读者了解我之前提到的两个头文件赋予 JIT 的权限。例如,让我们看一下 `ICorMethodInfo` 的第一个方法:

virtual const char* __stdcall getMethodName (
            CORINFO_METHOD_HANDLE       ftn,        /* IN */
            const char                **moduleName  /* OUT */
            ) = 0;

此函数检索方法的名称和类。我将用它来演示如何钩子 JIT 并从中检索基本信息。但首先,我必须介绍另一件事:.NET 程序集加载器。

.NET 程序集加载器

由于我们需要在目标程序集被 JIT 编译之前钩子 JIT,最好的方法是从另一个在此期间已经钩子 JIT 的程序集加载目标程序集。我称之为加载器,这种方法仅在保护没有将程序集包装成本机可执行文件时才有效。在这种情况下,您可以考虑将钩子 DLL 添加到本机 EXE 的导入表中,或者以挂起状态创建目标进程,注入 DLL,然后恢复执行。

有几种方法可以将程序集加载到当前地址空间。不幸的是,最常用的方法通常并非在所有情况下都有效。例如,一种常见的方法是使用 `Assembly.Load` / `LoadFrom` 函数。如果托管程序集需要自己的 `appdomain`,这种方法将导致应用程序崩溃。

这是我在这篇文章中使用的非常简单的方法

namespace rbloader
{
    static class Program
    {
        /// 
        /// The main entry point for the application.
        /// 
        ///

        [DllImport("rbcoree.dll", CallingConvention=CallingConvention.Cdecl)]
        static extern void HookJIT();

        [STAThread]
        static void Main(string[] args)
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            OpenFileDialog openFileDialog = new OpenFileDialog();

            openFileDialog.Filter = "Exe Files (*.exe)|*.exe|Dll Files (*.dll)|
                    *.dll|All Files (*.*)|*.*";
            if (openFileDialog.ShowDialog() == DialogResult.OK)
            {
                HookJIT();

                AppDomain ad = AppDomain.CreateDomain("subAppDomain");
                ad.ExecuteAssembly(openFileDialog.FileName);
            }
        }
    }
}

请记住,对于某些保护,这种代码加载方法将不起作用。因此,您必须评估每个案例(因为它取决于注入器的实现方式),并找到一种在保护之前钩子 JIT 的方法。

此外,第一个执行的程序集决定了 .NET Framework 平台。因此,加载器应该相应地编译。这意味着您可以从 Visual Studio 选项中选择特定程序集要运行的平台。如果选择 64 位平台,则程序集的 PE 将是 64 位 PE,只能在其预期的平台上运行,而 32 位 PE 可以运行在所有平台上。要强制 32 位 PE 使用 x86 框架,必须在 .NET Directory 的 Flags 字段中设置一个标志。

我在这篇文章中编写的代码与 64 位兼容,但我只在 x86 上测试过。因此,加载器设置了 32 位代码。要在 64 位上下文中使用加载器,您必须取消设置此标志。

JIT 钩子示例

我将在这里展示一个如何钩子 JIT 并从中检索信息的小例子。我要做的是显示每个 JIT 编译方法的类名和方法名,以及该方法调用的每个方法的类名和方法名。现在是时候介绍一下,`compileMethod` 函数不仅会 JIT 编译它应该编译的方法,还会 JIT 编译该方法调用的所有方法,以及这些方法调用的所有方法等等。这显然是这样,因为如果被调用的方法及其子方法等尚未 JIT 编译,则无法 JIT 编译调用。然而,即使这很明显,也值得牢记。为了反汇编方法的 MSIL,我使用了我的 DisasMSIL 引擎。

#include "stdafx.h"
#include <tchar.h>
#include <CorHdr.h>
#include "corinfo.h"
#include "corjit.h"
#include "DisasMSIL.h"

HINSTANCE hInstance;

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

VOID DisplayMethodAndCalls(ICorJitInfo *comp, CORINFO_METHOD_INFO *info);


BOOL APIENTRY DllMain( HMODULE hModule,
               DWORD  dwReason,
               LPVOID lpReserved
        )
{
    hInstance = (HINSTANCE) hModule;

    HookJIT();

    return TRUE;
}

//
// Hook JIT's compileMethod
//

BOOL bHooked = FALSE;

ULONG_PTR *(__stdcall *p_getJit)();
typedef int (__stdcall *compileMethod_def)(ULONG_PTR classthis, ICorJitInfo *comp,
                       CORINFO_METHOD_INFO *info, unsigned flags,
                       BYTE **nativeEntry, ULONG  *nativeSizeOfCode);
struct JIT
{
    compileMethod_def compileMethod;
};

compileMethod_def compileMethod;

int __stdcall my_compileMethod(ULONG_PTR classthis, ICorJitInfo *comp,
                   CORINFO_METHOD_INFO *info, unsigned flags,
                   BYTE **nativeEntry, ULONG  *nativeSizeOfCode);

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;
        }
    }
}

//
// hooked compileMethod
//
/*__declspec (naked) */
int __stdcall my_compileMethod(ULONG_PTR classthis, ICorJitInfo *comp,
                   CORINFO_METHOD_INFO *info, unsigned flags,
                   BYTE **nativeEntry, ULONG  *nativeSizeOfCode)
{
    // in case somebody hooks us (x86 only)
#ifdef _M_IX86
    __asm
    {
        nop
            nop
            nop
            nop
            nop
            nop
            nop
            nop
            nop
            nop
            nop
            nop
            nop
            nop
    }
#endif

    // call original method
    // I'm not using the naked + jmp approach to avoid x64 incompatibilities
    int nRet = 
        compileMethod(classthis, comp, info, flags, nativeEntry, nativeSizeOfCode);

    //
    // Displays the current method and its calls
    //

    DisplayMethodAndCalls(comp, info);

    return nRet;
}

VOID DisplayMethodAndCalls(ICorJitInfo *comp,
                           CORINFO_METHOD_INFO *info)
{
    const char *szMethodName = NULL;
    const char *szClassName = NULL;

    szMethodName = comp->getMethodName(info->ftn, &szClassName);

    char CurMethod[200];

    sprintf_s(CurMethod, 200, "%s::%s", szClassName, szMethodName);

    char Calls[0x1000];

    strcpy_s(Calls, 0x1000, "Methods called:\r\n\r\n");

    //
    // retrieve calls
    //

#define MAX_INSTR      100

    ILOPCODE_STRUCT ilopar[MAX_INSTR];

    DISASMSIL_OFFSET CodeBase = 0;

    BYTE *pCur = info->ILCode;
    UINT nSize = info->ILCodeSize;

    UINT nDisasmedInstr;

    while (DisasMSIL(pCur, nSize, CodeBase, ilopar, MAX_INSTR,
        &nDisasmedInstr))
    {
        //
        // check the instructions for calls
        //

        for (UINT x = 0; x < nDisasmedInstr; x++)
        {
            if (info->ILCode[ilopar[x].Offset] == ILOPCODE_CALL)
            {
                DWORD dwToken = *((DWORD *) &info->ILCode[ilopar[x].Offset + 1]);

                CORINFO_METHOD_HANDLE hCallHandle =
                    comp->findMethod(info->scope, dwToken, info->ftn);

                szMethodName = comp->getMethodName(hCallHandle, &szClassName);

                strcat_s(Calls, 0x1000, szClassName);
                strcat_s(Calls, 0x1000, "::");
                strcat_s(Calls, 0x1000, szMethodName);
                strcat_s(Calls, 0x1000, "\r\n");
            }
        }

        //
        // end loop?
        //

        if (nDisasmedInstr < MAX_INSTR) break;

        //
        // next instructions
        //

        DISASMSIL_OFFSET next = ilopar[nDisasmedInstr - 1].Offset - CodeBase;
        next += ilopar[nDisasmedInstr - 1].Size;

        pCur += next;
        nSize -= next;
        CodeBase += next;
    }

    //
    // Show MessageBox
    //

    MessageBoxA(0, Calls, CurMethod, MB_ICONINFORMATION);
}

`findMethod` 有一些限制,我稍后会讨论。这个小例子的输出是一系列消息框,通知用户当前正在 JIT 编译的方法及其调用。

正如您所看到的,我没有将 `callvirt` 和 `calli` 操作码包含在搜索中。这仅仅是为了简化,没有其他原因。如果您想创建一个完整的日志记录器,您也必须考虑这些操作码。

.NET 代码弹出器

在为代码注入保护编写转储器,或者更确切地说是代码弹出器时,必须选择如何进行以收集原始 MSIL 代码。有两种方法:一种是“隐身”,另一种是“暴力”。“隐身”是指只转储在执行期间被 JIT 编译的方法的 MSIL。如果您以这种方式进行,除了 MSIL 代码之外,您还需要检索方法的令牌,以便稍后使用 *Rebel.NET* 重建程序集。有一个非常有用的函数可以从 `CORINFO_METHOD_HANDLE` 中检索令牌。

comp->getMethodDefFromMethod(info->ftn)

隐身方法将始终 100% 有效,但缺点是无法确定程序集中的所有方法都将被 JIT 编译。然而,值得一提的是,在某些情况下,目标只是转储几个方法以进行反编译和分析。

我称之为“暴力”的另一种弹出 MSIL 代码的方式。通过暴力,我指的是强制保护一次性解密 .NET 程序集中的所有方法。需要做的是收集 `CORINFO_METHOD_INFO` 数据(或至少是 `ILCode` 和 `ILCodeSize`),然后从 `getJit` 函数中检索 `compileMethod`。此时,`compileMethod` 已经通过保护钩子。因此,调用它意味着解密 MSIL 数据。当保护的 `compileMethod` 解密了 MSIL 代码后,它将调用代码弹出器的 `compileMethod`,代码弹出器会通过检查参数以某种方式注意到这是代码弹出过程,并且不会调用真实的 `compileMethod` 函数。

我编写的代码弹出器还具有一个小的已编译程序集转储器。这非常有用,因为大多数时候您需要转储受保护的程序集。这也可以通过通用 .NET 解包器实现。

如您所见,对话框中包含一个“生成 Rebel 文件”按钮。按下此按钮时,会请求一个 rebel 报告文件作为输入。基本上,您需要首先转储(如有必要)程序集以进行重建。然后,使用 Rebel.NET 从该程序集创建报告文件。报告文件中只应包含方法。

此报告文件将在代码弹出过程中用于计算方法数量并检索 MSIL 代码地址和大小。实际上,JIT 可以用来检索这些信息,但存在一个问题。如果您注意到,`findMethod` 接受三个参数,最后一个是名为 `hContext` 的 `CORINFO_METHOD_HANDLE`。此模块用于检查请求的上下文。如果一个方法尝试在未经授权的情况下访问另一个方法,框架将显示错误并终止进程。主要目标是为程序集中的每个方法令牌获取一个有效的 `CORINFO_METHOD_HANDLE`。如前所述,这些句柄只是指针。让我们看看 `CORINFO_METHOD_HANDLE` 指向的内存。

03B8E1F0                            01 00 00 3B 74 01 00 00           ...;t...

03B8E200   02 00 01 08 0D 00 00 00  03 00 02 08 75 01 00 00   ............u...

03B8E210   04 00 03 39 76 01 20 00  05 00 04 08 77 01 00 00   ...9v. .....w...

第一个字似乎代表方法的编号,8 字节后是下一个方法。因此,理论上,即使给定方法的令牌,也可能计算出正确的 `CORINFO_METHOD_HANDLE`。在检索到 `CORINFO_METHOD_HANDLE` 后,只需调用 `getMethodInfo` 即可获取 `CORINFO_METHOD_INFO` 结构。**注意**:我稍后将讨论 `CORINFO_METHOD_HANDLE` 真正代表什么:这个主题需要单独一个段落。

但是,如前所述,我并没有采用这种方式。我使用了一个 *Rebel.NET* 报告文件来检索必要的数据。这种方法显然可以改变。以下是 .NET 代码弹出器的代码。

#include "stdafx.h"
#include <CommCtrl.h>
#include <CommDlg.h>
#include <tlhelp32.h>
#include <tchar.h>
#include <CorHdr.h>
#include "corinfo.h"
#include "corjit.h"
#include "RebelDotNET.h"
#include "resource.h"

#ifndef PAGE_SIZE
#define PAGE_SIZE 0x1000
#endif

#define IS_FLAG(Value, Flag) ((Value & Flag) == Flag)

HINSTANCE hInstance;

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

VOID ListThread();


BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  dwReason,
                       LPVOID lpReserved
                )
{
   hInstance = (HINSTANCE) hModule;

   HookJIT();

   if (dwReason == DLL_PROCESS_ATTACH)
   {
      CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) ListThread,
         NULL, 0, NULL);
   }

   return TRUE;
}

// unimportant
extern "C" __declspec(dllexport) int __stdcall _CorExeMain(void)
{
   return 0;
}


//
// Hook JIT's compileMethod
//

BOOL bHooked = FALSE;

ULONG_PTR *(__stdcall *p_getJit)();
typedef int (__stdcall *compileMethod_def)(ULONG_PTR classthis, ICorJitInfo *comp,
                                        CORINFO_METHOD_INFO *info, unsigned flags,
                                        BYTE **nativeEntry, ULONG  *nativeSizeOfCode);
struct JIT
{
   compileMethod_def compileMethod;
};

compileMethod_def compileMethod;

int __stdcall my_compileMethod(ULONG_PTR classthis, ICorJitInfo *comp,
                               CORINFO_METHOD_INFO *info, unsigned flags,
                               BYTE **nativeEntry, ULONG  *nativeSizeOfCode);

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;
      }
   }
}

//
// Logging
//

struct AssemblyInfo
{
   CORINFO_MODULE_HANDLE hCorModule;

   WCHAR AssemblyName[MAX_PATH];

   VOID *ImgBase;
   UINT ImgSize;

   BOOL bIdentified;

   HANDLE hRebReport;

   BOOL bDump;
   TCHAR DumpFileName[MAX_PATH];

} LoggedAssemblies[100];

UINT NumberOfLoggedAssemblies = 0;

VOID LogAssembly(ICorJitInfo *comp, CORINFO_METHOD_INFO *info);

DWORD GetTokenFromMethodHandle(ICorJitInfo *comp, CORINFO_METHOD_INFO *info);

VOID AddMethod(CORINFO_METHOD_INFO *mi);
BOOL CreateRebFile(AssemblyInfo *ai);
//
// hooked compileMethod
//
/*__declspec (naked) */
int __stdcall my_compileMethod(ULONG_PTR classthis, ICorJitInfo *comp,
                               CORINFO_METHOD_INFO *info, unsigned flags,
                               BYTE **nativeEntry, ULONG  *nativeSizeOfCode)
{
   // in case somebody hooks us (x86 only)
#ifdef _M_IX86
   __asm
   {
      nop
      nop
      nop
      nop
      nop
      nop
      nop
      nop
      nop
      nop
      nop
      nop
      nop
      nop
   }
#endif

   //
   // check if it's the dump process
   //

   if (comp == NULL)
   {
      AddMethod(info);
      return 0;
   }

   LogAssembly(comp, info);

   // call original method
   // I'm not using the naked + jmp approach to avoid x64 incompatibilities
   int nRet = 
        compileMethod(classthis, comp, info, flags, nativeEntry, nativeSizeOfCode);

   return nRet;
}

//
// convert an address to its module ImgBase and Name (if possible)
//

VOID AddressToModuleInfo(VOID *pAddress, WCHAR *AssemblyName,
                        VOID **pImgBase, UINT *pImgSize,
                        BOOL *pbIdentified)
{
   DWORD dwPID = GetCurrentProcessId();
   HANDLE hModuleSnap = INVALID_HANDLE_VALUE;
   MODULEENTRY32 me32;

   static BOOL bFirstUnkAsm = TRUE;

   hModuleSnap = CreateToolhelp32Snapshot( TH32CS_SNAPMODULE, dwPID );

   if (hModuleSnap == INVALID_HANDLE_VALUE)
      return;

   me32.dwSize = sizeof (MODULEENTRY32);

   if (!Module32First(hModuleSnap, &me32 ))
   {
      CloseHandle(hModuleSnap);
      return;
   }

   do
   {
      if (((ULONG_PTR) pAddress) > ((ULONG_PTR) me32.modBaseAddr) &&
         ((ULONG_PTR) pAddress) < (((ULONG_PTR) me32.modBaseAddr) +
         me32.modBaseSize))
      {
         if (pImgBase) *pImgBase = (VOID *) me32.modBaseAddr;
         if (pImgSize) *pImgSize = me32.modBaseSize;
         wcscpy_s(AssemblyName, MAX_PATH, me32.szExePath);
         if (pbIdentified) *pbIdentified = TRUE;
         return;
      }

   } while (Module32Next(hModuleSnap, &me32));

   CloseHandle(hModuleSnap);

   if (pbIdentified) *pbIdentified = FALSE;

   MEMORY_BASIC_INFORMATION mbi = { 0 };
   VirtualQuery(pAddress, &mbi, sizeof (MEMORY_BASIC_INFORMATION));

   if (pImgBase) *pImgBase = mbi.AllocationBase;

   DWORD ImgSize = 0;

   __try
   {
      IMAGE_DOS_HEADER *pDosHeader = (IMAGE_DOS_HEADER *)
         mbi.AllocationBase;

      if (pDosHeader->e_magic == IMAGE_DOS_SIGNATURE)
      {
         IMAGE_NT_HEADERS *pNtHeaders = (IMAGE_NT_HEADERS *)
            (pDosHeader->e_lfanew + (ULONG_PTR) pDosHeader);

         if (pNtHeaders->Signature == IMAGE_NT_SIGNATURE)
         {
            ImgSize = pNtHeaders->OptionalHeader.SizeOfImage;
         }
      }
   }
   __except (EXCEPTION_EXECUTE_HANDLER)
   {
      goto endinfo;
   }

endinfo:

   if (pImgSize) *pImgSize = ImgSize;

   if (bFirstUnkAsm)
   {
      wsprintfW(AssemblyName, L"Base: %p - Size: %08X - Primary Assembly",
         mbi.AllocationBase, ImgSize);
      bFirstUnkAsm = FALSE;
   }
   else
   {
      wsprintfW(AssemblyName, L"Base: %p - Size: %08X - unidentfied",
         mbi.AllocationBase, ImgSize);
   }

}

VOID LogAssembly(ICorJitInfo *comp, CORINFO_METHOD_INFO *info)
{
   // already in the list?
   for (UINT x = 0; x < NumberOfLoggedAssemblies; x++)
   {
      if (LoggedAssemblies[x].hCorModule == info->scope)
         return;
   }

   //
   // Add assembly to the logged list
   //

   AddressToModuleInfo(info->ILCode,
      LoggedAssemblies[NumberOfLoggedAssemblies].AssemblyName,
      &LoggedAssemblies[NumberOfLoggedAssemblies].ImgBase,
      &LoggedAssemblies[NumberOfLoggedAssemblies].ImgSize,
      &LoggedAssemblies[NumberOfLoggedAssemblies].bIdentified);

   LoggedAssemblies[NumberOfLoggedAssemblies].hCorModule = info->scope;
   LoggedAssemblies[NumberOfLoggedAssemblies].bDump = FALSE;

   NumberOfLoggedAssemblies++;
}

//
// Listing
//

LRESULT CALLBACK ListDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);

VOID ListThread()
{
   Sleep(2000);

   InitCommonControls();

   DialogBox(hInstance, MAKEINTRESOURCE(IDD_ASMLIST), NULL, (DLGPROC) ListDlgProc);
}

LRESULT CALLBACK ListDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
   switch (uMsg)
   {

   case WM_INITDIALOG:
      {
         HWND hList = GetDlgItem(hDlg, LST_ASMS);

         LV_COLUMN lvc;

         ZeroMemory(&lvc, sizeof (LV_COLUMN));

         lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
         lvc.fmt = LVCFMT_LEFT;

         lvc.cx = 500;
         lvc.pszText = _T("Assembly Path");
         ListView_InsertColumn(hList, 0, &lvc);

         SendMessage(hList, LVM_SETEXTENDEDLISTVIEWSTYLE,
            LVS_EX_FULLROWSELECT | LVS_EX_INFOTIP,
            LVS_EX_FULLROWSELECT | LVS_EX_INFOTIP);


         SendMessage(hDlg, WM_COMMAND, IDC_REFRESH, 0);

         break;
      }

   case WM_CLOSE:
      {
         EndDialog(hDlg, 0);
         break;
      }

   case WM_COMMAND:
      {
         switch (LOWORD(wParam))
         {

         case IDC_REFRESH:
            {
               HWND hList = GetDlgItem(hDlg, LST_ASMS);

               ListView_DeleteAllItems(hList);

               LV_ITEM lvi;

               ZeroMemory(&lvi, sizeof (LV_ITEM));

               lvi.mask = LVIF_TEXT | LVIF_STATE | LVIF_PARAM;

               for (UINT x = 0; x < NumberOfLoggedAssemblies; x++)
               {
                  lvi.lParam = (LPARAM) LoggedAssemblies[x].hCorModule;
                  lvi.pszText = LoggedAssemblies[x].AssemblyName;
                  ListView_InsertItem(hList, &lvi);
               }

               break;
            }

         case IDC_DUMPASM:
            {
               HWND hList = GetDlgItem(hDlg, LST_ASMS);

               int nSel = ListView_GetNextItem(hList, -1, LVNI_SELECTED);

               if (nSel == -1) break;

               OPENFILENAME SaveFileName;

               TCHAR DumpFileName[MAX_PATH];

               ZeroMemory(DumpFileName, MAX_PATH * sizeof (TCHAR));

               ZeroMemory(&SaveFileName, sizeof (OPENFILENAME));

               SaveFileName.lStructSize = sizeof (OPENFILENAME);
               SaveFileName.hwndOwner = hDlg;
               SaveFileName.lpstrFilter = _T("All Files (*.*)\0*.*\0");
               SaveFileName.lpstrFile = DumpFileName;
               SaveFileName.nMaxFile = MAX_PATH;
               SaveFileName.lpstrTitle = _T("Save Assembly As...");

               if (!GetSaveFileName(&SaveFileName))
                  break;

               LV_ITEM lvi;
               ZeroMemory(&lvi, sizeof (LV_ITEM));

               lvi.mask = LVIF_PARAM;
               lvi.iItem = nSel;

               ListView_GetItem(hList, &lvi);

               for (UINT x = 0; x < NumberOfLoggedAssemblies; x++)
               {
                  if (LoggedAssemblies[x].hCorModule ==
                     (CORINFO_MODULE_HANDLE) lvi.lParam)
                  {
                     HANDLE hFile = CreateFile(DumpFileName, GENERIC_WRITE,
                        FILE_SHARE_READ, NULL, CREATE_ALWAYS, 0, NULL);

                     if (hFile == INVALID_HANDLE_VALUE)
                        break;

                     DWORD dwOldProtect;

                     VirtualProtect(LoggedAssemblies[x].ImgBase,
                        LoggedAssemblies[x].ImgSize, PAGE_EXECUTE_READ,
                        &dwOldProtect);

                     for (UINT nPage = 0;
                        nPage < (LoggedAssemblies[x].ImgSize / PAGE_SIZE);
                        nPage++)
                     {
                        DWORD BW;

                        __try
                        {
                           VOID *pPage = (VOID *) ((nPage * PAGE_SIZE) +
                              (ULONG_PTR) LoggedAssemblies[x].ImgBase);

                           WriteFile(hFile, pPage, PAGE_SIZE, &BW, NULL);
                        }

                        __except (EXCEPTION_EXECUTE_HANDLER)
                        {
                           SetFilePointer(hFile, PAGE_SIZE, NULL, FILE_CURRENT);
                           SetEndOfFile(hFile);
                        }
                     }

                     CloseHandle(hFile);

                     MessageBox(hDlg, _T("Assembly successfully dumped."),
                        _T("Dumped"), MB_ICONINFORMATION);
                  }
               }

               break;
            }

         case IDC_REBFILE:
            {
               HWND hList = GetDlgItem(hDlg, LST_ASMS);

               int nSel = ListView_GetNextItem(hList, -1, LVNI_SELECTED);

               if (nSel == -1) break;

               LV_ITEM lvi;
               ZeroMemory(&lvi, sizeof (LV_ITEM));

               lvi.mask = LVIF_PARAM;
               lvi.iItem = nSel;

               ListView_GetItem(hList, &lvi);

               for (UINT x = 0; x < NumberOfLoggedAssemblies; x++)
               {
                  if (LoggedAssemblies[x].hCorModule ==
                     (CORINFO_MODULE_HANDLE) lvi.lParam)
                  {
                     OPENFILENAME OpenFileName;

                     TCHAR ReportFileName[MAX_PATH];

                     ZeroMemory(ReportFileName, MAX_PATH * sizeof (TCHAR));

                     ZeroMemory(&OpenFileName, sizeof (OPENFILENAME));

                     OpenFileName.lStructSize = sizeof (OPENFILENAME);
                     OpenFileName.hwndOwner = hDlg;
                     OpenFileName.lpstrFilter = 
                                       _T("Report Rebel File (*.rebel)\0*.rebel\0");
                     OpenFileName.lpstrFile = ReportFileName;
                     OpenFileName.nMaxFile = MAX_PATH;
                     OpenFileName.lpstrTitle = _T("Select a Report Rebel File...");

                     if (!GetOpenFileName(&OpenFileName))
                        break;

                     LoggedAssemblies[x].hRebReport = CreateFile(ReportFileName,
                        GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);

                     if (LoggedAssemblies[x].hRebReport == INVALID_HANDLE_VALUE)
                        break;

                     OPENFILENAME SaveFileName;

                     ZeroMemory(LoggedAssemblies[x].DumpFileName, 
                                                     MAX_PATH * sizeof (TCHAR));

                     ZeroMemory(&SaveFileName, sizeof (OPENFILENAME));

                     SaveFileName.lStructSize = sizeof (OPENFILENAME);
                     SaveFileName.hwndOwner = hDlg;
                     SaveFileName.lpstrFilter = _T("Rebel File (*.rebel)\0*.rebel\0");
                     SaveFileName.lpstrFile = LoggedAssemblies[x].DumpFileName;
                     SaveFileName.nMaxFile = MAX_PATH;
                     SaveFileName.lpstrTitle = _T("Save Rebel File As...");
                     SaveFileName.lpstrDefExt = _T("rebel");

                     if (!GetSaveFileName(&SaveFileName))
                     {
                        CloseHandle(LoggedAssemblies[x].hRebReport);
                        break;
                     }

                     // dump
                     CreateRebFile(&LoggedAssemblies[x]);

                     break;
                  }
               }

               break;
            }
         }

         break;
      }
   }

   return FALSE;
}

//
// Dumping
//

DWORD RvaToOffset(VOID *pBase, DWORD Rva)
{
   __try
   {
      DWORD Offset = Rva, Limit;

      IMAGE_DOS_HEADER *pDosHeader = (IMAGE_DOS_HEADER *) pBase;

      if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
         return 0;

      IMAGE_NT_HEADERS *pNtHeaders = (IMAGE_NT_HEADERS *) (
         pDosHeader->e_lfanew + (ULONG_PTR) pDosHeader);

      if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE)
         return 0;

      IMAGE_SECTION_HEADER *Img = IMAGE_FIRST_SECTION(pNtHeaders);

      if (Rva < Img->PointerToRawData)
         return Rva;

      for (WORD i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++)
      {
         if (Img[i].SizeOfRawData)
            Limit = Img[i].SizeOfRawData;
         else
            Limit = Img[i].Misc.VirtualSize;

         if (Rva >= Img[i].VirtualAddress &&
            Rva < (Img[i].VirtualAddress + Limit))
         {
            if (Img[i].PointerToRawData != 0)
            {
               Offset -= Img[i].VirtualAddress;
               Offset += Img[i].PointerToRawData;
            }

            return Offset;
         }
      }
   }
   __except (EXCEPTION_EXECUTE_HANDLER)
   {
      return 0;
   }

   return 0;
}

UINT GetMethodSize(REBEL_METHOD *rbMethod)
{
   UINT nMethodSize = sizeof (REBEL_METHOD);

   if (!IS_FLAG(rbMethod->Mask, REBEL_METHOD_MASK_NAMEOFFSET))
      nMethodSize += rbMethod->NameOffsetOrSize;

   if (!IS_FLAG(rbMethod->Mask, REBEL_METHOD_MASK_SIGOFFSET))
      nMethodSize += rbMethod->SignatureOffsetOrSize;

   if (!IS_FLAG(rbMethod->Mask, REBEL_METHOD_MASK_LOCVARSIGOFFSET))
      nMethodSize += rbMethod->LocalVarSigOffsetOrSize;

   nMethodSize += rbMethod->CodeSize;
   nMethodSize += rbMethod->ExtraSectionsSize;

   return nMethodSize;
}

static HANDLE hRebuildDump = INVALID_HANDLE_VALUE;

VOID AddMethod(CORINFO_METHOD_INFO *mi)
{
   REBEL_METHOD rbMethod;

   ZeroMemory(&rbMethod, sizeof (REBEL_METHOD));

   rbMethod.Token = mi->locals.token;
   rbMethod.CodeSize = mi->ILCodeSize;

   DWORD BRW;

   SetFilePointer(hRebuildDump, 0, NULL, FILE_END);

   WriteFile(hRebuildDump, &rbMethod, sizeof (REBEL_METHOD), &BRW, NULL);
   WriteFile(hRebuildDump, mi->ILCode, mi->ILCodeSize, &BRW, NULL);

   //
   // Increase number of methods
   //

   SetFilePointer(hRebuildDump, 0, NULL, FILE_BEGIN);

   REBEL_NET_BASE rbBase;

   ReadFile(hRebuildDump, &rbBase, sizeof (REBEL_NET_BASE), &BRW, NULL);

   rbBase.NumberOfMethods++;

   SetFilePointer(hRebuildDump, 0, NULL, FILE_BEGIN);

   WriteFile(hRebuildDump, &rbBase, sizeof (REBEL_NET_BASE), &BRW, NULL);
}

BOOL CreateRebFile(AssemblyInfo *ai)
{
   DWORD BRW;

   hRebuildDump = CreateFile(ai->DumpFileName, GENERIC_READ |
      GENERIC_WRITE, FILE_SHARE_READ, NULL,
      CREATE_ALWAYS, 0, NULL);

   if (hRebuildDump == INVALID_HANDLE_VALUE)
      return FALSE;

   REBEL_NET_BASE rbBase;

   ZeroMemory(&rbBase, sizeof (REBEL_NET_BASE));

   rbBase.Signature = REBEL_NET_SIGNATURE;
   rbBase.MethodsOffset = sizeof (REBEL_NET_BASE);

   WriteFile(hRebuildDump, &rbBase, sizeof (REBEL_NET_BASE), &BRW, NULL);

   //
   // Get the new JIT compileMethod which by now should be
   // hooked by the code protection
   //

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

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

   if (!p_getJit)
   {
      CloseHandle(hRebuildDump);
      hRebuildDump = INVALID_HANDLE_VALUE;
      return FALSE;
   }

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

   //

   REBEL_NET_BASE repBase;

   if (!ReadFile(ai->hRebReport, &repBase, sizeof (REBEL_NET_BASE), &BRW, NULL))
   {
      CloseHandle(hRebuildDump);
      hRebuildDump = INVALID_HANDLE_VALUE;
      return FALSE;
   }

   UINT CurMethodOffset = repBase.MethodsOffset;

   for (UINT x = 0; x < repBase.NumberOfMethods; x++)
   {
      SetFilePointer(ai->hRebReport, CurMethodOffset, NULL, FILE_BEGIN);

      REBEL_METHOD rbRepMethod;

      ReadFile(ai->hRebReport, &rbRepMethod, sizeof (REBEL_METHOD), &BRW, NULL);

      //
      // Calculate current method's code location
      // Use RvaToOffset only when the module wasn't mapped
      //

      BYTE *pMethodCode;

      if (ai->bIdentified)
      {
         pMethodCode = (BYTE *) (rbRepMethod.RVA + (ULONG_PTR) ai->ImgBase);
      }
      else
      {
         DWORD Offset = RvaToOffset(ai->ImgBase, rbRepMethod.RVA);

         if (Offset == 0) continue;

         pMethodCode = (BYTE *) (Offset + (ULONG_PTR) ai->ImgBase);
      }

      //
      // we should check the validity of the memory
      // pointer by pMethodCode
      //

      // TODO

      //
      // skips the method header
      //

      BYTE HeaderFormat = *pMethodCode;

      HeaderFormat &= 3;

      if (HeaderFormat == 2)         // Tiny = 2 (CorILMethod_TinyFormat)
         pMethodCode++;
      else                     // Fat = 3 (CorILMethod_FatFormat)
         pMethodCode += (sizeof (DWORD) * 3);

      //
      // Do the fake compileMethod request
      //

      CORINFO_METHOD_INFO mi = { 0 };

      mi.ILCode = pMethodCode;
      mi.ILCodeSize = rbRepMethod.CodeSize;
      mi.scope = ai->hCorModule;
      // use this to pass the token to our AddMethod
      mi.locals.token = rbRepMethod.Token;

      pJit->compileMethod((ULONG_PTR) pJit, NULL, &mi, 0, NULL, NULL);

      //
      // next method
      //

      CurMethodOffset += GetMethodSize(&rbRepMethod);
   }

   //
   // Close file and notify the user
   //

   CloseHandle(hRebuildDump);
   hRebuildDump = INVALID_HANDLE_VALUE;

   MessageBox(0, _T("Assembly code successfully dumped."), _T("JIT Dumper"),
      MB_ICONINFORMATION);

   return TRUE;
}

如果代码有点凌乱,我深表歉意,但我并没有费心去编写它,因为一开始根本没有花时间设计它。我还不得不重写代码的几个部分,因为我改变了三次方法。尽管如此,我希望您仍然能够理解它。

代码弹出演示

当然,演示是必不可少的。由于我不打算侵犯商业产品的许可,因此本段的“受害者”将是 rendari 的“`cryxenet 2 unpackme`”,这是一个使用通过钩子 `compileMethod` 函数进行代码注入的小型 .NET crackme。该 crackme 附带 3 个文件:一个 *unpackme.exe*,一个 *native.dll* 和一个 *cryxed.dll*。启动后,它会显示一个表单,要求输入姓名/序列号,然后当用户按下“检查”按钮时进行检查。当序列号检查过程显示有效姓名/序列号的消息框时,该 crackme 应该被认为是已解决的。感谢 rendari 使此演示成为可能。

实际上,为了解决 crackme,并不需要分析它。但我还是要这样做,因为它可能会让你了解一个基本的代码注入器是如何工作的。

如果尝试用 Reflector 反编译 *unpackme.exe* 的主函数,它会显示一个错误。所以,让我们看看 MSIL 代码:

.method public static void  main() cil
managed

// SIG: 00 00 01
{
  .entrypoint
  .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 ) 
  // Method begins at RVA 0x2050
  // Code size       1483 (0x5cb)
  .maxstack  4
  .locals init (int64 V_0,
           class [mscorlib]System.IO.FileInfo V_1,
           class [mscorlib]System.Reflection.Assembly V_2,
           object[] V_3,
           string[] V_4,
           int64 V_5,
           int64 V_6,
           int64 V_7,
           int64 V_8,
           int64 V_9,
           class [mscorlib]System.IO.FileStream V_10,
           native int V_11,
           uint8[] V_12,
           class Project1.Program/obfuscation8 V_13,
           uint8[] V_14,
           int64 V_15,
           int64 V_16,
           int64 V_17)
  IL_0000:  /* 00   |                  */ nop
  IL_0001:  /* 1F   | 63               */ ldc.i4.s   99
  IL_0003:  /* 6A   |                  */ conv.i8
  IL_0004:  /* 13   | 05               */ stloc.s    V_5
  IL_0006:  /* 1F   | 4B               */ ldc.i4.s   75
  IL_0008:  /* 6A   |                  */ conv.i8
  IL_0009:  /* 13   | 06               */ stloc.s    V_6
  IL_000b:  /* 20   | 8D030000         */ ldc.i4     0x38d
  IL_0010:  /* 6A   |                  */ conv.i8
  IL_0011:  /* 13   | 07               */ stloc.s    V_7
  IL_0013:  /* 28   | (06)000002       */ call       int64
Project1.Program::IsDebuggerPresent()
  IL_0018:  /* 26   |                  */ pop
  IL_0019:  /* 11   | 06               */ ldloc.s    V_6
  IL_001b:  /* 2B   | 20               */ br.s       IL_003d
  IL_001d:  /* D6   |                  */ add.ovf
  IL_001e:  /* 13   | 05               */ stloc.s    V_5
  IL_0020:  /* 11   | 07               */ ldloc.s    V_7
  IL_0022:  /* 1D   |                  */ ldc.i4.7
  IL_0023:  /* 6A   |                  */ conv.i8
  IL_0024:  /* DA   |                  */ sub.ovf
  IL_0025:  /* 13   | 07               */ stloc.s    V_7
  IL_0027:  /* 11   | 07               */ ldloc.s    V_7
  IL_0029:  /* 11   | 05               */ ldloc.s    V_5
  IL_002b:  /* D6   |                  */ add.ovf
  IL_002c:  /* 13   | 06               */ stloc.s    V_6
  IL_002e:  /* 11   | 07               */ ldloc.s    V_7
  IL_0030:  /* 1D   |                  */ ldc.i4.7
  IL_0031:  /* 6A   |                  */ conv.i8
  IL_0032:  /* D6   |                  */ add.ovf
  IL_0033:  /* 13   | 07               */ stloc.s    V_7
  IL_0035:  /* 28   | (06)000002       */ call       int64
Project1.Program::IsDebuggerPresent()
  IL_003a:  /* 26   |                  */ pop
  IL_003b:  /* 11   | 06               */ ldloc.s    V_6


  IL_003d:  /* 11   | 07               */ ldloc.s    V_7
  IL_003f:  /* D6   |                  */ add.ovf
  IL_0040:  /* 13   | 05               */ stloc.s    V_5
  IL_0042:  /* 11   | 07               */ ldloc.s    V_7
  IL_0044:  /* 1D   |                  */ ldc.i4.7
  IL_0045:  /* 6A   |                  */ conv.i8
  IL_0046:  /* DA   |                  */ sub.ovf
  IL_0047:  /* 13   | 07               */ stloc.s    V_7
  IL_0049:  /* 11   | 07               */ ldloc.s    V_7
  IL_004b:  /* 11   | 05               */ ldloc.s    V_5
  IL_004d:  /* D6   |                  */ add.ovf
  IL_004e:  /* 13   | 06               */ stloc.s    V_6
  IL_0050:  /* 11   | 07               */ ldloc.s    V_7
  IL_0052:  /* 1D   |                  */ ldc.i4.7
  IL_0053:  /* 6A   |                  */ conv.i8

我不会粘贴所有代码,因为它数量非常庞大。正如我们所注意到的,代码中有很多 `nop`,但这无关紧要:反编译器会简单地忽略它们。由于我过去开发过混淆器,我知道什么会让反编译器崩溃。因此,我开始寻找跳转。我注意到代码开头有一个:

  IL_001b:  /* 2B   | 20               */
br.s       IL_003d
  IL_001d:  /* D6   |                  */ add.ovf
  IL_001e:  /* 13   | 05               */ stloc.s    V_5
  IL_0020:  /* 11   | 07               */ ldloc.s    V_7
  IL_0022:  /* 1D   |                  */ ldc.i4.7
  IL_0023:  /* 6A   |                  */ conv.i8
  IL_0024:  /* DA   |                  */ sub.ovf
  IL_0025:  /* 13   | 07               */ stloc.s    V_7
  IL_0027:  /* 11   | 07               */ ldloc.s    V_7
  IL_0029:  /* 11   | 05               */ ldloc.s    V_5
  IL_002b:  /* D6   |                  */ add.ovf
  IL_002c:  /* 13   | 06               */ stloc.s    V_6
  IL_002e:  /* 11   | 07               */ ldloc.s    V_7
  IL_0030:  /* 1D   |                  */ ldc.i4.7
  IL_0031:  /* 6A   |                  */ conv.i8
  IL_0032:  /* D6   |                  */ add.ovf
  IL_0033:  /* 13   | 07               */ stloc.s    V_7
  IL_0035:  /* 28   | (06)000002       */ call       int64
Project1.Program::IsDebuggerPresent()
  IL_003a:  /* 26   |                  */ pop
  IL_003b:  /* 11   | 06               */ ldloc.s    V_6


  IL_003d:  /* 11   | 07               */ ldloc.s    V_7

这个跳转是无条件的。偏移量 `0x1B` 和 `0x3D` 之间的操作码永远不会被执行:我检查了之后的所有代码,绝对没有引用这些操作码。因此,我用 CFF Explorer `nop` 掉了它们(包括跳转),然后再次用 Reflector 反编译。

[STAThread]
public static void main()
{
    long num6;
    long num2 = 0x63L;
    long num3 = 0x4bL;
    long num4 = 0x38dL;
    IsDebuggerPresent();
    num2 = num3 + num4;
    num4 -= 7L;
    num3 = num4 + num2;
    num4 += 7L;
    IsDebuggerPresent();
    num2 = num3 + num4;
    num4 -= 7L;
    num3 = num4 + num2;
    num4 += 7L;
    num2 = num3 + num4;
    num4 -= 7L;

它奏效了,但现在我们面临着代码丛林。我只粘贴了一些指令,因为丛林模式非常简单,如您所见,它只是重复。

x = y + z;z -= 7; / z += 7

我本来可以用记事本删除所有丛林,但一个做这件工作的 CFF Explorer 脚本对我来说似乎是一个更优雅的解决方案。您可以从这两个丛林示例中识别出模式。

  IL_003d:  /* 11   | 07               */
ldloc.s    V_7
  IL_003f:  /* D6   |                  */ add.ovf
  IL_0040:  /* 13   | 05               */ stloc.s    V_5
  IL_0042:  /* 11   | 07               */ ldloc.s    V_7
  IL_0044:  /* 1D   |                  */ ldc.i4.7
  IL_0045:  /* 6A   |                  */ conv.i8
  IL_0046:  /* DA   |                  */ sub.ovf
  IL_0047:  /* 13   | 07               */ stloc.s    V_7


  IL_0049:  /* 11   | 07               */ ldloc.s    V_7
  IL_004b:  /* 11   | 05               */ ldloc.s    V_5
  IL_004d:  /* D6   |                  */ add.ovf
  IL_004e:  /* 13   | 06               */ stloc.s    V_6
  IL_0050:  /* 11   | 07               */ ldloc.s    V_7
  IL_0052:  /* 1D   |                  */ ldc.i4.7
  IL_0053:  /* 6A   |                  */ conv.i8
  IL_0054:  /* D6   |                  */ add.ovf
  IL_0055:  /* 13   | 07               */ stloc.s    V_7

指令可能会略有变化,但仍然很容易找到模式。这是 CFF Explorer 脚本,用于解除代码丛林:

filename = GetOpenFile()

if filename == null
then
   return
end

hFile = OpenFile(filename)

if hFile == 
null then
   return
end

-- nop the initial assignment of the variables
-- and also the jump that causes the decompiler
-- to crash

FillBytes(hFile,
0x105C, 0x49, 0)

jungle = { 0x11, ND,
ND, ND,
ND, ND,
ND, 0x11, ND, 0x1D,

      0x6A, ND, 0x13, 0x07 }

Offset = SearchBytes(hFile,
0x105D, jungle)

while Offset !=
null do

   -- check if it exceeds the method function

   if Offset >
0x1050 + 1483 then
      break
   end

   -- nop jungle

   FillBytes(hFile,
Offset ,
#jungle, 0)

   Offset = SearchBytes(hFile,
Offset + 1, jungle)
end

if SaveFile(hFile)
== true then
   MsgBox("dejungled")
end

现在可以反编译(并阅读)代码了

public static void main()
{
    long num6;
    IsDebuggerPresent();
    FileStream stream = new FileInfo("native.dll").OpenRead();
    long length = stream.Length;
    byte[] array = new byte[((int) length) + 1];
    stream.Read(array, 0, (int) length);
    stream.Close();
    long num7 = length;
    for (num6 = 0L; num6 <= num7; num6 += 1L)
    {
        array[(int) num6] = (byte) (array[(int) num6] ^ 0x37);
    }
    IntPtr destination = new IntPtr();
    destination = Marshal.AllocCoTaskMem((int) length);
    Marshal.Copy(array, 0, destination, (int) length);
    obfuscation8 delegateForFunctionPointer = (obfuscation8) 
    Marshal.GetDelegateForFunctionPointer(destination, typeof(obfuscation8));
    IsDebuggerPresent();
    long num = new long();
    num = delegateForFunctionPointer();
    IsDebuggerPresent();
    stream = new FileInfo("cryxed.dll").OpenRead();
    length = stream.Length;
    byte[] buffer2 = new byte[((int) length) + 1];
    IsDebuggerPresent();
    stream.Read(buffer2, 0, (int) length);
    stream.Close();
    IsDebuggerPresent();
    long num8 = length;
    for (num6 = 0L; num6 <= num8; num6 += 1L)
    {
        buffer2[(int) num6] = (byte) (buffer2[(int) num6] ^ 0x37);
    }IsDebuggerPresent();
    Assembly assembly = Assembly.Load(buffer2);
    IsDebuggerPresent();
    object[] parameters = new object[1];
    string[] strArray = new string[] { "" };
    parameters[0] = strArray;
    assembly.EntryPoint.Invoke(null, parameters);
    IsDebuggerPresent();
}

这部分代码用 `xor` 解密了 *native.dll*,并将其转换为一个本机函数。

FileStream stream = new FileInfo("native.dll").OpenRead();
long length = stream.Length;
byte[] array = new byte[((int) length) + 1];
stream.Read(array, 0, (int) length);
stream.Close();
long num7 = length;
for (num6 = 0L; num6 <= num7; num6 += 1L)
{
    array[(int) num6] = (byte) (array[(int) num6] ^ 0x37);
}
IntPtr destination = new IntPtr();
destination = Marshal.AllocCoTaskMem((int) length);
Marshal.Copy(array, 0, destination, (int) length);
obfuscation8 delegateForFunctionPointer = (obfuscation8) 
Marshal.GetDelegateForFunctionPointer(destination, typeof(obfuscation8));

`GetDelegateForFunctionPointer` 使得通过传递函数指针来调用本机函数成为可能。要查看函数的代码,只需打开 CFF Explorer,转到十六进制编辑器,右键单击十六进制视图并按“全选”。然后再次右键单击并点击“修改”。在值框中输入字节 `0x37` 并按确定。您现在拥有已解密的文件,可以进行反汇编。

解密 *cryxed.dll*(受保护的 .NET 程序集)也使用了完全相同的方法。因此,为了获得要重建的程序集,不必从内存中转储它:可以通过遵循刚刚解释的简单解密方法来获得。应该注意的是,crackme 使用了我之前在.NET 加载器段落中讨论的 `Assembly.Load` 方法。总而言之,crackme 的主函数钩子 JIT,然后加载受保护的程序集并调用其入口点。

我们首先应该完成的是从解密的 *cryxed.dll* 或转储的程序集中创建一个 *Rebel.NET* 报告文件。受保护的程序集是未识别的。

如果已经创建了 rebel 报告文件,那么可以通过单击“生成 Rebel 文件”按钮来生成重建 rebel 文件。如果弹出过程成功,将弹出一个消息框通知用户操作成功。

成功创建重建 *rebl* 文件后,使用 Rebel.NET 进行简单的重建将生成一个完全可反编译/可运行的程序集。

既然我们有了原始程序集,就可以反汇编它了。`UnpackMe.Form1` 命名空间包含三个按钮事件。这是第一个按钮事件:

.method private instance void 
Button1_Click(object sender,
                                             class [mscorlib]System.EventArgs e)
cil managed
{
  // Code size       95 (0x5f)
  .maxstack  3
  .locals init (string V_0,
           string V_1,
           string V_2,
           string V_3,
           bool V_4)
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  callvirt   instance class
[System.Windows.Forms]System.Windows.Forms.TextBox
UnpackMe.Form1::get_TextBox1()
  IL_0007:  callvirt   instance string
[System.Windows.Forms]System.Windows.Forms.TextBox::get_Text()
  IL_000c:  stloc.1
  IL_000d:  ldarg.0
  IL_000e:  callvirt   instance class
[System.Windows.Forms]System.Windows.Forms.TextBox
UnpackMe.Form1::get_TextBox2()
  IL_0013:  callvirt   instance string
[System.Windows.Forms]System.Windows.Forms.TextBox::get_Text()
  IL_0018:  stloc.3
  IL_0019:  ldstr      "Such a naiive serial routine :D"
  IL_001e:  stloc.0
  IL_001f:  ldarg.0
  IL_0020:  ldloc.1
  IL_0021:  ldloc.0
  IL_0022:  callvirt   instance string UnpackMe.Form1::TripleDESEncode(string,
                                                                       string)
  IL_0027:  stloc.2
  IL_0028:  ldarg.0
  IL_0029:  callvirt   instance class
[System.Windows.Forms]System.Windows.Forms.TextBox
UnpackMe.Form1::get_TextBox2()
  IL_002e:  callvirt   instance string
[System.Windows.Forms]System.Windows.Forms.TextBox::get_Text()
  IL_0033:  ldloc.2
  IL_0034:  ldc.i4.0
  IL_0035:  call       int32
[Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Operators::
    CompareString(string, string, bool)
  IL_003a:  ldc.i4.0
  IL_003b:  ceq
  IL_003d:  stloc.s    V_4
  IL_003f:  ldloc.s    V_4
  IL_0041:  brfalse.s  IL_0050
  IL_0043:  ldstr      "Good work! Now go and post a solution or suggestion"
  + "ns so that I can improve the protector =)"
  IL_0048:  call       valuetype
[System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
  IL_004d:  pop
  IL_004e:  br.s       IL_005c
  IL_0050:  nop
  IL_0051:  ldstr      "Invalid Serial. Pls don't hack me :'("
  IL_0056:  call       valuetype
[System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
  IL_005b:  pop
  IL_005c:  nop
  IL_005d:  nop
  IL_005e:  ret
} // end of method Form1::Button1_Click

这显然是序列号检查例程。为了绕过它以显示有效序列号消息框,只需反转代码中突出显示的分支即可。这可以用 CFF Explorer 轻松完成。

现在,这个 crackme 可以被认为是已解决了,因为它总是显示正确的消息(当然,如果您插入了有效的名称和序列号除外,但不应期望仅通过猜测就能解决 3DES 加密)。当然,已解决的 crackme 及其原始文件均可下载。

当然,这个 crackme 可以用不同的方式解决。但这超出了本段的范围,本段是一个代码弹出演示。

.NET 内部机制(第二部分:MethodDesc)

我之前说过要探讨 `CORINFO_METHOD_HANDLE` 的真正含义。所以,这一段就是要做这个。

我第一次意识到这个指针的含义是在 *jitinterface.cpp* 中遇到这段代码时。

CHECK CheckContext(CORINFO_MODULE_HANDLE scopeHnd, CORINFO_CONTEXT_HANDLE context)
{
    CHECK_MSG(scopeHnd != NULL, "Illegal null scope");
    CHECK_MSG(((size_t) context & ~CORINFO_CONTEXTFLAGS_MASK) != 
            NULL, "Illegal null context");
    if (((size_t) context & CORINFO_CONTEXTFLAGS_MASK) == CORINFO_CONTEXTFLAGS_CLASS)
    {
        TypeHandle handle((CORINFO_CLASS_HANDLE) ((size_t) context 
                & ~CORINFO_CONTEXTFLAGS_MASK));
        CHECK_MSG(handle.GetModule() == GetModule(scopeHnd), 
                "Inconsistent scope and context");
    }
    else
    {
        MethodDesc* handle = (MethodDesc*) ((size_t) context 
                & ~CORINFO_CONTEXTFLAGS_MASK);
        CHECK_MSG(handle->GetModule() == GetModule(scopeHnd), 
                "Inconsistent scope and context");
    }

    CHECK_OK;
}

别介意 `CORINFO_CONTEXT_HANDLE` 是函数的第二个参数。调用 `CheckContext` 的代码将 `CORINFO_METHOD_HANDLE` 作为上下文传递。

可以得出结论,`CORINFO_METHOD_HANDLE` 只是指向 `MethodDesc` 类的一个指针。`MethodDesc` 类是框架最重要的部分之一,因为它提供了大量信息。这个类的声明在 *clr\src\vm\method.hpp* 文件中。

// The size of this structure needs to be a multiple of 8-bytes
//
// The following members insure that the size of this structure obeys this rule
//
//  m_pDebugAlignPad
//  m_dwAlign2
//
// If the layout of this struct changes, these may need to be revisited
// to make sure the size is a multiple of 8-bytes.
//
// @GENERICS:
// Method descriptors for methods belonging to instantiated types may be 
// shared between compatible instantiations
// Hence for reflection and elsewhere where exact types are important 
// it's necessary to pair a method desc
// with the exact owning type handle.
//
// See genmeth.cpp for details of instantiated generic method descriptors.
class MethodDesc
{
    friend class EEClass;
    friend class MethodTableBuilder;
    friend class ArrayClass;
    friend class NDirect;
    friend class InstantiatedMethodDesc;
    friend class MDEnums;
    friend class MethodImpl;
    friend class CheckAsmOffsets;
    friend class ClrDataAccess;
    friend class ZapMonitor;

    friend class MethodDescCallSite;

public:

    [...]


    inline BOOL HasStableEntryPoint()
    {
        LEAF_CONTRACT;
        return (m_bFlags2 & enum_flag2_HasStableEntryPoint) != 0;
    }

    inline TADDR GetStableEntryPoint()
    {
        WRAPPER_CONTRACT;
        _ASSERTE(HasStableEntryPoint());
        return *GetAddrOfSlotUnchecked();
    }

    BOOL SetStableEntryPointInterlocked(TADDR addr);

    BOOL HasTemporaryEntryPoint();
    TADDR GetTemporaryEntryPoint();

    void SetTemporaryEntryPoint(BaseDomain *pDomain, AllocMemTracker *pamTracker);

    inline BOOL HasPrecode()
    {
        LEAF_CONTRACT;
        return (m_bFlags2 & enum_flag2_HasPrecode) != 0;
    }

    inline void SetHasPrecode()
    {
        LEAF_CONTRACT;
        m_bFlags2 |= (enum_flag2_HasPrecode | enum_flag2_HasStableEntryPoint);
    }

    inline void ResetHasPrecode()
    {
        LEAF_CONTRACT;
        m_bFlags2 &= ~enum_flag2_HasPrecode;
        m_bFlags2 |= enum_flag2_HasStableEntryPoint;
    }

    inline Precode* GetPrecode()
    {
        LEAF_CONTRACT;
        PRECONDITION(HasPrecode());
        Precode* pPrecode = Precode::GetPrecodeFromEntryPoint(GetStableEntryPoint());
        PREFIX_ASSUME(pPrecode != NULL);
        return pPrecode;
    }

    inline BOOL MayHavePrecode()
    {
        WRAPPER_CONTRACT;
        return !MayHaveNativeCode() || PrestubMayInsertStub() || RequiresPrestub();
    }

    void InterlockedUpdateFlags2(BYTE bMask, BOOL fSet);

    Precode* GetOrCreatePrecode();

    inline BYTE* GetCallablePreStubAddr()
    {
        WRAPPER_CONTRACT;
        return HasStableEntryPoint() ? (BYTE*)GetStableEntryPoint() : 
            (BYTE*)GetTemporaryEntryPoint();
    }

    // return the address of the stub
    static inline MethodDesc* GetMethodDescFromStubAddr
            (TADDR addr, BOOL fSpeculative = FALSE);

    DWORD GetAttrs();

    DWORD GetImplAttrs();

    // This function can lie if a method impl was used to implement
    // more than one method on this class. Use GetName(int) to indicate
    // which slot you are interested in.
    // See the TypeString class for better control over name formatting.
    LPCUTF8 GetName();

    LPCUTF8 GetName(USHORT slot);

    FORCEINLINE LPCUTF8 GetNameOnNonArrayClass()
    {
        WRAPPER_CONTRACT;
        return (GetMDImport()->GetNameOfMethodDef(GetMemberDef()));
    }

    COUNT_T GetStableHash();

    // Non-zero for InstantiatedMethodDescs
    DWORD GetNumGenericMethodArgs();

    // Return the number of class type parameters that are in scope for this method
    DWORD GetNumGenericClassArgs()
    {
        WRAPPER_CONTRACT;
        return GetMethodTable()->GetNumGenericArgs();
    }

    BOOL IsGenericMethodDefinition();

    // True if the declaring type or instantiation of method (if any) 
    // contains formal generic type parameters
    BOOL ContainsGenericVariables();

    Module* GetDefiningModuleForOpenMethod();

    // True if this has a class or method instantiation that is anything other than <object,...,object />
    BOOL HasNonObjectClassOrMethodInstantiation();


    // True if and only if this is a method descriptor for :
    // 1. a non-generic method or a generic method at its typical method instantiation
    // 2. in a non-generic class or a typical instantiation of a generic class
    // This method can be called on a non-restored method desc
    BOOL IsTypicalMethodDefinition();

由于它包含的方法,这个类的大小令人印象深刻。我只能粘贴很小的一部分,只是为了让读者有一个概念。类声明上方的注释提醒我们 `CORINFO_METHOD_HANDLE` 指向的数据,它也是 8 字节对齐的。

这是在 `MethodDesc` 类声明的末尾可以找到的内容

    //================================================================
    // The actual data stored in a MethodDesc follows.

protected:
    UINT16      m_wTokenRemainder;
    BYTE        m_chunkIndex;

    enum {
        // enum_flag2_HasPrecode implies that enum_flag2_HasStableEntryPoint is set.
        // The method entrypoint is stable (either precode or actual code)
        enum_flag2_HasStableEntryPoint      = 0x01,   
                    // Precode has been allocated for this method
        enum_flag2_HasPrecode               = 0x02,   

        enum_flag2_IsUnboxingStub           = 0x04,
        // May have jitted code, ngened code or fcall entrypoint.
        enum_flag2_MayHaveNativeCode        = 0x08,   
            };
    BYTE        m_bFlags2;

    // The slot number of this MethodDesc in the vtable array.
    WORD           m_wSlotNumber;

    // Flags.
    WORD           m_wFlags;

而且这些数据与我之前的直觉完全吻合。我们现在可以将每个 `CORINFO_METHOD_HANDLE` 用作 `MethodDesc` 类。当然,考虑到其复杂性,包含整个 `MethodDesc` 类会相当痛苦。但是可以编写自己的简化版 `MethodDesc` 类:所有需要做的是包含我上面粘贴的成员,这将导致类的大小是 8 字节的倍数。

`MethodDesc` 类对于许多目的都很有用,而且它的使用相当安全,因为它不会很快改变。即使改变了:它的成员(除了方法)也相当少,所以我猜要有一个可用的简化版 `MethodDesc` 类并不困难。

我本会提供一个如何使用 `MethodDesc` 类的示例,但当我撰写本文时,它已经相当庞大,虽然现在缩短为时已晚,但我仍然希望它具有可读性。事实上,深入 .NET Framework 内部机制的旅程尚未结束,还有一些事情需要讨论。

.NET 内部机制(第三部分:IEE、内部调用等)

除了 JIT 之外,.NET Framework 还有其他非常有趣的部分需要讨论。当然,我无法在本文中全部讨论,我只是想让读者了解它们可以多么容易地被探索。

关于执行引擎,有些事情是必须说的,尽管可以从 `mscorwks` 轻松检索到的接口并不是很有趣。但在这一段中,我还将讨论一些看起来有用但实际上并非如此的事情。

mscorwks.dll 模块导出一个名为 `IEE` 的函数,这可能会引起逆向工程师的兴趣。然而,这个 API 的内部机制却相当令人失望。

// This is the instance that exposes interfaces out to all the other DLLs of the CLR
// so they can use our services for TLS, synchronization, memory allocation, etc.
static BYTE g_CEEInstance[sizeof(CExecutionEngine)];
static IExecutionEngine * g_pCEE = NULL;

PTLS_CALLBACK_FUNCTION CExecutionEngine::Callbacks[MAX_PREDEFINED_TLS_SLOT];

extern "C" IExecutionEngine * __stdcall IEE()
{
    LEAF_CONTRACT;

    if ( !g_pCEE )
    {
        // Create a local copy on the stack and then copy it over to the 
        // static instance.
        // This avoids race conditions caused by multiple initializations 
        // of vtable in the constructor
       CExecutionEngine local;
       memcpy(&g_CEEInstance, &local, sizeof(CExecutionEngine));

       g_pCEE = (IExecutionEngine *)(CExecutionEngine*)&g_CEEInstance;
    }
    //END_ENTRYPOINT_VOIDRET;

    return g_pCEE;
}

正如注释中所见,此函数仅提供内存分配和进程同步的接口。实际上,这是返回类的声明:

// We have an internal class that can be used to expose EE functionality to other CLR
// DLLs, via the deliberately obscure IEE DLL exports from the shim and the EE
class CExecutionEngine : public IExecutionEngine, public IEEMemoryManager
{
    //***************************************************************************
    // public API:
    //***************************************************************************
public:

    // Notification of a DLL_THREAD_DETACH or a Thread Terminate.
    static void ThreadDetaching(void **pTlsData);

    // Delete on TLS block
    static void DeleteTLS(void **pTlsData);

    // Fiber switch notifications
    static void SwitchIn();
    static void SwitchOut();

    static void **CheckThreadState(DWORD slot, BOOL force = TRUE);
    static void **CheckThreadStateNoCreate(DWORD slot);

    // Setup FLS simulation block, including ClrDebugState and StressLog.
    static void SetupTLSForThread(Thread *pThread);

    static DWORD GetTlsIndex () {return TlsIndex;}

    static BOOL HasDetachedTlsInfo();

    static void CleanupDetachedTlsInfo();

    static void DetachTlsInfo(void **pTlsData);

    //***************************************************************************
    // private implementation:
    //***************************************************************************
private:

    // The debugger needs access to the TlsIndex so that we can read it from OOP.
    friend class EEDbgInterfaceImpl;

    SVAL_DECL (DWORD, TlsIndex);

    static PTLS_CALLBACK_FUNCTION Callbacks[MAX_PREDEFINED_TLS_SLOT];


    //***************************************************************************
    // IUnknown methods
    //***************************************************************************

    HRESULT STDMETHODCALLTYPE QueryInterface(
            REFIID id,
            void **pInterface);

    ULONG STDMETHODCALLTYPE AddRef();

    ULONG STDMETHODCALLTYPE Release();

    //***************************************************************************
    // IExecutionEngine methods for TLS
    //***************************************************************************

    // Associate a callback for cleanup with a TLS slot
    VOID  STDMETHODCALLTYPE TLS_AssociateCallback(
            DWORD slot,
            PTLS_CALLBACK_FUNCTION callback);

    // May be called once to get the master TLS block slot for fast Get/Set operations
    DWORD STDMETHODCALLTYPE TLS_GetMasterSlotIndex();

    // Get the value at a slot
    LPVOID STDMETHODCALLTYPE TLS_GetValue(DWORD slot);

    // Get the value at a slot, return FALSE if TLS info block doesn't exist
    BOOL STDMETHODCALLTYPE TLS_CheckValue(DWORD slot, LPVOID * pValue);

    // Set the value at a slot
    VOID STDMETHODCALLTYPE TLS_SetValue(DWORD slot, LPVOID pData);

    // Free TLS memory block and make callback
    VOID STDMETHODCALLTYPE TLS_ThreadDetaching();

    //***************************************************************************
    // IExecutionEngine methods for locking
    //***************************************************************************

    CRITSEC_COOKIE STDMETHODCALLTYPE CreateLock(LPCSTR szTag, LPCSTR level, 
        CrstFlags flags);

    void STDMETHODCALLTYPE DestroyLock(CRITSEC_COOKIE lock);

    void STDMETHODCALLTYPE AcquireLock(CRITSEC_COOKIE lock);

    void STDMETHODCALLTYPE ReleaseLock(CRITSEC_COOKIE lock);

    EVENT_COOKIE STDMETHODCALLTYPE CreateAutoEvent(BOOL bInitialState);
    EVENT_COOKIE STDMETHODCALLTYPE CreateManualEvent(BOOL bInitialState);
    void STDMETHODCALLTYPE CloseEvent(EVENT_COOKIE event);
    BOOL STDMETHODCALLTYPE ClrSetEvent(EVENT_COOKIE event);
    BOOL STDMETHODCALLTYPE ClrResetEvent(EVENT_COOKIE event);
    DWORD STDMETHODCALLTYPE WaitForEvent
        (EVENT_COOKIE event, DWORD dwMilliseconds, BOOL bAlertable);
    DWORD STDMETHODCALLTYPE WaitForSingleObject(HANDLE handle, DWORD dwMilliseconds);

    SEMAPHORE_COOKIE STDMETHODCALLTYPE ClrCreateSemaphore(DWORD dwInitial, DWORD dwMax);
    void STDMETHODCALLTYPE ClrCloseSemaphore(SEMAPHORE_COOKIE semaphore);
    DWORD STDMETHODCALLTYPE ClrWaitForSemaphore
        (SEMAPHORE_COOKIE semaphore, DWORD dwMilliseconds, BOOL bAlertable);
    BOOL STDMETHODCALLTYPE ClrReleaseSemaphore
        (SEMAPHORE_COOKIE semaphore, LONG lReleaseCount, LONG *lpPreviousCount);

    MUTEX_COOKIE STDMETHODCALLTYPE ClrCreateMutex
                                              (LPSECURITY_ATTRIBUTES lpMutexAttributes,
                                               BOOL bInitialOwner,
                                               LPCTSTR lpName);
    void STDMETHODCALLTYPE ClrCloseMutex(MUTEX_COOKIE mutex);
    BOOL STDMETHODCALLTYPE ClrReleaseMutex(MUTEX_COOKIE mutex);
    DWORD STDMETHODCALLTYPE ClrWaitForMutex(MUTEX_COOKIE mutex,
                                            DWORD dwMilliseconds,
                                            BOOL bAlertable);

    DWORD STDMETHODCALLTYPE ClrSleepEx(DWORD dwMilliseconds, BOOL bAlertable);

    BOOL STDMETHODCALLTYPE ClrAllocationDisallowed();

    void STDMETHODCALLTYPE GetLastThrownObjectExceptionFromThread(void **ppvException);

    //***************************************************************************
    // IEEMemoryManager methods for locking
    //***************************************************************************
    LPVOID STDMETHODCALLTYPE ClrVirtualAlloc(LPVOID lpAddress, SIZE_T dwSize, 
        DWORD flAllocationType, DWORD flProtect);
    BOOL STDMETHODCALLTYPE ClrVirtualFree(LPVOID lpAddress, SIZE_T dwSize, 
        DWORD dwFreeType);
    SIZE_T STDMETHODCALLTYPE ClrVirtualQuery(LPCVOID lpAddress, 
        PMEMORY_BASIC_INFORMATION lpBuffer, SIZE_T dwLength);
    BOOL STDMETHODCALLTYPE ClrVirtualProtect(LPVOID lpAddress, 
        SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect);
    HANDLE STDMETHODCALLTYPE ClrGetProcessHeap();
    HANDLE STDMETHODCALLTYPE ClrHeapCreate(DWORD flOptions, 
        SIZE_T dwInitialSize, SIZE_T dwMaximumSize);
    BOOL STDMETHODCALLTYPE ClrHeapDestroy(HANDLE hHeap);
    LPVOID STDMETHODCALLTYPE ClrHeapAlloc(HANDLE hHeap, DWORD dwFlags, SIZE_T dwBytes);
    BOOL STDMETHODCALLTYPE ClrHeapFree(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem);
    BOOL STDMETHODCALLTYPE ClrHeapValidate(HANDLE hHeap, DWORD dwFlags, LPCVOID lpMem);
    HANDLE STDMETHODCALLTYPE ClrGetProcessExecutableHeap();
};

`IExecutionEngine` 和 `IEEMemoryManager` 都只是接口。因此,`IEE` 接口不提供任何额外功能。

该框架还导出了两个可能引起逆向工程师注意的函数:`GetRealProcAddress` 和 `GetCLRFunction`。不幸的是,它们都无用。`GetRealProcAddress` 只是调用 `LoadLibrary("mscorwks.dll")`,然后调用 `GetProcAddress`。

extern "C"
STDAPI GetRealProcAddress(LPCSTR pwszProcName, VOID** ppv)
{
    if(!ppv)
    {
        return E_POINTER;
    }

    HMODULE hLib = GetLibrary(LIB_mscorwks);
    if(hLib == NULL)
    {
        return HRESULT_FROM_GetLastError();
    }

    *ppv = (void*) GetProcAddress(hLib,pwszProcName);
    if(*ppv == NULL)
    {
        return HRESULT_FROM_GetLastError();
    }
    return S_OK;
}

而 `GetCLRFunction` 只能检索三个函数的地址,不接受任何其他函数。

.text:79EA0C1B ; int __stdcall GetCLRFunction(char *)
.text:79EA0C1B                 public ?GetCLRFunction@@YGPAXPBD@Z
.text:79EA0C1B ?GetCLRFunction@@YGPAXPBD@Z proc near
.text:79EA0C1B
.text:79EA0C1B
[...]

.text:79EA0C1B 
.text:79EA0C44                mov     esi, [ebp+arg_0]
.text:79EA0C47                push    offset aClrloadlibrary ; "CLRLoadLibraryEx"

.text:79EA0C4C                push    esi             ; char *
.text:79EA0C4D                call    _strcmp
.text:79EA0C52                test    eax, eax
.text:79EA0C54                pop     ecx
.text:79EA0C55                pop     ecx
.text:79EA0C56                jz      loc_79EEF97B
.text:79EA0C5C                push    offset aClrfreelibrary ; "CLRFreeLibrary"

.text:79EA0C61                push    esi             ; char *
.text:79EA0C62                call    _strcmp
.text:79EA0C67                test    eax, eax
.text:79EA0C69                pop     ecx
.text:79EA0C6A                pop     ecx
.text:79EA0C6B                jz      loc_7A0D7B8F
.text:79EA0C71                push    offset aEeheapallocinp ; "EEHeapAllocInProcessHeap"

.text:79EA0C76                push    esi             ; char *
.text:79EA0C77                call    _strcmp
.text:79EA0C7C                test    eax, eax
.text:79EA0C7E                pop     ecx
.text:79EA0C7F                pop     ecx
.text:79EA0C80                jnz     loc_79ED7512

我不得不反汇编该函数,因为 `GetCLRFunction` 在 `Rotor` 项目中不可用。既然我已经排除了这两个,我就可以谈论一个有趣的话题了:内部调用。

内部调用是由框架本机实现的方法,可以从托管代码中调用,尽管方式非常有限,我们稍后会看到。

此类函数在 *clr\src\vm\ecall.cpp* 中以这种方式定义:

FCFuncStart(gExceptionFuncs)
    FCFuncElement("GetClassName", ExceptionNative::GetClassName)
    FCFuncElement
        ("IsImmutableAgileException", ExceptionNative::IsImmutableAgileException)
    FCFuncElement("_InternalGetMethod", SystemNative::CaptureStackTraceMethod)
    FCFuncElement("nIsTransient", ExceptionNative::IsTransient)
    FCFuncElement("GetMessageFromNativeResources", 
            ExceptionNative::GetMessageFromNativeResources)
FCFuncEnd()

FCFuncStart(gSafeHandleFuncs)
    FCFuncElement("InternalDispose", SafeHandle::DisposeNative)
    FCFuncElement("InternalFinalize", SafeHandle::Finalize)
    FCFuncElement("SetHandleAsInvalid", SafeHandle::SetHandleAsInvalid)
    FCFuncElement("DangerousAddRef", SafeHandle::DangerousAddRef)
    FCFuncElement("DangerousRelease", SafeHandle::DangerousRelease)
FCFuncEnd()

FCFuncStart(gCriticalHandleFuncs)
    FCFuncElement("FireCustomerDebugProbe", CriticalHandle::FireCustomerDebugProbe)
FCFuncEnd()

FCFuncStart(gPathFuncs)
FCFuncEnd()

FCFuncStart(gFusionWrapFuncs)
    FCFuncElement("GetNextAssembly",  FusionWrap::GetNextAssembly)
    FCFuncElement("GetDisplayName",  FusionWrap::GetDisplayName)
    FCFuncElement("ReleaseFusionHandle",  FusionWrap::ReleaseFusionHandle)
FCFuncEnd()

// etc.

`FCFuncElement` 的第一个参数指定托管上下文中的函数名称,而第二个参数指定函数的位置。访问这些 ecall(我猜它代表 engine calls)的语法如下:

[MethodImpl(MethodImplOptions.InternalCall)]
internal extern type ECallMethodName();

要使用 `MethodImpl`,必须包含 `System.Runtime.CompilerServices` 命名空间。问题是,即使您可以在项目中实现这样的调用,当您尝试实际调用其中一个内部调用时,框架将传递这样的消息:

事实上,这些函数被框架封装了。当然,我引入内部调用并不是为了记录这一点。有趣的部分是托管代码和内部调用之间的交互。让我们以这个 ecall 为例:

FCIMPL2(MethodBody *, RuntimeMethodHandle::GetMethodBody, MethodDesc **ppMethod,
        EnregisteredTypeHandle enregDeclaringTypeHandle)

// MethodBody * RuntimeMethodHandle::GetMethodBody
                (MethodDesc **, EnregisteredTypeHandle)

_GetMethodBody` 内部调用将其第一个参数作为 `MethodDesc` 的指针的指针。这个函数在 `mscorlib` ("*clr\src\bcl\system\runtimehandles.cs*") 中的第一次托管包装。

[MethodImpl(MethodImplOptions.InternalCall)]
internal extern MethodBody _GetMethodBody(IntPtr declaringType);
internal MethodBody GetMethodBody(RuntimeTypeHandle declaringType)
{
    return _GetMethodBody(declaringType.Value);
}

第一个参数消失并变得隐含。包含此方法的类在开头也定义了隐含参数:

[Serializable()]
[System.Runtime.InteropServices.ComVisible(true)]
    public unsafe struct RuntimeMethodHandle : ISerializable
    {
        internal static RuntimeMethodHandle EmptyHandle { get 
                { return new RuntimeMethodHandle(null); } }

        private IntPtr m_ptr;

`m_ptr` 参数是 `private` 的,因此无法从外部正常访问。但也许有另一种方法可以获取等效值...

// ISerializable interface
private RuntimeMethodHandle(SerializationInfo info, StreamingContext context)
{
    if(info == null)
        throw new ArgumentNullException("info");

    MethodInfo m =(RuntimeMethodInfo)info.GetValue("MethodObj", 
            typeof(RuntimeMethodInfo));

    m_ptr = m.MethodHandle.Value;

    if(m_ptr.ToPointer() == null)
        throw new SerializationException(Environment.GetResourceString
                ("Serialization_InsufficientState"));
}

`MethodHandle.Value` 是一个 `public` 值。因此,我们可以通过 `MethodInfo` 类获取 `m_ptr` 中包含的相同值。而 `m_ptr` 只是指向 `MethodDesc` 类(也称为 `CORINFO_METHOD_HANDLE`)的指针。因此,为了通过托管代码获取 `MethodDesc` 指针,可以编写以下代码:

MethodInfo mi = typeof(Form1).GetMethod("button1_Click");
// displays pointer
MessageBox.Show(mi.MethodHandle.Value.ToString("X"));

我想表达的观点是,也可以从托管代码访问 .NET 内部机制的一部分。查看托管代码和 ecall 之间的交互是发现一些有趣事物的好方法。

其他注入/弹出方法

深入研究 .NET Framework 内部机制开启了许多新的可能性。例如,钩子 `MethodDesc` 类中与 MSIL 相关的方法可能是代码注入的另一种方式。事实是,并非只有“一种方式”。就像弹出 MSIL 代码的方法不止一种一样。实际上,代码弹出可以比代码注入走得更远。在本文中,我提出了一种非常简单、非侵入性的解决方案来检索程序集的原始 MSIL,但如果想认真对待代码弹出,可以考虑使用 `Rotor`(或 `Mono`)项目的修改版本来检索原始 MSIL。或者,为了简单起见,修改官方 .NET 框架,尽管不合法,但可能是一个有效的选择。无论哪种情况,当代码弹出过程进行到如此程度时,代码注入器根本无法保护原始 MSIL。当代码弹出器本身就是框架时,这种保护无能为力。这就是我一开始就说代码注入保护很弱的原因,只要逆向工程师不决定认真对待检索 MSIL 代码,它们就可以隐藏代码。

结论

由于我从未阅读过关于 CLR 基础设施的书籍或文章,因此本文中介绍的 .NET 内部机制是从逆向工程师的角度来看的。拥有(几乎完整的).NET Framework 源代码使得事情变得非常容易,撰写本文所花费的研究(包括开发)时间可以用两根手指头数过来。撰写文章付出了更大的努力。这种努力只能与阅读它所承受的痛苦相提并论。下一篇此类文章将是关于 .NET 本机编译的。它肯定会不那么无聊,因为我无需重新解释本文中已经涵盖的 .NET 内部机制的基础知识。

历史

  • 2008 年 5 月 14 日:首次发布
© . All rights reserved.