Hooking into the JIT compiler for .NET to monitor and log function arguments can be quite complex, as there is no straightforward way provided. Typically, you would need to use the profiling API (ICorProfilerInfo
) provided by the CLR (Common Language Runtime), which allows you to monitor and manipulate .NET applications as they are running.
If you insist on hooking into the JIT compiler, you should have a deep understanding of not only the .NET CLR and its internal workings but also the target platform's architecture and machine code.
The Compiler Explorer can be very useful when working with such low-level aspects of programming.
using System;
class Program
{
static string MyFunc(string a, string b)
{
return a + " *** " + b;
}
}
Considering instruction set amd64 and .NET 7.0.105 on Linux. A call to jitted MyFunc
may look like:
mov rdi, string_handle
mov rdi, gword ptr [rdi]
mov rsi, string_handle
mov rsi, gword ptr [rsi]
call [Program:MyFunc(System.String,System.String):System.String]
Here rdi
and rsi
are used to pass parameters. This refers to the System V AMD64 ABI used by most Unix-like systems, where rdi
and rsi
are used for the first two arguments. On Windows, however, it would be rcx
and rdx
.
Also, it's very important to be aware of the impact of function inlining when dealing with JIT compilation. If MyFunc
was inlined, you wouldn't see a call
instruction for MyFunc
in your assembly code.
In C#, you can prevent a method from being inlined by the JIT compiler using the MethodImpl
attribute with the MethodImplOptions.NoInlining
flag.
Here is a sample Frida script on Windows to hook into .NET Framework jit. You may manually add some code to check the methodInfo_ILCode
and Interceptor.attach
the entryAddress
.
(function () {
let p_LoadLibraryExW = Module.getExportByName("kernel32.dll", "LoadLibraryExW");
Interceptor.attach(p_LoadLibraryExW, {
onEnter: function (args) {
this.lpLibFileName = args[0];
},
onLeave: function (retval) {
if (retval === 0) {
return;
}
let fileName = this.lpLibFileName.readUtf16String().split("\\").reverse()[0];
if (fileName === "clrjit.dll") {
let p_getJit = Module.getExportByName("clrjit.dll", "getJit");
let getJit = new NativeFunction(p_getJit, "pointer", []);
let cilJit = getJit();
let cilJit_vftable = cilJit.add(0x0).readPointer();
let p_compileMethod = cilJit_vftable.add(0x0).readPointer();
Interceptor.attach(p_compileMethod, {
onEnter: function (args) {
this.self = args[0];
this.compHnd = args[1];
this.methodInfo = args[2];
this.flags = args[3];
this.entryAddress = args[4];
this.nativeSizeOfCode = args[5];
},
onLeave: function (retval) {
if (retval === 0) { //CORJIT_OK
let methodInfo_ftn = this.methodInfo.add(0x0).readPointer();
let methodInfo_scope = this.methodInfo.add(0x4).readPointer();
let methodInfo_ILCode = this.methodInfo.add(0x8).readPointer();
let methodInfo_ILCodeSize = this.methodInfo.add(0xc).readUInt();
console.log("ftn =", methodInfo_ftn);
console.log("scope =", methodInfo_scope);
console.log("entryAddress =", this.entryAddress.readPointer());
console.log("nativeSizeOfCode =", this.nativeSizeOfCode.readUInt());
console.log(methodInfo_ILCode.readByteArray(methodInfo_ILCodeSize));
console.log();
}
}
});
}
}
});
}());