Difficulty: Beginner

Module 1: EDR Hooks & The Detection Problem

Why calling NtAllocateVirtualMemory isn't as simple as it used to be.

Module Objective

Before diving into LayeredSyscall's implementation, you need to understand what problem it solves. This module covers how EDRs instrument Windows API calls, the three detection layers that catch most syscall evasion techniques, and why a new approach was needed.

1. What Is an EDR?

Endpoint Detection and Response (EDR) is the evolution of traditional antivirus. While AV relies on static signature matching, EDR monitors process behavior in real-time. It watches what your code does, not just what it looks like.

Every modern EDR deploys a user-mode component — typically a DLL that gets loaded into every process. This DLL patches (hooks) critical API functions so the EDR can inspect arguments and return values before they reach the kernel.

Key EDR Components

ComponentLocationPurpose
User-mode DLLLoaded into every processHooks ntdll functions, inspects API arguments
Kernel driverRing 0Kernel callbacks, ETW providers, minifilters
Cloud backendRemoteTelemetry correlation, threat intelligence
InstrumentationCallbackPer-process (kernel-set)Validates syscall return addresses

The user-mode hook is the most relevant to LayeredSyscall. It intercepts calls at the ntdll.dll level — the lowest user-mode layer before the kernel transition.

2. How EDR Hooks Work

Every Nt* function in ntdll.dll is a thin wrapper called a syscall stub. In its clean (unhooked) state, the stub looks like this:

Unhooked ntdll Syscall Stub

x86-64 ASMNtAllocateVirtualMemory:
  4C 8B D1          mov r10, rcx        ; save 1st arg (RCX clobbered by syscall)
  B8 18 00 00 00    mov eax, 0x18       ; SSN (System Service Number)
  0F 05             syscall             ; transition to Ring 0
  C3                ret                 ; return to caller

The stub is only 12 bytes. It loads the syscall number into EAX, executes the syscall instruction, and returns. No logic, no validation — just a gateway to the kernel.

Hooked ntdll Syscall Stub

An EDR replaces the first 5 bytes with a relative jump (E9 opcode) to its own inspection function:

x86-64 ASMNtAllocateVirtualMemory (HOOKED):
  E9 XX XX XX XX    jmp EDR_Hook        ; 5-byte relative jump to EDR DLL
  00 00 00          ; remaining bytes (corrupted/overwritten)
  0F 05             syscall             ; still present but never reached via normal flow
  C3                ret

The E9 jump replaces mov r10, rcx (3 bytes) and the first 2 bytes of mov eax, SSN. When your code calls NtAllocateVirtualMemory, execution is redirected to the EDR's hook handler, which:

EDR Hook Handler Flow

Your code calls Nt* function
JMP to EDR hook
EDR inspects args & context
Allow: execute real stub

If the EDR deems the call suspicious, it can block or alert instead of forwarding to the real stub.

3. Three Layers of Detection

Over time, EDRs developed multiple detection layers. Each one catches a different class of evasion technique. Understanding all three is critical to appreciating why LayeredSyscall exists.

Layer 1: NTDLL Remapping Detection

What it catches: Loading a fresh (unhooked) copy of ntdll.dll from disk.

Early evasion techniques loaded a second copy of ntdll from C:\Windows\System32\ntdll.dll or from the KnownDlls section object. This gave them clean syscall stubs without EDR hooks.

How EDRs detect it: They monitor for suspicious file reads of ntdll.dll, section mapping of \KnownDlls\ntdll.dll, or processes with multiple ntdll mappings. Some EDRs use kernel callbacks (PsSetLoadImageNotifyRoutine) to detect fresh DLL loads.

Layer 2: Direct Syscall Detection (InstrumentationCallback)

What it catches: Executing the syscall instruction from outside ntdll memory.

Direct syscall techniques (SysWhispers, Hell's Gate) embed the syscall instruction directly in the attacker's code. The syscall instruction saves RIP into RCX, and on return the kernel passes control to KiUserExceptionDispatcher or back to the return address.

How EDRs detect it: The InstrumentationCallback (set via NtSetInformationProcess) is invoked after every syscall return. It checks if the return address falls within ntdll's memory range. If the return address points to .text of your EXE or some unknown region, it's flagged as a direct syscall.

Detection Logic// InstrumentationCallback pseudo-code
if (returnAddress < ntdll_base || returnAddress > ntdll_end) {
    // syscall did NOT originate from ntdll
    // Flag as DIRECT SYSCALL → alert/block
}

Layer 3: Call Stack Analysis

What it catches: Anomalous call stacks that skip the normal API call chain.

A legitimate call to NtAllocateVirtualMemory produces a call stack like:

Legitimate Stackntdll!NtAllocateVirtualMemory      ; syscall stub
ntdll!RtlpAllocateHeapInternal      ; internal ntdll logic
KERNELBASE!VirtualAlloc             ; Win32 wrapper
kernel32!VirtualAllocStub           ; kernel32 thunk
myapp.exe!main                      ; application code

When attacker code calls an Nt* function directly (even via indirect syscall), the stack looks wrong:

Anomalous Stackntdll!NtAllocateVirtualMemory      ; syscall stub
myapp.exe!some_function             ; DIRECT jump from EXE to ntdll!
                                     ; Where's kernel32? KERNELBASE?

How EDRs detect it: They walk the call stack (using RtlVirtualUnwind or stack frame pointers) and check that expected intermediate frames exist. A call from user code directly to an ntdll Nt* function, without traversing kernel32/KERNELBASE, is flagged as suspicious.

4. Why Indirect Syscalls Aren't Enough

Indirect syscalls (used by SysWhispers2, HWSyscalls, etc.) solve Layer 2 by jumping to the syscall instruction inside ntdll's own memory. The return address now correctly points to ntdll, so InstrumentationCallback sees nothing wrong.

Indirect Syscall: Solves Layer 2, Fails Layer 3

myapp.exe
ntdll!NtAllocateVirtualMemory
(jumps directly, no kernel32 frame)
syscall executes from ntdll
(Layer 2: PASS)

But the call stack shows: myapp.exe → ntdll (missing kernel32/KERNELBASE frames) → Layer 3: FAIL

Indirect Syscall (e.g., SysWhispers2)
  • Syscall executes from ntdll memory (Layer 2 bypassed)
  • Call stack shows EXE → ntdll directly
  • Missing kernel32 / KERNELBASE frames
  • Call stack analysis flags the anomaly
  • Only addresses 2 of 3 detection layers
Legitimate API Call
  • Syscall executes from ntdll memory
  • Call stack: EXE → kernel32 → KERNELBASE → ntdll
  • All expected intermediate frames present
  • Call stack analysis finds nothing wrong
  • Passes all 3 detection layers

Some tools attempted to solve Layer 3 by spoofing call stack frames — manually pushing fake return addresses onto the stack before the syscall. However, advanced EDRs can validate these frames by checking if the return addresses actually correspond to valid CALL instruction sites in the supposed caller modules.

5. What LayeredSyscall Solves

The LayeredSyscall Approach

Instead of faking call stack frames, LayeredSyscall generates real ones. It works by:

  1. Calling a legitimate Win32 API (e.g., WriteFile) that naturally traverses kernel32 → KERNELBASE → ntdll
  2. Using hardware breakpoints to intercept execution just before the syscall instruction inside ntdll
  3. Swapping the syscall number (SSN in EAX) and arguments (in registers and on the stack) to those of the desired function
  4. Letting the syscall execute — the kernel sees the attacker's SSN but the call stack is completely legitimate

The result: every detection layer sees exactly what it expects. The syscall originates from ntdll (Layer 2), the call stack passes through kernel32 and KERNELBASE (Layer 3), and no fresh copy of ntdll was loaded (Layer 1).

LayeredSyscall Execution Model

Call legitimate API
(e.g., WriteFile)
kernel32 → KERNELBASE
(real frames built)
ntdll stub reached
(HW breakpoint fires)
VEH swaps SSN + args
(syscall hijacked)

Detection Layer Scorecard

Detection LayerDirect SyscallIndirect SyscallStack SpoofingLayeredSyscall
Layer 1: NTDLL RemappingPASSPASSPASSPASS
Layer 2: Direct SyscallFAILPASSPASSPASS
Layer 3: Call StackFAILFAILPARTIALPASS

6. Real-World EDR Hooking Examples

Different EDR vendors implement their hooks in slightly different ways, but the core technique remains the same. Here are common patterns observed in the wild:

Common EDR Hook Patterns

PatternBytesDescription
5-byte JMPE9 XX XX XX XXRelative jump. Most common. Overwrites first 5 bytes of the stub.
6-byte JMP [rip]FF 25 00 00 00 00Indirect jump via RIP-relative pointer. Followed by an 8-byte absolute address.
2-byte JMP shortEB XXShort relative jump (within ±127 bytes). Used when the trampoline is close.
MOV RAX + JMP48 B8 ... FF E0Load absolute address into RAX then JMP RAX. 12 bytes total. Used for far targets.

Detecting Hooks Programmatically

If you wanted to check whether a function is hooked, you could compare the first bytes against the expected clean stub pattern. This is how tools like Hell's Gate determine if they can read the SSN directly:

C++BOOL IsStubHooked(PVOID funcAddr) {
    BYTE* p = (BYTE*)funcAddr;

    // Clean stub starts with: 4C 8B D1 B8 (mov r10, rcx; mov eax, ...)
    if (p[0] == 0x4C && p[1] == 0x8B && p[2] == 0xD1 && p[3] == 0xB8) {
        return FALSE;  // Unhooked
    }

    // Check for common hook patterns
    if (p[0] == 0xE9)         return TRUE;  // JMP rel32
    if (p[0] == 0xFF)         return TRUE;  // JMP [rip+disp32]
    if (p[0] == 0xEB)         return TRUE;  // JMP short

    return TRUE;  // Unknown pattern, assume hooked
}

LayeredSyscall doesn't need this check because its SSN resolution method (Module 2) is completely independent of stub bytes. However, understanding hook detection helps appreciate why earlier techniques were fragile.

ETW (Event Tracing for Windows)

Beyond inline hooks, modern EDRs also leverage ETW (Event Tracing for Windows) for visibility. ETW provides kernel-level telemetry that user-mode evasion cannot disable. Key providers include:

LayeredSyscall's technique operates entirely in user mode and does not address kernel-level ETW telemetry. A complete evasion strategy would need to consider ETW as a separate detection surface.

Key Terminology

TermDefinition
SSNSystem Service Number — index into the SSDT that identifies which kernel function to call
Syscall StubThe small assembly sequence in ntdll that loads the SSN and executes the syscall instruction
Inline HookOverwriting the first bytes of a function with a JMP to redirect execution
InstrumentationCallbackA per-process callback invoked after every syscall return; used to validate return addresses
VEHVectored Exception Handling — process-wide exception handlers that intercept CPU exceptions
Hardware BreakpointA CPU debug register (Dr0–Dr3) that fires an exception when a specific address is executed

Module 1 Quiz: EDR Hooks & Detection

Q1: What opcode does an EDR use to hook the first bytes of an ntdll Nt* function?

EDRs use a 5-byte relative jump (opcode E9) to redirect the first instructions of the syscall stub to their hook handler DLL. The 5-byte JMP replaces the mov r10, rcx and part of the mov eax, SSN instructions.

Q2: Which detection layer does InstrumentationCallback implement?

InstrumentationCallback is invoked after every syscall return and checks whether the return address falls within ntdll's memory range. If the syscall instruction was executed from outside ntdll (e.g., from your EXE's .text section), this is Layer 2: Direct Syscall Detection.

Q3: Why do indirect syscalls still fail call stack analysis (Layer 3)?

Indirect syscalls execute the syscall from ntdll memory (passing Layer 2), but the call stack still shows your EXE calling the Nt* function directly without going through the normal Win32 API chain (kernel32 → KERNELBASE → ntdll). EDR call stack walkers flag this missing intermediate frame as anomalous.