Sometimes, when developing software, it is required to build additional functionality into already existing applications without modifying the source code of the applications. Moreover, the applications themselves often exist only in compiled binary form without source code. A well-known way to solve this problem is so-called. “Splicing” is a method of intercepting functions by changing the code of the objective function. Usually, when splicing, the first bytes of the target function are moved to other addresses, and an unconditional branch command (jmp) is written to their original place on the replacement function. Since splicing requires low-level memory operations, it is performed using assembly language and C / C ++, which also imposes certain restrictions on the implementation of replacement functions - they are usually also implemented in C / C ++ (less often in assembler).
The splicing method for intercepting API functions in Windows is widely described on the Internet and in various literary sources. The simplicity of this interception is determined by the following factors:
- the target function is static - it is immediately present in the memory of the loaded module;
- the address of the objective function is easy to determine (via the module export table or the GetProcAddress function).
Implementing replacement functions in C / C ++ when intercepting API functions is the best option, since Windows API is implemented, as is well known, in C, and replacement functions can operate with the same concepts as the replaced ones.
With the advent of the .NET technology, the situation has radically changed. Dynamic link libraries created for .NET no longer contain static functions (functions are generated dynamically based on IL intermediate language commands). As a consequence of this, it is difficult to predict the memory address by which functions will be placed after dynamic compilation (JIT compilation), as well as to track the very moment of JIT compilation. In addition, without additional efforts, it is impossible to use the .NET function as a replacement function, since it itself is not static and is not implemented in C / C ++.
In this article, an algorithm will be described, the use of which allows replacing .NET functions with functions that are also being developed in the .NET environment. To understand the algorithm we are going to have, we will have to delve into the implementation of the CLR (common language runtime) .NET. When describing the implementation of the CLR, we will simplify some of the details in order to avoid complicating the understanding of the general essence.
')
1. Method of calling methods in the CLR
In the CLR, each function (method) is a set of IL commands and all information about it is stored in the module's metadata. When loading a module for each of its classes, the CLR system creates a
MethodTable table containing information about the class methods. Each class method is described by a
MethodDesc structure, one of the fields of which contains the address of the compiled method in memory (when the method is JIT compiled), and the other contains an index in the
MethodTable table that contains the address of the adapter (thunk), the contents of which change during execution depending on whether the method is compiled or not.

Initially (before JIT compilation), one of four so-called acts as an adapter. CLR precode adapters:
StubPrecode ,
FixupPrecode ,
RemotingPrecode, or
NDirectImportPrecode . Since the last adapter is used only to call Windows API functions that can be intercepted directly, we will not consider it.
The main task of each of the precode adapters is to transmit the address of the
MethodDesc structure,
the method used to determine the internal function of
ThePreStub (
ThePreStubAMD64 for the x64 platform, marked as Stub in the figure), which performs the following tasks:
- JIT compilation of the method identified by the MethodDesc structure;
- setting a pointer in the MethodDesc structure to the generated native code;
- rewriting the adapter so that it performs an unconditional jump (jmp) to the generated native code;
- Execution of generated native code.
Thus, as a result of the initial call of the target method, not only will the method code be generated and executed, but the contents of the adapter will change, which will lead to a direct call of the generated native code on subsequent method calls.
Any .NET method called from the CLR passes through the address in the
MethodTable table of the class methods. However, the CLR provides the ability to call a method from an unmanaged C / C ++ environment. The following functions are
used for this:
GetFunctionPointer of the RuntimeMethodHandle class and
GetFunctionPointerForDelegate of the
Marshal class. The addresses returned by the specified functions are also addresses of adapters, among which may be the already mentioned
StubPrecode ,
FixupPrecode and
RemotingPrecode . As a result of the initial method call, it is compiled and executed; on a subsequent call, a direct transition to the generated code. At the same time, it is important for us that for an uncompiled method, when calling it both through the method table and through the pointers returned by the mentioned functions, the internal function
ThePreStub is called .
2. CLR Precode Adapters
Consider now separately the CLR precode adapter and specify how, knowing only the binary code of the adapter itself, during execution you can determine the address of the
MethodDesc structure associated with this adapter, as well as the address of the internal function
ThePreStub (it will be useful later). In addition, we specify how in the specified adapter to determine the address of the generated code after performing JIT compilation.
- StubPrecode . At the time of its creation, the value of the address of the MethodDesc structure is embedded into the specified adapter directly by the CLR system (as an immediate value in the assembler command). The adapter code depends only on the hardware platform and does not depend on the CLR version. For various hardware platforms, it looks like this:
x86: mov eax, pMethodDesc mov ebp, ebp jmp ThePreStub x64: mov r10, pMethodDesc jmp ThePreStub
Thus, the address of the MethodDesc structure is passed to ThePreStub function in the eax register (for x86) or r10 (for x64). During the execution of the memory analysis, the specified address can be clearly read at offset 1 (for x86) or 2 (for x64) adapters, taking into account the processor bitness. The address of the ThePreStub function can be calculated by adding the relative offset embedded in the last jmp command with the end address of the specified command.
After performing JIT compilation, the transition address is replaced with the address of the ThePreStub function with the address of the generated code and the contents of the adapter become as follows:
x86: mov eax, pMethodDesc mov ebp, ebp jmp NativeCode x64: mov r10, pMethodDesc jmp NativeCode
The method for determining the address of the generated code after JIT compilation is the same as the method for determining the address of ThePreStub function before performing JIT compilation.
- FixupPrecode . The specified adapter was designed to optimize memory usage. It occupies 8 bytes on all hardware platforms, which is smaller than the size of the StubPrecode adapter (12 bytes for x86 and 16 bytes for x64). The adapter code for all hardware platforms and CLR versions is as follows:
call PrecodeFixupThunk db 0x5E db MethodDescChunkIndex db PrecodeChunkIndex call PrecodeFixupThunk db 0x db MethodDescChunkIndex db PrecodeChunkIndex
When using FixupPrecode Adapters, the CLR complies with the following two requirements:
- MethodDesc structures for adapters are combined in MethodDescChunk contiguous memory blocks :

- FixupPrecode adapters are also merged into a continuous memory block, with the base address of the pDMethodDescChunkBase MethodDesc structures in the MethodDescChunk memory block embedded in the specified block after the CLR adapters :
call PrecodeFixupThunk db ? db MethodDescChunkIndex db PrecodeChunkIndex ... call PrecodeFixupThunk db ? db MethodDescChunkIndex db 2 call PrecodeFixupThunk db ? db MethodDescChunkIndex db 1 call PrecodeFixupThunk db ? db MethodDescChunkIndex db 0 dd pMethodDescChunkBase (x86) dq pMethodDescChunkBase (x64)
With such a memory organization, the address of the MethodDesc structure for a specific adapter FixupPrecode is given by the following formula:
Address MethodDesc = pMethodDescChunkBase + MethodDescChunkIndex * sizeof (void *),
where the base offset ( pMethodDescChunkBase ) is retrieved at the following address:
address pMethodDescChunkBase = address FixupPrecode + 8 + PrecodeChunkIndex * 8,
MethodDescChunkIndex and PrecodeChunkIndex are byte values ​​embedded in PrecodeFixupThunk .
The value of the Address of the MethodDesc structure is calculated by the CLR inside the additional adapter PrecodeFixupThunk , which exists in the memory in the singular and is intended only for calculating and transmitting the specified address of ThePreStub function in the eax register (for x86) or r10 (for x64). We give the code for the PrecodeFixupThunk adapter for various hardware platforms.
x86: pop eax push esi push edi movzx esi, byte ptr [eax + 0x2] movzx edi, byte ptr [eax + 0x1] mov eax, dword ptr [eax + esi * 8 + 0x3] lea eax, [eax + edi * 4] pop edi pop esi jmp dword ptr [g_dwPreStubAddr] ( CLR 2.0) jmp ThePreStub ( CLR 4.0 ) x64: pop rax movzx r10, byte ptr [rax + 0x2] movzx r11, byte ptr [rax + 0x1] mov rax, qword ptr [rax + r10 * 8 + 0x3] lea r10, [rax + r11 * 8] jmp ThePreStub
The address of the internal function of ThePreStub using the FixupPrecode adapter can be calculated in two steps:
- calculate the address of the PrecodeFixupThunk adapter by adding the relative offset embedded in the first command of the Fix FixPrecode adapter of the adapter with the end address of the specified command;
- for all platforms except CLR 2.0 x86, calculate ThePreStub address by adding the relative offset embedded in the last jmp command of the PrecodeFixupThunk adapter, with the end address of the specified command;
- for the CLR 2.0 x86 platform, extract the ThePreStub address to the address that is embedded in the last jmp command (indirect addressing via the g_dwPreStubAddr internal variable).
After JIT compilation in the FixupPrecode adapter , the first call command is replaced with the jmp command, replacing the transition address from the address of the PrecodeFixupThunk adapter with the address of the generated code. In addition, if the first command is followed by byte 0x5E, then it is replaced by byte 0x5F (the indicated bytes are an indicator of the presence or absence of JIT compilation, byte 0xCC means no information). Thus, after replacing the contents of the adapter is the following:
jmp NativeCode db 0x5E db MethodDescChunkIndex db PrecodeChunkIndex jmp NativeCode db 0x db MethodDescChunkIndex db PrecodeChunkIndex
The address of the generated code after JIT compilation is calculated by adding the relative offset embedded in the first jmp command with the address of the completion of the specified command.
- RemotingPrecode . The specified adapter is used when calling methods of objects that may exist in another application domain. The adapter code is as follows:
x86: mov eax, pMethodDesc nop call PrecodeRemotingThunk jmp ThePreStub x64: test rcx,rcx je Local mov rax, qword ptr [rcx] mov r10, ProxyAddress cmp rax, r10 je Remote Local: mov rax, ThePreStub jmp rax Remote: mov r10, pMethodDesc mov rax, RemotingCheck jmp rax
As in the case of the StubPrecode adapter, in RemotingPrecode at the time of its creation, the value of the address of the MethodDesc structure is embedded by the CLR system directly (as an immediate value in the assembler command). The specified value can be retrieved at offset 1 (for x86) and 37 (for x64). The address of the ThePreStub function is the result of adding the relative offset embedded in the last jmp command with the address of the completion of the specified command (for x86) or the immediate value at offset 25 (for x64).
For objects that do not belong to other domains, after performing JIT compilation, the transition address is changed from the address of the ThePreStub function to the address of the generated code, so the method for determining the address of the generated code after performing JIT compilation is the same as the method of determining the address of ThePreStub function before performing JIT compilation. For objects belonging to other domains, after JIT compilation, the body of the adapter RemotingPrecode does not change. For simplicity, we do not consider the use of RemotingPrecode for objects that do not belong to the application domain.
3. ThePreStub Function
As already mentioned, the internal function of
ThePreStub performs the following actions:
- JIT compilation of the method identified by the MethodDesc structure;
- setting a pointer in the MethodDesc structure to the generated native code;
- rewriting the adapter so that it performs an unconditional jump (jmp) to the generated native code;
- Execution of generated native code.
In all versions of the CLR and hardware platforms, the
ThePreStub function
is implemented in the CLR at the hardware level by calling the internal function
PreStubWorker followed by transferring control (via the jmp command) to the address returned by the specified function. For completeness, here’s the code for the
ThePreStub function for various platforms.
ThePreStub function code (x64) CLR 4.6 : push r15 push r14 push r13 push r12 push rbp push rbx push rsi push rdi sub rsp,68h mov qword ptr [rsp+0B0h],rcx mov qword ptr [rsp+0B8h],rdx mov qword ptr [rsp+0C0h],r8 mov qword ptr [rsp+0C8h],r9 movdqa xmmword ptr [rsp+ 20h],xmm0 movdqa xmmword ptr [rsp+ 30h],xmm1 movdqa xmmword ptr [rsp+ 40h],xmm2 movdqa xmmword ptr [rsp+ 50h],xmm3 lea rcx,[rsp+68h] mov rdx,r10 call PreStubWorker movdqa xmm0,xmmword ptr [rsp+20h] movdqa xmm1,xmmword ptr [rsp+ 30h] movdqa xmm2,xmmword ptr [rsp+ 40h] movdqa xmm3,xmmword ptr [rsp+ 50h] mov rcx,qword ptr [rsp+0B0h] mov rdx,qword ptr [rsp+0B8h] mov r8,qword ptr [rsp+0C0h] mov r9,qword ptr [rsp+0C8h] add rsp,68h pop rdi pop rsi pop rbx pop rbp pop r12 pop r13 pop r14 pop r15 jmp rax CLR 4.0: lea rax, [rsp + 0x08] push r10 push r15 push r14 push r13 push r12 push rbp push rbx push rsi push rdi push rax sub rsp, 0x78 mov qword ptr [rsp + 0xD0], rcx mov qword ptr [rsp + 0xD8], rdx mov qword ptr [rsp + 0xE0], r8 mov qword ptr [rsp + 0xE8], r9 movdqa xmmword ptr [rsp + 0x20], xmm0 movdqa xmmword ptr [rsp + 0x30], xmm1 movdqa xmmword ptr [rsp + 0x40], xmm2 movdqa xmmword ptr [rsp + 0x50], xmm3 lea rcx, qword ptr [rsp + 0x68] call PreStubWorker movdqa xmm0, xmmword ptr [rsp + 0x20] movdqa xmm1, xmmword ptr [rsp + 0x30] movdqa xmm2, xmmword ptr [rsp + 0x40] movdqa xmm3, xmmword ptr [rsp + 0x50] mov rcx, qword ptr [rsp + 0xD0] mov rdx, qword ptr [rsp + 0xD8] mov r8 , qword ptr [rsp + 0xE0] mov r9 , qword ptr [rsp + 0xE8] nop add rsp, 0x80 pop rdi pop rsi pop rbx pop rbp pop r12 pop r13 pop r14 pop r15 pop r10 jmp rax CLR 2.0: lea rax, [rsp + 0x08] push r10 push r15 push r14 push r13 push r12 push rbp push rbx push rsi push rdi push rax sub rsp, 0x78 mov qword ptr [rsp + 0xD0], rcx mov qword ptr [rsp + 0xD8], rdx mov qword ptr [rsp + 0xE0], r8 mov qword ptr [rsp + 0xE8], r9 movdqa xmmword ptr [rsp + 0x20], xmm0 movdqa xmmword ptr [rsp + 0x30], xmm1 movdqa xmmword ptr [rsp + 0x40], xmm2 movdqa xmmword ptr [rsp + 0x50], xmm3 call PrestubMethodFrame::GetMethodFrameVPtr mov qword ptr [rsp + 0x68], rax mov rax, qword ptr [s_gsCookie] mov qword ptr [rsp + 0x60], rax call GetThread mov r12, rax mov rdx, qword ptr [r12 + 0x10] mov qword ptr [rsp + 0x70], rdx lea rcx, [rsp + 0x68] mov qword ptr [r12 + 0x10], rcx call PreStubWorker mov rcx, qword ptr [r12 + 0x10] mov rdx, qword ptr [rcx + 0x08] mov qword ptr [r12 + 0x10], rdx movdqa xmm0, xmmword ptr [rsp + 0x20] movdqa xmm1, xmmword ptr [rsp + 0x30] movdqa xmm2, xmmword ptr [rsp + 0x40] movdqa xmm3, xmmword ptr [rsp + 0x50] mov rcx, qword ptr [rsp + 0xD0] mov rdx, qword ptr [rsp + 0xD8] mov r8 , qword ptr [rsp + 0xE0] mov r9 , qword ptr [rsp + 0xE8] nop add rsp, 0x80 pop rdi pop rsi pop rbx pop rbp pop r12 pop r13 pop r14 pop r15 pop r10 jmp rax
ThePreStub function code (x86) CLR 4.6 : push ebp mov ebp,esp push ebx push esi push edi push ecx push edx mov esi,esp push eax push esi call PreStubWorker pop edx pop ecx pop edi pop esi pop ebx pop ebp jmp eax CLR 4.0: push ebp mov ebp, esp push ebx push esi push edi push ecx push edx push eax sub esp, 0x0C lea esi, [esp + 0x04] push esi call PreStubWorker add esp, 0x10 pop edx pop ecx pop edi pop esi pop ebx pop ebp jmp eax CLR 2.0: push eax push edx push PrestubMethodFrame::'vftable' push ebp push ebx push esi push edi lea esi, [esp + 0x10] push dword ptr [esi + 0x0C] push ebp mov ebp, esp push ecx push edx mov ebx, dword ptr fs:0x0E34 mov edi, dworp ptr [ebx + 0x0C] mov dword ptr [esi + 0x04], edi mov dword ptr [ebx + 0x0C], esi push cookie push esi call PreStubWorker mov dword ptr [ebx + 0x0C], edi mov ecx, dword ptr [esi + 0x08] mov dword ptr [esi + 0x08], eax mov eax, ecx add esp, 0x04 pop edx pop ecx mov esp, ebp pop ebp add esp, 0x04 pop edi pop esi pop ebx pop ebp add esp, 0x08 ret
Knowing the binary structure of the precode adapters, the address of the
ThePreStub function can be determined as follows:
- Define an arbitrary static CLR method (you can even make it empty) by disallowing inline embedding and precompiling:
public delegate void EmptyDelegate(); [MethodImplAttribute( MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] public static void Empty() {}
- Create and block the delegate of the method in memory and determine the address returned by the RuntimeMethodHandle.GetFunctionPointer function:
EmptyDelegate function = Empty; GCHandle gc = GCHandle.Alloc(function); IntPtr methodPtr = function.Method.MethodHandle.GetFunctionPointer();
- If the commands at methodPtr match the StubPrecode adapter sample , you should use the method for calculating the ThePreStub function address from section 1, section 2. If the commands at the received address match the FixupPrecode adapter sample , you should use the method for calculating the address of ThePreStub function from section 2, section 2 .
- To unlock the memory of the delegate method:
gc.Free();
4. PreStubWorker function
The
PreStubWorker function performs the following actions:
- JIT compilation of the method identified by the MethodDesc structure;
- setting a pointer in the MethodDesc structure to the generated native code;
- rewriting the adapter so that it performs an unconditional jump (jmp) to the generated native code;
- return function ThePreStub address of the modified adapter.
The
PreStubWorker function has the following C declaration (according to the CLR source code):
CLR 4.6 : void* __stdcall PreStubWorker(TransitionBlock* pTransitionBlock, MethodDesc* pMD); CLR 4.6: void* __stdcall PreStubWorker(PrestubMethodFrame *pPFrame);
Using this fact, the listings of the
ThePreStub function
code , and the fact that the
ThePreStub functions in the eax (for x86) and r10 (for x64)
registers are passed the value of the
MethodDesc address, you can determine how the
PreStubWorker function internally accesses the
MethodDesc value:
- for CLR 4.6 (and higher), the specified value is retrieved from the second passed parameter function;
- for CLR below 4.6 of the x86 platform, the value is at the 8 offset of the structure addressed by the pPFrame parameter;
- for CLR below 4.6 platform x64 value is the address, 16 bytes less than the value of the address located at offset 16 of the structure addressed by the pPFrame parameter.
Knowing the address of the internal function of
ThePreStub and based on the listed listings of its code, you can specify the algorithm for calculating the address of the internal function of
PreStubWorker without using fixed offsets within the function of
ThePreStub (which, as you can see, change with each new CLR version):
- for x86 and x64 platforms (except CLR 2.0), the specified address will be the result of adding the relative offset embedded in the call command, the only command in ThePreStub function, with the address of the completion of the specified command;
- for CLR 2.0 x64 platforms, the specified address will be the result of the addition of the relative offset embedded in the call command, preceded by the lea command, with the end address of the call command.
You can find the required call commands during execution if you have a built-in disassembler that can determine the codes and sizes of commands in the run mode.
5. Interception Algorithm
Summarizing all the above, we can suggest the following method of intercepting .NET functions:
- get the address of the replacement method using the RuntimeMethodHandle.GetFunctionPointer call;
- if the replacement method is already JIT-compiled, then find the address in the memory of the generated native code and intercept the specified address to perform the replacement method;
- if the replacement method is not yet JIT compiled, then
- calculate the address of its structure MethodDesc ;
- calculate the address and intercept the PreStubWorker function so that the original implementation is called in the replacement PreStubWorker method;
- add additional logic to the replacement PreStubWorker function for the case when the function uses MethodD address that matches the required address. In this case, after calling the original implementation, obtain the address of the generated native method and intercept the obtained address to perform the replacement method.
After all of the above, the above points of the algorithm do not require detailed explanations, except for paragraphs 2 and 3.1.
In paragraph 2, the definition of the address of the real generated native-code (without any adapters). The algorithm below is based on knowledge of the binary structure of the adapters generated by the CLR, and calculates the specified address (or returns NULL in the absence of JIT compilation).
- Get the address of the .NET method via a call to RuntimeMethodHandle.GetFunctionPointer .
- If the commands at the received address match the sample adapter StubPrecode or RemotingPrecode , then extract the address of the compiled code, as described in paragraph 1 and paragraph 3 of section 2. If the specified address matches the address of the ThePreStub function, then the JIT compilation of the method was not performed and follows return null. Otherwise, return the address of the compiled code.
- Until the current address matches the address of ThePreStub function , do the following:
- if the current address indicates a jmp command, then go to the destination for the jmp command;
- otherwise, if the current address indicates a call command, then check the destination address of the call command. If it is equal to the PrecodeFixupThunk adapter (a case of a FixupPrecode adapter before the JIT compilation), then return NULL. Otherwise, return the address where the call command is located (or the destination address for the call command);
- otherwise, return the current address.
- Return NULL, because the address of ThePreStub function has been reached .
Clause 3.1 refers to determining the address of the
MethodDesc structure for an
uncompiled method. The algorithm below is based on knowledge of the binary structure of adapters generated by the CLR, and calculates the specified address (or NULL in some cases with JIT compilation).
- Get the address of the .NET method via a call to RuntimeMethodHandle.GetFunctionPointer .
- If the commands at the received address match the sample adapter StubPrecode or RemotingPrecode , then calculate the address of the MethodDesc structure, as described in paragraph 1 and paragraph 3 of section 2.
- Until the current address matches the address of ThePreStub function , do the following:
- if the current address points to the jmp command, then check the byte immediately after the jmp command. If it is 0x5F (the case of FixupPrecode after performing JIT compilation), then calculate the address of the MethodDesc structure, as described in Section 2, paragraph 2. Otherwise, go to the address for the jmp command;
- otherwise, if the current address indicates a call command, then check the destination address of the call command. If it is equal to the PrecodeFixupThunk adapter (a case of a FixupPrecode adapter prior to JIT compilation), then calculate the address of the MethodDesc structure, as described in paragraph 2 of section 2. Otherwise, return NULL;
- otherwise, return NULL.
- Return NULL (the specified item must be unreachable).
6. Conclusion
The efficiency of the above algorithm has been repeatedly tested in practice (including industrial developments) on various versions of .NET and hardware platforms. Based on it, the .NET library was developed, using which the interception of .NET functions becomes quite easy to use. Let us give an example of the use of interception using the developed library.
Suppose you want to hook the
Open function of the
SqlConnection class. Then the interception code using the developed library may look like in C # as follows:
public static class HookedConnection { public static RTX.NET.HookHandle OpenHandle; [MethodImplAttribute(MethodImplOptions.NoInlining)] public static void Open(SqlConnection connection) {
Here, the
OpenHandle variable contains a handle that can be used to invoke the implementation of the function being replaced and which is initialized as a result of the interception assignment:
using (ConnectionEntry entry = new ConnectionEntry()) { Test(); }
where the
ConnectionEntry class is so called. “Interception manager”:
public class ConnectionEntry : RTX.NET.HookDispatcher, RTX.NET.IHookLoadHandler {
Then when you run the
Test function
public static void Test() { SqlConnection connection = new SqlConnection(); connection.ConnectionString = @"Server=(localdb)\v11.0;" + @"AttachDbFileName=C:\MyFolder\MyData.mdf;Integrated Security=true;"; connection.Open (); connection.Close(); }
the console will display the following message:
Server=(localdb)\v11.0;AttachDbFileName=C:\MyFolder\MyData.mdf;Integrated Security=true;