Back to Blogs

July 23, 2026

Syscalls - Part 1: Direct Syscalls

What is a direct syscall, and how does it get around usermode hooks? Lets take a look.

Syscalls Explained: The Gateway to Bypassing User-Mode Hooks (Part 1)

After covering the basics of system calls in the first part, it's time to get practical. In this article, I'll show you how to bypass User-Mode hooks using direct syscalls.

Our Test Setup

First, we'll demonstrate what a normal call to NtAllocateVirtualMemory looks like with one twist: we've placed a Detour Hook on this function beforehand.

After that, we'll call the same function using a direct syscall to bypass the hook.

The following diagram illustrates the current flow with the hook installed:

Syscall execution flow

As you can see, the call to VirtualAlloc (which internally calls NtAllocateVirtualMemory) gets redirected to our hook function HkNtAllocateVirtualMemory. The hook simply prints "Hook called!". It's only there so we can verify whether our direct syscall actually bypasses the hook.

The Hook

NTSTATUS NTAPI HkNtAllocateVirtualMemory(
    HANDLE ProcessHandle,
    PVOID* BaseAddress,
    ULONG_PTR ZeroBits,
    PSIZE_T RegionSize,
    ULONG AllocationType,
    ULONG Protect)
{
    std::cout << "Hook called!\n";
    
    return reinterpret_cast<tNtAllocateVirtualMemory>(oNtAllocateVirtualMemory)(
        ProcessHandle,
        BaseAddress,
        ZeroBits,
        RegionSize,
        AllocationType,
        Protect
    );
}

Now let's call the function normally:

auto ret = reinterpret_cast<tNtAllocateVirtualMemory>(NtAllocateVirtualMemoryAddr)(
    GetCurrentProcess(), 
    &baseAddress, 
    0, 
    &size, 
    MEM_COMMIT | MEM_RESERVE, 
    PAGE_READWRITE
);

We should see the following output:

Hook triggered output

The hook was triggered as expected.


Direct Syscalls

Direct Syscalls do exactly what the name suggests: they invoke the syscall directly by embedding the assembly instruction in our own program. This completely bypasses ntdll.dll.

Assembly Stub

First, we create a simple stub in an .asm file:

.code

MyNtAllocateVirtualMemory PROC
    mov r10, rcx          ; x64 Fast Call Convention
    mov eax, 24           ; Syscall number for NtAllocateVirtualMemory
    syscall
    ret
MyNtAllocateVirtualMemory ENDP

END

Now we can call the stub like a regular function:

ret = MyNtAllocateVirtualMemory(
    NtCurrentProcess(),
    &baseAddress,
    0,
    &size,
    MEM_COMMIT | MEM_RESERVE,
    PAGE_READWRITE
);

And voilà, the hook is not triggered:

Direct syscall output - no hook

Problems with Direct Syscalls

Direct syscalls are very powerful against User-Mode hooks and many EDR/AV solutions. However, they still have some drawbacks:

1. Return Address on the Stack

With a normal call flow, the call stack looks roughly like this (Calling NtAllocateVirtualMemory directly!):

Direct syscall output - no hook

EDR solutions that monitor syscalls often validate the call stack at the time the syscall is executed. They check whether the return address leads back into a legitimate Windows module, specifically ntdll.dll. This is a reasonable heuristic: under normal circumstances, every syscall instruction is executed inside ntdll.

With a direct syscall, the picture changes dramatically:

Direct syscall output - no hook

The return address now points directly into your own binary. No ntdll, no legitimate Windows module in sight. Modern EDRs use a technique called call stack unwinding to walk up the stack at syscall time and flag exactly this pattern. Some go further and check whether the memory region containing the return address is backed by a signed PE on disk.. Your custom stub will fail that check immediately.

This is also why simply moving your stub to a manually mapped region doesn't help much on its own: the absence of a proper backing module is just as suspicious as the presence of an unsigned one.

To make direct syscalls less detectable, researchers have developed call stack spoofing techniques, but that's a topic for another post.

2. Syscall Instruction in the Binary

The syscall instruction lives directly in your .exe/.dll. This makes it trivial to statically detect that direct syscalls are being used.

3. Syscall Numbers Change

Syscall numbers are not consistent across Windows versions. Hardcoded values will only work on specific builds.


The End

Thank you for reading this article! In Part 2, we'll tackle the weaknesses covered above — specifically how Indirect Syscalls solve the return address problem by letting the syscall instruction execute from within ntdll.dll itself, making the call stack look legitimate to EDRs. Stay tuned.

As usual, you can find the code on my GitHub.

Previous

Syscalls - Part 0: A Short Introduction

Back to All Blogs