· 2 min read

NTDLL stubs, SSNs and where EDRs really watch

The difference between an EDR seeing an attack or missing it is often 5 bytes in memory. A walkthrough of user-land hooks, the classic bypasses, and why kernel callbacks make them insufficient today.

I have been digging into how EDRs work under the hood, and one thing struck me as particularly interesting: the difference between an EDR seeing an attack or missing it can be, literally, 5 bytes in memory.

The stubs

When a process needs to talk to the kernel — open a process, read memory, spawn a remote thread — it doesn’t jump straight there. It goes through a stub in ntdll.dll that boils down to this:

mov r10, rcx
mov eax, <SSN>
syscall
ret

That SSN (System Service Number) is the index the kernel uses to look up the real function (NtOpenProcess, NtReadVirtualMemory, NtAllocateVirtualMemory, …) in the System Service Descriptor Table. Without that number, the kernel does not know which function you want to call.

Where the EDR steps in

Most EDRs inject a DLL (umppc*.dll for Falcon, for instance) into every process and patch the beginning of the sensitive ntdll stubs with a jmp to their own code:

; NtOpenProcess, original
4c 8b d1           mov  r10, rcx
b8 26 00 00 00     mov  eax, 26h
0f 05              syscall

; NtOpenProcess, hooked
e9 ?? ?? ?? ??     jmp  <EDR DLL>

The jmp redirects into the EDR’s DLL, which inspects the arguments (target PID, requested access rights, …) and decides whether to let the call through, block it, or just emit telemetry.

Diagram: NTDLL with NtAllocateVirtualMemory hooked (5-byte jmp into the EDR DLL) and NtOpenProcess unhooked, showing the classic mov r10, rcx / mov eax, SSN / syscall / ret stub

How this is bypassed

There is a family of techniques that keep evolving, since EDR developers do not stand still either. The best-known ones are Hell’s Gate, Halo’s Gate and Tartarus Gate.

They all parse ntdll in memory at runtime. Halo’s Gate goes further and resolves SSNs even when the stubs are hooked, by finding neighbouring clean stubs and computing the number by offset — SSNs are incremental, and since an EDR never hooks every syscall you can always work out the one you want. Tartarus Gate steps ahead of that and also looks for jmps introduced by the EDR further inside the stub.

And here is the part many people still ignore

Against a serious EDR, jumping over the user-land hooks is no longer enough.

CrowdStrike Falcon, for example, has been leaning heavily on kernel callbacks registered from its driver for a while now. The most relevant is probably ETW Threat Intelligence (ETW-TI): kernel-side visibility over sensitive operations like NtReadVirtualMemory.

That means even if you fire off the cleanest possible syscall, the kernel still executes the real function — and ETW-TI emits the event from there, not from ntdll.

An example

You try to dump LSASS with a direct syscall to NtReadVirtualMemory. Your shellcode never touches ntdll, so Falcon’s user-land hooks see nothing. But when the kernel runs the read, ETW-TI generates a ReadVm event with your PID, the LSASS PID and the address that was read. Falcon picks it up from its driver and ships it to the cloud.

Takeaway

If your Red Team methodology stops at direct syscalls, you are missing a layer. And if on the Blue Team side you only look at what the agent sees in user-land, you are missing the same one.