# Endpoint Detection Response Security and Bypass

AV/EDR Security bypass written by Zero

# Table of contents

  1. Introduction
  2. Assembly code
  3. Windows architecture
  4. Syscall or system call
  5. User-mode and Kernel-mode
  6. Remapping a clean DLL
  7. Direct use of syscall

# Introduction

Anyone who follows the malware community and company has come across the terms Userland hooking, Syscalls, P/Invoke/, D-Invoke and many more. Because of the increasing number of computer attacks, more and more companies are starting to create a security team (SOC) or a CERT. Their main goal is to prevent or identify and block potential attackers. EDR systems are therefore increasingly used. As a result, EDR bypassing is becoming more and more relevant. Before we dive into the main topic, we need to take a look at some basics of the Windows operating system architecture as well as a small part on assembly code. Feel free to skip this part.
In this article I will discuss different techniques to bypass AV/EDR.

  • Mapping a clean DLL in memory
  • Direct use of syscall

# Assembly code

If you are writing a program, regardless of the language you will probably use a compiler to create the program. The code is transformed into machine language; at the end it is binary code, and can be directly executed by a CPU. Some compilers, such as gcc for example, produce assembly code before translating it into machine language. Assembly code is the closest code to machine language and looks like this:

By disassembling via IDA Pro or Ghidra you will also get the assembly code.

# Windows architecture

Programmers generally don't want to reinvent the wheel, so basic functions are imported from existing libraries. For example printf() is imported via stdio.h in C language. For example Windows developers use an API, which can also be imported into a program. The so-called Win32 API is documented and consists of a multitude of library files (DLL-files), located in the C:\windows\system32 folder, such as kernel32.dll or User32.dll. Here is a diagram of the Windows architecture. There is a multitude of them, a Google search is enough to find some.

ntdll.dll is not part of the Win32 API and is not officially documented.

# Syscall or system call

What are direct system calls? If you've worked with old OS, you might remember that a simple application crash could result in a complete system crash. This was because the OS was running in Real-mode, which means that the processor is running in a mode where no memory isolation and protection is applied. A bug could result in a complete crash. Annoying, right?
Now, there's a Protected-mode. It introduce many safeguards and can protect the system from crashes by isolating running programs from each other using virtual memory and privilege levels or ring. On a Windows system, there's two rings that are actually used. Application are running in User-mode which is equivalent to Ring 3 and critical system components like the kernel and device drivers are running in Kernel-mode, which is equivalent to Ring 0.
When an application needs to switch into the Ring 0, it gives the execution flow into Kernel-mode. This is where system calls come in.
Using a picture might be easier to understand. So here it is:

# User-mode and Kernel-mode

The Windows OS has two different privilege levels, which have been implemented to protect the OS from possible crashes caused by applications. All applications installed on a Windows system run in the so-called User-mode. The kernel and drivers run in Kernel-mode. Applications in User-mode cannot access or manipulate memory in Kernel-mode. AV/EDR systems can only monitor the behavior of applications in User-mode, because of Kernel Patch Protection. And the very last instance of User-mode is composed of the Windows API functions of ntdll.dll. If a function of ntdll.dll is called, the CPU goes into Kernel-mode, and it is not monitored by AV/EDR anymore. The functions of ntdll.dll are called syscalls.

# Remapping a clean DLL

If your program loads certain functions from kernel32.dll or ntdll.dll, a copy of the library file is loaded into memory. One of the EDR detection mechanisms is the use of "hooking userland". This method modifies ntdll.dll directly in the process memory in order to monitor its calls to the WinAPI. What they do is that they (usually) modify the in-memory copy and place a JMP assembler instruction at the beginning of the code to redirect the Windows API function to an inspection code of the AV/EDR software itself. Therefore, before calling the real function, an analysis is performed. If no suspicious behavior is detected, the original Windows API function is then called. If the opposite is true, the call is blocked or the process is killed. Here is a nice image from ired.team that helps to understand better:

There is a technique to get rid of the hooks placed in the DLL. The problem is that it is possible to have to patch differently for each AV/EDR. The idea of this technique is based on the fact that the dll as it is on the disk is healthy, so it is enough to overwrite the DLL in memory with the DLL on the disk. By applying this method we get a healthy .text section in memory, and we bypass the hooks posed by the EDR.
To use this technique you need to know the exact NTDLL.dll functions needed for your project and extract the corresponding assembler code for them via disassembling. Afterwards you need to build an ASM-file containing all different offsets for different Windows OS-Versions. Sounds complicated.
Using this technique will enable us to bypass Userland-Hooking in general, so regardless of the seller of the AV/EDR. But as soon as a new Windows version is released, it won't work anymore.
It is possible to add a function for this, or you can use two tools:

int unHookAll() { 
  HANDLE process = GetCurrentProcess();
    MODULEINFO mi = {};
    HMODULE ntdllModule = GetModuleHandleA("kernel32.dll");
    GetModuleInformation(process, ntdllModule, &mi, sizeof(mi));
    LPVOID ntdllBase = (LPVOID)mi.lpBaseOfDll;
    HANDLE ntdllFile = CreateFileA("c:\\windows\\system32\\kernel32.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
    HANDLE ntdllMapping = CreateFileMapping(ntdllFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
    LPVOID ntdllMappingAddress = MapViewOfFile(ntdllMapping, FILE_MAP_READ, 0, 0, 0);

    PIMAGE_DOS_HEADER hookedDosHeader = (PIMAGE_DOS_HEADER)ntdllBase;
    PIMAGE_NT_HEADERS hookedNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)ntdllBase + hookedDosHeader->e_lfanew);

    for (WORD i = 0; i < hookedNtHeader->FileHeader.NumberOfSections; i++) {
        PIMAGE_SECTION_HEADER hookedSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(hookedNtHeader) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));

        if (!strcmp((char*)hookedSectionHeader->Name, (char*)".text")) {
            DWORD oldProtection = 0;
            BOOL isProtected = VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize, PAGE_EXECUTE_READWRITE, &oldProtection);
            memcpy((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), (LPVOID)((DWORD_PTR)ntdllMappingAddress + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize);
            isProtected = VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize, oldProtection, &oldProtection);
        }
    }
   
    CloseHandle(process);
    CloseHandle(ntdllFile);
    CloseHandle(ntdllMapping);
    FreeLibrary(ntdllModule);

    return 0;
}

Here is what this code does:

  • we map the kernel32.dll in memory.
  • look for the address of the text section of the hooked dll
  • get the protections present on the hooked dll
  • copy the text section of the original dll into the text section of the hooked dll.
  • put back the protections previously applied.

# Direct use of syscalls

The EDR sets its hooks in "userland", so it will inspect the parameters of a WinAPI function that will link to a syscall. But what happens when we use directly the syscall?
In x64, Windows makes the transition between userland and kernelland via syscalls. If we look at the NtWriteFile function in ntdll.dll we can clearly see how Windows prepares the jump to kernel-land. It first moves the right value in eax and then jumps to kernel land and uses the SSDT "System Service Dispatch Table" to find the api corresponding to the requested syscall. What is interesting to notice is that here only the assignment of a value to eax and the syscall are important: if we prepare the stack with the desired arguments we will be able to call the syscall directly and therefore not go through the EDR hooks.
So we can simply turn our WinAPI calls into direct syscall. Thus, if an EDR uses hooks on these WinAPI functions, it would no longer be able to detect the malware (at least not in this way).
One slight problem... system call numbers change between OS version and sometimes even between built. Luckily, j00ru from Google project Zero created an online system calls table. He did an amazing job keeping up with all systems numbers, so you have a great resource there.
All we need to do is to get information about the OS version we are using and create references between the native API function definitions and OS version specific system call functions in assembly language. For this we can use the RtlGetVersion routine and save the version. Once this is done we can use the system calls in our code as if they were normal functions.

However, that is not the best way.

# Introducing Hell's Gate

Hell's Gate is the name given by @am0nsec and @RtlMateusz in this paper. The idea is to find the syscalls dynamically on the host by reading through ntdll.dll and call them from our own implementation.