EDR Bypass : Retrieving Syscall ID with Hell's Gate, Halo's Gate, FreshyCalls and Syswhispers2

This post is not an extensive presentation of Hell’s Gate1, Halo’s Gate, FreshyCalls2 or Syswhispers23.

You can find detailed explaination on these techniques on their Github repo, various articles and the amazing Sektor7 Windows Evasion Course4.

So whats the point of this article then ? Well, I find the various techniques used to dynamically retrieve syscall identifiers very interesting and I wanted to present the difference between them.

Soooo let’s begin shall we ?

User Mode vs Kernel Mode

Nowadays, to protect servers and workstations, company uses EDR (Endpoint Detection and Response). To detect malicious activities, EDR works in the User Mode and the Kernel Mode.

To make it simple, the User Mode is where you and your process can interact directly with. To monitor this part, EDR uses their own DLL injected in processes and places hooks on key API Windows functions. In User Mode, each processes has is own private Virtual Address Space.

Illustration of User Mode/Kernel Mode in the Windows OS (source)

Illustration of User Mode/Kernel Mode in the Windows OS (source)

The Kernel Mode is the part that you cannot (theorically) interact directly with. It contains drivers, operating system code, system services, etc. In Kernel Mode, there is only one virtual address space. It means no segmentation, which basically means God mode.

Another illustration of User Mode/Kernel Mode in the Windows OS (source)

Another illustration of User Mode/Kernel Mode in the Windows OS (source)

In this post, we will stay in User Mode.

What is a Hook ?

A hook is a technique used to redirect the execution flow of a legitimate API call to a portion of code controlled by an attacker/AV/EDR. Usually done by writing an inconditional jump in the legitimate API code or by replacing API function addresses in the Import Address Table of the process. I wrote a little post with some code about hooks in Import Address Table here.

EDRs often place their hooks when the following Dll are loaded in the process memory:

  • kernel32.dll
  • kernelbase.dll
  • ntdll.dll

In an EDR context, if there is a hook, there is an EDR Dll where the execution flow is redirected to. Let’s see how it works.

I’m calling CreateProcess() which is located in Kernel32. However, Since Windows 7, CreateProcess() in Kernel32 perform a redirection to Kernelbase.

OpenProcess in Kernel32 redirected in KernelBase

OpenProcess in Kernel32 redirected in KernelBase

Once in Kernelbase, we can see something strange… We hit a inconditional jump (jmp) not related to a Windows Dll.

Inconditional jump to an unknown location

Inconditional jump to an unknown location

If we follow the execution, we hit another redirection. This one is not as obvious as the previous one.

The redirection address is moved in RAX and RAX is then pushed in the stack. One instruction later, a ret is executed.

ret basically perform a jump in the top value of the stack. Since an address was pushed via RAX, and no other value was pushed since. The pushed address is the last value on the stack, and the execution flow will be redirected to this address.

Redirection via ret

Redirection via ret

In the code were we are redirected to, we can see an address value belonging to atcuf64 moved into RAX and immediately after, a call to RAX.

Call to an address in atcuf64

Call to an address in atcuf64

If we check what is atcuf64 in the loaded modules, we can see that it’s a DLL belonging to BitDefender.

BitDefender atcuf64

BitDefender atcuf64

In an nutshell, BitDefender placed a hook to the OpenProcess() code in Kernelbase in order to redirect the execution to his DLL.

One way to bypass these types of hooks is to perform direct syscalls.

Direct Syscall you say ?

When a program need to interact with other processes, memory, drives or the file system it uses the Windows API functions in Kernel32.dll. However, to interact with the Windows GUI, a program will use functions located in user32.dll and gdi32.dll.

Some of these functions can be seen as wrappers for direct syscalls to the Kernel. For instance, if you perform an OpenProcess() the execution will be:

OpenProcess direct syscall

OpenProcess direct syscall

If you perform a SetMenu() the execution will be:

OpenProcess direct syscall

OpenProcess direct syscall

Finally, if you perform a GetRandomRgn() the execution will be:

OpenProcess direct syscall

OpenProcess direct syscall

In the native windows execution flow, direct syscalls are performed via Nt* and Zw* functions that can be found in ntdll.dll or by NtUser* and NtGdi* functions that can be found in win32u.dll.

Small warning here, not all the Kernel32, user32 and gdi32 functions ends up in direct syscall.

The “real” code of Nt*, Zw*, NtUser* and NtGdi* function runs in Kernel Mode.

A direct syscall look like this:

OpenProcess direct syscall

OpenProcess direct syscall

The code of a direct syscall is also call syscall stub.

The purpose of the syscall stub is to forward the execution flow of the function to the related code in the Kernel. It’s the last step in User Mode. This transfer of execution is done by the assembly instruction syscall.

The only difference between syscall stub are the number moved in EAX. This number is called syscall ID or syscall number or system service number (SSN).

It allows the Kernel to retrieve the function code related to this identifier. Syscall identifier are unique on a system and linked to a single function. They can change between different OS version or service pack.

The hexadecimal sequence of opcode for a syscall stub look like this (here ZZ is the unknown syscall ID):

0x4c 0x8b 0xd1 0xb8 0xZZ 0xZZ 0x00 0x00 0x0f 0x05 0xc3

mov r10,rcx 			// 0x4c 0x8b 0xd1
mov eax, SyscallNumber	// 0xb8 0xZZ 0xZZ 0x00 0x00
syscall                 // 0x0f 0x05   
ret                     // 0xc3

Remember it, it will be important.

The 4 techniques discussed in this post uses direct syscalls to bypass user-mode hook in DLLs.

A direct syscall for NtOpenProcess() using one of these techniques will look like this

OpenProcess direct syscall

OpenProcess direct syscall

The common ground

To avoid User Mode hooks, attackers can use direct syscalls. But to do that they need to:

  • Find the base address of ntdll without using GetModuleHandle (it can be hooked);
  • Parse the Export Address Table of the DLL to retrieve the functions addresses;
  • Retrieve the syscall ID;
  • Perform the syscall;

Retrieving Windows DLL addresses: The Process Environment Block (PEB)

This part is not mandatory to understand the next parts of the post. So if you find this boring you can jump to the next chapter.

To retrieve Windows DLL addresses without using GetModuleHandle the common technique is via the Process Environment Block(PEB) of the current process.

The Process Environment Block is a data structure initialized at the process creation. For each thread, there is an equivalent data structure dedicated to the threads, it’s called a Thread Environment Block (TEB).

To retrieve the DLL base addresses, we need to go through different structures. The First one is the TEB.

Within the process, you can retrieve the PEB address via the TEB.

typedef struct _TEB {
  PVOID Reserved1[12];
  PPEB  ProcessEnvironmentBlock; // The address of the PEB
  PVOID Reserved2[399];
  BYTE  Reserved3[1952];
  PVOID TlsSlots[64];
  BYTE  Reserved4[8];
  PVOID Reserved5[26];
  PVOID ReservedForOle;
  PVOID Reserved6[4];
  PVOID TlsExpansionSlots;

Then you have to retrieve the address of the PEB_LDR_DATA structure from the PEB.

typedef struct _PEB {
  BYTE                          Reserved1[2];
  BYTE                          BeingDebugged;
  BYTE                          Reserved2[1];
  PVOID                         Reserved3[2];
  PPEB_LDR_DATA                 Ldr; // The Address of PEB_LDR_DATA
  PVOID                         Reserved4[3];
  PVOID                         AtlThunkSListPtr;
  PVOID                         Reserved5;
  ULONG                         Reserved6;
  PVOID                         Reserved7;
  ULONG                         Reserved8;
  ULONG                         AtlThunkSListPtr32;
  PVOID                         Reserved9[45];
  BYTE                          Reserved10[96];
  BYTE                          Reserved11[128];
  PVOID                         Reserved12[1];
  ULONG                         SessionId;

The PEB_LDR_DATA structure contains very few information. However, one very useful is InMemoryOrderModuleList.

typedef struct _PEB_LDR_DATA {
  BYTE       Reserved1[8];
  PVOID      Reserved2[3];
  LIST_ENTRY InMemoryOrderModuleList; // Structure containing links to modules

I will let the Microsoft documentation explain what is InMemoryOrderModuleList.

InMemoryOrderModuleList: The head of a doubly-linked list that contains the loaded modules for the process. Each item in the list is a pointer to an LDR_DATA_TABLE_ENTRY structure. For more information.

InMemoryOrderModuleList look like this:

typedef struct _LIST_ENTRY {
   struct _LIST_ENTRY *Flink;
   struct _LIST_ENTRY *Blink;

So InMemoryOrderModuleList.Flink of the _PEB_LDR_DATA points to the InMemoryOrderLinks of the first loaded module. This InMemoryOrderLinks is in a _LDR_DATA_TABLE_ENTRY structure. The information of loaded module will be in _LDR_DATA_TABLE_ENTRY structure.

typedef struct _LDR_DATA_TABLE_ENTRY {
    PVOID Reserved1[2];
    LIST_ENTRY InMemoryOrderLinks;
    PVOID Reserved2[2];
    PVOID DllBase; // Base address of the module in memory 
    PVOID EntryPoint;
    PVOID Reserved3;
    UNICODE_STRING FullDllName; // Full path + name of the dll
    BYTE Reserved4[8];
    PVOID Reserved5[3];
    union {
        ULONG CheckSum;
        PVOID Reserved6;
    ULONG TimeDateStamp;

If we want to go through each module, we just need to jump to each InMemoryOrderLinks.Flink. In _LDR_DATA_TABLE_ENTRY structure, we can retrieve the base address of the module, its name etc.

A little note, Flink goes Forward on the modules and Blink goes backward. The first module is always (I think), the _LDR_DATA_TABLE_ENTRY of the process itself.

Here a illustration of the relation between the structure.

Get base address DLL via PEB illustration

Get base address DLL via PEB illustration

In the illustration above the first module is ntdll but don’t pay attention to this. Just imagine that there is the module of the process before ntdll.

Here you can find a piece of code I wrote allowing you to retrieve the base address for a given dll5.

Retrieving Windows API functions addresses: Parsing the Export Address Table (EAT)

Once we retrieved the Dll base address, we need to find the address of the target function. To do that, we have to parse The Export section of the DLL to find the Export Address Table (EAT). The EAT contains all functions addresses of the DLL.

In this chapter, I’m not going to focus on the different structures we go through to retrieve the data.

However, I’ll focus on the _IMAGE_EXPORT_DIRECTORY structure and some important value of it. The _IMAGE_EXPORT_DIRECTORY contains all the crucial information of the DLL and look like this.

typedef struct _IMAGE_EXPORT_DIRECTORY {              
    DWORD   Characteristics;            
    DWORD   TimeDateStamp;                
    WORD    MajorVersion;             
    WORD    MinorVersion;             
    DWORD   Name;                   // The name of the Dll
    DWORD   Base;                   // Number to add to the values found in AddressOfNameOrdinals to retrieve the "real" Ordinal number of the function (by real I mean used to call it by ordinals).
    DWORD   NumberOfFunctions;      // Number of all exported functions      
    DWORD   NumberOfNames;          // Number of functions exported by name      
    DWORD   AddressOfFunctions;     // Export Address Table. Address of the functions addresses array.   
    DWORD   AddressOfNames;         // Export Name table. Address of the functions names array.        
    DWORD   AddressOfNameOrdinals;  // Export sequence number table.  Address of the Ordinals (minus the value of Base) array.             

3 fields are super important for us: AddressOfFunctions, AddressOfNames and AddressOfNameOrdinals.

Why ? Because when you call GetProcAddress on a function (let’s say A_SHAFinal) it goes like this:

  • The string “A_SHAFinal” will be searched in the array AddressOfNames (Export Name table);
  • When found, the position in the array is used to find the corresponding Ordinal number in the AddressOfNameOrdinals array (Export sequence number table).
  • Finally, the value of the Ordinals retrieved in the AddressOfNameOrdinals array is the index of the address function for A_SHAFinal in AddressOfFunctions (Export Address Table).

The string A_SHAFinal is at position 0 in AddressOfNames.
Then to retrieve the ordinal: AddressOfNameOrdinals[0]. 
The value of AddressOfNameOrdinals[0] is 1.
To retrieve A_SHAFinal function address we do AddressOfFunctions[1].

Maybe a little illustration will be more clear:

Going through the EAT

Going through the EAT

However, the retrieved address is a Relative Virtual Address (RVA). RVA must be added to a base address to retrieve the real address on the process.

Lucky for us ! We retrieved our Dll base address via the PEB ! So we just have to do RVA+DLLBaseAddress and it’s all good !

But why did we have to go through these painful techniques like parsing the PEB and the EAT? Well, because all of the following techniques use this to access the Ntdll functions code and retrieve the syscall identifier.

You can find my code to retrieve function addresses via PEB and EAT here

Retrieve syscall ID dynamically

With Hell’s Gate

Hell’s gate was developped by am0nsec and RtlMateusz. You can find the code and a detailed explanation on how it works here.

To retrieve dynamically the Syscall ID, Hell’s Gate use this piece of code:

if (*((PBYTE)pFunctionAddress + cw) == 0x4c
					&& *((PBYTE)pFunctionAddress + 1 + cw) == 0x8b
					&& *((PBYTE)pFunctionAddress + 2 + cw) == 0xd1
					&& *((PBYTE)pFunctionAddress + 3 + cw) == 0xb8
					&& *((PBYTE)pFunctionAddress + 6 + cw) == 0x00
					&& *((PBYTE)pFunctionAddress + 7 + cw) == 0x00) {
					BYTE high = *((PBYTE)pFunctionAddress + 5 + cw); 
					BYTE low = *((PBYTE)pFunctionAddress + 4 + cw); 
					pVxTableEntry->wSystemCall = (high << 8) | low;

Remember the following suite: 0x4c 0x8b 0xd1 0xb8 0xZZ 0xZZ 0x00 0x00 ? Its the beginning of a syscall stub introduce earlier.

mov r10,rcx 			// 0x4c 0x8b 0xd1
mov eax, SyscallNumber	// 0xb8 0xZZ 0xZZ 0x00 0x00

Well, it’s exactly what Hell’s gate is searching in Ntdll. To retrieve the Syscall ID, it gets the function address with the technique explained previously (EAT parsing), and then it goes to this address and searches for the pattern of a syscall stub begining.

// Search the pattern mov r10,rcx mov eax
if (*((PBYTE)pFunctionAddress + cw) == 0x4c
	&& *((PBYTE)pFunctionAddress + 1 + cw) == 0x8b
	&& *((PBYTE)pFunctionAddress + 2 + cw) == 0xd1
	&& *((PBYTE)pFunctionAddress + 3 + cw) == 0xb8
	&& *((PBYTE)pFunctionAddress + 6 + cw) == 0x00
	&& *((PBYTE)pFunctionAddress + 7 + cw) == 0x00) 

If the pattern is found, the syscall ID is retrieved:

BYTE high = *((PBYTE)pFunctionAddress + 5 + cw); 
BYTE low = *((PBYTE)pFunctionAddress + 4 + cw); 
pVxTableEntry->wSystemCall = (high << 8) | low;

Let’s stop one second on this piece of code. If the function syscall is 1D6 the mov instruction of the syscall will be like this:

mov eax, 1D6 // 0xb8 0xD6 0x01 0x00 0x00

The syscall is written 0xD6 0x01 and not 0x01 0xD6 (Little-endian shenanigans). So if we want to store the proper value we need to do some shifty shift magic.

In this example “high” value is 0x01 and the “low” value 0xD6.

In binary 0x01 is 0000 0001 and 0xD6 is 1101 0110. The variable wSystemCall is a DWORD (32bits).

If we shift “high” 8 bits to the left (the instruction (high << 8)), we obtain the value: 1 0000 0000. Now, if we perform a bitwise or between the (high << 8) value and “low” we obtain 1 1101 0110 which is 1D6

	00000000 00000000 00000001 00000000 // (high << 8) = 256
or	00000000 00000000 00000000 11010110 // D6 = 214
=	00000000 00000000 00000001 11010110 // 1D6 = 470

And there, we have our syscall ID. Beautiful isn’t it ?

However, Hells’s Gate technique has limitation. If you never find 0x4c 0x8b 0xd1 0xb8 0xZZ 0xZZ 0x00 0x00 because mov r10,rcx is replaced by a hook, you will not be able to retrieve the syscall ID.

It’s why Halo’s Gate as been created !

With Halo’s Gate

Halo’s Gate tries to resolve the Hell’s Gate issue in case of hooks on the beginning of the syscall stub. If the syscall stub is hooked, it searches the syscall ID of the neighboor function.

Why ? Because syscall ID in the syscall stub follows each other incrementally ! This means that, for instance, if you are hooked but the next function below isn’t, you just have to retrieve its syscall and substract 1 to obtain the syscall of your current function.

Syscall following incrementally

Syscall following incrementally

Easy peasy !

The gap between the begining of two syscall stub is 32bits. If we go 32 bits up from the start of our current syscall start, we reach the begining of the previous syscall function. If we go 32 bits down, we reach the beginning of the next syscall function.

In a nutshell, to retrieve syscall ID when a function is hooked Halo’s Gate do this:

  • Check if the begining of the function starts with an inconditional jump instruction. If yes;
  • Go up and down until it finds the beginning of a function that isn’t hooked. (max 500 functions up and 500 functions down). If found,
  • Substract the index (number of function between our hooked target function and the one without hook) from the syscall identifier.
  • Tadam !

The code:

int GoUp -32;
int GoDown 32;
// If the first instruction of the syscall is a an inconditional jump (aka it's hooked)
if (*((PBYTE)pFunctionAddress) == 0xe9) {
	// Search beginning pattern of syscall stub through 500 function up and down from our location
	for (WORD index = 1; index <= 500; index++) {
		// Search the begining of a syscall stub in the next function down
		if (*((PBYTE)pFunctionAddress + index * GoDown) == 0x4c
			&& *((PBYTE)pFunctionAddress + 1 + index * GoDown) == 0x8b
			&& *((PBYTE)pFunctionAddress + 2 + index * GoDown) == 0xd1
			&& *((PBYTE)pFunctionAddress + 3 + index * GoDown) == 0xb8
			&& *((PBYTE)pFunctionAddress + 6 + index * GoDown) == 0x00
			&& *((PBYTE)pFunctionAddress + 7 + index * GoDown) == 0x00) {
			BYTE high = *((PBYTE)pFunctionAddress + 5 + index * GoDown);
			BYTE low = *((PBYTE)pFunctionAddress + 4 + index * GoDown);
			// substract the index from the current syscall identifier to find the one of our target function
			pVxTableEntry->wSystemCall = (high << 8) | low - index;
			return TRUE;
		// Search the begining of a syscall stub in the next function down
		if (*((PBYTE)pFunctionAddress + index * GoUp) == 0x4c
			&& *((PBYTE)pFunctionAddress + 1 + index * GoUp) == 0x8b
			&& *((PBYTE)pFunctionAddress + 2 + index * GoUp) == 0xd1
			&& *((PBYTE)pFunctionAddress + 3 + index * GoUp) == 0xb8
			&& *((PBYTE)pFunctionAddress + 6 + index * GoUp) == 0x00
			&& *((PBYTE)pFunctionAddress + 7 + index * GoUp) == 0x00) {
			BYTE high = *((PBYTE)pFunctionAddress + 5 + index * GoUp);
			BYTE low = *((PBYTE)pFunctionAddress + 4 + index * GoUp);
			// substract the index from the current syscall identifier to find the one of our target function
			pVxTableEntry->wSystemCall = (high << 8) | low + index;
			return TRUE;

With FreshyCalls

In Halo’s Gate, we learned that syscall ID are following each other incrementally. So if you think hard, like very hard, you can have an epiphany like, “Oh my god, I can retrieve the syscall ID by deducing them from the functions addresses!

Indeed ! if you sort the Ntdll functions by addresses in your debugger, you can see that the first Nt function has the syscall ID 0, the next is 1 etc.

Nt functions sorted by addresses

Nt functions sorted by addresses

(don’t pay attention to the Zw in the function name of the screen below, we will explain on the next chapter)

Syscall following incrementally

Syscall following incrementally

Sooo this is means, that if we retrieve the addresses of all Nt functions and if we sort them by addresses, we can deduce the syscall identifiers without parsing the syscall stub ???

And yes this is exactly it !!! We just need to retrieve the DLL base address (parse PEB), parse the EAT of the DLL to retrieve the RVA of “Nt” functions, sort them and BOOM !

Look at this beautiful code:

  //This function retrieve the RVA and name of "Nt" functions and put them in the sorted map
  //We go trough the functions names in the Export Directory
  for (size_t i = 0; i < export_dir->NumberOfNames; i++) {
    function_name = reinterpret_cast<const char *>(ntdll_base + names_table[i]);

    // If the name of the function start with "Nt" and don't start with "Ntdll"
    // we retrieve the info
    if (function_name.rfind("Nt", 0) == 0 && function_name.rfind("Ntdll", 0) == std::string::npos) {
      stub_ordinal = names_ordinals_table[i];
      stub_address = ntdll_base + functions_table[stub_ordinal];
	  // We put the RVA as a key and the function name as the value.
	  // This is a sorted map.
      // The elements are automatically sorted using the key value.
	  // This means that when all the Nt function will be loaded, 
      // the first element will be the Nt function with the lowest address 
      // and the last the one with the biggest address.
      stub_map.insert({stub_address, function_name});

  // `stub_map` is ordered from lowest to highest using the stub address. Syscalls numbers are
  // assigned using this ordering too. The lowest stub address will be the stub with the lowest
  // syscall number (0 in this case). We just need to iterate `stub_map` and iterate the syscall
  // number on every iteration.

  static inline void ExtractSyscallsNumbers() noexcept {
    uint32_t syscall_no = 0;
    // The stub_map filled previously in the code presented above
    for (const auto &pair: stub_map)  {
	  //Creation of a map associating function name and syscall identifier.
      syscall_map.insert({pair.second, syscall_no});

With Syswhispers2

And last but not least, Syswhispers2.

To retrieve the syscall identifiers dynamically, Syswhispers2 uses almost the same technique as FreshyCalls.

But, there is a tiny difference on how the syscall ID are retrieved.

The interesting difference is that instead of searching for functions beginning with “Nt” but not “Ntdll” in the Export Directory. Syswhispers2 searches for function starting with “Zw” and when creating it’s syscall ID array, it stores the name by replacing “Zw” by “Nt”.

Why? Because “Zw” functions and “Nt” functions point to the same syscall stubs.

If you want to know what is a “Zw” function and the differences between a “Zw” and a “Nt” version of a function. Again, quoting Microsoft documentation:

The Windows native operating system services API is implemented as a set of routines that run in kernel mode. These routines have names that begin with the prefix Nt or Zw. Kernel-mode drivers can call these routines directly. User-mode applications can access these routines by using system calls.

With a few exceptions, each native system services routine has two slightly different versions that have similar names but different prefixes. For example, calls to NtCreateFile and ZwCreateFile perform similar operations and are, in fact, serviced by the same kernel-mode system routine.

For system calls from user mode, the Nt and Zw versions of a routine behave identically. For calls from a kernel-mode driver, the Nt and Zw versions of a routine differ in how they handle the parameter values that the caller passes to the routine.

A kernel-mode driver calls the Zw version of a native system services routine to inform the routine that the parameters come from a trusted, kernel-mode source. In this case, the routine assumes that it can safely use the parameters without first validating them. However, if the parameters might be from either a user-mode source or a kernel-mode source, the driver instead calls the Nt version of the routine, which determines, based on the history of the calling thread, whether the parameters originated in user mode or kernel mode (source)

Look at the addresses for ZwOpenProcess and NtOpenProcess, ZwOpenProcessToken and NtOpenProcessTokenEx/ZwOpenProcessTokenEx.

Zw and Nt function

Zw and Nt function

By doing this, you don’t have to check the presence of “Ntdll” at the start of the name. Clever right:)

Let’s go through the code:

    DWORD i = 0;
	//For info. It's in the header file.
	typedef struct _SW2_SYSCALL_ENTRY
		DWORD Hash;
		DWORD Address;
	#define SW2_MAX_ENTRIES 500
    PSW2_SYSCALL_ENTRY Entries = SW2_SyscallList.Entries;
        // Retrieve function name from the Dll.
        PCHAR FunctionName = SW2_RVA2VA(PCHAR, DllBase, Names[NumberOfNames - 1]);

        // Check if the function name starts with "Zw"
        if (*(USHORT*)FunctionName == 'wZ')
			// If yes, hash the name (for AV/EDR/Malware Analyst evasion reasons) and put it in an Entries element
            Entries[i].Hash = SW2_HashSyscall(FunctionName);
			// Put also the address of the function
            Entries[i].Address = Functions[Ordinals[NumberOfNames - 1]];

            if (i == SW2_MAX_ENTRIES) break;
    } while (--NumberOfNames);

    // Save total number of system calls found.
    SW2_SyscallList.Count = i;

    // Sort the list by address in ascending order.
    for (i = 0; i < SW2_SyscallList.Count - 1; i++)
        for (DWORD j = 0; j < SW2_SyscallList.Count - i - 1; j++)
            if (Entries[j].Address > Entries[j + 1].Address)
                // Swap entries.
                SW2_SYSCALL_ENTRY TempEntry;

                TempEntry.Hash = Entries[j].Hash;
                TempEntry.Address = Entries[j].Address;

                Entries[j].Hash = Entries[j + 1].Hash;
                Entries[j].Address = Entries[j + 1].Address;

                Entries[j + 1].Hash = TempEntry.Hash;
                Entries[j + 1].Address = TempEntry.Address;

    return TRUE;

The end, Finally !

Aaaand this is the end of this post. Of course once the syscall Id are retrieved, you have to use them to perform the syscall you need so much.

Hell’s gate, Halo’s gate and Syswhispers2 will use .asm code added during the compilation of your binary.

FreshyCalls will use an array of opcode added to the binary during the compilation.

In a nutshell:

  • Hell’s Gate: Parse syscall stub to find the pattern of a syscall stub beginning and then retrieve the syscall ID. But if the syscall stub is hooked, we are screwed;
  • Halo’s Gate: Same as Hell’s Gate, but in case of hooks it searches the syscall ID from the neighbouring functions and deduce it;
  • FreshyCalls: Search the functions beginning with “Nt” in the Export Directory and sort them by addresses. The lowest address is syscall identfier 0.
  • Syswhispers2: Same as FreshyCalls, but search for “Zw” functions in the Export Directory and store the name by replacing “Zw” by “Nt”.

Thanks for reading me! And thanks to Aurélien Denis, Jean-Côme Estienney and 🤫 for the proofreading! And thanks to @modexpblog6 for answering to my questions.

This article was presented (in french) on a talk during the ESE 2022. You can find the slides here

  1. (repository) ↩︎

  2. (repository) ↩︎

  3. (repository) ↩︎

  4. (training) ↩︎

  5. (repository) ↩︎

  6. (awesome reference) ↩︎