This post breaks down how to detect classic and reflective DLL injection on a live Windows host by enumerating running processes and their threads for signs of malicious code injection. I’ll be using code snippets from my tool GetInjectedThreads throughout this post to explain the detection process and including screenshots of my tool’s output to show detection of some common malware that primarily lives in-memory. While my tool currently finds injected threads and outputs information useful for responders, a Part 2 of this post will take it a step further, scanning identified processes for malware signatures and extracting useful configuration information from them! GetInjectedThreads is essentially a C# implementation of Jared Atkinson’s Get-InjectedThread.ps1 Which I’ll also discuss below.
Background
Over time I’ve become increasingly curious as to how process injection works and how one might detect it. I’ve recently been using meterpreter and Cobalt Strike a lot, which both rely heavily on reflective DLL injection for execution of their second stages in memory. After doing some research, I decided I’d have a crack at writing some code to detect process injection and came across the following useful resources that I relied upon for writing this post and my tool:
- This SpecterOps Post by Jared Atkinson, without which I would have been lost!
- The post breaks down Get-InjectedThread.ps1 and details what information is collected from injected threads. I decided to collect the exact same information upon initial detection and output results to console in the same format.
- This 2019 Blackhat Paper on Windows process injection, and
- Chapter 8 of The Art of Memory Forensics
Process Injection Overview
I won’t go into a detailed explanation here, particularly as there are many different techniques for injecting code into another process, but at its crux, process injection always involves three key steps:
- Allocating memory in the target process
- Writing code into the allocated memory
- Executing the code written to the target process
The image below summarises the steps used in the type of DLL injection being detected by GetInjectedThreads. It leaves behind unbacked executable memory sections in the target process where code has been injected - which will be used to enable detection.
Housekeeping
GetInjectedThreads requires Administrator privileges, or specifically the SeDebugPrivilege privilege due to the nature of how it functions (accessing remote processes and their tokens, reading their paged memory, etc.) and a simple check for Admin privileges is included before process enumeration occurs.
Because of the program’s reliance on Win32 API functions, it was necessary to recreate many of the structs and enums used to retrieve data from processes and pass options/flags to functions. This involved marhshaling the Win32 types specified in the struct declarations from the MS documentation (Enumerating tokens is a great example) to a CLR type with a matching specification. For this reason, GetInjectedThreads currently works only against 64bit Windows systems because the MEMORY_BASIC_INFORMATION struct differs between 32 and 64 bit processes. Although I have currently included the 32bit version of the struct in the code, I have not yet coded a 32bit version of the program.
Assuming that more than a single injected thread may be detected on a host and that information collected about injected threads could be useful in other programs, a List of InjectedThread objects is initialised before process enumeration occurs. Here’s what information is collected and stored for each InjectedThread
class InjectedThread
{
public string ProcessName { get; set; }
public int ProcessID { get; set; }
public string Path { get; set; }
public string KernelPath { get; set; }
public string CommandLine { get; set; }
public bool PathMismatch { get; set; }
public int ThreadId { get; set; }
public string AllocatedMemoryProtection { get; set; }
public string MemoryProtection { get; set; }
public string MemoryState { get; set; }
public string MemoryType { get; set; }
public int BasePriority { get; set; }
public bool IsUniqueThreadToken { get; set; }
public string Integrity { get; set; }
public string Privileges { get; set; }
public string LogonId { get; set; }
public string SecurityIdentifier { get; set; }
public string Username { get; set; }
public DateTime LogonSessionStartTime { get; set; }
public string LogonType { get; set; }
public string AuthenticationPackage { get; set; }
public IntPtr BaseAddress { get; set; }
public int Size { get; set; }
public byte[] ProcessBytes { get; set; }
public byte[] ThreadBytes { get; set; }
public DateTime ThreadStartTime { get; set; }
...
GetInjectedThreads Overview
While I chose to write GetInjectedthreads in C#, P/Invoke is required throughout to use unmanaged code from the Windows API to access process and thread memory as well as token information. Below is an overview of the process GetInjectedThreads uses to detect injection.
- Enumerate all running processes and get a handle to each process via OpenProcess()
- Iterate over each thread in the process’ ProcessThreadCollection and get a handle to the thread via OpenThread()
- Retrieve the base address of the thread’s allocated memory via NtQueryInformationThread()
- Retrieve the thread’s MEMORY_BASIC_INFORMATION structure passing its base address and the process handle to VirtualQueryEx()
- If the thread’s MEMORY_BASIC_INFORMATION indicates that the thread contains pages associated to physical storage (State = MEM_COMMIT) and those pages are NOT mapped to an executable or DLL on disk (Type != MEM_IMAGE), then it is highly likely that code injection has occurred.
For each thread where likely injection has been detected:
- Get a handle to the thread or process token
- If a particular thread has its own token, it can be retrieved by calling OpenThreadToken()
- If OpenThreadToken() returns false, the thread is likely using its parent process’ token which is retrieved by calling OpenProcessToken()
- Retrieve token information by passing the token handle to GetTokenInformation()
- Read the contents of the thread and its process’ memory via ReadProcessMemory()
- Retrieve other useful thread/process-specific information via .NET methods, unmanaged code and WMI.
GetInjectedThreads in action
The code snippet below displays exactly how detection occurs within GetInjectedThreads. This snippet starts immediately after checking that opening a handle to a running process was successful. Each Process object’s threads are enumerated by iterating over its ProcessThreadCollection.
ProcessThreadCollection threadCollection = process.Threads;
foreach (ProcessThread thread in threadCollection)
{
// Get handle to the thread
IntPtr hThread = OpenThread(ThreadAccess.AllAccess, false, thread.Id);
// Create buffer to store pointer to the thread's base address - NTQueryInformationThread writes to this buffer
IntPtr buf = Marshal.AllocHGlobal(IntPtr.Size);
// Retrieve thread's Win32StartAddress - Different to thread.StartAddress
Int32 result = NtQueryInformationThread(hThread, ThreadInfoClass.ThreadQuerySetWin32StartAddress, buf, IntPtr.Size, IntPtr.Zero);
if(result == 0)
{
// Need to Marshal Win32 type pointer from CLR type IntPtr to access the thread's base address via pointer
IntPtr threadBaseAddress = Marshal.ReadIntPtr(buf);
MEMORY_BASIC_INFORMATION64 memBasicInfo = new MEMORY_BASIC_INFORMATION64();
VirtualQueryEx(hProcess, threadBaseAddress, out memBasicInfo, (uint)Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION64)));
if (memBasicInfo.State == MemoryBasicInformationState.MEM_COMMIT && memBasicInfo.Type != MemoryBasicInformationType.MEM_IMAGE)
{
// Get information about the process/thread
...
If the base address of a thread is successfully retrieved using NtQueryInformationThread(), the information required to detect injection is stored in a MEMORY_BASIC_INFORMATION struct. If the thread’s State flag is set to MEM_COMMIT and its Type flag is not set to MEM_IMAGE, the thread and process should be further inspected. Here’s GetInjectedThreads’ MEMORY_BASIC_INFORMATION struct with CLR-type variables and enums for variables with flag values:
public struct MEMORY_BASIC_INFORMATION64
{
public ulong BaseAddress;
public ulong AllocationBase;
public MemoryBasicInformationProtection AllocationProtect;
public int __alignment1;
public UIntPtr RegionSize;
public MemoryBasicInformationState State;
public MemoryBasicInformationProtection Protect;
public MemoryBasicInformationType Type;
public int __alignment2;
}
Here’s the tool detecting a meterpreter reverse TCP shell that has been injected into svchost.exe. I was also able to successfully test detection of other meterpreter (x86 and x64) payloads such as reverse HTTP/S, bind TCP as well as Cobalt Strike Beacon payloads (x86 and x64).
Where from here?
I’ll continue to try and work detections for other process injection methods into GetInjectedThreads. For now though, I thought it would be useful to dump process memory where injected threads had been detected for further analysis. While replicating Get-InjectedThread.ps1 I had already captured the memory for a thread where process injection was detected using ReadProcessMemory(); however, there are cases where for the purposes of analysis, capturing all memory of the target process is more valuable than just capturing the thread’s memory. Reflective DLL injection targets PAGE_EXECUTE_READWRITE sections of memory in the target process for injection and in certain cases where RATs such as meterpreter or Cobalt Strike execute, pages outside the injected thread’s memory may contain useful information for defenders. For this reason, GetInjectedThreads also dumps all committed pages from the target process whose memory protection is either PAGE_READWRITE or PAGE_EXECUTE_READWRITE.
Analysing Process Memory
In a fure post I’ll use GetInjectedThreads to explore this much further, but for now I thought it was interesting to find useful command and control information in memory dumped by GetInjectedThreads.
Here’s the beginning of Cobalt Strike’s C2 block in a dump produced by GetInjectedThreads.
A simple XOR decode on this blob reveals plenty of useful information for defenders.
Here’s C2 information from a meterpreter process dump.