Process injection (T1055) refers to injecting code into other live processes. Adversaries use this technique to either evade detection based on process monitoring or to elevate privileges.
Process injection was ranked 6th most used technique by adversaries in Red Canary’s 2021 Threat Detection Report.
Anyone that has used popular C2 frameworks such as Metasploit and Empire has most likely used process injection. Metasploit’s migrate and Empire’s psinject does the classic process injection under the hood for injecting shellcode to another live process. Why classic? It’s because now there exist many process injection techniques such as process hollowing, PE injection, etc.
To be able to hunt simple shellcode injection, we first must understand how it is performed. There are many resources available on this topic. I highly recommend Secarma’s two-part blog series that nicely explains process injection with accompanying demo codes.
The generic shellcode injection process involves:
- Getting a handle to the remote process using OpenProcess().
- Changing the state of a region of memory within the remote process using VirtualAllocEx().
- Writing data to an area of memory of the remote process using WriteProcessMemory().
- Creating a thread that runs in the memory of the remote process using CreateRemoteThread().
For invoking these aforementioned WinAPIs, we require three process-related access rights:
- PROCESS_VM_OPERATION – Required to perform an operation on the address space of a process
- PROCESS_VM_WRITE – Required to write to memory in a process
- PROCESS_CREATE_THREAD – Required to create a thread in the process
For brevity, let’s name them our three amigos.
So, one way to hunt for shellcode injections is to detect the use of these access rights in a process access operation. So, how do we do this?
Many blue teamers might be familiar with Sysinternal’s Sysmon that nicely complements Windows’s native event logs. Sysmon provides Event ID 8 (Create Remote Thread) and Event ID 10 (Process Access) that just might do the job for us. The latter event provides the crucial access right used by the process that is accessing another process’s memory.
So, let’s hunt for migrate and psinject!
My testbed consists of a Windows 10 and a Kali Linux system. I use the community version of NxLog to write Windows and Sysmon events (in JSON) to a local file which I then import into my Jupyter notebook for analysis.
I use two popular open-source C2 frameworks for this test – Metasploit and Empire.
For anyone new to Sysmon, @SwiftOnSecurity and @olafhartong provide great starting baseline configurations. I will be using the former but keep in mind that Swift has disabled process access logging since it needs careful tuning to avoid log flooding. For this test, I have added rules that log all process access events of my C2 payloads.
<ProcessAccess onmatch="include"> <!--NOTE: Process Injection Test!! --> <SourceImage condition="end with">msf.exe</SourceImage> <SourceImage condition="end with">emp.exe</SourceImage> </ProcessAccess>
The snippet of Sysmon config used during testing
Before diving ahead, I first need to convert the log file (that has JSON logs separated by newline) into a single JSON object that pandas can directly import.
perl -p -e 's/\r\n/,/' NxLog-Dump.txt > tmp.txt perl -p -e 's/^/[/' tmp.txt > tmp2.txt perl -p -e 's/\n/]/' tmp2.txt > NxLog-JSON.txt
To kick off our analysis, I will import the log file into a DataFrame. I use two lists – eid_8_columns and eid_10_columns that contain useful fields of EID 8 and EID 10 respectively.
Importing the log file
Analysis of EID 8 is simple. I first filter out EID 8 events to another DataFrame – eid_8_pdf. Then I will only keep necessary columns such as Hostname, SourceImage, TargetImage, etc.
Filtering EID 8 events and dropping unnecessary fields
I repeat the same for EID 10.
Filtering EID 10 events and dropping unnecessary fields
Let’s see the unique access masks in EID 10 events. To weed out the noise, I have ported PSGumshoe’s Get-SysmonAccessMask to python. I will use the parse_access_mask function to parse the access mask to its respective access rights and store that back to the DataFrame.
Parsing access masks
Next, I have defined a list – PERMISSIONS_OF_INTEREST – that consists of our three amigos access rights required for process injection. I will only keep those access masks that contain all these access rights. We can see this has reduced the unique access rights to two values – 0x1fffff and 0x147a.
Filtering out un-relevant access masks
Finally, I can correlate EID 8 and 10 for further validation. Simple inner-join operation between them on common fields like Hostname, SourceImage, TargetImage, etc. is all we require.
Correlating EID 8 and EID 10
This leaves us with our Metasploit payload msf.exe accessing svchost.exe with an access mask of 0x147a.
I did the same analysis for Empire and found that Empire’s psinject uses access mask of 0x1f3fff.
I noticed a false-positive case for Empire’s psinject. The csharp_exe payload uses 0x1f3fff access mask for querying process information which is overkill. This means using ps command to enumerate running processes will generate false positives if the detection in question relies only on EID 10.
I will use Microsoft’s awesome open-source MSTICPY library for plotting the process tree created by process injection. I ran some reconnaissance commands after migrating to a svchost instance.
Process Tree visualization using MSTICPY
Contribute to the Community
I can contribute to the infosec community in the form of Sysmon and Sigma rule. For the former, in addition to access masks, I will look for any interesting artifacts in CallTrace.
Printing CallTrace column
The presence of the UNKNOWN module is an artifact shared by both Metasploit and Empire. Armed with this information, my Sysmon rule looks as follows.
<ProcessAccess onmatch="include"> <!-- Process access pattern of Metasploit's migrate (0x147a) and Empire's psinject (0x1f3fff) --> <Rule groupRelation="and"> <CallTrace condition="contains">UNKNOWN</CallTrace> <GrantedAccess name="T1055" condition="contains any">0x147a;0x1f3fff</GrantedAccess> </Rule> </ProcessAccess>
Sysmon rule snippet for detecting shellcode injection
Looks like Olaf already had a Sysmon rule for possible DLL injections but is missing the 0x147a access mask. Olaf merged my small pull request (#126) for supporting this access mask.
The corresponding sigma rule is shown below.
title: Shellcode Injection id: 250ae82f-736e-4844-a68b-0b5e8cc887da status: experimental description: Detects shellcode injection by Metasploit's migrate and Empire's psinject author: Bhabesh Raj date: 2022/03/11 tags: - attack.defense_evasion - attack.privilege_escalation - attack.t1055 logsource: category: process_access product: windows detection: selection: GrantedAccess: - '0x147a' - '0x1f3fff' CallTrace|contains: 'UNKNOWN' condition: selection falsepositives: - Empire's csharp_exe payload uses 0x1f3fff for process enumeration as well level: high
Sigma rule for detecting shellcode injection
I have requested Florian to test for any false positives of this sigma rule (#2796). I will update this blog once there are any updates.
EDIT – Florian didn’t see any false positives and so has approved the pull request.
We can confidently say that adversaries are not forsaking this technique anytime soon. It is best for Defenders to detect this technique via a defense-in-depth approach. Always keep in mind that your deployed detections can be and will be bypassed by a determined adversary. Analyze your collected telemetries from your systems and assess your blindspots.
This is all I have for today. Au revoir and stay safe!