APT groups and malware developers routinely use system calls (AKA syscalls) to avoid hooks implemented by modern security tools like EDRs. Syscall analysis for behavioral malware detection is already a popular detection technique, but, despite numerous available techniques for syscall extraction, some of them still fly under the radar.
As red teamers or security researchers, we often use syscalls when developing attack paths.
For example, we can use direct syscalls in the malicious program we develop but it has problems. You see, syscall numbers differ across windows OS versions and service builds, forcing us to write a different syscall for each version. While there are tools to automate this process, EDRs are starting to flag this technique.
We can also use indirect syscalls. For those, we get a handle to ntdll.dll from the disk during run-time. To do that, we have to read the ntdll file bytes, parse the .rdata and .text sections, and extract the syscall functions we want to use. Even if we avoid writing the syscalls in a hard-coded way to our program, our behavior is suspicious as we get a handle to ntdll on disk and map its content and EDRs are starting to flag this technique.
Let’s try to explore a different technique that will enable us to extract syscalls from a suspended process. We will first look at the attack path, then detail how to develop the attack, and – bonus – see how we can take it one step further.
We need somehow to get a fresh copy of ntdll and copy it to our process memory and bypass the EDR hooks. As we can see in the example below, when opening explorer.exe in its active process state, suspicious functions will be hooked.
The jmp instruction leads to the EDR’s DLL for inspection. If the thread we create is malicious, the EDR will flag it.
But what would happen if explorer.exe was in a suspended state?
In the sample displayed below, we tried just that. We opened explorer.exe in a suspended state, and, as we can see, the only DLL that was loaded into it is ntdll.
Why it is that the ntdll was the only one loaded? Why none of the other DLLs, including the EDR’s? According to this StackOverflow thread, only ntdll.dll is initially mapped, and an APC is queued to run when the thread resumes.
This calls ntdll!LdrpInitializeProcess, which initializes the execution environment (e.g. language support, the heap, thread-local storage, the KnownDlls directory), loads kernel32.dll, gets the address of BaseThreadInitThunk. and performs static DLL imports.
Let’s check if NtCteateThreadEx was hooked this time.
No it wasn’t! This is simply because the EDR’s DLL was not loaded when in suspended process, which produced a clean copy of ntdll without any hooks.
So, theoretically, we could create a new process in a suspended state, read its memory, find the loaded ntdll, map its memory to our hooked ntdll, and get a fresh copy without any hooks.
Developing the attack:
Now that we have the theory, let’s see how it works in practice.
First, we create a new process in suspended mode:
Now we find our ntdll base address. This is necessary because the process we create will be our child process. It will have the same ntdll base address, enabling us to use the same address to read from the suspended process.
To get the ntdll address, we use a simple function that will iterate through our PEB until it finds the ntdll module.
After getting a handle to ntdll, we cast it and get its base address.
Note: Some EDRs will hook the PEB.
When this happens, the base addresses stored in this location is different. As a consequence, trying to read from the PEB would land us in a region controlled by the EDR.
To retrieve the original ntdll, we could theoretically use the EPROCESS kernel structure as it stores valuable information that could lead us to the original ntdll. According to Microsoft, “The EPROCESS structure is an opaque structure that serves as the process object for a process.”
Further research showed that when used NtQuerySystemInformation with the following classes:
SystemExtendedHandleInformation and SystemModuleInformation could leak the EPROCESS memory address. As EPROCESS is outside the scope of this post, I will not dig any deeper into that topic, though there would be a lot to say.
After getting the ntdll base address , we read the not-hooked ntdll memory of the suspended process and copy its data into a buffer.
But first, we need to declare the dos header, nt headers, and optional headers to get the size of ntdll.
After copying the content to the buffer, we terminate the suspended process as we no longer need it.
Now, the only thing left to do is to iterate through all sections to find the virtual address of the .text section, change the protection to PAGE_EXECUTE_READWRITE, and copy the .text section of the new mapped buffer (newNtdllBuffer) to the original hooked version of ntdll. This will overwrite the hooks. The last step to conclude this part of the attack is to restore the original protection.
Taking the attack one step further:
The goal here is to combine what we created above and successfully run it against an EDR.
I’m going to combine an encrypted msfvenom calc shellcode. I’ll modify the GetModuleFromPEB and GetAPIFromPEBModule functions to accept a hash instead of the DLL name. It will then iterate through all DLLs, calculate their hash, and compare it with ours. The shellcode would decrypt itself at run-time and load to the current process.
Though this technique is nothing new, researching this topic led me to discover some POCs online, and I wanted to share my take on it.
While testing the technique against some EDRs, it effectively bypassed detection, unhooked ntdll, and loaded the shellcode successfully, but it is worth mentioning that some EDRs could flag this technique.