Intercepting .NET / CLR Functions
Sometimes, when developing software, it is necessary to embed additional functionality into existing applications without modifying the source code of the applications. Moreover, often the applications themselves exist only in compiled binary form without source code. A widely known method of solving this problem is the so-called. “Splicing” is a method of intercepting functions by changing the code of the target function. Usually, when splicing, the first bytes of the target function are moved to other addresses, and the unconditional jump command (jmp) to the replacement function is written to their original place. Since splicing requires low-level operations with memory, it is carried out using assembly language and C / C ++,
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 implementation of substitute functions in C / C ++ when intercepting API functions is the best option, since the Windows API is implemented, as you know, in the C language, and substitute functions can use the same concepts as replaced ones.
With the advent of .NET technology, the situation has radically changed. Dynamically linked libraries created for .NET no longer contain static functions (functions are generated dynamically based on IL intermediate language commands). As a result of this, it is difficult to predict the address in memory at which functions will be placed after dynamic compilation (JIT compilation), as well as to track the moment of JIT compilation itself. In addition, without additional efforts, it is impossible to use the .NET function as a replacement function, since it is not static and is not implemented in C / C ++.
In this article, an algorithm will be described whose application allows you to replace .NET functions with functions also developed in the .NET environment. To understand the given algorithm, we will have to delve into the implementation of the CLR (common language runtime) .NET. In describing the implementation of the CLR, we will simplify some details in order to avoid complicating the understanding of the general essence.
In the CLR, each function (method) is a set of IL-commands and all information about it is stored in the metadata of the module. When loading a module for each of its classes, the CLR system creates a MethodTable table containing information about the methods of the class. Each class method is described by the 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 the index in the MethodTable table , which indicates the address of the adapter (thunk), the contents of which change during execution in depending on whether the method is compiled or not.
Initially (before performing the JIT compilation), one of the four so-called ones acts as an adapter. precode CLR adapters:StubPrecode , FixupPrecode , RemotingPrecode or NDirectImportPrecode . Since the last adapter is used only to call Windows API functions, which can be intercepted directly, we will not consider it.
The main task of each of the precode adapters is to pass the address of the MethodDesc structure that
defines the method used to the ThePreStub internal function ( ThePreStubAMD64 for the x64 platform, marked as Stub in the figure), which performs the following tasks:
Thus, as a result of the initial call to the target method, the method code will not only be generated and executed, but the contents of the adapter will change, which will lead to a direct call to the generated native code during subsequent method calls.
Any .NET method called from the common language runtime passes through the address in the MethodTable table of class methods. However, the CLR provides the ability to invoke a method from an unmanaged C / C ++ environment. To do this, use the following functions: GetFunctionPointer class RuntimeMethodHandle and GetFunctionPointerForDelegate Class Marshal . Addresses returned by the specified functions are also addresses of adapters, among which may be already mentionedStubPrecode , FixupPrecode and RemotingPrecode . As a result of the initial call to the method, it is compiled and executed, and upon the next call, a direct transition to the generated code is performed. At the same time, it is important for us that for an uncompiled method, when it is called both through the method table and through the pointers returned by the functions mentioned, the internal function ThePreStub is called .
Let us now take a separate look at the CLR precode adapters and indicate how, knowing only the binary code of the adapter itself, we can determine the address of the MethodDesc structure associated with this adapter and the address of the ThePreStub internal function (in the future, this will come in handy). In addition, we indicate how to determine the address of the generated code in the specified adapter after performing JIT compilation.
As already mentioned, the internal ThePreStub function does the following:
In all versions of the CLR and hardware platforms, ThePreStub function is implemented in the CLR at the hardware level by calling the internal PreStubWorker function and then transferring control (via the jmp command) to the address returned by the specified function. For completeness, we present the code of the ThePreStub function for various platforms.
Knowing the binary structure of precode adapters, the address of ThePreStub function can be determined as follows:
The PreStubWorker function performs the following actions:
The PreStubWorker function has the following C declaration (according to the CLR source):
Using this fact, the code listings of the ThePreStub function , and the fact that the TheDreStub function in the eax (for x86) and r10 (for x64) registers is passed the value of the MethodDesc address , you can determine how the PreStubWorker function accesses the value of the MethodDesc inside :
Knowing the address of the ThePreStub internal function and based on the above listings of its code, you can specify an algorithm for calculating the address of the PreStubWorker internal function without using fixed offsets inside the ThePreStub function (which, as you can see, change with each new version of the CLR):
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 runtime.
Summarizing all of the above, we can suggest the following way to intercept .NET functions:
After all the foregoing, the above paragraphs of the algorithm do not require detailed explanations, with the exception of paragraphs 2 and 3.1.
Clause 2 talks about determining the address of a 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 environment and calculates the specified address (or returns NULL if there is no JIT compilation).
Clause 3.1 talks about defining the address of a MethodDesc structure for an uncompiled method. The algorithm below is based on the knowledge of the binary structure of the adapters generated by the CLR environment and calculates the specified address (or NULL in some cases if there is a JIT compilation).
The performance of the above algorithm has been repeatedly tested in practice (including in 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 simple to use. Here is an example of using interception using the developed library.
Suppose you want to intercept the Open function of the SqlConnection class . Then the interception code when using the developed library may look like in C # in the following way:
Here, the OpenHandle variable contains a descriptor with which you can call the implementation of the replaced function and which is initialized as a result of assigning an interception:
where the ConnectionEntry class is the so-called “Interception manager”:
Then when executing the Test function
the following message will be displayed in the console:
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 target function is easy to determine (via the module export table or the GetProcAddress function ).
The implementation of substitute functions in C / C ++ when intercepting API functions is the best option, since the Windows API is implemented, as you know, in the C language, and substitute functions can use the same concepts as replaced ones.
With the advent of .NET technology, the situation has radically changed. Dynamically linked libraries created for .NET no longer contain static functions (functions are generated dynamically based on IL intermediate language commands). As a result of this, it is difficult to predict the address in memory at which functions will be placed after dynamic compilation (JIT compilation), as well as to track the moment of JIT compilation itself. In addition, without additional efforts, it is impossible to use the .NET function as a replacement function, since it is not static and is not implemented in C / C ++.
In this article, an algorithm will be described whose application allows you to replace .NET functions with functions also developed in the .NET environment. To understand the given algorithm, we will have to delve into the implementation of the CLR (common language runtime) .NET. In describing the implementation of the CLR, we will simplify some details in order to avoid complicating the understanding of the general essence.
1. Method call 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 metadata of the module. When loading a module for each of its classes, the CLR system creates a MethodTable table containing information about the methods of the class. Each class method is described by the 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 the index in the MethodTable table , which indicates the address of the adapter (thunk), the contents of which change during execution in depending on whether the method is compiled or not.
Initially (before performing the JIT compilation), one of the four so-called ones acts as an adapter. precode CLR adapters:StubPrecode , FixupPrecode , RemotingPrecode or NDirectImportPrecode . Since the last adapter is used only to call Windows API functions, which can be intercepted directly, we will not consider it.
The main task of each of the precode adapters is to pass the address of the MethodDesc structure that
defines the method used to the ThePreStub internal function ( 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;
- rewrite the adapter so that it makes an unconditional jump (jmp) to the generated native code;
- execution of the generated native code.
Thus, as a result of the initial call to the target method, the method code will not only be generated and executed, but the contents of the adapter will change, which will lead to a direct call to the generated native code during subsequent method calls.
Any .NET method called from the common language runtime passes through the address in the MethodTable table of class methods. However, the CLR provides the ability to invoke a method from an unmanaged C / C ++ environment. To do this, use the following functions: GetFunctionPointer class RuntimeMethodHandle and GetFunctionPointerForDelegate Class Marshal . Addresses returned by the specified functions are also addresses of adapters, among which may be already mentionedStubPrecode , FixupPrecode and RemotingPrecode . As a result of the initial call to the method, it is compiled and executed, and upon the next call, a direct transition to the generated code is performed. At the same time, it is important for us that for an uncompiled method, when it is called both through the method table and through the pointers returned by the functions mentioned, the internal function ThePreStub is called .
2. Precode CLR Adapters
Let us now take a separate look at the CLR precode adapters and indicate how, knowing only the binary code of the adapter itself, we can determine the address of the MethodDesc structure associated with this adapter and the address of the ThePreStub internal function (in the future, this will come in handy). In addition, we indicate how to determine the address of the generated code in the specified adapter after performing JIT compilation.
- StubPrecode . At the moment of its creation, the value of the address of the MethodDesc structure is embedded into thespecified adapterdirectly by the CLR system (as a direct 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 has the following form:
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 the ThePreStub function in the eax register (for x86) or r10 (for x64). During the analysis of memory, the specified address can be read explicitly at offset 1 (for x86) or 2 (for x64) of the adapter, taking into account the processor capacity. The address of ThePreStub function can be calculated by adding the relative offset built into the last jmp command with the completion address of the specified command.
After the JIT compilation is completed, the transition address is replaced from the address of ThePreStub function with the address of the generated code and the contents of the adapter become the following: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 the JIT compilation is the same as the method for determining the address of the ThePreStub function before the JIT compilation. - FixupPrecode . The specified adapter was designed to optimize memory usage. It takes 8 bytes on all hardware platforms, which is less 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:- adapter- specific MethodDesc structures are combined in continuous blocks of MethodDescChunk memory :
- FixupPrecode-переходники также объединяются в непрерывный блок памяти, причем в указанном блоке после окончания переходников системой CLR встраивается базовый адрес pMethodDescChunkBase структур MethodDesc в блоке памяти MethodDescChunk:
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)
При такой организации памяти адрес структуры MethodDesc для определенного переходника FixupPrecode задается по следующей формуле:
aдрес MethodDesc = pMethodDescChunkBase + MethodDescChunkIndex * sizeof(void*),
где базовое смещение (pMethodDescChunkBase) извлекается по следующему адресу:
адрес pMethodDescChunkBase = адрес FixupPrecode + 8 + PrecodeChunkIndex * 8,
а MethodDescChunkIndex и PrecodeChunkIndex — байтовые значения, встроенные в PrecodeFixupThunk.
The value of the address of the MethodDesc structure by the CLR is calculated inside the optional adapter PrecodeFixupThunk , which exists in the singular and is intended only for calculating and passing the specified address to ThePreStub in the eax (for x86) or r10 (for x64) register. Here is 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 ThePreStub internal function using the FixupPrecode adapter can be calculated in two stages:- calculate the address of the PrecodeFixupThunk adapter by adding the relative offset built into the first command of the call FixupPrecode- adapter to the completion address of the specified command;
- for all platforms except CLR 2.0 x86, calculate the ThePreStub address by adding the relative offset built into the last jmp command of the PrecodeFixupThunk adapter with the completion address of the specified command;
- for the CLR 2.0 x86 platform, extract the ThePreStub address to the address that is built into the last jmp command (indirect addressing through the internal variable g_dwPreStubAddr ).
After completing the 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 to the address of the generated code. In addition, if the first command is followed by byte 0x5E, then it is replaced by byte 0x5F (these bytes are an indicator of the presence or absence of JIT compilation, byte 0xCC means no information). Thus, after replacement, the contents of the adapter are as follows:jmp NativeCode db 0x5E db MethodDescChunkIndex db PrecodeChunkIndex или jmp NativeCode db 0xСС db MethodDescChunkIndex db PrecodeChunkIndex
After JIT compilation, the address of the generated code is calculated by adding the relative offset built into the first jmp command to the completion address of the specified command. - adapter- specific MethodDesc structures are combined in continuous blocks of MethodDescChunk memory :
- 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 with the StubPrecode adapter , at the time of its creation in RemotingPrecode , the value of the address of the MethodDesc structure is built in by the CLR system directly (as a direct value in the assembler command). The specified value can be extracted at offset 1 (for x86) and 37 (for x64). The address of ThePreStub function is the result of adding the relative offset built into the last jmp command with the address of the completion of the specified command (for x86) or the direct value at offset 25 (for x64).
For objects that do not belong to other domains, after the JIT compilation, the transition address is replaced with the address of the ThePreStub functionto the address of the generated code, therefore, the method of determining the address of the generated code after performing the JIT compilation is the same as the method of determining the address of the ThePreStub function before performing the JIT compilation. For objects belonging to other domains, after the JIT compilation, the body of the RemotingPrecode adapter does not change. For simplicity, we do not consider the option of using RemotingPrecode for objects that do not belong to the application domain.
3. ThePreStub function
As already mentioned, the internal ThePreStub function does the following:
- JIT compilation of the method identified by the MethodDesc structure;
- setting a pointer in the MethodDesc structure to the generated native code;
- rewrite the adapter so that it makes an unconditional jump (jmp) to the generated native code;
- execution of the generated native code.
In all versions of the CLR and hardware platforms, ThePreStub function is implemented in the CLR at the hardware level by calling the internal PreStubWorker function and then transferring control (via the jmp command) to the address returned by the specified function. For completeness, we present the code of 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 precode adapters, the address of ThePreStub function can be determined as follows:
- We define an arbitrary static CLR method (you can even make it empty), prohibiting inline embedding and precompilation:
public delegate void EmptyDelegate(); [MethodImplAttribute( MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] public static void Empty() {}
- Create and lock the delegate of the method in memory and define 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 sample adapter StubPrecode , then you should use the method of calculating the address of ThePreStub function from paragraph 1 of section 2. If the commands at the received address match the sample adapter FixupPrecode , then use the method of calculating the address of ThePreStub function from paragraph 2 of section 2 .
- To cancel the memory lock of the delegate of the 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;
- rewrite the adapter so that it makes an unconditional jump (jmp) to the generated native code;
- return the ThePreStub function of the address of the changed adapter.
The PreStubWorker function has the following C declaration (according to the CLR source):
для CLR 4.6 и выше: void* __stdcall PreStubWorker(TransitionBlock* pTransitionBlock, MethodDesc* pMD);
для CLR ниже 4.6: void* __stdcall PreStubWorker(PrestubMethodFrame *pPFrame);
Using this fact, the code listings of the ThePreStub function , and the fact that the TheDreStub function in the eax (for x86) and r10 (for x64) registers is passed the value of the MethodDesc address , you can determine how the PreStubWorker function accesses the value of the MethodDesc inside :
- for CLR 4.6 (and above), the specified value is extracted from the second parameter passed to the function;
- for CLR below 4.6 x86 platform the value is located at offset 8 of the structure addressed by the pPFrame parameter ;
- for the CLR below 4.6 x64 platform, the value is located at 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 ThePreStub internal function and based on the above listings of its code, you can specify an algorithm for calculating the address of the PreStubWorker internal function without using fixed offsets inside the ThePreStub function (which, as you can see, change with each new version of the CLR):
- for x86 and x64 platforms (except for CLR 2.0), the specified address will be the result of adding the relative offset built into the call command, which is unique in ThePreStub function , with the completion address of the specified command;
- for x64 CLR 2.0, the specified address will be the result of adding the relative offset built into the call command, which is preceded by the lea command, with the address of the call command completion.
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 runtime.
5. The interception algorithm
Summarizing all of the above, we can suggest the following way to intercept .NET functions:
- Get the address of the replacement method using a call to RuntimeMethodHandle.GetFunctionPointer ;
- if the replaced method is already JIT-compiled, then find the address in the memory of the generated native code and intercept the specified address to execute the replacement method;
- if the replaced method is not yet JIT-compiled, then
- calculate the address of its MethodDesc structure ;
- calculate the address and intercept the PreStubWorker function in such a way that the original implementation is called in the substitute PreStubWorker method;
- add additional logic to the PreStubWorker replacement function for the case when the function uses the MethodDesc address that matches the required address. In this case, after calling the original implementation, get the address of the generated native method and intercept the received address to execute the replacement method.
- calculate the address of its MethodDesc structure ;
After all the foregoing, the above paragraphs of the algorithm do not require detailed explanations, with the exception of paragraphs 2 and 3.1.
Clause 2 talks about determining the address of a 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 environment and calculates the specified address (or returns NULL if there is no JIT compilation).
- Get the address of a .NET method by calling RuntimeMethodHandle.GetFunctionPointer .
- If the commands at the received address coincide with the sample adapter StubPrecode or RemotingPrecode , then extract the address of the compiled code as described in section 1 and 3 of Section 2. If the specified address matches the address of the ThePreStub function , then the JIT method was not compiled and should be return null. Otherwise, return the address of the compiled code.
- Until the current address matches the address of the ThePreStub function , do the following:
- if the current address points to the jmp command, then go to the destination address for the jmp command;
- otherwise, if the current address points to the call command, then check the destination address of the call command. If it is equal to the PrecodeFixupThunk adapter (the case of the 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.
- if the current address points to the jmp command, then go to the destination address for the jmp command;
- Return NULL since the address of ThePreStub function has been reached .
Clause 3.1 talks about defining the address of a MethodDesc structure for an uncompiled method. The algorithm below is based on the knowledge of the binary structure of the adapters generated by the CLR environment and calculates the specified address (or NULL in some cases if there is a JIT compilation).
- Get the address of a .NET method by calling RuntimeMethodHandle.GetFunctionPointer .
- If the commands at the received address coincide with 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 the 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 ( FixupPrecode case after JIT compilation), then calculate the address of the MethodDesc structure , as described in section 2, section 2. Otherwise, go to the address for the jmp command;
- otherwise, if the current address points to the call command, then check the destination address of the call command. If it is equal to the PrecodeFixupThunk adapter (the case of the FixupPrecode- adapter before JIT compilation), then calculate the address of the MethodDesc structure , as described in Section 2, section 2. Otherwise, return NULL;
- otherwise, return NULL.
- if the current address points to the jmp command, then check the byte immediately after the jmp command. If it is 0x5F ( FixupPrecode case after JIT compilation), then calculate the address of the MethodDesc structure , as described in section 2, section 2. Otherwise, go to the address for the jmp command;
- Return NULL (the specified item must be unreachable).
6. Conclusion
The performance of the above algorithm has been repeatedly tested in practice (including in 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 simple to use. Here is an example of using interception using the developed library.
Suppose you want to intercept the Open function of the SqlConnection class . Then the interception code when using the developed library may look like in C # in the following way:
public static class HookedConnection
{
public static RTX.NET.HookHandle OpenHandle;
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void Open(SqlConnection connection)
{
// вывести строку соединения
Console.WriteLine(connection.ConnectionString);
// вызвать базовую функцию
OpenHandle.Call(connection);
}
}
Here, the OpenHandle variable contains a descriptor with which you can call the implementation of the replaced function and which is initialized as a result of assigning an interception:
using (ConnectionEntry entry = new ConnectionEntry())
{
Test();
}
where the ConnectionEntry class is the so-called “Interception manager”:
public class ConnectionEntry : RTX.NET.HookDispatcher, RTX.NET.IHookLoadHandler
{
// обрабатываемые типы
public virtual string[] GetTypes()
{
// указать класс для перехватываемых методов
return new string[] { "System.Data.SqlClient.SqlConnection"};
}
// обработчик загрузки типов
public virtual void OnLoad(RTX.NET.HookDispatcher dispatcher, Type type)
{
// перехватить методы
HookedConnection.OpenHandle = HookOpen(dispatcher, type);
}
private RTX.NET.HookHandle HookOpen(
RTX.NET.HookDispatcher dispatcher, Type targetType)
{
// указать имя и тип параметров метода
string name = "Open"; Type[] types = Type.EmptyTypes;
// указать атрибуты метода
BindingFlags flags = BindingFlags.Public |
BindingFlags.Instance | BindingFlags.InvokeMethod;
// выполнить перехват
return dispatcher.Install(targetType, name,
typeof(HookedConnection), name, flags, types
);
}
}
Then when executing 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 following message will be displayed in the console:
Server=(localdb)\v11.0;AttachDbFileName=C:\MyFolder\MyData.mdf;Integrated Security=true;