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:

circle-info

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.teamarrow-up-right 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-openprocessarrow-up-right) 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.teamarrow-up-right 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-virtualprotectarrow-up-right) 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:

  1. The executable is run

  2. The EDR hooked version of the DLL is loaded into memory

  3. Read the unmodified DLL on the system

  4. Within that unmodified DLL, take the .text section and store it

  5. Use the VirtualProtect Windows API function to change the memory protection of the .text section of the EDR-modified DLL that was loaded into memory to allow writing

  6. Overwrite the contents of the .text section in memory (the EDR-modified DLL that’s in memory) so that it’s copies the unmodified DLL’s .text section

  7. A 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/x4wauJZPnKgarrow-up-right):

  • Execute an application which utilizes an API (example: VirutalAlloc)

  • The actual API function/code is stored in kernelbase.dll

  • The 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 ntdll library which is where the actual system call version of the function is stored (in this example, the system call version of VirtualAlloc is NtAllocateVirtualMemory)

    • 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 ntdll

  • Response 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-edrsarrow-up-right.

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 paperarrow-up-right 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 postarrow-up-right 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.

source: https://winternl.com/detecting-manual-syscalls-from-user-mode/#:~:text=Nirvana%20is%20a%20lightweight%2C%20dynamic,sandboxing%2C%20emulation%2C%20or%20virtualization.arrow-up-right

Vectored Exception Handling - Back to Main Article

To recap the background section:

  • EDR hooking is done by inserting a jmp command 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 executed

  • One method of unhooking is to read the .text section of the unmodified DLL, and overwrite the DLL running in memory (the version that is modified by the EDR) with the original .text using the API VirtualProtect()

  • 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:

  1. Declare global variables, e.g. declaring global variable to store the syscall address

  2. Declare a VEH function to modify the registers (RAX/RIP) when an exception occurs

  3. Calculate/find the syscall addresses

  4. Declare variables for syscall numbers of the NT APIs (the kernel-level APIs) that will be used

  5. 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:

  1. Initialize a function pointer with a syscall number

  2. Call the function pointer to trigger an exception (It’s expecting a syscall address, not a syscall number)

  3. Exception is handled via the VEH

  4. VEH function modifies the registers to specify the syscall number and the memory address of the syscall instruction

  5. VEH function says to continue execution by returning the value EXCEPTION_CONTINUE_EXECUTION

  6. Kernel continues executing at the RIP address - which is the syscall address. The RIP register will now contain the memory address of the syscall instruction

  7. Syscall is executed

Resources

Last updated