Article Bypassing EDR through a direct syscall call PART 1/2

Depov

Activist
ULTIMATE
SUPREME
PREMIUM
MEMBER
Joined
Feb 18, 2025
Messages
126
Reaction score
115
Deposit
0$
Today, the conversation will not go about another “Malvari-invisible”from the Githab, which detects 5 minutes after pouring on VT. It'sabout the foundation. Direct system calls (direct syscals). Atechnique that, from the category of “magic for the chosen”category, goes into the category of must-have skill for anyone who istired of false positives and wants to understand how it works at thelevel of atoms. Not for the script-kiddi, but for someone who wantsto hold a screwdriver, not a plastic knife.

Disclamer:Everything written below is to explore legitimate safety products, apentetus, the development of secure software and expand the horizons.Not for destruction. We are here for knowledge, not for chaos.Knowledge is neutral. The instrument is neutral.
Your choice isnot.





1. Introduction to Hell: Why Does EDR See Anything at All?


Imagine a city (thisis your process). The city has rules - Windows API (kernel32.dll,user32.dll). To ask the nucleus (the government) to do somethingglobal - to allocate land (memory), build a road (thread), you needto apply. You go to the local city hall (ntdll.dll), write astatement (form arguments in registers) and lower it in the box (youfollow the instructions syscal). The courier (processor) instantlydelivers him to the government (core).

Now imagine thatEDR is a corrupt guard. They can't forbid you to live in the city,but they:


  1. The camera was hung in the city hall (Userland Hooks). Every time you come for the NtCreateFile form, the guard (hook) records who you are and what you want.
  2. We agreed with officials within the government (Kernel Callbacks). When your application comes into the core, the inner notification works: “Hey, the guy wants to highlight the memory of the rights to execution!”
  3. Listen to the talk of couriers (ETW). The Windows logging system itself is chatty. EDR subscribes to these events (as Microsoft-Windows-Threat-Intelligence) and receives detailed reports about each of your steps.

Traditional malwaregoes to the mayor’s office, takes a standard form, fills it andlowers it in a box. He is seen by all the cameras, all the officialsknow about him.

Our goal: Get a clean, unused form (findthe address of syscal-instructions inside nddll), copy its form(SSN), learn to fill it yourself (form arguments in the registers),and lower it into the drawer without going to the city hall itself.And even better - to find the service entrance (alternativesyscal-gadget) or bribe a courier (vulnerability in thenucleus).

Key point: EDR does not hog the system callitself in the nucleus (this is difficult and dangerous), but adaptersin the userland - functions in the platll.dll. A direct syscall jumpover the head of this adapter.





2. Syscal as a Philosopher's Stone.


In the x64 world,the system challenge is caused by the instructions of syscal. It'snot a function. It's the door. Every door has a number - SSN (SystemService Number). This number is the index in a huge table inside thecore that tells the system which function to perform.

Whereto get it?
It is rigidly sewn into the body of every ntdllfunction. Open the ntdll.dll in the disassembler. You see somethinglike that?




NtCreateFile:


mov r10, rcx ; Thefirst argument goes into r10


mov eax, 55h ; Hereit is! The SSN for NtCreateFile in this version of Windows


syscall


ret








0x55 is SSN. Buthere's the problem: It is different for each version of Windows. OnWindows 10 1909, one on Windows 11 22H2 - the other. Therefore,hardcoding is the way to hell.

Challenge agreement (x64fastcolall):

  • The first 4 arguments go to registers: rcx, rdx, r8, r9.
  • The rest of the arguments are fluttering into the stack. Important: For direct syscall, you have to yourself, before sycall, to reserve in the stack 32 bytes ("shadow space" - shadow space) + space for the rest of the arguments. And yes, it's a headache.
  • mov r10, rcx is a mandatory ritual. The core expects arguments in r10 and rdx... why? That's historically.
  • The return value goes to rax.




3. Classic genre: Gates.


So we came to theheart of the technique.

Hell's Gate (2016, originally fromReWolf):
The idea is brilliant in simplicity:

  1. Load a copy of platll.dll from the disk (clean, without hooks).
  2. Manually paredize its PE-heading, we find the export of NtCreateThreadEx.
  3. Disassemble several bytes starting with the function address to extract SSN (the same mov eax, XX).
  4. We use this SSN in our code.

Problem: EDR beganto search in the memory of processes such “static” calls mov eax,0xXX; syscal. Signature.

Halo's Gate (development):
Theauthors offered a brilliant trick. If in the huckled function of thentdll in memory, the instruction syscals is replaced by jmp[add_Huder_edr], then we:

  1. We are looking for the syscall instruction before starting the function.
  2. Or jump over jmp and look for ret after syscal.
    In fact, we will “download” through the scraps of code to find the untouched pair of mov eax, SSN; syscal. This is already a dynamic search, resistant to a simple signature detect.

Tartarus Gate andthe like (advanced level):
Why go to the ntdll at all? SSN isstored in the core, in KeServiceDescriptTable (KSDT) orKeServiceDescriptTableShadow (KSDTS). If we can read the kernelmemory (through vulnerability, legitimate driver, etc.), we can getSSN directly from the original source. This is the level of royalpower.

Practical piece of Hell's Gate-style code (verysimplified):


#include <windows.h>


#include <stdio.h>





// Structure forstoring a Syscall Number -> Address pair


typedef struct_SYSCALL_ENTRY {


DWORD SSN;


PVOID Address;


} SYSCALL_ENTRY;





// Rough search forSSN based on the signature mov eax, [SSN] (bytes B8 ?? ?? ?? ??)


DWORDFindSSNFromBytes(PBYTE functionAddress) {


for (int i = 0; i <32; i++) { // Scan the first 32 bytes


if(functionAddress == 0xB8) { // opcode for ‘mov eax, imm32’


DWORD ssn =*((PDWORD)(functionAddress + i + 1)); // The next 4 bytes are the SSN


return ssn;


}


}


return 0;


}





// The call itself.Inline assembly for clarity.


NTSTATUSMyNtCreateThreadEx(


PHANDLEThreadHandle,


ACCESS_MASKDesiredAccess,


POBJECT_ATTRIBUTESObjectAttributes,


HANDLEProcessHandle,


PVOID StartRoutine,


PVOID Argument,


ULONG CreateFlags,


SIZE_T ZeroBits,


SIZE_T StackSize,


SIZE_TMaximumStackSize,


PVOID AttributeList,


DWORD SSN) {





NTSTATUS status = 0;





__asm {


// Prepare argumentsin registers (omitted for brevity)


// ...


mov r10, rcx //Required!


mov eax, SSN // Loadthe dynamically found SSN


syscall // The gatesopen


mov status, eax //Save the result


}





return status;


}





int main() {


// 1. Load a cleanntdll.dll from disk


HMODULE hNtdll =LoadLibraryExW(L“C:\\Windows\\System32\\ntdll.dll”, NULL,DONT_RESOLVE_DLL_REFERENCES);


// 2. Get theaddress of NtCreateThreadEx


FARPROCpNtCreateThreadEx = GetProcAddress(hNtdll, “NtCreateThreadEx”);


// 3. Extract theSSN
b

DWORD ssn =FindSSNFromBytes((PBYTE)pNtCreateThreadEx);


printf(“[+] FoundSSN for NtCreateThreadEx: 0x%X\n”, ssn);





// 4. Use it in yourcode...


//MyNtCreateThreadEx(..., ssn);





return 0;


}








It's a skeleton. Inreality, everything is more difficult: you need to correctly parsePE, take into account the relocation, work with a stack, processWoW64. But the point is clear.
 
Top Bottom