Bypassing AV/EDR Hooks via Vectored Syscall
Dissecting the following article to do better threat research, learn about EDR hooking, bypassing EDRs, Windows APIs, and more:
This page contains my notes about the cyberwarfare article and all the outlying relevant information, resources I've read/researched from are linked on the bottom of this page
High Level Summary
There has been research done on how data was stored during exception handling, specifically when purposely triggering a crash. The data is passed into the exception portion of assembly, and when jumping to that address you can bypass AV/EDR.
Background Information
This section will go over some background information to understand Vectored Syscall better
Hooking APIs
Read the first few sections of this ired.team in which it outlines the concept of hooking. According to the article, when an application runs, all system API calls are first intercepted (”hooked”) by the AV/EDR who decides if that action of the application is malicious.
An example would be if an application used the common Windows API OpenProcess (https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess) like below:
During execution, once the kill_process function is reached and the OpenProcess API is called, that action is going to be first intercepted by that AV/EDR.
EDR products hook these API calls by hijacking/modifying the API function definitions in Windows DLLs (e.g. kernel32 and ntdll). These products will insert a jmp instruction at the beginning, which will point to the EDR’s inspection module where it will then evaluate if the action is malicious (by analyzing arguments that were passed to the function that the EDR is hooking/monitoring). Afterwards, it will point back to the normal execution flow.
Below is a diagram from that same ired.team article metnioned above that visualizes how hooking is done. Note the jmp EDR.DLL!Inspect instruction in the kernel32.dll:
Note: Not all APIs are hooked, usually only ones that are known to be used maliciously will get hooked, the article mentions these two as an example: CreateRemoteThread and NtQueueApcThread.
EDR Unhooking
We can unhook an EDR by utilizing one of the Windows API functions, VirtualProtect which according to the Microsoft documentation (https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect) will allow the caller to change the permissions on a region of committed memory of a process. This means we can changes certain areas of memory to be writeable. Note that when a program runs, the EDR-modified DLL is loaded into memory.
In addition, there are “clean” versions of DLLs that exist (in C:\Windows\System32 that are not modified/hooked by an EDR, this is so that the main OS remains stable. We can read the “clean” version of the DLL. Within a DLL, the .text section is where it stores all the executable code, such as functions and imports (where the Windows API functions are).
So knowing the above, the following process is how unhooking is done:
The executable is run
The EDR hooked version of the DLL is loaded into memory
Read the unmodified DLL on the system
Within that unmodified DLL, take the
.textsection and store itUse the
VirtualProtectWindows API function to change the memory protection of the.textsection of the EDR-modified DLL that was loaded into memory to allow writingOverwrite the contents of the
.textsection in memory (the EDR-modified DLL that’s in memory) so that it’s copies the unmodified DLL’s.textsectionA clean, unhooked from EDR, should now be running in memory
Direct Syscall and Hooking Nirvana
“Direct Syscall” is a technique in which you circumvent the use of ntdll.dll by doing system calls directly to the operating system. This allows system calls (that would originally first be used by ntdll.dll which would then interact with the OS) to be made without being passed into an EDR that has ntdll.dll hooked.
Normal execution flow using a Windows API looks like the following (taken from this talk https://youtu.be/x4wauJZPnKg):
Execute an application which utilizes an API (example:
VirutalAlloc)The actual API function/code is stored in
kernelbase.dllThe API function will then set up the registers/resources necessary to execute the system call
Once set up, the function passes the data to the
ntdlllibrary which is where the actual system call version of the function is stored (in this example, the system call version ofVirtualAllocisNtAllocateVirtualMemory)Note: system call instructions are low-level assembly code to interact with the OS
The system call version of the function is executed in kernel mode
Response is sent back (callback to user mode) to
ntdllResponse is sent back to
kernelbase
The main article (the cyberware live one mentioned at the top) mentions the Direct Syscall technique as a possibly in the case that VirtualProtect or NtVirtualProtectMemory (this is called inside of VirtualProtect) is hooked. This is because Direct Syscalls completely circumvent the use of ntdll.dll .
An example is made in this article: https://www.ired.team/offensive-security/defense-evasion/using-syscalls-directly-from-visual-studio-to-bypass-avs-edrs.
In summary, to do a direct system call you would write your own assembly and function. You would also have to do some research on what Windows version you'll be running it on since some of the syscall values are dependent upon the Windows version. An example would be in the ired team article right above, it mentions using 55 as the syscall number since that is what's reserved for NtCreateFile in Windows 10. In addition, the assembly code would have to setup the variables/registers with the correct values to execute the syscall correctly.
The article then mentions that system calls that do not come from ntdll or other known dll's are considered suspicious and that there's a technique called hooking nirvana that can be used to detect syscalls. "Nirvana" is an internal instrumentation engine that's outlined in this research paper by Microsoft. Essentially, Nirvana is a dynamic translation framework that can be leverage to monitor and control (user mode) execution of a running process without having to change any code or recompile anything.
In addition, this blog post by Winternl outlines a defensive plan of utilizing Nirvana: use it to hook kernel to user mode callbacks (recall at the top of this sub-section the normal execution flow of a Windows API) as "All syscalls which do not transition from the kernel back to usermode at a known valid location [such as an exported function in ntdll.dll or win32u.dll], are in fact crafted for evasive purposes."
The main vectored syscall article (the one outlined at the top, which is being dissected in this note page) summarizes it by saying Nirvana can be leveraged by checking the RIP register when going from kernel to user mode. If the RIP register is NOT pointing to a known memory location (e.g. ntdll.dll or win32u.dll), then that is indicative of potential manual system calls. The blog post by Winternl also summarizes it pretty well:
The second, and main part of the instrumentation routine is responsible for analyzing the execution context at the point of kernel to usermode return...... check to determine whether RIP is pointing to a memory location within ntdll.dll or win32u.dll. If not, the program will warn of a potential manual syscall and break execution.
Vectored Exception Handling - Back to Main Article
To recap the background section:
EDR hooking is done by inserting a
jmpcommand in specific API function definitions within DLLS. Because of this, once the API is called, the arguments will be passed to the EDR (where it’ll determine if malicious or not) before the API function is even executedOne method of unhooking is to read the
.textsection of the unmodified DLL, and overwrite the DLL running in memory (the version that is modified by the EDR) with the original.textusing the APIVirtualProtect()Another method of unhooking is to do direct system calls (eliminate the use of having to call a DLL), and writing your own assembly/code
This is sometimes referred to as manual system calls
Manual syscalls can be detected by checking the RIP register: “check to determine whether RIP is pointing to a memory location within ntdll.dll or win32u.dll. If not, the program will warn of a potential manual syscall and break execution.”
The main article (the cyberwarefare live article linked above) says:
… we’ll leverage the VEH (Vectored Exception Handler) to modify the context, especially RIP register to take us to the syscall address.
A vectored exception handler (VEH) is a function that you can register (create) to handle in the case a specific error occurs. It has the capability to modify the RAX and RIP registers so that it can handle/pass program execution flow to appropriate resources if an exception is triggered. In this situation, we will be using a VEH function to modify the RIP register so that it will execute a system call.
The article then provides two images/code blocks with the following code:
The first one liner code block initializes a VEH so that it can handle exceptions, should they occur. They will be passed into the HandleException function which is the next thing in the second code block.
HandleException is the function that will be called once an exception is triggered. If the exception is EXCEPTION_ACCESS_VIOLATION then it modifies the registers R10, RAX, and RIP to a specified system call number and memory address . It then tells the OS to continue executing the program at the new RIP address (which is to the syscall).
💡 *Note: The error `EXCEPTION_ACCESS_VIOLATION` will be used since we can force an exception to it by using a syscall number instead of address*
Next, the article describes defining the NT APIs (the ones that are used by the kernel) that will be used in the proof of concept, specifically they’ll define them in the code by listing the syscall numbers:
The article then describes the process of finding the syscall address within ntdll so that the RIP register within the VEH can be set to it. The method used is retrieving the address of a random NT API call and calculating the syscall; they gave the following code:
The above code will retrieves the memory address of the ZwDrawText function within ntdll and pass it into the FindSyscallAddr function which will scan the memory address range to find the memory address syscall instruction within the ntdll module. The address of the syscall is then stored into the syscall_addr variable.
The following is the code for the FindSyscallAddr function:
Last, the code will call the NT functions:
The TLDR list of steps that the program does:
Declare global variables, e.g. declaring global variable to store the syscall address
Declare a VEH function to modify the registers (RAX/RIP) when an exception occurs
Calculate/find the syscall addresses
Declare variables for syscall numbers of the NT APIs (the kernel-level APIs) that will be used
Call the NT APIs which triggers an exception, which will be handled by the VEH
Here’s a high-level summary of the program execution flow:
Initialize a function pointer with a syscall number
Call the function pointer to trigger an exception (It’s expecting a syscall address, not a syscall number)
Exception is handled via the VEH
VEH function modifies the registers to specify the syscall number and the memory address of the syscall instruction
VEH function says to continue execution by returning the value
EXCEPTION_CONTINUE_EXECUTIONKernel continues executing at the
RIPaddress - which is the syscall address. TheRIPregister will now contain the memory address of thesyscallinstructionSyscall is executed
Resources
Last updated
