Alternative Process Injection
21 December 2021
Introduction
Process injection is a well-known defense evasion technique that has been used for decades to execute malicious code in a legitimate process. Until now, it is still a common technique used by hackers/red teamers.
From the attacker's perspective, signature-based detection from Anti Virus is no longer the main challenge for defense evasion. Instead, Endpoint Detection and Response (EDR) solutions become their pain point because of its various types of telemetry sources available to identify process injection attacks, using the following ways:
Kernel callbacks (e.g., PsSetCreateProcessNotifyRoutine)
ETW (Event Tracing for Windows) Threat Intelligence
Sysmon like events
API hooking and monitoring
With these challenges, security researchers developed different evasion techniques (e.g., DInvoke, Syscall, API unhooking). However, those user-mode evasion techniques are still insufficient to bypass some of the EDR solutions especially when they have data sources on the kernel level such as ETW TI.
To have a deeper understanding, I built a custom ETW TI agent to study what data is collected. Then, I learned that it could provide incredible visibility for EDR vendors to monitor commonly abused API calls (e.g., SetThreaContext, memory allocation APIs) and create detection rules similar to Get-InjectedThread.
Particularly, CreateRemoteThread is one of the most popular techniques and it usually comes with the following API call sequence:
VirtualAllocEx -> allocate memory space to stage the shellcode
WriteProcessMemory -> write the decrypted/decoded shellcode in the memory space
CreateRemoteThread -> create a new thread on the process with the start address pointing to the memory space
Since this good old technique has been abused for years, it is not surprising to see EDR products detecting it. Therefore, I started looking for alternative techniques that use less suspicious API calls and parameters to minimize footprint and abnormality (e.g., starting a thread on a private memory page), I then found XPN's Understanding and Evading Get-InjectedThread discussed different ways to bypass the detection of Get-InjectedThread.
Since the memory page allocated using NtAllocateVirtualMemory/VirtualAllocEx is always assigned to a private type (i.e., MEM_PRIVATE) unlike MEM_IMAGE for images (EXE/DLL), this becomes a strong Indicators of Compromise (IOC) for Get-InjectedThread to detect CreateRemoteThread injection attack when the start address of a thread is on a MEM_PRIVATE memory page.
In XPN's blog post, he provided several ways to bypass the detection, including:
Inject DLL via LoadLibrary
CreateRemoteThread + SetThreadContext
Leverage the instructions of an existing MEM_IMAGE binary to pass execution to shellcode
So this post would like to discuss an alternative injection by injecting the shellcode into the already loaded DLL memory page.
Advantages of the technique
Before we walk through the implementation, let's talk about the advantages of this technique in terms of detection evasion:
No memory allocation APIs are used (e.g., NtAllocateVirtualMemory, VirtualAllocEx, NtMapViewOfSection)
Thread is executed within .text section of a DLL which makes more sense to have execution right (i.e., PAGE_EXECUTE_READ) on the memory page
Start address of the newly created thread is located in MEM_IMAGE memory region without using the traditional thread hijacking technique (heavily monitored!)
Walkthrough
The main problem with this technique is that most of the time the process will crash when the shellcode is overwritten to an existing DLL memory page because the memory page has been used by the process to make it work.
To find a good memory page candidate for shellcode injection, I defined the following requirements:
The memory page should belong to a .text section since it has execution right (i.e., PAGE_EXECUTE_READ) on the memory page by nature
The memory page should provide sufficient space to store the shellcode
Overwriting the bytes in the memory page should not crash the process
The DLL candidate should be commonly loaded by different processes
To find a suitable candidate, I made a dirty C# script that will inject shellcode into the .text section of each DLL module loaded by the target process (e.g., notepad.exe) and return the result if the injection did not crash the process.
Since this study aims to improve my C# tradecraft to bypass EDR solutions so all demonstrations will be using C# code.
After executing the above scanning script, several potential candidates show up and msvcp_win.dll is selected for demonstration purposes based on the fact that this DLL is commonly loaded by different processes (e.g., notepad.exe, explorer.exe, iexplore.exe) and the region size of its .text section is sufficient to store common shellcode (staged/stageless).
When you pick your DLL candidate, you should think about whether that DLL will be used by your shellcode too.
The following code is used to locate the base address of the msvcp_win.dll and increase 0x1000 bytes to get the .text start address.
Once the address of the .text section was found, the memory protection flag will be changed from RX to RW using VirtualProtectEx to allow copying the shellcode into the memory page.
Then, WriteProcessMemory is used to copy the shellcode and VirtualProtectEx again will be used to restore the memory protection flag from RW back to RX. In the end, a new thread will be created using CreateRemoteThread.
Since WriteProcessMemory API has a feature allowing writing data to a read-only memory page by re-protecting it to a writeable memory page using NtProtectVirtualMemory, it could be unnecessary to change the protection flag manually using VirtualProtectEx. (Thanks for the reminder from @kyREcon)
However, this feature will temporarily set the protection flag of the memory page to RWX/WCX, which could be an IOC for suspicious activity. Therefore, updating the protection flag manually using VirtualProtectEx (RX->RW->RX) could be an OPSEC consideration to avoid this happening.
By combining the above all together, below is the final code:
Once the above code is compiled and executed, you should be able to get your shellcode executed as below:
By using this technique, a new thread is created from a start address of an existing loaded DLL instead of a private memory page.
Detection
This technique could bypass different common IOCs such as abnormal private executable memory and thread within non-image memory regions.
However, using Moneta, you will see it could be detected by an IOC regarding "Modified code".
As mentioned by Forrest Orr's "Masking Malicious Memory Artifacts – Part II: Blending in with False Positives", once we modified the original memory page of an existing DLL, 0x1000 bytes of memory of data in .text section will be marked as private. This nature becomes an IOC to detect modified code in legitimate DLL.
Final Word
While writing this page, I found there is an existing technique called "DLL Hollowing" that will create an image section to the process and replace the memory space with the shellcode. From my perspective, each has its advantages.
By comparing with different types of "DLL Hollowing", this technique has the following advantages:
Not required to load any new legitimate library
Avoid IOC for missing PEB module since the newly loaded library is not called using LdrLoadDll
However, the key disadvantage is that:
it is not as stable as other injection techniques because the target process most likely will be unusable after injection. You should avoid using this technique against any existing running process (e.g., injecting a keylogger to explorer.exe).
Reference:
Last updated