This is the twentieth seventh 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#
Today we are going to grab an existing running thread and make it run some other code.
Let’s take this:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; namespace RerouteThreadWithDebugger { class Program { static void Main(string[] args) { var infinite = new Thread(() => InfiniteThread()); infinite.Start(); Thread.Sleep(2000); HackThread(infinite); infinite.Join(); Console.WriteLine("Finished!"); } static void InfiniteThread() { while (true) { Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - Still looping"); Thread.Sleep(1000); } } public static void InfiniteThreadReplacement() { Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} - Replaced."); throw new Exception("Killed"); } //... } } |
We have a thread which executes some infinite loop. We’d like to make it jump out of that loop and do something else. How can we do that?
The trick is to modify its instruction pointer register. We can’t do it from the same process directly as we need to debug it so we’ll use CDB for that. Obviously, this can be done manually if needed:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
static void HackThread(Thread thread) { var method = typeof(Program).GetMethod(nameof(InfiniteThreadReplacement), BindingFlags.Static | BindingFlags.Public); RuntimeHelpers.PrepareMethod(method.MethodHandle); var methodAddress = method.MethodHandle.GetFunctionPointer(); var processId = Environment.ProcessId; var threadsCommand = @$"-p {processId} -noio -logo cdb.log -c """ + string.Join("; ", LOAD_SOS, $@"!threads", $@"qd" ) + @$""""; var process = Process.Start( DBG_PATH, threadsCommand ); process.WaitForExit(); var threadsOutput = File.ReadAllText("cdb.log"); var nativeThreadToKill = threadsOutput .Substring(threadsOutput.IndexOf("Hosted Runtime")) .Split("\r", StringSplitOptions.RemoveEmptyEntries).Where(line => { var parts = line.Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries); return parts.Length > 2 && parts[1] == $"{thread.ManagedThreadId}"; }).Select(line => line.Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries)[0]) .FirstOrDefault(); var rerouteCommand = @$"-p {processId} -noio -c """ + string.Join("; ", $@"~{nativeThreadToKill}s", $@"r rip=0x{methodAddress.ToString("X")}", // Reroute a thread $@"qd" ) + @$""""; Process.Start( DBG_PATH, rerouteCommand ); } static string DBG_PATH = @"path_to_cdb.exe"; static string LOAD_SOS = @"path_to_sos.dll"; |
How does it work? We first run CDB and dump threads to get the native thread id from the output of the !threads
command. There are other ways to do so but this one is the most reliable.
Next, we run CDB again. This time it switches to thread, modifies its rip register and then exists.
The problem with this approach is that once we modify the thread it becomes unreliable. We don’t know if the stack is correct or whether we can safely exit the method. Ideally, we’d like to kill the thread. We’ll discuss this aspect in some later part.