Direct Syscalls in Practice: Bypassing Userland EDR Hooks with WinDbg, SysWhispers3 & Havoc C2

This post is the written companion to our YouTube demo Direct Syscalls in Practice. In the video we walk through the full pipeline live: extracting a System Service Number (SSN) with WinDbg, generating clean stubs with SysWhispers3, integrating them into a Havoc implant, and testing against Microsoft Defender and Bitdefender. We recommend watching the video alongside this post for the complete picture.

This blog series is the written companion to our Malware Development: Red Team Tradecraft YouTube playlist, where we walk through live demos of the evasion techniques discussed here, including payload staging, AV/EDR bypass, and in-memory, on disk, and network evasion. We recommend following the playlist along with these posts. 

In Part 1, we covered the foundational concepts of processes, threads, and memory. In Part 2, we explored Windows APIs, the PE format, and injection techniques; in Part 3, we covered Shellcode Fundamentals, Cryptography in Malware, Shellcode Encryption & Obfuscation, Evasion Techniques, etc.

Disclaimer

This content is intended for cybersecurity professionals, red teamers, purple teamers, and defenders who need to understand modern evasion techniques in authorized environments. All techniques described here must only be used in controlled lab settings or with explicit written permission. Misuse of these methods is illegal and unethical. The goal of this research is to improve detection and harden defenses.

Introduction

Modern endpoint detection and response (EDR) products place heavy reliance on userland hooks. By patching the first bytes of critical ntdll stubs with an “E9 JMP” instruction, EDR can inspect every call to sensitive native APIs before the syscall instruction executes.

Direct syscalls reverse this advantage. Instead of going through kernel32 → KernelBase → ntdll (where the hooks live), the implant builds its own tiny stub:

```asm
mov r10, rcx
mov eax, SSN
syscall
ret
```
The call never touches the hooked ntdll functions, so the EDR’s userland inspection never fires.
In this post we cover:
1. The Windows syscall architecture and exactly where EDR hooks sit  
2. Manual SSN extraction with WinDbg  
3. Automated stub generation with SysWhispers3  
4. Clean integration into a Havoc C2 beacon  
5. Real world results against Microsoft Defender and Bitdefender  

The Normal Call Chain and Where EDR Hooks Live

When user-mode code calls a Win32 API such as VirtualAlloc, the call path looks like this:

```
Your code
  → kernel32!VirtualAlloc
    → KernelBase!VirtualAlloc
      → ntdll!NtAllocateVirtualMemory   ← EDR hooks here
        → syscall instruction
          → KiSystemCall64 (Ring 0)
            → SSDT lookup
              → nt!NtAllocateVirtualMemory
```

The ntdll stub itself is extremely simple on x64:
```asm
ntdll!NtAllocateVirtualMemory:
  4C 8B D1          mov r10, rcx
  B8 18 00 00 00    mov eax, 18h          ; SSN
  0F 05             syscall
  C3                ret
```

Most EDR products overwrite the first five bytes with an E9 relative jump that redirects execution into their own inspection DLL. The classic tell is simple: if the first byte of an ntdll stub is 0xE9 instead of 0xB8 (or 0x4C), the function is hooked.
Direct syscalls completely skip this path. The implant never calls the ntdll export, so the hook is never reached.

Visual Overview

Direct Syscalls Example

Manual Implementation: Extracting SSNs with WinDbg

Before relying on generators, it is essential to understand the raw mechanics.
1. Attach WinDbg to any process (or simply load ntdll).
2. Disassemble the target stub:

```
0:000> uf ntdll!NtAllocateVirtualMemory
ntdll!NtAllocateVirtualMemory:
00007ff8`...  4c8bd1          mov     r10,rcx
00007ff8`...  b818000000      mov     eax,18h
00007ff8`...  0f05            syscall
00007ff8`...  c3              ret
```
The SSN for NtAllocateVirtualMemory has remained 0x18 across a very wide range of Windows 10 and Windows 11 builds. Other SSNs change more frequently, which is why hard-coding them is fragile.
A minimal direct-syscall stub in assembly therefore looks exactly like the original ntdll version, except it lives inside the implant instead of ntdll.

Automation: SysWhisper3

Manually maintaining stubs for every Windows build is impractical. SysWhispers3 (klezVirus) solves this by generating ready-to-compile header, C, and MASM files.

```
git clone https://github.com/klezVirus/SysWhispers3.git
cd SysWhispers3

python3 syswhispers.py -a x64 -c msvc -m jumper_randomized -f NtAllocateVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx,NtWaitForSingleObject -o syscalls -v

```

This produces:
- syscalls.h / syscalls.c – function prototypes and resolution helpers  
- syscalls-asm.x64.asm – one clean stub per NT function  
In our demo we used the pure direct mode (embedded syscall instruction). SysWhispers3 can also emit indirect variants that jump into legitimate ntdll stubs, which is useful for call-stack spoofing.
Key operational notes from the demo:
- We renamed internal variables and registers inside the generated files.  
- The Havoc payload was encrypted.  
- After compilation the PE import table contained zero Nt* imports. All sensitive calls were resolved through the embedded stubs.

Integration into Havoc C2

Havoc’s implant is an excellent option for this technique because the source is open and cleanly structured.
High-level steps we followed:
1. Add the three SysWhispers3 files to the Visual Studio project and enable MASM.  
2. Replace the standard injection calls:
``
// Before
VirtualAlloc(...);
WriteProcessMemory(...);
CreateRemoteThread(...);

// After
SW3_NtAllocateVirtualMemory(...);
SW3_NtWriteVirtualMemory(...);
SW3_NtCreateThreadEx(...);
```
3. Rebuild the implant as x64 Release, strip symbols, and optionally sign.  
4. Deploy and establish a beacon.
Because the stubs never touch the hooked ntdll exports, userland EDR hooks are completely bypassed.

Live Results

Microsoft Defender

CheckResult
On-disk static scanCLEAN
Real-time protectionSilent
C2 sessionEstablished and stable

Bitdefender

CheckResult
On-disk static scanCLEAN
Runtime executionDetected
C2 sessionNever established

Summary Comparison

TechniqueBypasses Userland HooksBypasses Kernel CallbacksCall Stack CleanImplementation ComplexityStatic Signature Risk
Normal Win32 APIsNoNoYesLowLow
Direct SyscallsYesNoNoMediumMedium (if not obfuscated)
Indirect SyscallsYesNoBetterMedium-HighLower
Indirect + Stack SpoofYesNoYesHighLow
+ ETW Patch + Sleep MaskingYesPartialYesHighLow

Work With US:

Want to see what this looks like deployed against your AV/EDR and infrastructure? We build and operate the same tooling in authorized engagements. book a session or get in touch

Resources

Tools
- klezVirus/SysWhispers3 – Syscall stub generator  
- Havoc Framework – Modern C2 with built-in indirect syscall support  
- hfiref0x/SyscallTables – Current SSN mappings  
- WinDbg  – Manual disassembly and verification
- x64dbg  - GUI Windows-based Debugger
- Microsoft NT API Prototypes - Undocumented NT Internals
Research & Further Reading
- MITRE ATT&CK T1106 – Native API  
- Elastic Security – Detecting direct system calls  
- Outflank – Combining Direct System Calls and sRDI  
- Palo Alto Unit 42 – Malicious Direct Syscall Detection  
Related RBT Security Posts
- Shellcode Reflective DLL Injection (sRDI)  
- API Hooking Techniques: Trampoline Hooks, IAT Hooking, and Inline Patching  

Share the Post:

Table of Contents

Subscribe To Our Blog

Light