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.
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.
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.
Once in Kernelbase, we can see something strange… We hit a inconditional jump (jmp
) not related to a Windows Dll.
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.
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.
If we check what is atcuf64
in the loaded modules, we can see that it’s a DLL belonging to BitDefender.
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:
If you perform a SetMenu()
the execution will be:
Finally, if you perform a GetRandomRgn()
the execution will be:
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:
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
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;
} TEB, *PTEB;
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
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;
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
} PEB_LDR_DATA, *PPEB_LDR_DATA;
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;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
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;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
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.
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.
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
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 inAddressOfFunctions
(Export Address Table).
Example:
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:
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;
break;
}
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.
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.
(don’t pay attention to the Zw in the function name of the screen below, we will explain on the next chapter)
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});
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
.
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;
} SW2_SYSCALL_ENTRY, *PSW2_SYSCALL_ENTRY;
#define SW2_MAX_ENTRIES 500
*/
PSW2_SYSCALL_ENTRY Entries = SW2_SyscallList.Entries;
do
{
// 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]];
i++;
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
-
(repository) https://github.com/am0nsec/HellsGate ↩︎
-
(repository) https://github.com/crummie5/FreshyCalls ↩︎
-
(repository) https://github.com/jthuraisamy/SysWhispers2 ↩︎
-
(training) https://institute.sektor7.net/rto-win-evasion ↩︎
-
(repository) https://github.com/xalicex/Get-DLL-and-Function-Addresses ↩︎
-
(awesome reference) https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-system-calls-for-red-teams/ ↩︎