DLL
A compiled binary that contains code + data which is not a standalone program, but is meant to be loaded into another process.
Has main() / WinMain()
No main function
Can run by itself
Cannot run alone
Owns the process
Lives inside a process
Entry: process start
Entry: load/unload events
Entry point is:
BOOL WINAPI DllMain(
HINSTANCE hinstDLL,
DWORD fdwReason,
LPVOID lpvReserved
);DllMain:
PROCESS_ATTACH → DLL loaded
PROCESS_DETACH → DLL unloaded
THREAD_ATTACH → new thread
THREAD_DETACH → thread exitsDll minimal:
#include <windows.h>
BOOL WINAPI DllMain(
HINSTANCE hinstDLL,
DWORD reason,
LPVOID reserved
) {
if (reason == DLL_PROCESS_ATTACH) {
MessageBoxA(NULL, "DLL Loaded!", "Hello", MB_OK);
}
return TRUE;
}DllMain is dangerous if abused:
No heavy logic
No threads
No LoadLibrary inside DllMain
No network
No malloc-heavy stuff
Why? Because the loader lock is held. DLL is just a PE as : It contains:
DOS header
NT headers
Sections:
.text→ code.rdata→ constants.data→ globals.idata→ imports.edata→ exports.reloc→ relocations DLL-specific difference:
Has an Export Table
No entry like
mainMarked as
IMAGE_FILE_DLL
How dlls are loaded:
Load Time Linking:
You link against a .lib at compile time. Behind the scenes:
EXE imports
user32.dllWindows loads it automatically
Runtime loading (explicit)
This is where things get interesting.
Manual mapping (advanced, later)
No LoadLibrary
No Windows loader
You manually map sections, relocations, imports
How dlls export functions
or using .def :
Then one can call via:
stack goes:
Rust does:
Process:
Phase 1 – Basics
What is a process
What is virtual memory
What is a PE file
EXE vs DLL
Phase 2 – WinAPI + DLL
LoadLibrary / GetProcAddress
Export tables
Dependency Walker
PE headers
Phase 3 – Loader internals
ntdll
LdrLoadDll
Import resolution
Relocations
Phase 4 – Advanced
Manual mapping
Reflective DLLs
IAT/EAT hooking
Phase 1
Process
Whenever we create a process windows does:
creates a process object (kernel)
creates a Virtual Address Space
maps the exe into the memory
maps required DLL
sets up threads
Transfers execution to code A process is a container.
Every process gets:
Its own virtual address space
Its own loaded DLLs
Its own heap(s)
Its own stack(s) Two processes can load the same dll but virtual address may differ and physical pages maybe shared {copy-on-write}
This is what it looks like:
Virutal Memory says:
Addresses are lies backed by page tables.
Each process believes: "I own memory from 0x0 to 0x7FFFFFFFFFFF"
The kernel translates virtual → physical
Processes cannot see each other’s memory
Same virtual address ≠ same physical memory So thats why:
Crashes don’t kill the OS
Malware needs injection
DLLs are mapped, not copied
What does windows process contains:
User land:
EXE image
Loaded Dlls
Heaps
Stacks
TLS
PEB
Kernel land:
EPROCESS
Handle table
security token
thread objects
PEB
Which every user-mode process has: The PEB contains:
Process parameters
Loaded module list
Heap pointers
OS version info
BeingDebugged flag 👀
This is where:
Loaders find DLLs
Malware hides modules
Tools enumerate loaded libraries PEB lives at:
Threads
A process does nothing without threads. Each thread has:
Stack
Registers
TEB (Thread Environment Block)
TEB contains:
Thread ID
TLS
Pointer to PEB
Where do DLLs live:
DLLs are:
Mapped into the process address space
Stored as memory-mapped PE images
Listed in the PEB loader data
Each DLL:
Has a base address
Has sections mapped (.text, .data, etc)
Has imports resolved
Memory
Windows memory is managed in pages.
Page size (x64): 4 KB
Everything happens page-by-page
Protection is page-level
Page states
Free – unused
Reserved – address space claimed
Committed – backed by RAM / pagefile
Memory protection flags
PAGE_READONLY
Read
PAGE_READWRITE
Read + write
PAGE_EXECUTE
Execute
PAGE_EXECUTE_READ
Execute + read
PAGE_EXECUTE_READWRITE
RWX (dangerous)
Stack
One per thread
Grows downward
Fast
Fixed size (mostly)
Stores local variables, return addresses
Heap
Shared per process
Grows upward
Managed by allocator
Used for dynamic memory
DLLs can:
Use process heap
Create private heaps
Use TLS (thread-local storage)
VirtualAlloc (why it exists)
C/C++ malloc()
Uses heap
You don’t control pages directly
VirtualAlloc():
Talks directly to the VM manager
Reserves and commits pages
Controls protection flags Manual loaders, shellcode, and mappers require this.
What is a PE file really?
A PE (Portable Executable) is just a file format. Nothing more. Nothing magical.
Windows doesn’t “run code” — it parses a PE and maps it. What windows sees:
DOS headers
Every PE starts with:
This is the IMAGE_DOS_HEADER for backward compatibility
e_lfanew → offset to NT headers
NT Headers
base + e_lfanew
PE signature:
PE\0\0File header
Optional header (misnamed, it’s mandatory) This tells Windows:
x86 or x64
EXE or DLL
Entry point
Image size
Subsystem
Data directories
Optional Header
This is where the loader gets its marching orders
ImageBase
Preferred load address
AddressOfEntryPoint
Where execution starts
SizeOfImage
Total memory size
SectionAlignment
Memory alignment
FileAlignment
Disk alignment
DataDirectory
Imports, exports, relocations
Section table
Each section entry tells:
Name
Virtual size
Virtual address
Raw size
Raw file offset
Characteristics
EXE vs DLL (PE-level difference)
Structurally:
They are almost identical
IMAGE_FILE_DLL flag
❌
✅
Entry point meaning
Process start
DllMain
Can be root image
✅
❌
Export table required
❌
Usually
Code-A-Code
We create a dll and an exe and see how they are loaded , we load the dll with the help of EXE:
DLL
The dll just pops a message box
EXE
short explaination
Windows:
Maps the DLL into memory
Fixes relocations (ASLR)
Resolves imports
Makes exports available Now the DLL is loaded as an IMAGE, not a raw file.
So this is correct:
Import table (how functions are found)
The import table tells Windows:
“I need these functions from these DLLs”
Example:
Loader:
Loads required DLL
Finds exported functions
Writes addresses into IAT
Code calls via IAT
Export table (how DLLs expose functions)
DLLs expose functions via export table. Exports can be:
Named
Ordinal-based
Forwarded This is what
GetProcAddress()reads.
Relocation table (why memory matters)
Relocations fix:
Absolute addresses
When DLL isn’t loaded at preferred base
If .reloc is missing and ASLR happens:
[X] Load fails Manual loaders must apply relocations manually.
Entry Point
Exe:
Dll:
Imagine:
EXE loads
Needs
user32.dlluser32 needs
win32u.dll
Loader:
Maps EXE
Walks import table
Loads dependencies recursively
Resolves everything before first instruction runs
NOTE: when we double click the exe and open it -> explorer opens it. While from cmdline parent process is powershell or cmd. But why explorer.exe spawn a black cmd and when we kill it , msg box disappears? [ANS] The loader.exe is a Console subsystem application. That means in the PE Optional Header:
When you double-click a console app from Explorer:
Explorer sees it’s a console program
It creates a new console window (conhost.exe)
Your process attaches to that console So the black window is not cmd.exe. Its:
Parent process is simply whoever called CreateProcess.
Also note in process explorer we see these many dlls being loaded which are not in the dependency walker of both: loader.exe and mydll.dll:
![[Pasted image 20260212003248.png]] Dependency Walker shows:
Static imports only.
Process Explorer shows:
Everything actually loaded at runtime.
Static vs Dynamic loading
Static imports
These are inside .idata section of PE. Example:
kernel32.dll
user32.dll These appear in Dependency Walker.
Dynamic loads (runtime)
Some DLLs are loaded:
By the CRT
By user32 internally
By COM
By Windows itself
By debug runtime
By NLS (localization) EG:
<Pagefile Backed>
<Pagefile Backed>These are:
Heaps
Stacks
Allocations
Memory-mapped regions
.NLS file
These are:
National Language Support files
Loaded by Windows for encoding / locale support They are memory-mapped data files.
Export Table Internals
We are going to answer:
How does Windows find
"Add"inside your DLL?
Inside every DLL that exports functions there is:
This structure lives inside the PE’s Data Directory. In Optional Header:
This gives you:
RVA of export table
Size note: RVA means
relative virtual address{comes after be patient}
What the Export Directory Contains
AddressOfFunctions
Array of RVAs.
Index = ordinal - Base Gives you:
AddressOfNames
Array of RVAs to ASCII strings:
AddressOfNameOrdinals
Array of WORDs. Maps:
How GetProcAddress Works (Simplified)
When we call:
Windows does roughly:
Get base address (HMODULE)
Find NT headers
Locate export directory
Iterate names:
Compare string with "Add"
Get corresponding ordinal
Use ordinal to index into
AddressOfFunctionsCompute:
Return pointer
Very Important Concept — RVA
RVA = Relative Virtual Address It is:
To comvert:
Everything in PE tables is an RVA.
Let's parse it manually:
We add this function to the loader code:
This code crashes why?: ft chatgpt: find it here: [[Dlls support paper]] Correct code:
Search Order and a simple loader
LoadLibraryA("msg.dll") searches in:
The executable directory
System32
Windows directory
Current working directory
PATH
Calling convention:
A calling convention is a contract between:
The caller (your Rust EXE)
The callee (your DLL function) It defines:
How arguments are passed
Where return value goes
Who cleans the stack
Which registers must be preserved Talking about thread stack
__cdecl
Used by default in C. Rule: 👉 Caller cleans the stack
__stdcall
Used by WinAPI. Rule: 👉 Callee cleans the stack
Caller thinks:
"Callee will clean the stack."
But callee thinks:
"Caller will clean the stack."
Result:
No one cleans it.
Stack pointer is wrong. Return address becomes wrong. CPU jumps to garbage. Crash: 0xc0000005
Cleaning means:
Move stack pointer back to where it was before arguments were pushed.
Why RVA:
This is what we did:
Locate export directory
Iterate name array
Match string
Get ordinal
Get function RVA
Compute address = base + RVA
Security tools often hook:
LoadLibraryGetProcAddressVirtualAllocCreateRemoteThread
When windows loads dlls it might load different every time but it RVA remains the same.
What Is Shellcode?
Shellcode is:
Small, position-independent machine code designed to execute inside another process.
Historically it was used to spawn a shell (hence the name), but today it simply means:
Raw executable bytes
No PE header
No loader
No import table
No runtime support
Just instructions
How Is Shellcode Different From a DLL or EXE?
A normal DLL:
Has PE headers
Has import table
Has export table
Gets loaded by Windows loader
Imports resolved automatically
Shellcode:
Has none of that
Is just bytes
Gets copied into memory
CPU jumps into it
It must resolve everything itself
How Is Shellcode Created?
Conceptually:
Write position-independent assembly
Assemble it
Extract raw machine bytes
Embed those bytes somewhere
Unlike a DLL, shellcode has:
no PE header
no import table
no relocation section
no loader metadata
It must resolve everything itself.
That’s why earlier we talked about:
PEB walking
Export parsing
Manual resolution
Because shellcode doesn’t get help from Windows loader.
Important Concept: Memory Protections
Modern OSes use:
DEP (Data Execution Prevention)
NX bit (Non-Executable memory)
On Windows x64:
RCX → 1st argument
RDX → 2nd argument
R8 → 3rd
R9 → 4th Return value → RAX
So assembly for : return a + b;
machine code:
so it goes in cpp as:
code becomes:
Executing calc.exe:
Step 1: No shellcode:
Step 2: shellcode:
Now this shellcode will be flagged as harmful why?:
The "Egg Hunter" Pattern: Most shellcodes start by looking for the
PEB(Process Environment Block) to findkernel32.dllin memory. This specific assembly sequence (\x65\x48\x8b\x52\x60) is a massive "red flag" for AV/EDR because legitimate programs almost never do this.
Last updated