Partager via


Using CreateRemoteThread from C#

If you want to create a new thread in your process in C#, you can use Thread.Start. But things are a little harder if you want to create a thread in another process, ala kernel32!CreateRemoteThread. Disclaimer: CreateRemoteThread is evil, could dead-lock, doesn't always work (particularly across-sessions), and should be avoided in favor or friendlier inter-process communication libraries unless you're a rocket scientist.

Dangers and evils aside, here's how you can use it from C#.

The challenge with CreateRemoteThread is to to pass a thread proc that's a valid function pointer in the target process. In C#, we use delegates instead of function-pointers, but delegates are only valid in the current process and don't serialize. The basic trick is to use Marshal.GetFunctionPointerForDelegate to get an IntPtr to the method that will serve as the thread proc. While delegates can't be serialized across a process boundary, IntPtrs can.

Here's a snippet that demos CreateRemoteThread in C#. When invoked with no command line args, it will:

  1. Create a delegate to the thread proc, MyThreadProc
  2. Get an IntPtr (fpProc) for that via Marshal.GetFunctionPointerForDelegate
  3. spin up a 2nd instance of its own process, and pass it the parent's process id and  fpProc on the command line.  (So we're using the command line + parent/child process relationships as our initial IPC mechanism)
  4. The child (which knows it's the child because it has command line args) then parses the command line to recover the parents's pid and fpProc. and then gets the handle for the parent process.
  5. The child then makes the call to CreateRemoteThread, and passes some arbitrary data (in this case, the number 6789) as the thread proc's data.

Here's the output. Both apps print to the same console so that their text is interweaved to display causality:

 Pid 5944:Started Parent process
Pid 5796:Started Child process
Pid 5944: Inside my new thread!. Param=6789
Pid 5796: Thread exited with code: 1

 

Here's the interesting code snippet (built with VS2005):

 

        // stdcall        
        static int MyThreadProc(IntPtr param)
        {
            int pid = Process.GetCurrentProcess().Id;
            Console.WriteLine("Pid {0}: Inside my new thread!. Param={1}", pid, param.ToInt32());
            return 1;
        }

        // Helper to wait for a thread to exit and print its exit code
        static void WaitForThreadToExit(IntPtr hThread)
        {
            WaitForSingleObject(hThread, unchecked((uint)-1));

            uint exitCode;
            GetExitCodeThread(hThread, out exitCode);
            int pid = Process.GetCurrentProcess().Id;
            Console.WriteLine("Pid {0}: Thread exited with code: {1}", pid, exitCode);
        }

        // Main function 
        static void Main(string[] args)
        {
            int pid = Process.GetCurrentProcess().Id;
            if (args.Length == 0)
            {
                Console.WriteLine("Pid {0}:Started Parent process", pid);
                
                // Spawn the child
                string fileName = Process.GetCurrentProcess().MainModule.FileName.Replace(".vshost", "");                

                // Get thread proc as an IntPtr, which we can then pass to the 2nd-process.
                // We must keep the delegate alive so that fpProc remains valid
                ThreadProc proc = new ThreadProc(MyThreadProc);
                IntPtr fpProc = Marshal.GetFunctionPointerForDelegate(proc);

                // Spin up the other process, and pass our pid and function pointer so that it can
                // use that to call CreateRemoteThraed
                string arg = String.Format("{0} {1}", pid, fpProc);
                ProcessStartInfo info = new ProcessStartInfo(fileName, arg);
                info.UseShellExecute = false; // share console, output is interlaces.
                Process processChild = Process.Start(info);
                    
                processChild.WaitForExit();
                GC.KeepAlive(proc); // keep the delegate from being collected
                return;
            }
            else
            {
                Console.WriteLine("Pid {0}:Started Child process", pid);
                uint pidParent = uint.Parse(args[0]);
                IntPtr fpProc = new IntPtr(int.Parse(args[1]));

                IntPtr hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, pidParent);

                uint dwThreadId;
                // Create a thread in the first process.
                IntPtr hThread = CreateRemoteThread(
                    hProcess,
                    IntPtr.Zero,
                    0,
                    fpProc, new IntPtr(6789),
                    0,
                    out dwThreadId);
                WaitForThreadToExit(hThread);
                return;
            }
        }

 

The full sample code is available at: https://blogs.msdn.com/jmstall/articles/sample_create_remote_thread.aspx.

More on the marshalling:

The simple way to do call CreateThread (the local version) from managed code would be something like this:

         // Thread proc, to be used with Create*Thread
        public delegate int ThreadProc(IntPtr param);
         // Friendly version, marshals thread-proc as friendly delegate
        [DllImport("kernel32")]
        public static extern IntPtr CreateThread(
            IntPtr lpThreadAttributes,
            uint dwStackSize,
            ThreadProc lpStartAddress, // ThreadProc as friendly delegate
            IntPtr lpParameter,
            uint dwCreationFlags,
            out uint dwThreadId);

Notice that the CLR's marshaller is smart enough to marshal the friendly type-safe ThreadProc delegate as an evil unsafe function pointer. At this pointer, unless we needed some very fine control over thread-creation (eg specific flags), we're probably still better off using Thread.Start. The other IntPtrs (like lpThreadAttributes) could also be marshaled as type-safe structures instead of intptrs (see on https://pinvoke.net for more examples). We could have marshaled it like:

         // Marshal with ThreadProc's function pointer as a raw IntPtr.
        [DllImport("kernel32", EntryPoint="CreateThread")]
        public static extern IntPtr CreateThreadRaw(
            IntPtr lpThreadAttributes,
            uint dwStackSize,
            IntPtr lpStartAddress, // ThreadProc as raw IntPtr
            IntPtr lpParameter,
            uint dwCreationFlags,
            out uint dwThreadId);

The raw function pointer doesn't really buy us much in the local case. But once we view it like that, the leap to CreateRemoteThread is smaller:

         // CreateRemoteThread, since ThreadProc is in remote process, we must use a raw function-pointer.
        [DllImport("kernel32")]
        public static extern IntPtr CreateRemoteThread(        
          IntPtr hProcess,
          IntPtr lpThreadAttributes,
          uint dwStackSize,
          IntPtr lpStartAddress, // raw Pointer into remote process
          IntPtr lpParameter,
          uint dwCreationFlags,
          out uint lpThreadId
        );

Comments

  • Anonymous
    September 29, 2006
    Will this work on a 64 bit machine ? WaitForSingleObject(hThread, unchecked((uint)-1));

  • Anonymous
    September 30, 2006
    Pour ceux qui auraient un jour besoin d'utiliser l'API CreateRemoteThread en C#, Mike Stall nous livre...

  • Anonymous
    September 30, 2006
    Blandest - it should still work, though it's not the cleanest.
    INFINITE is defined in the windows headers as:
    #define INFINITE            0xFFFFFFFF  // Infinite timeout

    and 'uint' in C# is 32-bits on both 32 and 64 machines.
    If you have a C# app print
      (unchecked((uint)-1).ToString("x"))
    it will print the same thing on both 32 and 64-bit.
    It would have been cleaner if I defined a constant and used that, but this was just a quick demo of CreateRemoteThread.

  • Anonymous
    October 03, 2006
    How would make sure that the delegate wasn't collected. Would you make it static?

  • Anonymous
    October 05, 2006
    Dmitriy - Ah! You got me. I updated the sample with a GC.KeepAlive to address that.

  • Anonymous
    April 22, 2008
    In the previous article I discussed a few of the benefits of stack allocation as well as a couple of