In today’s post we will talk about how most AV/EDRs detect malicious behaviours and a really interesting way to bypass them in Windows. PEzor is a tool developed by @phra which I have analyzed to understand how Direct System Calls can be used for this purpose.
References
- @phra’s blog
- PEzor GitHub Repo
- Red Team Tactics: Combining Direct System Calls and sRDI to bypass AV/EDR
About AVs and EDRs
Modern AVs and EDRs perform both static and dynamic analysis using multiple techniques. They can detect whether or not a file on disk is malicious by checking several signatures, like known strings, hashes, keys… However, attackers have developed a huge amount of obfuscation techniques, which have made static analysis almost useless.
Modern EDRs are focused on dynamic/heuristic analysis, which means they can monitor the behaviour of every process on the system, looking for suspicious activities. Therefore, we can download malicious files and, if they have been obfuscated, our EDR will not be able to detect them. However, once the malware is launched, the EDR will detect it and block it.
How can the EDR monitor the activity from every process?
API Hooks
Hooking is a technique used by AV/EDRs to intercept a function call and redirect the code flow to a controlled environment where they will be able to inspect the call and determine whether it is malicious or not.
Taking a look at the Windows Architecture, we can see that the interaction between the surface and the depths of the OS is controlled by a library called NTDLL.DLL.
The Native API (NTDLL.DLL) is the real interface between user-mode applications and the OS. Therefore, every application will use it to interact with the OS. For example, commonly used Native APIs like ZwWriteFile are contained into NTDLL.DLL.
When a process is started, it will load several DLLs into its memory address space. AV/EDRs can modify the assembly instructions of a function inside a loaded DLL and insert an inconditional jump at the beginning, which points to the EDR’s code.
This way, AV/EDRs can see every API function call, analyze it and block it if necessary.
User/Kernel Modes
Modern Operating Systems isolate running programs from each other using virtual memory and privilege levels. Windows OS has two different privilege levels for running processes: kernel-mode and user-mode.
Using this approach, Windows makes sure that applications are isolated and cannot directly access critical memory sections or system resources, which is really insecure and could lead to system crashes. When the application needs to execute a privileged operation, the processor will switch into kernel-mode.
Here we can see that the Win32 API function WriteFile calls Native API’s ZwWriteFile, after which the processor will jump into kernel-mode.
Syscalls
Syscalls allows any program to jump into kernel-mode in order to perform privileged operations, like writing a file. As an example, we will use the previously mentioned Win32 API function WriteFile.
When a process, like NOTEPAD.EXE, tries to write a file, it will call WriteFile, which is a user-land API function. As we can see in the image, the WriteFile function will call ZwWriteFile, which is included into NTDLL.DLL.
If we use IDA to take a look into the assembly code of the ZwWriteFile:
We will see that the function is loading the EAX register with the system call number and executing the syscall instruction. After that, the processor will jump into kernel-mode and will use the dispatch table (SSDT) to find the right API call belonging to the system call number. The arguments from the user-mode stack will be copied into the kernel-mode stack and the kernel version of NtWriteFile will be executed.
Avoiding the Hooks
Most AVs, EDRs and sandboxes use user-landed hooks, which means they can monitor and intercept any user-land API call. However, if we execute a system call and jump into kernel-mode, they won’t be able to track us!
The problem comes when we realize that system call numbers change between OS versions and even service built numbers… However, there is a library called inline_syscall which can be used to scrape the in-memory NTDLL module in order to extract the syscall numbers.
The problem here is that this library will use Windows API calls to scrape the syscall number. If an AV/EDR hooks this functions, we will not be able to retrieve the correct number.
Another solution could be looking for the right syscall number using an online system call table.
PEzor
PEzor is an incredible tool, defined as an Open-Source PE Packer. It will take an executable file, get its shellcode and create an obfuscated executable capable of injecting it using direct syscalls.
The steps PEzor will follow are:
- Getting the shellcode from an executable file.
- Shellcode injection using direct syscalls and in-memory NTDLL scraping.
- User-Mode Hooks Removal.
- Generate polymorphic executable to avoid signatures.
- Binary obfuscation.
- Implementing anti-debugging techniques.
For this purpose, PEzor has a GNU Bash orchestrator, which will coordinate every phase. This file is called PEzor.sh.
From now on , we will analyze PEzor’s source code, which you can find in this GitHub repo.
Getting the shellcode
Once we have our malicious executable file (for example, a MSFVenom’s meterpreter), PEzor will use Donut in order to obtain the shellcode. We can find this into PEzor.sh:
Donut is a really powerfull tool which allows us to create shellcode loaders. As an example, if we want to get the shellcode from CALC.EXE, we can run:
root@PwnedC0ffee:~# donut calc.exe -f 4
[ Donut shellcode generator v0.9.3
[ Copyright (c) 2019 TheWover, Odzhan
[ Instance type : Embedded
[ Module file : "calc.exe"
[ Entropy : Random names + Encryption
[ File type : EXE
[ Target CPU : x86+amd64
[ AMSI/WDLP : continue
[ Shellcode : "loader.c"
The -f 4 is telling Donut to create a C output file which looks like:
unsigned char buf[] =
"\xe8\x4b\xf2\x28\x00\x4b\xf2\x28\x00\x69\x43\x1d\x0d\x83\x8e\x81"
"\x34\x4b\x9e\x24\x3c\x0c\x57\x24\x08\x93\xed\x08\x75\xa8\x71\x23"
"\xcb\x90\x1e\x09\xc5\xcf\x28\x42\xfb\x00\x00\x00\x00\x5e\xd5\xa2"
...
Shellcode Injection
We have already talked about direct syscalls and why we want to use them to bypass AV/EDR hooks. However, we have not talked about the how.
In order to inject shellcode into a process, a common way to do it is invoking the following Win32 API calls: VirtualAlloc, WriteMemoryProcess, CreateRemoteThread and WaitForSingleObject.
This will allocate a memory space in which we will write our shellcode. After that, we will create a remote thread and wait for it to finish its execution.
But we want to avoid API calls, right?
Each of the mentioned Win32 API calls has an equivalent syscall:
- VirtualAlloc -> NtAllocateVirtualMemory
- WriteMemoryProcess -> NtWriteVirtualMemory
- CreateRemoteThread -> NtCreateThreadEx
- WaitForSingleObject -> NtWaitForSingleObject
So now we can create a shellcode injector using only direct syscalls. Although the following code snippet will not compile, we can use it as an example:
int injector() {
jm::init_syscalls_list();
void *allocation = nullptr;
NTSTATUS status = STATUS_PENDING;
HANDLE hThread = -1;
//Allocate memory
status = INLINE_SYSCALL(NtAllocateVirtualMemory)(
(HANDLE)-1,
&allocation,
0,
&size,
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE
);
if (status || !allocation) {
return -1;
}
// Inject shellcode
status = INLINE_SYSCALL(NtWriteVirtualMemory)(
(HANDLE)-1,
allocation,
shellcode,
size,
&bytesWritten);
if (status) {
return -1;
}
// Create thread for shellcode execution
status = INLINE_SYSCALL(NtCreateThreadEx)(
&hThread,
THREAD_ALL_ACCESS,
nullptr,
(HANDLE)-1,
allocation,
allocation,
0,
0,
0,
0,
nullptr);
if (status) {
return -1;
}
// Wait for execution
status = INLINE_SYSCALL(NtWaitForSingleObject)(hThread, TRUE, NULL);
if (status) {
return -1;
}
return 0;
}
User-Mode Hook Removal
Since we don’t want to check for every syscall number and hardcode it, we have no choice but to remove the hooks. This can be done restoring the in-memory NTDLL.DLL copy, removing the mentioned inconditional jumps. PEzor uses DLLRefresher for this purpose.
Polymorphic Executable
As we already know, most AV/EDRs can detect malware comparing file signatures. This means that, if a malware has been already analyzed, it will be really easy to detect unless it changes. As an example, we could get the hash of a suspicious file and compare it with a list of known malware hashes.
But what if we could modify our malware and make it undetectable every time we compile it?
Such technique is known as polymorphic binary encoding. Shikata ga nai is one of the most known polymorphic binary encoders, and it is included in the Metasploit Framework.
However, the classic implementation of Shikata ga nai has been proven detectable and nowadays almost every AV/EDR will detect it.
@phra decided to use a reimplementation of this algorithm to obfuscate the shellcode, called sgn, which has improved the classical algorithm patching known detectable features.
At this moment, we will generate a different output every time we run PEzor.
And the loader?
Although sgn is being used to obfuscate the shellcode, we will need to modify the Donut loader too.
For this purpose, the author decided to use LLVM bytecode obfuscation. LLVM (Low Level Virtual Machine) is a set of compiler technologies which can be used to develop a front end for any programming language, and can be used to modify and obfuscate an executable file. Since LLVM obfuscation modify the bytecode, it will not be language-dependant.
I haven’t researched a lot about this topic, but hopefully I will come with an interesting post about binary obfuscation in the future.
For the moment, let’s just say that LLVM obfuscation will be able to modify our Donut loader avoiding signature detection.
Proof of Concept
I’m an “I’ll believe it when I see it” person. So after playing around with PEzor for some time, I decided to record a video where you can see how it works.
It starts showing how Windows Defender will detect and block a polymorphic Meterpreter. After that, I use PEzor to create an obfuscated shellcode injector.
To run PEzor, we just need to execute:
root@PwnedC0ffee:~/Desktop/Demo# PEzor -sgn -unhook -syscalls rev.exe
And it will generate a file called rev.packed.exe, which can be run on Windows withouth being detected.
Conclusion
At this point we will be able to execute Meterpreter without being blocked by the AV/EDR. However, if we use noisy functionalities, like getsytem, the EDR will kill our connection and remove the executable.
This is because Meterpreter’s getsystem will use user-landed API calls and will try to access sensible registers, so the AV/EDR can easily detect it.
As always, this technique will probably be useless in the future, since AV/EDR’s detection tecniques are under continuous improvement. However, this post have tought me some interesting things about how Windows APIs and modern AV/EDRs work.