Escolar Documentos
Profissional Documentos
Cultura Documentos
Introduction
This article is the obvious culmination of the previous effort of writing the Rebel.NET application and the first of a two series of articles
about the .NET framework internals and the protections available for .NET assemblies. The next article will be about .NET native
compiling. As the JIT inner workings haven't been analyzed yet, .NET protections are quite naf nowadays. This situation will rapidly
change as soon as the reverse engineering community will focus its attention on this technology. These two articles are aimed to raise
the consiousness about the current state of .NET protections and what is possible to achieve but hasn't been done yet. In particular,
the current article about .NET code injection represents, let's say, the present, whereas the next one about .NET native compiling
represents the future. What I'm presenting in these two articles is new at the time I'm writing it, but I expect it to become obsolete in
less than a year. Of course, this is obvious as I'm moving the first steps out from current .NET protections in the direction of better
ones. But this article isn't really about protections: exploring the .NET framework internals can be useful for many purposes. So, talking
about protections is just a means to an end.
RVA2
000460A0 000460A0
000460A1 000460A1
000460A2 000460A2
000460A3 000460A3
Number of differences found: 4
By looking at the patched dword through a disassembler, it is possible to understand the kind of hook:
.text:790A60A0 ??_7CILJit@@6B@
dd offset
ntcore.com/files/netint_injection.htm
1/38
2/9/12
The patched dword is the offset of the compileMethod method of the CILJit class. Which brings us to the next paragraph.
ftn;
scope;
ILCode;
ILCodeSize;
maxStack;
EHcount;
options;
args;
locals;
The only thing code injectors have to do is to provide a valid MSIL pointer and size given the two members of this structure: ILCode and
ILCodeSize. Code injectors rely on the ILCode pointer to know which method is being requested. In fact, this pointer addresses the
original MSIL code inside the .NET assembly. Many code injectors don't even need to know which method is being requested as the
ILCode points to data which only needs to be decrypted.
The pointer to the vftable which contains the address to compileMethod is retrieved through the only API exported by mscorjit: 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);
}
And this is about all that code injectors ought to know to do their job. But we go further. The FJitCompiler class is this:
class FJitCompiler : public ICorJitCompiler
{
public:
/* the jitting function */
CorJitResult __stdcall compileMethod (
ICorJitInfo*
comp,
CORINFO_METHOD_INFO*
info,
unsigned
flags,
BYTE **
nativeEntry,
ULONG *
nativeSizeOfCode
);
/* IN */
/* IN */
/* IN */
/* OUT */
/* OUT */
ntcore.com/files/netint_injection.htm
2/38
2/9/12
private:
/* grab and remember the jitInterface helper addresses that we need at runtime */
BOOL GetJitHelpers(ICorJitInfo* jitInfo);
};
ICorJitCompiler is only an interface, so we don't have to discuss it. compileMethod is the first memeber of the class, of course. The idea
that I could gain complete control of the JIT hit me pretty fast. It's a bit difficult to explain in words, but in about ten minutes I noticed
that the correspondencies between the disassembled mscorjit code and the Rotor one were just too many. So, I decided to include the
header files necessary to use the ICorJitInfo class from the Rotor project. Actually, to use this class, only two header files were
necessary: corinfo.h and corjit.h. All include files can be found in the path "sscli20\clr\src\inc" of the Rotor project. While the path of
the JIT code is "sscli20\clr\src\fjit". Here's a brief summary of what the main files in the JIT path contain:
fjit.cpp
The actual JIT. It's a huge file of code since it contains all the code to convert MSIL to
native code, although the native code is defined externally.
fjitcompiler.cpp
Contains the getJit and compileMethod functions. It's the interface provided by mscorjit
to communicate with the JIT.
Basically, the ICorJitCompiler is the interface accessed by the Execution Engine to convert MSIL to native code. One of the arguments
of compileMethod is ICorJitInfo which is a class used by the JIT to call back to the Execution Engine in order to retrieve the information
it needs. Having complete access to the ICorJitInfo class doesn't open up the whole framework for us. But it's a very good start. Let's
have a look at what this class can do. Here's the base declaration:
/*********************************************************************************
* 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;
ntcore.com/files/netint_injection.htm
3/38
2/9/12
};
This doesn't seem much, but the ICorJitInfo class inherits from the ICorDynamicInfo one.
/*****************************************************************************
* ICorDynamicInfo contains EE interface methods which return values that may
* change from invocation to invocation. They cannot be embedded in persisted
* data; they must be requeried each time the EE is run.
*****************************************************************************/
class ICorDynamicInfo : public virtual ICorStaticInfo
{
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;
ntcore.com/files/netint_injection.htm
4/38
2/9/12
ntcore.com/files/netint_injection.htm
5/38
2/9/12
ntcore.com/files/netint_injection.htm
6/38
2/9/12
};
Now this seems already much more interesting indeed. But we aren't done yet as the ICorDynamicInfo class inherits from the
ICorStaticInfo. The ICorStaticInfo class inherits from many classes:
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;
};
Let's look at just one of them (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
ntcore.com/files/netint_injection.htm
7/38
2/9/12
/* IN */
/* IN */
/* OUT */
/* IN */
/* IN */
/* IN */
ntcore.com/files/netint_injection.htm
8/38
2/9/12
/* IN */
/* IN */
/* OUT */
ntcore.com/files/netint_injection.htm
9/38
2/9/12
// IN
// IN
// OUT
ntcore.com/files/netint_injection.htm
10/38
2/9/12
information from it. But first, I have to introduce another thing: the .NET assembly loader.
}
}
}
Keep in mind that with certain protections this code loading method won't work. So, you have to evaluate each case (since it depends
on how the injector is implemented) and find a way to hook the JIT before the protection does.
Also, the first executed assembly decides the .NET framework platform. Thus, the loader should be compiled consequently. What this
means is that you can choose the platform on which a certain assembly has to run from the Visual Studio options. If you choose an 64bit platform, then the PE of the assembly will be a 64-bit PE which can run only on the platform it was meant for. Whereas 32-bit PEs
can run on every platform. To force a 32-bit PE to use the x86 framework, a flag has to be set in the .NET Directory's Flags field:
The code I wrote in this article is 64-bit compatible, but I only tested it on x86. Thus, the loader has the 32-bit code set. To use the
loader in a 64-bit context, you have to unset this flag.
ntcore.com/files/netint_injection.htm
11/38
2/9/12
//
// 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);
ntcore.com/files/netint_injection.htm
12/38
2/9/12
}
//
// 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
//
ntcore.com/files/netint_injection.htm
13/38
2/9/12
}
The findMethod has some limitations which I will talk about later. The output of this little example is a series of message boxes informing
the user about the method currently being jitted and its calls.
As you can see, I didn't include the callvirt and calli opcodes in the search. This was only for the sake of simplicity, there's no other
reason. If you want to create a complete logger, you have to consider those opcodes as well.
ntcore.com/files/netint_injection.htm
14/38
2/9/12
The stealth method will always work 100%, but it has the disadvantage that one can't be sure that all the methods in an assembly will
be jitted. However, it is worth mentioning, since in some cases the goal to achieve is to dump just a few methods in order to decompile
and analyze them.
The other way to eject the MSIL code I called "brute". By brute I mean forcing the protection to decrypt every method in a .NET
assembly at once. What is necessary to do is to collect the CORINFO_METHOD_INFO data (or at least ILCode and ILCodeSize), then
retrieve the compileMethod from the getJit function. By now the compileMethod has already been hooked by the protection. Thus,
calling it means to decrypt the MSIL data. When the protection's compileMethod has decrypted the MSIL code, it will call the code
ejector's compileMethod, which, in some way, will notice by checking the parameters that this is the code ejection process and won't
call the real compileMethod function.
The code ejector I wrote features also a little dumper of the jitted assemblies. This is quite useful, since most times you have to dump
the protected assembly. This can also be achieved by a generic .NET unpacker.
As you can see from the image, the dialog contains a "Generate Rebel File" button. When this button is pressed a rebel report file is
requested as input. Basically, one needs to dump (if necessary) the assembly to rebuild first. Then, use the Rebel.NET to create a
report file from that assembly. Only the methods should be included in the report file:
This report file will be used during the code ejection process to calculate the number of methods and to retrieve the MSIL code address
and size. Actually, the JIT can be used to retrieve this information, but there's a problem. If you noticed, the findMethod takes three
arguments, the last one is a CORINFO_METHOD_HANDLE called hContext. This module is used to check the context of the request. If a
method tries to access another method without being entitled to, the framework will show an error and terminate the process. The main
goal is to obtain a valid CORINFO_METHOD_HANDLE for every method token in the assembly. As I said earlier, these kind of handles are
just pointers. Let's have a look at the memory pointer by a CORINFO_METHOD_HANDLE:
03B8E1F0
03B8E200
03B8E210
01 00 00 3B 74 01 00 00
02 00 01 08 0D 00 00 00 03 00 02 08 75 01 00 00
04 00 03 39 76 01 20 00 05 00 04 08 77 01 00 00
...;t...
............u...
...9v. .....w...
The first word seems to represent the method's number and after 8 bytes comes the next method. So, in theory, it might even be
ntcore.com/files/netint_injection.htm
15/38
2/9/12
possible to calculate the right CORINFO_METHOD_HANDLE, given a method's token. After having retrieved the
CORINFO_METHOD_HANDLE, it would be only a matter of calling getMethodInfo to get the CORINFO_METHOD_INFO structure. Note: I'll
discuss later what the CORINFO_METHOD_HANDLE really represents: this topic needs a paragraph on his own.
But, as said, this is not the way I proceeded. I used a Rebel.NET report file to retrieve the necessary data. This approach could
obviously be changed. What follows is the code of the .NET code ejector.
- Download the Code Ejector
#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();
//
// 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"));
ntcore.com/files/netint_injection.htm
16/38
2/9/12
}
//
// 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
//
ntcore.com/files/netint_injection.htm
17/38
2/9/12
}
//
// 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;
ntcore.com/files/netint_injection.htm
18/38
2/9/12
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");
ntcore.com/files/netint_injection.htm
19/38
2/9/12
ntcore.com/files/netint_injection.htm
20/38
2/9/12
ntcore.com/files/netint_injection.htm
21/38
2/9/12
}
//
// 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;
}
ntcore.com/files/netint_injection.htm
22/38
2/9/12
}
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);
ntcore.com/files/netint_injection.htm
23/38
2/9/12
ntcore.com/files/netint_injection.htm
24/38
2/9/12
}
I excuse myself if the code is a bit messy, but I didn't take much care in writing it as no time at all was put into designing it in the first
place. I also had to re-write several parts of the code as I changed approach three times. I hope you're able to understand it
nonetheless.
/* 6A
/* 13
/* 20
/* 6A
|
| 06
| 8D030000
|
ntcore.com/files/netint_injection.htm
*/ conv.i8
*/ stloc.s
*/ ldc.i4
*/ conv.i8
V_6
0x38d
25/38
2/9/12
/* 13
/* 28
/* 26
/* 11
/* 2B
/* D6
/* 13
/* 11
/* 1D
/* 6A
/* DA
/* 13
/* 11
/* 11
/* D6
/* 13
/* 11
/* 1D
/* 6A
/* D6
/* 13
/* 28
/* 26
/* 11
| 07
| (06)000002
|
| 06
| 20
|
| 05
| 07
|
|
|
| 07
| 07
| 05
|
| 06
| 07
|
|
|
| 07
| (06)000002
|
| 06
*/ stloc.s
*/ call
*/ pop
*/ ldloc.s
*/ br.s
*/ add.ovf
*/ stloc.s
*/ ldloc.s
*/ ldc.i4.7
*/ conv.i8
*/ sub.ovf
*/ stloc.s
*/ ldloc.s
*/ ldloc.s
*/ add.ovf
*/ stloc.s
*/ ldloc.s
*/ ldc.i4.7
*/ conv.i8
*/ add.ovf
*/ stloc.s
*/ call
*/ pop
*/ ldloc.s
V_7
int64 Project1.Program::IsDebuggerPresent()
IL_003d:
IL_003f:
IL_0040:
IL_0042:
IL_0044:
IL_0045:
IL_0046:
IL_0047:
IL_0049:
IL_004b:
IL_004d:
IL_004e:
IL_0050:
IL_0052:
IL_0053:
/* 11
/* D6
/* 13
/* 11
/* 1D
/* 6A
/* DA
/* 13
/* 11
/* 11
/* D6
/* 13
/* 11
/* 1D
/* 6A
| 07
|
| 05
| 07
|
|
|
| 07
| 07
| 05
|
| 06
| 07
|
|
*/ ldloc.s
*/ add.ovf
*/ stloc.s
*/ ldloc.s
*/ ldc.i4.7
*/ conv.i8
*/ sub.ovf
*/ stloc.s
*/ ldloc.s
*/ ldloc.s
*/ add.ovf
*/ stloc.s
*/ ldloc.s
*/ ldc.i4.7
*/ conv.i8
V_7
V_6
IL_003d
V_5
V_7
V_7
V_7
V_5
V_6
V_7
V_7
int64 Project1.Program::IsDebuggerPresent()
V_6
V_5
V_7
V_7
V_7
V_5
V_6
V_7
I'm not going to paste all of the code, since it's a very huge amount. As we can notice, there are plenty of nops in the code, but that
doesn't matter: a decompiler simply ignores them. Since I have developed an obfuscator in the past, I know what makes a decompiler
crash. Thus, I started looking for jumps. I noticed one at the beginning of the code:
IL_001b:
IL_001d:
IL_001e:
IL_0020:
IL_0022:
IL_0023:
IL_0024:
IL_0025:
IL_0027:
IL_0029:
IL_002b:
IL_002c:
IL_002e:
IL_0030:
IL_0031:
IL_0032:
IL_0033:
IL_0035:
IL_003a:
IL_003b:
/* 2B
/* D6
/* 13
/* 11
/* 1D
/* 6A
/* DA
/* 13
/* 11
/* 11
/* D6
/* 13
/* 11
/* 1D
/* 6A
/* D6
/* 13
/* 28
/* 26
/* 11
IL_003d: /* 11
| 20
|
| 05
| 07
|
|
|
| 07
| 07
| 05
|
| 06
| 07
|
|
|
| 07
| (06)000002
|
| 06
*/ br.s
*/ add.ovf
*/ stloc.s
*/ ldloc.s
*/ ldc.i4.7
*/ conv.i8
*/ sub.ovf
*/ stloc.s
*/ ldloc.s
*/ ldloc.s
*/ add.ovf
*/ stloc.s
*/ ldloc.s
*/ ldc.i4.7
*/ conv.i8
*/ add.ovf
*/ stloc.s
*/ call
*/ pop
*/ ldloc.s
IL_003d
| 07
*/ ldloc.s
V_7
V_5
V_7
V_7
V_7
V_5
V_6
V_7
V_7
int64 Project1.Program::IsDebuggerPresent()
V_6
The jump is unconditional. The opcodes between offset 0x1B and 0x3D will never be executed: I checked all the code after that, there's
absolutely no reference to these opcodes. So, I nopped them (jump included) with the CFF Explorer and then decompiled again with the
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();
ntcore.com/files/netint_injection.htm
26/38
2/9/12
It works, but now we're facing code jungle. I only pasted a few instructions, since the jungle pattern is very easy, as you can see, it
just repeats:
x = y + z;
z -= 7; / z += 7
I could have just removed all the jungle with the notepad, but a little CFF Explorer script to do the job seemed a much more elegant
solution to me. You can identify the pattern from these two jungle examples:
IL_003d:
IL_003f:
IL_0040:
IL_0042:
IL_0044:
IL_0045:
IL_0046:
IL_0047:
/* 11
/* D6
/* 13
/* 11
/* 1D
/* 6A
/* DA
/* 13
| 07
|
| 05
| 07
|
|
|
| 07
*/ ldloc.s
*/ add.ovf
*/ stloc.s
*/ ldloc.s
*/ ldc.i4.7
*/ conv.i8
*/ sub.ovf
*/ stloc.s
V_7
IL_0049:
IL_004b:
IL_004d:
IL_004e:
IL_0050:
IL_0052:
IL_0053:
IL_0054:
IL_0055:
/* 11
/* 11
/* D6
/* 13
/* 11
/* 1D
/* 6A
/* D6
/* 13
| 07
| 05
|
| 06
| 07
|
|
|
| 07
*/ ldloc.s
*/ ldloc.s
*/ add.ovf
*/ stloc.s
*/ ldloc.s
*/ ldc.i4.7
*/ conv.i8
*/ add.ovf
*/ stloc.s
V_7
V_5
V_5
V_7
V_7
V_6
V_7
V_7
The instructions can change a bit, but it's still very simple to find a pattern. Here's the CFF Explorer script to de-jungle the code:
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
And now it's possible decompile (and read) the code:
public static void main()
{
long num6;
IsDebuggerPresent();
ntcore.com/files/netint_injection.htm
27/38
2/9/12
}
This part of the code decrypts (with a xor) native.dll and transforms it into a native function:
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 makes it possible to call a native function by passing the function's pointer. To view the code of the
function, just open the CFF Explorer, go to Hex Editor, right click on the hex view and press Select All. Then right click again and click
on Modify. Put the byte 0x37 in the value box and press ok. You now have the decrypted file which can be disassembled.
Exactly the same approach is used to decrypt cryxed.dll, which is the protected .NET assembly. So, in order to obtain the assembly to
rebuild, it's not necessary to dump it from memory: it can be obtained following the simple decryption approach just explained. It should
be noted that the crackme uses the Assembly.Load approach which I have addressed earlier in the .NET loader paragraph. To sum up,
the main function of the crackme hooks the JIT, then loads the protected assembly and invokes its entry point.
The first thing we should be done is to create a Rebel.NET report file out of either the decrypted cryxed.dll or the dumped assembly.
The protected assembly is the unidentfied one:
ntcore.com/files/netint_injection.htm
28/38
2/9/12
If the rebel report file has already been created, then it is possible to generate the rebuilding rebel file by clicking on "Generate Rebel
File". If the ejection process succeded, a message box will prompt informing the user about the success of the operation.
After having successfully created the rebuilding rebl file, a simple rebuilding with Rebel.NET will generate a fully decompilable / runnable
assembly:
Now that we have the virgin assembly, we can disassemble it. The UnpackMe.Form1 namespace contains three button events. Here's
the first button event:
.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
ntcore.com/files/netint_injection.htm
29/38
2/9/12
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 suggestio"
+ "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
This obviously is the serial check routine. To overcome it in order to display the valid serial message box, it is only necessary to invert
the highlighted branch in the code. This can easily be accomplished with the CFF Explorer:
Now, the crackme can be considered as solved, as it always shows the right message (except of course if you insert the valid name and
serial, but solving a 3DES encryption just by taking a guess shouldn't be expected). Of course, the solved crackme along with its original
files are available to download.
- Download Crackme + Solution
The crackme, of course, could have been solved in a different manner. But this goes beyond the scope of this paragraph which was a
code ejection demonstration.
ntcore.com/files/netint_injection.htm
30/38
2/9/12
}
Nevermind the fact that an CORINFO_CONTEXT_HANDLE is the second argument of the function. The code which calls CheckContext
passes a CORINFO_METHOD_HANDLE as context.
What can be concluded is that CORINFO_METHOD_HANDLE only is a pointer to a MethodDesc class. The MethodDesc class is one of the
most important parts of the framework as it provides an incredible amount of information. The declaration of this class is inside the
"clr\src\vm\method.hpp" file.
// 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:
[...]
ntcore.com/files/netint_injection.htm
31/38
2/9/12
ntcore.com/files/netint_injection.htm
32/38
2/9/12
m_wTokenRemainder;
m_chunkIndex;
enum {
// enum_flag2_HasPrecode implies that enum_flag2_HasStableEntryPoint is set.
enum_flag2_HasStableEntryPoint
= 0x01, // The method entrypoint is stable (either precode or
actual code)
enum_flag2_HasPrecode
= 0x02, // Precode has been allocated for this method
enum_flag2_IsUnboxingStub
enum_flag2_MayHaveNativeCode
};
BYTE
= 0x04,
= 0x08,
m_bFlags2;
m_wFlags;
And this data exactly matches my previous intuition. We can now use every CORINFO_METHOD_HANDLE as a MethodDesc class. Of
course, including the whole MethodDesc class would be rather painful given its complexity. But one could write his own simplified version
of the MethodDesc class: all what is necessary to do is to include the members I pasted above which will result in the 8-byte multiple
size of the class.
The MethodDesc class is useful for many purposes and its use is rather safe, since it is not supposed to change any time soon. And
even if: its members (excluding the methods) are rather few, so I guess it won't be difficult to have a working simplified MethodDesc
class.
I would have provided an example of how to use the MethodDesc class myself, but as I'm writing the article is already rather big and,
although it's too late to keep it short, I'm still hoping to keep it readable. In fact, the journey into the .NET framework internals is not
yet concluded and some things have still to be discussed.
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;
ntcore.com/files/netint_injection.htm
33/38
2/9/12
As can be seen from the comments, this function only offers an interface for memory allocation and process synchronization. In fact,
this is the declaration of the return class:
// 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
ntcore.com/files/netint_injection.htm
34/38
2/9/12
ntcore.com/files/netint_injection.htm
35/38
2/9/12
}
And GetCLRFunction can only retrieve the address of three functions and won't accept any other.
.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
.text:79EA0C47
mov
push
.text:79EA0C4C
.text:79EA0C4D
.text:79EA0C52
.text:79EA0C54
.text:79EA0C55
.text:79EA0C56
.text:79EA0C5C
push esi
; char *
call _strcmp
test eax, eax
pop ecx
pop ecx
jz
loc_79EEF97B
push offset aClrfreelibrary ; "CLRFreeLibrary"
.text:79EA0C61
.text:79EA0C62
.text:79EA0C67
.text:79EA0C69
.text:79EA0C6A
.text:79EA0C6B
.text:79EA0C71
push
call
test
pop
pop
jz
push
.text:79EA0C76
.text:79EA0C77
.text:79EA0C7C
.text:79EA0C7E
.text:79EA0C7F
.text:79EA0C80
push esi
; char *
call _strcmp
test eax, eax
pop ecx
pop ecx
jnz loc_79ED7512
esi, [ebp+arg_0]
offset aClrloadlibrary ; "CLRLoadLibraryEx"
esi
; char *
_strcmp
eax, eax
ecx
ecx
loc_7A0D7B8F
offset aEeheapallocinp ; "EEHeapAllocInProcessHeap"
I had to disassemble the function, because GetCLRFunction is not available in the Rotor project. Now that I got those two out of the
way, I can talk about an interesting topic: internal calls.
Internal calls are methods implemented natively by the framework which can be called from managed code, although only in a very
limited way, as we'll see later.
Such functions are defined in the "clr\src\vm\ecall.cpp" in this way:
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)
ntcore.com/files/netint_injection.htm
36/38
2/9/12
FCFuncEnd()
// etc.
The first argument of FCFuncElement specifies the name of the function in the managed context, whereas the second one specifies the
location of the function. The syntax to access one of these ecalls (I suppose it stands for engine calls) is the following:
[MethodImpl(MethodImplOptions.InternalCall)]
internal extern type ECallMethodName();
In order to use MethodImpl, one has to include the System.Runtime.CompilerServices namespace. The problem is, even though you can
implement such a call in your project, when you try to actually call one of these internal calls, such a message will be delivered by the
framework:
These functions are, in fact, wrapped by the framework. Of course, I didn't introduce internal calls just to take note of that. The
interesting part is the interaction between managed code and internal calls. Let's take for instance this ecall:
FCIMPL2(MethodBody *, RuntimeMethodHandle::GetMethodBody, MethodDesc **ppMethod, EnregisteredTypeHandle
enregDeclaringTypeHandle)
// MethodBody * RuntimeMethodHandle::GetMethodBody(MethodDesc **, EnregisteredTypeHandle)
The _GetMethodBody internal call takes as first paramater a MethodDesc pointer to pointer. The first managed wrapping of this function
happens in the 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);
}
The first parameter disappears and becomes implicit. The class which contains this method also defines the implicit parameter at the
beginning:
[Serializable()]
[System.Runtime.InteropServices.ComVisible(true)]
public unsafe struct RuntimeMethodHandle : ISerializable
{
internal static RuntimeMethodHandle EmptyHandle { get { return new RuntimeMethodHandle(null); } }
private IntPtr m_ptr;
The m_ptr paramater is private, so it can't be accessed normally from the outside. But maybe there's another way to obtain an
equivalent value...
// 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)
ntcore.com/files/netint_injection.htm
37/38
2/9/12
throw new
SerializationException(Environment.GetResourceString("Serialization_InsufficientState"));
}
MethodHandle.Value is a public value. Thus, we can obtain the same value contained in m_ptr through the MethodInfo class. And m_ptr
is just a pointer to a MethodDesc class, also known as CORINFO_METHOD_HANDLE. So, in order to obtain a MethodDesc pointer through
managed code one can write this kind of code:
MethodInfo mi = typeof(Form1).GetMethod("button1_Click");
// displays pointer
MessageBox.Show(mi.MethodHandle.Value.ToString("X"));
The point I wanted to make is that it's possible access part of the .NET internals from managed code as well. Looking at the interaction
between managed code and ecalls is one good way to discover some interesting things.
Conclusions
As I've never read a book nor an article about the CLR infrastructure, what has been presented in this article are the .NET internals from
the perspective of a reverser. Having the (almost complete) source code of the .NET framework made things very easy and the days of
research (development included) spent to write this article can be counted on a hand with only two fingers. It has been a much bigger
effort writing the article. An effort which can only be compared to the pain one endures from actually reading it. The next article of this
kind will be about .NET native compiling. It'll surely be less boring as I don't have to re-explain the basics of .NET internals already
covered in this article.
Daniel Pistelli
ntcore.com/files/netint_injection.htm
38/38