Offensive Rust: EDR Evasion via Direct System Calls
Modern Endpoint Detection and Response (EDR) solutions aggressively hook user-mode APIs. This technical deep dive explores how advanced threat actors use Rust to execute direct syscalls, bypassing NTDLL hooks entirely.

For years, the standard approach to malware development and red team tooling relied heavily on the Windows API. You want to allocate memory for your shellcode? You call VirtualAllocEx. You want to create a thread? CreateRemoteThread.
The problem is that modern Endpoint Detection and Response (EDR) solutions know exactly what you are doing.
EDRs achieve this visibility through User-Mode API Hooking. When a process loads, the EDR injects its own DLL into the process space. It overwrites the first few instructions of critical functions inside ntdll.dll (like NtAllocateVirtualMemory) with a JMP instruction. This redirect forces the execution flow into the EDR’s inspection engine before the system call ever reaches the Windows kernel.
If you use standard Windows APIs in 2026 for offensive operations, you are going to get caught. Period.
To bypass this, advanced threat actors have shifted to using Direct System Calls. By bypassing ntdll.dll entirely and manually moving the System Service Number (SSN) into the EAX register before invoking the syscall instruction, the EDR's user-mode hooks are completely blinded.
Here is how we implement this concept using Rust.
The Architecture of a Syscall
In a normal execution flow, calling VirtualAlloc looks like this:
Application -> kernel32.dll -> ntdll.dll -> [EDR HOOK] -> Kernel Mode (Ring 0)
By using direct syscalls, we change the flow to:
Application -> [Direct Assembly Syscall] -> Kernel Mode (Ring 0)
Every native Windows function corresponds to a specific System Service Number (SSN) in the kernel. For example, on Windows 10/11, the SSN for NtAllocateVirtualMemory is typically 0x18 (though this changes depending on the exact Windows build).
Implementing the Syscall in Rust
Rust has become the language of choice for modern offensive tooling due to its memory safety, powerful macro system, and seamless ability to drop into inline assembly.
To execute a direct syscall in Rust, we use the core::arch::asm! macro. Here is a simplified implementation of a direct syscall to NtAllocateVirtualMemory:
#![feature(asm)]
use std::ffi::c_void;
use std::ptr::null_mut;
// The System Service Number for NtAllocateVirtualMemory (Windows 11)
const NT_ALLOCATE_VIRTUAL_MEMORY_SSN: u32 = 0x18;
// Define the NTSTATUS return type
type NtStatus = i32;
unsafe fn direct_syscall_allocate(
process_handle: isize,
base_address: *mut *mut c_void,
zero_bits: usize,
region_size: *mut usize,
allocation_type: u32,
protect: u32,
) -> NtStatus {
let status: NtStatus;
core::arch::asm!(
"mov r10, rcx",
"mov eax, {ssn}",
"syscall",
ssn = const NT_ALLOCATE_VIRTUAL_MEMORY_SSN,
inout("rcx") process_handle => _,
inout("rdx") base_address => _,
inout("r8") zero_bits => _,
inout("r9") region_size => _,
// Arguments 5 and 6 are passed on the stack
// in x64 calling convention, but for simplicity
// this example abstracts the stack setup.
lateout("eax") status,
options(nostack)
);
status
}
Why does this work?
- No
ntdll.dllinteraction: The code never calls the hookedntdll.dllfunctions. - Raw execution: We manually load the SSN (
0x18) into theeaxregister. - Hardware boundary: The
syscallinstruction triggers a hardware interrupt, seamlessly crossing the boundary from user-mode (Ring 3) into the kernel (Ring 0).
The Catch: Dynamic System Service Numbers
The code above has a massive flaw: Hardcoded SSNs.
Microsoft frequently changes the SSNs between Windows versions, service packs, and even minor updates. If you hardcode 0x18 and deploy your payload to a Windows 10 machine where the SSN is 0x16, your tool will crash instantly, triggering an alert.
To build production-grade offensive tools (or to defend against them), you must implement Dynamic SSN Resolution. This involves manually reading the ntdll.dll file from disk (before the EDR has a chance to hook it in memory), parsing the PE headers, and extracting the clean SSNs dynamically at runtime. Techniques like "Hell's Gate" or "Halo's Gate" are famous implementations of this concept.
Defending Against Direct Syscalls
If EDRs are blinded in user-mode, how do blue teams defend against this? The battleground is shifting to Kernel-Mode Telemetry.
Modern EDRs rely heavily on Event Tracing for Windows (ETW), specifically the ETW Threat Intelligence provider, and kernel callbacks (ObRegisterCallbacks). While an attacker can hide how they allocated the memory by bypassing user-mode hooks, the kernel still records that the memory was allocated.
Need advanced red teaming to test your EDR configuration? Engage NadahWeb's offensive security team.
Need expert help with Offensive Security?
Engage NadahWeb’s elite security team. Free 30-minute strategic consultation.