Listen to this Post

Introduction:
Traditional API monitoring is the cornerstone of modern EDR solutions—user‑land hooks planted inside `ntdll.dll` allow security products to scrutinise every sensitive operation. Direct system calls bypass these hooks entirely by invoking kernel services from your own code without ever calling the monitored API. This article walks through the prerequisite knowledge and C implementation of direct syscalls for red‑team tradecraft, including managed‑unmanaged bridging, syscall stub generation, and evasion logic that defeats user‑land instrumentation.
Learning Objectives:
- Understand how Windows system calls work and why EDRs rely on user‑land API hooks
- Implement a direct syscall in C by dynamically executing x64 assembly stubs
- Build a modular class structure that can be reused across multiple post‑exploitation tools
You Should Know:
- Prerequisite Knowledge – System Calls, Ntdll Hooking, and .NET Interop
Before writing any code, we need to clarify three foundational concepts.
First, a system call (syscall) is the transition from user mode to kernel mode to request an operating system service. On Windows, every high‑level API—CreateFile, VirtualAlloc, WriteProcessMemory—ultimately ends up in ntdll.dll, where a tiny stub loads the system service number (SSN) into the `eax` register and executes the `syscall` instruction. The kernel then uses that SSN to dispatch the call to the appropriate routine.
EDRs leverage this by hooking the `ntdll.dll` functions that red‑team tools call most often. For example, an EDR places a `jmp` instruction at the start of `NtAllocateVirtualMemory` to redirect execution into its own inspection engine. If the call is malicious, it blocks the operation; otherwise it passes control back to the original stub. This is user‑land hooking, and it is the primary defence that direct syscalls aim to defeat.
Second, we must understand managed vs. unmanaged code in .NET. C normally runs inside the .NET runtime (managed code), which is memory‑safe and heavily abstracted. However, syscalls are an unmanaged, low‑level mechanism. To use them from C, we need to “pinvoke” (Platform Invoke) or execute raw assembly bytes. The blog series by Jhalon shows how to place a syscall stub – a small piece of x64 assembly that contains the `syscall` instruction – into an executable memory page and then call it from C via a delegate.
Third, the syscall number (SSN) changes between Windows versions. The same kernel function may have SSN `0x18` on Windows 10 1809 but `0x22` on Windows 11 24H2. This means hard‑coding SSNs will break your tool on different builds. Modern techniques either query the SSN from a clean copy of `ntdll.dll` stored on disk, or generate stub files for all target versions and select the correct one at runtime using the Process Environment Block (PEB).
Command to view loaded `ntdll.dll` base address (PowerShell):
Get-Process -Id $pid | Select-Object -ExpandProperty Modules | Where-Object ModuleName -eq ntdll.dll
- Devising the Code Structure – Building a Reusable Syscall Class
A clean class structure is essential for operational stability and for integrating the technique into larger implants. The blog uses a simple but powerful layout inspired by tools like SharpSploit:
- Syscalls.cs – Contains the delegate definition and the method that invokes the syscall stub.
- Program.cs – Demonstration code that requests memory, writes shellcode, and executes it.
The delegate is a type‑safe function pointer that tells C how to call our unmanaged stub. The stub is stored as a byte array, copied into an executable memory region using VirtualAlloc, and then converted into a delegate using Marshal.GetDelegateForFunctionPointer. That delegate can be called like any other C method.
Complete step‑by‑step code walkthrough:
Step 1 – Define the delegate for the syscall.
[UnmanagedFunctionPointer(CallingConvention.StdCall)] public delegate int NtAllocateVirtualMemoryDelegate( IntPtr ProcessHandle, ref IntPtr BaseAddress, IntPtr ZeroBits, ref IntPtr RegionSize, uint AllocationType, uint Protect);
Step 2 – Embed the x64 syscall stub as a byte array.
byte[] syscallStub = new byte[] {
0x4C, 0x8B, 0xD1, // mov r10, rcx
0xB8, 0x18, 0x00, 0x00, 0x00, // mov eax, 0x18 (SSN placeholder)
0xF6, 0x04, 0x25, 0x08, 0x03, 0xFE, 0x7F, 0x01, // test byte ptr...
0x75, 0x03, // jne continue
0x0F, 0x05, // syscall
0xC3 // ret
};
The SSN bytes (0x18) must be replaced with the correct value for the target Windows build.
Step 3 – Allocate executable memory and copy the stub.
IntPtr stubPtr = VirtualAlloc( IntPtr.Zero, (uint)syscallStub.Length, 0x3000, // MEM_COMMIT | MEM_RESERVE 0x40 // PAGE_EXECUTE_READWRITE ); Marshal.Copy(syscallStub, 0, stubPtr, syscallStub.Length);
Step 4 – Convert the memory address to a delegate and invoke it.
NtAllocateVirtualMemoryDelegate allocateMethod = Marshal.GetDelegateForFunctionPointer<NtAllocateVirtualMemoryDelegate>(stubPtr); IntPtr baseAddr = IntPtr.Zero; IntPtr size = (IntPtr)4096; int status = allocateMethod(GetCurrentProcess(), ref baseAddr, IntPtr.Zero, ref size, 0x3000, 0x40);
This pattern works for any syscall; you simply change the delegate signature and the SSN in the stub. The full PoC is available on SharpCall repository.
Linux equivalent – `syscall` function:
include <sys/syscall.h> include <unistd.h> long res = syscall(SYS_getpid);
3. Dynamic SSN Resolution – Staying Version‑Agnostic
Hard‑coding SSNs limits your tool to a specific Windows build. A more robust approach resolves the SSN at runtime by reading a clean, unhooked copy of `ntdll.dll` from disk or from the `KnownDlls` directory. The Windows parallel loader mechanism guarantees that the on‑disk `ntdll.dll` is not yet instrumented by the EDR, so its syscall stubs still contain the original SSNs.
Extracting SSN from `ntdll.dll` – manual method (PowerShell + WinDbg):
Dump the first few bytes of NtAllocateVirtualMemory from the disk copy $ntdll = "C:\Windows\System32\ntdll.dll" $bytes = Get-Content $ntdll -Encoding Byte -TotalCount 200 Then use a disassembler to locate the mov eax, imm32 instruction
Automated method (C):
- Map the on‑disk `ntdll.dll` into memory as a read‑only file mapping.
- Parse the PE headers to find the export directory.
- Locate the target function (e.g.,
NtAllocateVirtualMemory) and read the first few bytes. - Extract the 4‑byte immediate value after the `mov eax` opcode – that is the SSN.
A simpler approach used by many red‑team tools is to generate stub files for every Windows version in advance using SysWhispers3. The generator creates `syscalls.h` and `syscalls.c` with conditional logic that picks the correct SSN based on the operating system version retrieved from the PEB, without calling any API that could be hooked.
SysWhispers3 generation command (Linux or Windows with Python):
python syswhispers3.py --preset all -o syscalls_all
This produces headers for every supported function across Windows 10 and 11 builds.
- Detecting and Bypassing EDR Hooks – Technical Deep Dive
To understand why direct syscalls work, we must examine how EDRs place their hooks. A typical EDR will:
– Load its driver early in the boot process.
– Register a call‑back that receives notifications when a new process loads ntdll.dll.
– Overwrite the first few bytes of selected `ntdll.dll` functions with a `jmp` or `call` instruction pointing to its own analysis code.
If a malicious process calls `NtAllocateVirtualMemory` normally, execution enters the hooked stub and the EDR inspects the parameters. If the call is suspicious (e.g., requesting PAGE_EXECUTE_READWRITE), the EDR blocks it and raises an alert.
A direct syscall bypasses this by never executing the hooked stub. Instead, your own stub issues the `syscall` instruction directly, jumping from user mode straight into the kernel. The EDR’s user‑land hook is simply not triggered.
Indirect syscalls add another layer: rather than calling the syscall instruction directly from your own stub, you jump to the `syscall` instruction that exists inside a clean copy of `ntdll.dll` (e.g., one loaded from disk or from a remote process). This defeats EDRs that also hook the `syscall` instruction itself or that use Vectored Exception Handling (VEH) to trap `int 2e` or syscall. Several open‑source projects implement indirect syscalls, such as DoomSyscalls and SysWhispers2.
Checking if a function is hooked (WinDbg command):
0:000> u ntdll!NtAllocateVirtualMemory
If the first instruction is `jmp` or `call` to an address outside ntdll.dll, the function is hooked.
Linux perspective – `strace` vs. direct syscall:
strace -e trace=memory ls
Monitoring tools on Linux use `ptrace` or eBPF; a direct `syscall()` function can evade some user‑land hooking there too.
- Tooling and Ecosystem – SysWhispers, Dumpert, and SharpCall
Several tools have implemented direct and indirect syscalls in C and C++, providing a rich ecosystem for red‑team development.
| Tool | Language | Purpose |
||-||
| SharpCall | C | Simple PoC demonstrating syscall execution via unmanaged stub. |
| SysWhispers3 | Python/C/C | Generator that creates syscall stubs for any Windows version. |
| Dumpert | C | LSASS dumping using direct syscalls to bypass EDR. |
| DynamicSyscalls | C | Library that resolves syscalls dynamically without hard‑coded SSNs. |
| DoomSyscalls | C++ | Indirect syscalls with return address spoofing. |
To integrate syscalls into a C implant, you can take the stub generation from SharpCall and the SSN resolution from DynamicSyscalls, then package everything into a dedicated `Syscall` class. The blog series strongly recommends this modular approach, as it allows you to swap out SSN resolution strategies without rewriting your main logic.
Using SysWhispers3 with C – hybrid approach:
- Run `python syswhispers3.py -f NtAllocateVirtualMemory -o syscall -m direct`
2. Compile the generated.c/.hinto a native DLL. - Pinvoke that DLL from C. The native code performs the direct syscall while your C logic remains simple.
-
Advanced Techniques – Dynamic Invocation and Return Address Spoofing
Beyond basic direct syscalls, red teams have developed two advanced evasions.
Dynamic Invocation (DInvoke) loads the syscall stub or even the entire `ntdll.dll` into memory without creating an Import Address Table (IAT) entry. This defeats IAT‑based EDR hooks because the function address is resolved at runtime. The technique uses `LoadLibrary` and `GetProcAddress` but calls them from within the stub, not from the main executable’s import table. This is fully feasible in C by pinvoking the necessary loader APIs.
Return address spoofing is employed by tools like DoomSyscalls. When a syscall returns, the return address on the stack points back to your stub. An EDR that inspects kernel call stacks may detect this pattern. Spoofing replaces the return address with an address that appears to be inside a legitimate module (e.g., kernel32.dll), making the call look like a normal API invocation. This technique is more advanced and typically implemented in assembly, but it can be combined with the C stub approach by manipulating the stack before issuing the `syscall` instruction.
Linux return address check: Use `__builtin_return_address(0)` inside a syscall wrapper to inspect the caller.
What Undercode Say:
- Direct syscalls in C provide a stealthy alternative to pinvoking Win32 APIs, but they require careful management of SSN versioning and stub memory. They shine in post‑exploitation scenarios where disk‑less operation is mandatory.
- The trade‑off is complexity: you must maintain per‑version SSN tables or implement runtime resolution, and the stub injection into executable memory may itself be flagged by memory scanners. For many red‑team operations, the evasion benefits outweigh these costs – especially when combined with indirect syscalls and dynamic invocation.
Prediction:
- -1 As Microsoft continues to harden Windows, features like Kernel Patch Protection (PatchGuard) and HVCI (Hypervisor‑protected Code Integrity) will make direct syscalls more difficult. Future Windows versions may require signatures for syscall stubs or enforce control‑flow integrity checks on the transition from user to kernel mode.
- +1 The red‑team community will respond by shifting to indirect syscalls and hardware‑assisted evasions (e.g., Intel PT‑based obfuscation). Tools will become more modular, with SSN resolution abstracted behind a simple API that works across all Windows 10 and 11 builds.
- +1 Defenders will increasingly rely on kernel‑call‑stack analysis and ETW (Event Tracing for Windows) telemetry from the kernel, rather than user‑land hooks. This will force attackers to develop kernel‑level bypasses, escalating the cat‑and‑mouse game into ring‑0.
▶️ Related Video (80% Match):
🎯Let’s Practice For Free:
🎓 Live Courses & Certifications:
Join Undercode Academy for Verified Certifications
🚀 Request a Custom Project:
Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands
IT/Security Reporter URL:
Reported By: Aleborges Programming – Hackers Feeds
Extra Hub: Undercode MoN
Basic Verification: Pass ✅


