Concepts

Background

Windows syscalls, ntdll.dll, EDR hooks — the foundations you need to understand what Sysplant does and why it works.

Background

This page explains the concepts that all Sysplant iterators and methods build on. If you are already familiar with Windows syscall internals and EDR hooking, you can skip to the Iterators section.


Windows syscalls

A syscall is the mechanism by which a user-mode process asks the OS kernel to perform a privileged operation — allocating memory, opening a handle, creating a thread, and so on.

On x86-64 Windows, the transition works as follows:

  1. The user-mode code sets EAX to the Syscall Service Number (SSN) — an integer that identifies which kernel function to invoke.
  2. R10 is set to the first argument (the calling convention for syscalls requires R10 = RCX).
  3. The syscall instruction is executed, switching the CPU into kernel mode at a fixed entry point.
  4. The kernel reads EAX, dispatches to the right handler, and returns.

The SSN is the critical piece: it is a small integer (e.g. 0, 1, 2 …) that is Windows-version-specific and changes between releases.


ntdll.dll — the system call bridge

ntdll.dll is the lowest-level DLL in every Windows process. It exposes the Nt* and Zw* API families, which are thin wrappers around the syscall instruction.

A typical unhooked x64 stub looks like:

NtOpenProcess:
    mov r10, rcx          ; save first arg (syscall ABI requirement)
    mov eax, 0x26         ; load SSN for NtOpenProcess
    syscall               ; kernel transition
    ret

The opcodes for mov r10, rcx; mov eax, SSN produce the byte sequence:

4C 8B D1   B8 [SSN_lo] [SSN_hi] 00 00

This fixed pattern is what the opcode-scanning iterators look for.


EDR hooking

Endpoint Detection & Response (EDR) products need to observe what processes do at the kernel level. The most common technique is to patch the first bytes of sensitive Nt* functions in the in-memory copy of ntdll.dll with a JMP instruction that redirects execution to the EDR's own callback:

NtOpenProcess:   E9 xx xx xx xx   ← JMP to EDR DLL
...
(original bytes now unreachable or displaced)

The EDR callback inspects arguments, logs telemetry, then optionally continues into the real syscall.

Hooks are applied to the in-memory copy of ntdll.dll — the file on disk is untouched. The clean bytes are therefore still accessible in the DLL file or in the memory of another process.

What is hooked?

The E9 (relative JMP near) opcode is a 5-byte instruction. It typically replaces the first 5 bytes of the stub, overwriting MOV R10, RCX and part of MOV EAX, SSN. After the hook, reading opcode bytes at the function's entry point no longer reveals the SSN.

Some hooks target the third byte instead, leaving MOV R10, RCX intact but overwriting MOV EAX, SSN itself.


Why this breaks naive code

If your code simply calls a function like NtOpenProcess(&handle, ...), the execution path is:

your code → NtOpenProcess() → JMP → EDR callback → (optionally) kernel

The EDR always sees the call.

If instead you skip the hook and call the kernel directly with the right SSN, the EDR callback is never entered. The problem then becomes: how do you learn the correct SSN at runtime, without reading the hooked bytes?

That is exactly what each of Sysplant's iterators solves, each in a different way.


The Process Environment Block (PEB)

All iterators locate ntdll.dll at runtime by walking the PEB (Process Environment Block) — a Windows data structure accessible without any API call:

  • x64: GS:[0x60] → pointer to the PEB
  • x86: FS:[0x30] → pointer to the PEB

The PEB contains a Ldr pointer to the loader module list, which enumerates every loaded DLL with its name and in-memory base address. Iterators walk this list to find the base of ntdll.dll, then parse its PE headers directly.

Using the PEB avoids calling GetModuleHandle("ntdll.dll"), which itself is a Win32 call that could be monitored.


The PE Export Directory

Once the ntdll base address is known, all iterators parse its Export Directory — the PE table that maps function names to their Relative Virtual Addresses (RVAs). From this, each function's in-memory address is derived.

Some iterators (Canterlot) additionally parse the Exception Directory (IMAGE_DIRECTORY_ENTRY_EXCEPTION), which contains IMAGE_RUNTIME_FUNCTION_ENTRY records used by the x64 exception-handling mechanism.


Summary

ConceptRole in Sysplant
SSNThe integer passed in EAX to identify the kernel function — what iterators must recover
ntdll.dll stubThe user-mode wrapper containing MOV EAX, SSN; syscall — what iterators read from
EDR hookA JMP patch replacing the stub's first bytes — what iterators work around
PEBHow ntdll's base address is found without API calls
Export DirectoryHow function addresses are enumerated
Exception DirectoryUsed by Canterlot to enumerate stubs in SSN order
Copyright © 2026