This is the fifth part of the .NET Inside Out series. For your convenience you can find other parts in the table of contents in Part 1 – Virtual and non-virtual calls in C#
Unhandled exception crashes application domain which in turn crashes whole process. What if we want to create some background job and handle its exceptions? We can create tasks and add continuation for failure, we can use asynchronous code and catch exceptions in place of awaiting. However, sometimes we are forced to use ordinary threads. It is manageable if we control the thread start function — we can simply catch exceptions there. However, what should we do if one of libraries used by our process creates thread and doesn’t handle exceptions? Well, we can try to modify CLR code to handle this case. Let’s go.
Idea
Thread class has a constructor accepting thread start function. We can inject our code into it and wrap function in catch handler. However, since this function is ngened (its code is compiled to machine code when .NET is being installed) we can’t simply override it by rewriting CLR metadata. We also cannot modify it directly without unblocking memory pages since they are protected by the OS. We already know how to inject jump into method but this case is a little different — we want to execute the original code but we want to modify method parameter.
Let’s start with something easy: wrapping thread start function with exception handler.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public static ThreadStart ModifyHandler(object _, ThreadStart threadStart) { return () => { try { threadStart(); } catch (Exception e) { Console.WriteLine(e); } }; } |
This is rather obvious.
Next, we need to unblock memory page in order to modify it. Here is the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public enum Protection { PAGE_NOACCESS = 0x01, PAGE_READONLY = 0x02, PAGE_READWRITE = 0x04, PAGE_WRITECOPY = 0x08, PAGE_EXECUTE = 0x10, PAGE_EXECUTE_READ = 0x20, PAGE_EXECUTE_READWRITE = 0x40, PAGE_EXECUTE_WRITECOPY = 0x80, PAGE_GUARD = 0x100, PAGE_NOCACHE = 0x200, PAGE_WRITECOMBINE = 0x400 } [DllImport("kernel32.dll", SetLastError = true)] static extern bool VirtualProtect(IntPtr lpAddress, uint dwSize, uint flNewProtect, out uint lpflOldProtect); var threadConstructorInfo = typeof (Thread).GetConstructor(new[] {typeof (ThreadStart)}); var threadConstructorMethodHandle = threadConstructorInfo.MethodHandle; RuntimeHelpers.PrepareMethod(threadConstructorMethodHandle); var threadConstructorAddress = threadConstructorMethodHandle.GetFunctionPointer(); uint old; VirtualProtect(threadConstructorAddress, (uint) 512, (uint) Protection.PAGE_EXECUTE_READWRITE, out old); |
Now, before we modify original code, we need to examine it and see what exactly we modify. We want to jump to a method which will modify parameter on a stack and get back to original code without changing any other things (registers, stack etc). Since we will override few starting bytes, we need to execute them directly.
In theory we could copy existing constructor code somewhere else and execute it as a whole. But this is pretty difficult — all jumps and calls are usually based on relative addresses so we would need to recalculate them manually. It is easier to simply jump back to the same code and execute few lines manually.
So our helper function looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Function needs to change lambda and call thread // Original Thread constructor is called with fastcall convention, parameters via ecx + edx, return value in eax // We then jump back to original Thread constructor in order to avoid recalculating relative addresses static byte[] methodCode = { 0x51, // push ecx 0xE8, // call 0x0, 0x0, 0x0, 0x0, // helper method call address, replaced in runtime, it returns new lambda in eax 0x59, // pop ecx 0x8B, 0xD0, // mov edx, eax 0x8B, 0xC1, // mov eax, ecx 0x55, // push ebp 0x8B, 0xEc, // mov ebp,esp 0x50, // push eax 0x90, // nop 0xE9, // jmp 0x0, 0x0, 0x0, 0x0, // original thread constructor call address, replaced in runtime 0xC3 //retn }; |
As stated in comments, original method is called using fastcall convention. This means that parameters are passed via ECX and EDX registers and return value is in EAX.
We first remember ECX (since it might be modified by helper function). All parameters are already in registers so we simply call helper method modifying thread start function. Next, we restore ECX and other registers. Finally, we execute function preamble (which we overwrite with jump instruction in original code) and jump back to thread code.
All we need to do is calculate addresses:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var modifierMethodMethodHandle = typeof (ThreadHandler).GetMethod(nameof(ModifyHandler)).MethodHandle; RuntimeHelpers.PrepareMethod(modifierMethodMethodHandle); var modifierMethodAddress = modifierMethodMethodHandle.GetFunctionPointer(); IntPtr modifierCallerMethodAddress = GetArrayPosition(methodCode); Marshal.WriteInt32(modifierCallerMethodAddress, 2, (int)modifierMethodAddress - (int)(modifierCallerMethodAddress + 6)); Marshal.WriteInt32(modifierCallerMethodAddress, 17, (int)threadConstructorAddress + 5 - ((int)modifierCallerMethodAddress + 21) - 1); // Fix Thread constructor to jump HijackMethod(threadConstructorAddress, modifierCallerMethodAddress); // … private static IntPtr GetArrayPosition(byte[] array) { unsafe { TypedReference reference = __makeref(array); IntPtr methodAddress = (IntPtr) (*(int*) *(int*) &reference + 8); return methodAddress; } } |
And we are done. Test program looks as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class Program { static void Main(string[] args) { ThreadHandler.EnableHandling(); MakeUnhandled(); Thread.Sleep(TimeSpan.FromSeconds(3)); Console.WriteLine("All done"); } private static void MakeUnhandled() { ThreadStart lambda = () => { Console.WriteLine("Running in new thread"); throw new Exception("This is unhandled!"); }; var thread = new Thread(lambda); thread.Start(); } } |
You can see the code here. It was tested on .NET 4.5 with Any CPU on Windows 10 Professional x64.
EDIT:
I am extending this note some time later.It looks like there are two changes required. First, you need to unlock the helper method code as well:
1 |
VirtualProtect(modifierCallerMethodAddress, (uint)OriginalMethodCode.Length, (uint)Protection.PAGE_EXECUTE_READWRITE, out old); |
Also, I don’t know why I calculated the jump address in this way:
1 |
Marshal.WriteInt32(modifierCallerMethodAddress, 17, (int)threadConstructorAddress + 5 - ((int)modifierCallerMethodAddress + 21) - 1); |
As now it looks like there is an off-by-one error. It should be:
1 |
Marshal.WriteInt32(modifierCallerMethodAddress, 17, (int)threadConstructorAddress + 6 - ((int)modifierCallerMethodAddress + 21) - 1); |
I am sure it was working when I was testing it last time so apparently something in my environment has changed. Yet another reason not to play with things like that!