Jack O'Sullivan
April 26 2021
Introduction
In part 1 of this series, we looked at the theory behind Process Injection and the things that need to be in place on the Windows host for it to be successful. At the end of that blog, two examples were given:
- Shellcode Injection
- DLL Injection
However, none of these were given any Operational Security considerations. In this blog, we will look at how to weaponise and secure Process Injection against Modern Endpoint-Protection - modern process injection, if you will.
Indicators of Compromise
To make this blog simpler, we are going to focus on one method: Shellcode Injection.
As a reminder, here is the standard method to do so:
void Inject(int pid) {LPVOID pAddress;
HANDLE hThread;
HANDLE hProcess;
DWORD id;
SIZE_T bytesWritten;
hProcess = ::OpenProcess(PROCESS_CREATE_THREAD|PROCESS_VM_WRITE|PROCESS_VM_OPERATION, TRUE, pid);
if (!hProcess) {
printf("[!] OpenProcess(): %u\n", GetLastError());
goto Cleanup;
}
printf("[+] Process Handle: %p\n", hProcess);
pAddress = ::VirtualAllocEx(hProcess, nullptr, bufsize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!pAddress) {
printf("[!] VirtualAllocEx(): %u\n", GetLastError());
goto Cleanup;
}
printf("[+] Base Address: %p\n", pAddress);
if (!::WriteProcessMemory(hProcess, pAddress, buf, bufsize, &bytesWritten)) {
printf("[!] WriteProcessMemory(): %u\n", GetLastError());
}
printf("[+] Wrote %lld bytes!\n", bytesWritten);
hThread = ::CreateRemoteThread(hProcess, nullptr, bufsize, (LPTHREAD_START_ROUTINE)pAddress, nullptr, GENERIC_EXECUTE, &id);
if (!hThread) {
printf("[!] CreateRemoteThread(): %u\n", GetLastError());
goto Cleanup;
}
printf("[+] Thread Handle: %p\n", hThread);
::CloseHandle(hThread);
Cleanup:
if (hProcess) ::CloseHandle(hProcess);
return;
}
If this doesn't look familiar, then refer to part one. Let’s look at some of the points of detection in PEStudio:
The imports can be seen above, everything else will be the Multi-threaded Visual Studio compilation stuff that we don't need to worry about. The calls we made are all there though:
This begs the question, "why is this an issue?". Endpoint Protection & Response (EDR) are considered the modern solution to protecting workstations and servers. Over the past few years, a lot of research has gone into dealing with EDR, including:
- Silencing Cylance: A Case Study in Modern EDRs
- A tale of EDR bypass methods
- Red Teaming in the EDR age
The way most of this works is by utilising a Function Hooking which allows the EDR to identify the call the Windows API the application wants and redirects it through its own DLL as a sort of proxy. This works by copying the target function's first few instructions into executable memory and replacing them with a jmp to a hook. Another jmp instruction is added to allow the call to return to the original function. This is detailed quite well in FireWalker: A New Approach to Generically Bypass User-Space EDR Hooking.
To recap, function hooking allows for the applications calls to be assessed on execution, which is a problem. Even if the shellcode being injected is clean, this methodology of calls has a high possibility of being flagged. A lot of research has gone into dealing with this, and we will get to it later - but first we need to discuss the Protection Ring.
User-land vs Kernel-land
Before jumping into dealing with EDRs, there is a bit of background to be done first. The following diagram has come up in almost every other discussion of user-land and kernel-land architecture:
Giving this some context, most user activity will occur at ring 3, known as User Mode. And the Kernel operates (surprisingly) within Kernel Mode. More detail can be found in Windows Programming/User Mode vs Kernel Mode. Cross-over between user mode and kernel mode can and does happen.
For a better representation of Kernel Mode and User Mode, see the Overview of Windows Components documentation:
Rings 1 and 2 are typically left for device drivers. But why is this useful? Well, it turns out that the Windows API utilises the Native API which operates within Kernel Mode. As an example, API Monitor can be used to look at the calls being executed:
Two for one, the above shows CreateThread being called and then, subsequently, NtCreateThreadEx being called shortly after. The same happens with WaitForSingleObject.
So, what have we learned? Well, when a call to a Windows API is made, say from Kernel32.dll, it will then call NTDLL.DLL. This NTDLL.DLL will then load the EAX register with the system service number for calls equivalent kernel function. For example, CreateThread calls NtCreateThreadEx. Finally, NTDLL.dll will then issue a SYSENTER instruction. This causes the processor switch to kernel mode, and jumps to a predefined function, called the System Service Dispatcher. The following image is from Rootkits: Subverting the Windows Kernel, in the section on Userland Hooks:
Back in 2019 Cneelis published Red Team Tactics: Combining Direct System Calls and sRDI to bypass AV/EDR which had a subsequent release of SysWhispers:
“SysWhispers provides red teamers the ability to generate header/ASM pairs for any system call in the core kernel image (ntoskrnl.exe). The headers will also include the necessary type definitions.”
Then modexp provided an update which corrected a shortcoming with version 1 and gave us SysWhispers2:
“The specific implementation in SysWhispers2 is a variation of @modexpblog's code. One difference is that the function name hashes are randomized on each generation. @ElephantSe4l, who had published this technique earlier, has another implementation based in C++17 which is also worth checking out.”
The main change is the introduction of base.c which is a result of Bypassing User-Mode Hooks and Direct Invocation of System Calls for Red Teams.
Hooking Example
Before updating the injection function, some sort of hooking application is required. For this, SylantStrike can be adapted. More specifically the main DLL. The logic behind this "EDR" is described in:
All of which is written by CCob. The library used for hooking is MinHook but a full list can be found on GitHub.
Using the same logic as part 1, the DLL can be injected into the sacrificial notepad process:
In this case, the "EDR" is called Po and the DLL has been successfully loaded. The hook is currently present on NtProtectVirtualMemory which also corresponds to VirtualProtect:
MH_STATUS status = MH_CreateHookApi(TEXT("ntdll"), "NtProtectVirtualMemory", NtProtectVirtualMemory, reinterpret_cast<LPVOID*>(&pOriginalNtProtectVirtualMemory));
For this hook to work, this call needs to be present in the injection process - which it currently isn't. This modern process injection blog won't cover this in as much detail as I'd like, but another common point of signature is Read, Write and Execute (RWX) sections in memory. Hunting this has been documented and it is highly recommended that this is avoided. This issue is easy enough to fix:
- Allocate as PAGE_READWRITE
- Update to PAGE_EXECUTE_READ
More on this can be found on MSDN.
Update VirtualAllocEx to use PAGE_READWRITE:
pAddress = ::VirtualAllocEx(hProcess, nullptr, bufsize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
After the data is written with WriteProcessMemory, update the constant:
if (!::VirtualProtectEx(hProcess, pAddress, bufsize, PAGE_EXECUTE_READ, &flProtect)) {
printf("[!] VirtualProtectEx(): %u\n", GetLastError());
}
printf("[+] Updated to PAGE_EXECUTE_READ!\n");
Doing all this and running against the process with Po.DLL loaded will bypass it. This is because of the following code block:
if ((NewAccessProtection & PAGE_EXECUTE_READWRITE) == PAGE_EXECUTE_READWRITE) {//It was, so notify the user of naughtly behaviour and terminate the running program
MessageBox(nullptr, TEXT("You've been a naughty little hax0r, terminating program"), TEXT("Hax0r Detected"), MB_OK);
TerminateProcess(GetCurrentProcess(), 0xdead1337);
//Unreachable code
return 0;
}
If PAGE_EXECUTE_READWRITE is set, flag it. This reiterates the point of RWX sections. So, removing this and having it flag on any call to VirtualAllocEx:
DWORD NTAPI NtProtectVirtualMemory(IN HANDLE ProcessHandle, IN OUT PVOID* BaseAddress, IN OUT PULONG NumberOfBytesToProtect, IN ULONG NewAccessProtection, OUT PULONG OldAccessProtection) {//It was, so notify the user of naughtly behaviour and terminate the running program
MessageBox(nullptr, TEXT("You've been a naughty little hax0r, terminating program"), TEXT("Hax0r Detected"), MB_OK);
TerminateProcess(GetCurrentProcess(), 0xdead1337);
//Unreachable code
return 0;
}
This code is not practical, but it will prove a point. If the call is identified, show SylantStrike’s default message, and terminate:
Thus far we've had a quick overview of RWX and gave a short example of hooking. So, lets bypass it.
Bypassing the Hooks
Part of modern process injection is dealing with function hooking, and there are a few ways to do it. This demonstration will go discuss achieving it with syscalls as previously discussed. However, some other techniques will be discussed briefly afterwards. For this, SysWhispers2 will be used.
As shown in previous sections, the Native API call can be found by using API Monitor or a debugger to identify the call. The required calls for this example are:
Windows API | Native API |
VirtualAllocEx | NtAllocateVirtualMemory |
WriteProcessMemory | NtWriteVirtualMemory |
VirtualProtectEx | NtProtectVirtualMemory |
CreateRemoteThread | NtCreateThreadEx |
To get the correct data for these calls, the following command can be used:
python3 syswhispers.py -f NtAllocateVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx,NtProtectVirtualMemory -o syscalls
As the code is being written in Visual Studio, the instructions from the repository should be used:
- Copy the generated H/C/ASM files into the project folder.
- In Visual Studio, go to Project → Build Customizations... and enable MASM.
- In the Solution Explorer, add the .h and .c/.asm files to the project as header and source files, respectively.
- Go to the properties of the ASM file and set the Item Type to Microsoft Macro Assembler.
- Ensure that the project platform is set to x64. 32-bit projects are not supported at this time.
Running through the replacements, VirtualAllocEx is replaced with:
status = NtAllocateVirtualMemory(hProcess, &pAddress, 0, (PSIZE_T)&bufsize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory:
status = NtWriteVirtualMemory(hProcess, pAddress, buf, bufsize, nullptr);
VirtualProtectEx:
status = NtProtectVirtualMemory(hProcess, &pAddress, (PSIZE_T)&bufsize, PAGE_EXECUTE_READ, &flProtect);
CreateRemoteThread:
status = NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, hProcess, pAddress, NULL, FALSE, 0, 0, 0, NULL);
Testing the code:
The updated code works, reinjecting the DLL:
Now that it is reinjected, the new process injection utility will be used to test that the hook has been bypassed.
As a reminder, it is set to catch NtProtectVirtualMemory. For a change of scenery, let’s try catch NtWriteVirtualMemory:
DWORD NTAPI NtWriteVirtualMemory(IN HANDLE ProcessHandle, IN PVOID BaseAddress, IN PVOID Buffer, IN SIZE_T NumberOfBytesToWrite, OUT PSIZE_T NumberOfBytesWritten OPTIONAL){MessageBox(nullptr, TEXT("CAUGHT YOUR WRITE!"), TEXT("Best EDR maybe ever"), MB_OK);
return 0;
}
No specific circumstances have been set, so this should flag whenever it sees anything:
And then in Cobalt Strike:
PID 7864 has been injected into. So, lets recap. So far, we've taken a benign process injection technique and borrowed some hooking logic from SylantStrike to replicate one aspect of an EDR. This was then bypassed utilising x64 syscalls, which is one method of doing so. If x86 is required, then SysWhispers2_x86 can be used.
Instead of using syscalls, another common technique is Function Unhooking which does exactly as it suggests. It undoes the hook that was implemented. This is well documented in Universal Unhooking: Blinding Security Software and a Beacon Object File has taken this logic and wrapped it up into unhook-bof. SpecterOps have also published Adventures in Dynamic Evasion which is a highly recommended read.
Taking it a step further
The technique used to inject into the process followed the most common methodology and there are plenty of others that provide more control and are harder to detect from an inter-process perspective. If this is something that is being implemented, it is highly recommended to do some research into these which will be left as homework for the reader. There are several reasons to use process injection:
- Initial Implant Execution
- Fork & Run for Post Exploitation
- Lateral Movement/Privilege Escalation
And a few others. Depending on the requirement, it may be worth looking into Parent PID Spoofing to avoid awkward process architecture such as WORD.EXE running MALWARE.EXE, for example, it’s just not a "natural" structure processes will take. Furthermore, it may be worth protecting the execution even further by utilising "Block non-Microsoft DLLs" which has been documented by xpn.
The more functionality used, the more imports the PE will have; that is another thing to bear in mind and can be solved with dynamic function resolution, or more syscalls. Either way, no matter the reason for requiring process injection; there is always more opsec you can and should take.
Conclusion
Okay, so; this two-parter has introduced process injection and its place in the Windows world. How it works, why it works and why it’s used. With the theory covered, we moved into understanding the requirements for modern process injection, which introduces a primitive EDR where we knew the calls that were being hooked. An honourable mention here is the EDRs repository from Mr.Un1k0d3r which aims to document all the hooked functions across multiple EDRs. If there are any inaccuracies or any general questions, message me on Twitter.
Want more insights from the team? We’ve got you covered: check out Secarma Labs’ Twitter for more offensive security musings.
If you’re interested in developing your pentesting knowledge, we’re running a series of Hacking & Defending security training courses, where you get hands-on experience in ethical hacking. If you're interested in modern process injection, this could be the course for you. If you’d like to get involved, check out our Training page, or contact us here.