DLL

A compiled binary that contains code + data which is not a standalone program, but is meant to be loaded into another process.

EXE
DLL

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 exits

Dll 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 main

  • Marked 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.dll

  • Windows 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

Flag
Meaning

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\0

  • File 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

Field
Meaning

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

Feature
EXE
DLL

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:

  1. Maps the DLL into memory

  2. Fixes relocations (ASLR)

  3. Resolves imports

  4. 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:

  1. Loads required DLL

  2. Finds exported functions

  3. Writes addresses into IAT

  4. 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.dll

  • user32 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>

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:

  1. Get base address (HMODULE)

  2. Find NT headers

  3. Locate export directory

  4. Iterate names:

    • Compare string with "Add"

  5. Get corresponding ordinal

  6. Use ordinal to index into AddressOfFunctions

  7. Compute:

  1. 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:

  1. The executable directory

  2. System32

  3. Windows directory

  4. Current working directory

  5. PATH

Calling convention:

A calling convention is a contract between:

  • The caller (your Rust EXE)

  • The callee (your DLL function) It defines:

  1. How arguments are passed

  2. Where return value goes

  3. Who cleans the stack

  4. 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:

  • LoadLibrary

  • GetProcAddress

  • VirtualAlloc

  • CreateRemoteThread

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:

  1. Write position-independent assembly

  2. Assemble it

  3. Extract raw machine bytes

  4. 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 find kernel32.dll in 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