3a. Syscalls Deep dive

Windows Syscall Flow: Input & Output

In Windows, syscalls are handled through the Native API (ntdll.dll) and then passed to the Windows Kernel (ntoskrnl.exe). The system call interface works differently from Linux, as Windows uses NTSTATUS codes for returns.

https://xacone.github.io/mitigate-indirect-syscalls.html


1. Windows Syscall Input & Output Flow

1.1 Syscall Execution Flow

graph TD;
    A[User Mode: Application] --> |Calls ntdll.dll API| B[ntdll.dll: Nt Function];
    B --> |Sets up syscall number| C[Syscall Instruction];
    C --> |Switches to Kernel Mode| D[Kernel Mode: ntoskrnl.exe];
    D --> |Processes syscall| E[Returns NTSTATUS Code];
    E --> |Back to User Mode| A;

*Awesome descriptions regarding registers --> https://hackmd.io/@paolieri/x86_64


2. Windows Syscall Example: NtOpenProcess

Opening a process (e.g., for injection or debugging):

#include <windows.h>
#include <iostream>

extern "C" NTSTATUS NtOpenProcess(
    PHANDLE ProcessHandle, 
    ACCESS_MASK DesiredAccess, 
    POBJECT_ATTRIBUTES ObjectAttributes, 
    PCLIENT_ID ClientId
);

int main() {
    HANDLE hProcess;
    CLIENT_ID clientId = { (HANDLE)1234, 0 }; // Process ID 1234
    OBJECT_ATTRIBUTES objAttr = { sizeof(OBJECT_ATTRIBUTES) };

    NTSTATUS status = NtOpenProcess(&hProcess, PROCESS_ALL_ACCESS, &objAttr, &clientId);
    
    if (status == 0) {
        std::cout << "NtOpenProcess succeeded! Handle: " << hProcess << std::endl;
    } else {
        std::cout << "NtOpenProcess failed with NTSTATUS: " << std::hex << status << std::endl;
    }

    return 0;
}

All syscalls reutrn NTSTATUS


3. Windows Syscall Structure

3.1 Calling Convention (x64)

Register Purpose
RCX First argument
RDX Second argument
R8 Third argument
R9 Fourth argument
Stack Additional arguments
EAX Syscall number
RAX Return value

3.2 Syscall Number Lookup

Example to extract NtOpenProcess syscall number:

$ntdll = [System.Runtime.InteropServices.Marshal]::GetModuleHandle("ntdll.dll")
$addr = [System.Runtime.InteropServices.Marshal]::GetProcAddress($ntdll, "NtOpenProcess")
Write-Output ("NtOpenProcess syscall address: " + $addr.ToString("X"))

4. Syscall Flow with Example

sequenceDiagram
    participant User as User Mode (Process)
    participant NTDLL as ntdll.dll
    participant Kernel as Kernel Mode (ntoskrnl.exe)
    
    User->>NTDLL: Call NtOpenProcess(Process ID)
    NTDLL->>NTDLL: Load syscall number (0x23 in Windows 10)
    NTDLL->>Kernel: Execute syscall instruction
    Kernel->>Kernel: Validate arguments & check permissions
    Kernel-->>NTDLL: Return handle or NTSTATUS error
    NTDLL-->>User: Return process handle

5. Windows Syscall Return Values

Windows syscalls return NTSTATUS codes, which differ from standard errno values in Linux.

NTSTATUS Code Meaning
0x00000000 STATUS_SUCCESS (Success)
0xC0000022 STATUS_ACCESS_DENIED
0xC0000008 STATUS_INVALID_HANDLE
0xC0000005 STATUS_ACCESS_VIOLATION

Example: Checking return values

if (status == 0xC0000022) {
    std::cout << "Access Denied!" << std::endl;
}