Chapter 03

The Windows API Bootstrap Problem

Before your shellcode can call a single Win32 function, it has to solve a circular dependency: to find LoadLibrary, you need kernel32.dll's base address — but to get that, you'd normally call GetModuleHandle, which lives in kernel32.dll. This chapter breaks that circle using a structure the Windows loader leaves in every process's memory before a single instruction of your code runs.

The Circular Dependency

Imagine you're writing shellcode that will be injected into a remote process. Your first task is to call MessageBoxA to show a proof-of-concept dialog. MessageBoxA lives in user32.dll. To load user32.dll you'd call LoadLibraryA. That lives in kernel32.dll. To find LoadLibraryA, you'd call GetProcAddress. That also lives in kernel32.dll. To get kernel32.dll's base address, you'd call GetModuleHandleA("kernel32.dll"). That also lives in kernel32.dll.

You can't use any of these functions until you've found them, and you can't find them without already having them. That's the bootstrap problem.

The circular dependency
  "I want to call MessageBoxA"
         │
         ▼
  Need LoadLibraryA → lives in kernel32.dll
         │
         ▼
  Need GetProcAddress → lives in kernel32.dll
         │
         ▼
  Need kernel32.dll base address
         │
         ▼
  Call GetModuleHandleA("kernel32.dll") → lives in kernel32.dll
         │
         ▼
         ⟲  CIRCULAR — you cannot start here
        

The CRT startup code has the same problem and solves it the same way you're about to learn — by reading a data structure the Windows loader populates in every process before it transfers control to any user code.

How the Windows Loader Helps You

When Windows creates a new process or injects a DLL into an existing one, it does a lot of work before your code runs. Part of that work is loading every DLL your binary depends on and recording their base addresses in a per-process data structure. This structure is called the Process Environment Block, or PEB.

The critical insight: the PEB is accessible from user-mode at all times, through a chain of pointers that starts from a CPU segment register. No API call required. No DLL needed. The PEB is just memory — and you can read it directly.

From CPU register to loaded module list — x64
  GS segment register
       │
       │  GS:[0x60]  (offset to PEB pointer inside TEB)
       ▼
  TEB (Thread Environment Block)
  ┌─────────────────────────────────────┐
  │ offset 0x00  ExceptionList          │
  │ offset 0x08  StackBase              │
  │ ...                                 │
  │ offset 0x30  ProcessEnvironmentBlock│ ← pointer to PEB
  │ ...                                 │
  └───────────────┬─────────────────────┘
                  │
                  ▼
  PEB (Process Environment Block)
  ┌─────────────────────────────────────┐
  │ offset 0x00  InheritedAddressSpace  │
  │ offset 0x02  BeingDebugged          │ ← anti-debug flag (later)
  │ ...                                 │
  │ offset 0x18  Ldr                    │ ← pointer to PEB_LDR_DATA
  │ ...                                 │
  └───────────────┬─────────────────────┘
                  │
                  ▼
  PEB_LDR_DATA
  ┌─────────────────────────────────────┐
  │ offset 0x00  Length                 │
  │ offset 0x08  Initialized            │
  │ offset 0x10  SsHandle               │
  │ offset 0x18  InLoadOrderModuleList  │ ← doubly-linked list (load order)
  │ offset 0x28  InMemoryOrderModuleList│ ← doubly-linked list (memory order)
  │ offset 0x38  InInitializationOrderModuleList │
  └─────────────────────────────────────┘
                  │
                  ▼ (walking InMemoryOrderModuleList)
  LDR_DATA_TABLE_ENTRY  (one per loaded module)
  ┌─────────────────────────────────────┐
  │ InLoadOrderLinks    (LIST_ENTRY)    │
  │ InMemoryOrderLinks  (LIST_ENTRY)    │ ← Flink/Blink here
  │ InInitializationOrderLinks          │
  │ DllBase             (PVOID)         │ ← the module's base address!
  │ EntryPoint          (PVOID)         │
  │ SizeOfImage         (ULONG)         │
  │ FullDllName         (UNICODE_STRING)│ ← full path
  │ BaseDllName         (UNICODE_STRING)│ ← just the filename
  └─────────────────────────────────────┘
        

Every DLL loaded into the process appears in this list. The Windows loader populates it during process initialization — before your entry point or any injected shellcode runs. This means kernel32.dll is already in this list when your code starts.

The Structures in C

The Windows SDK headers define most of these structures, but they're spread across different header files and sometimes incomplete. For offensive code, it's cleaner to define the fields you actually need yourself. Here are the minimal definitions for the PEB walk:

// peb.h — minimal structure definitions for PEB walking
// No Windows SDK headers required for these definitions
#pragma once
#include <windows.h>

// A doubly-linked list entry
typedef struct _MY_LIST_ENTRY {
    struct _MY_LIST_ENTRY* Flink;  // forward pointer
    struct _MY_LIST_ENTRY* Blink;  // backward pointer
} MY_LIST_ENTRY;

// A counted Unicode string (how Windows stores wide strings internally)
typedef struct _MY_UNICODE_STRING {
    USHORT Length;         // length in BYTES (not characters)
    USHORT MaximumLength;
    WCHAR* Buffer;         // the actual string (not null-terminated, use Length)
} MY_UNICODE_STRING;

// One entry in the module list — one per loaded DLL/EXE
typedef struct _MY_LDR_DATA_TABLE_ENTRY {
    MY_LIST_ENTRY InLoadOrderLinks;
    MY_LIST_ENTRY InMemoryOrderLinks;
    MY_LIST_ENTRY InInitializationOrderLinks;
    PVOID         DllBase;           // base address of the loaded module
    PVOID         EntryPoint;
    ULONG         SizeOfImage;
    MY_UNICODE_STRING FullDllName;   // full path: C:\Windows\System32\kernel32.dll
    MY_UNICODE_STRING BaseDllName;   // just the name: kernel32.dll
} MY_LDR_DATA_TABLE_ENTRY;

// The module list header
typedef struct _MY_PEB_LDR_DATA {
    ULONG         Length;
    BOOLEAN       Initialized;
    PVOID         SsHandle;
    MY_LIST_ENTRY InLoadOrderModuleList;
    MY_LIST_ENTRY InMemoryOrderModuleList;
    MY_LIST_ENTRY InInitializationOrderModuleList;
} MY_PEB_LDR_DATA;

// The PEB itself (only the fields we need)
typedef struct _MY_PEB {
    BYTE          Reserved1[2];
    BYTE          BeingDebugged;   // non-zero if a debugger is attached
    BYTE          Reserved2[21];
    MY_PEB_LDR_DATA* Ldr;         // offset 0x18 on x64
} MY_PEB;
Why define your own structures?
The Windows SDK does define PEB and LDR_DATA_TABLE_ENTRY, but they're in <winternl.h> and the definitions are intentionally incomplete (Microsoft doesn't officially support direct PEB access from user code). Defining your own also means your code doesn't depend on a specific SDK version and you understand every field you're accessing. In shellcode, understanding what you're reading at what offset matters — you can't afford mysterious bugs.

Reading the PEB From C

Getting the PEB pointer on x64 requires reading from the GS segment register at a specific offset. There is no C statement that does this directly — you need either an intrinsic or a tiny inline assembly stub:

// Method 1: MSVC intrinsic (works on MSVC and recent MinGW)
#include <intrin.h>
MY_PEB* get_peb(void) {
    return (MY_PEB*)__readgsqword(0x60);
    // On x86 (32-bit), use: __readfsdword(0x30)
}

// Method 2: Inline assembly (MinGW / GCC)
MY_PEB* get_peb(void) {
    MY_PEB* peb;
    __asm__ volatile (
        "mov %%gs:0x60, %0"   // read 64-bit value at GS:0x60
        : "=r"(peb)
    );
    return peb;
}

// Method 3: NtCurrentPeb() macro (if you have winternl.h)
// #include <winternl.h>
// MY_PEB* peb = (MY_PEB*)NtCurrentPeb();

All three methods produce the same result: a pointer to the current thread's PEB. On x64, GS:[0x60] always holds this pointer — this is a documented, stable ABI that Microsoft has not and cannot change without breaking all existing Windows software.

Walking the Module List

With the PEB in hand, walking to the module list takes three pointer dereferences. Then you iterate the doubly-linked list, comparing each entry's BaseDllName against the module you're looking for:

// Case-insensitive wide-string comparison (no CRT)
static int wstr_iequal(WCHAR* a, USHORT a_len, const WCHAR* b) {
    USHORT i = 0;
    while (i < a_len / sizeof(WCHAR) && b[i]) {
        WCHAR ca = a[i];
        WCHAR cb = b[i];
        // Uppercase both (simple ASCII range only — DLL names are ASCII)
        if (ca >= L'a' && ca <= L'z') ca -= 32;
        if (cb >= L'a' && cb <= L'z') cb -= 32;
        if (ca != cb) return 0;
        i++;
    }
    // Match only if both strings ended at the same point
    return (i == a_len / sizeof(WCHAR)) && !b[i];
}

// Find a loaded module by name — returns its base address or NULL
void* find_module(const WCHAR* name) {
    MY_PEB* peb = get_peb();

    // Ldr->InMemoryOrderModuleList is the head of the circular list
    MY_LIST_ENTRY* head = &peb->Ldr->InMemoryOrderModuleList;
    MY_LIST_ENTRY* curr = head->Flink;

    while (curr != head) {
        // Each list entry IS the InMemoryOrderLinks field inside an
        // LDR_DATA_TABLE_ENTRY. To get the full entry, subtract the
        // offset of InMemoryOrderLinks from the pointer.
        MY_LDR_DATA_TABLE_ENTRY* entry = (MY_LDR_DATA_TABLE_ENTRY*)(
            (BYTE*)curr - offsetof(MY_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks)
        );

        if (entry->BaseDllName.Buffer &&
            wstr_iequal(entry->BaseDllName.Buffer,
                        entry->BaseDllName.Length, name)) {
            return entry->DllBase;  // found it
        }

        curr = curr->Flink;  // advance to next entry
    }

    return NULL;  // not found
}
The offsetof trick explained
The linked list pointer stored in each LDR_DATA_TABLE_ENTRY is the InMemoryOrderLinks field — not the start of the structure. When you dereference curr, you're pointing at the InMemoryOrderLinks field inside the entry, not at the entry itself. To get back to the start of the LDR_DATA_TABLE_ENTRY, you subtract the compile-time offset of that field using offsetof(). This is the standard CONTAINING_RECORD / container_of pattern used throughout the Windows kernel and any code that walks kernel-style doubly-linked lists.

Parsing the Export Directory to Find Functions

You now have kernel32.dll's base address. But you can't call GetProcAddress yet — you need to find GetProcAddress itself first, along with any other function you want. The way to do this is to parse the PE's export directory, which lists every function the DLL exports by name and ordinal.

PE export directory structure
  DLL base address (e.g. 0x7FFB12340000)
       │
       │  + e_lfanew → PE signature → Optional Header → DataDirectory[0]
       ▼
  IMAGE_EXPORT_DIRECTORY
  ┌─────────────────────────────────────────────────────┐
  │ Characteristics      (unused)                       │
  │ TimeDateStamp                                       │
  │ MajorVersion / MinorVersion                         │
  │ Name              RVA → "KERNEL32.dll\0"            │
  │ Base              (ordinal base, usually 1)         │
  │ NumberOfFunctions (total exported functions)        │
  │ NumberOfNames     (functions exported by name)      │
  │ AddressOfFunctions    RVA → DWORD array [N]         │ ← function RVAs
  │ AddressOfNames        RVA → DWORD array [N]         │ ← name string RVAs
  │ AddressOfNameOrdinals RVA → WORD  array [N]         │ ← name→ordinal map
  └─────────────────────────────────────────────────────┘

  To find function "GetProcAddress":
  1. Iterate AddressOfNames array
  2. At index i, AddressOfNames[i] is an RVA → a string
  3. Compare that string to "GetProcAddress"
  4. When found at index i, read AddressOfNameOrdinals[i] → ordinal j
  5. Read AddressOfFunctions[j] → RVA of the function
  6. Add DLL base → actual address of GetProcAddress
        
// Resolve a function by name from a module's export directory
// base_addr: the DllBase from find_module()
// func_name: ASCII function name to find
void* get_export(void* base_addr, const char* func_name) {
    BYTE* base = (BYTE*)base_addr;

    // Step 1: get to the PE headers
    IMAGE_DOS_HEADER*  dos = (IMAGE_DOS_HEADER*)base;
    IMAGE_NT_HEADERS*  nt  = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);

    // Step 2: find the export directory RVA
    DWORD exp_rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
                      .VirtualAddress;
    if (!exp_rva) return NULL;

    IMAGE_EXPORT_DIRECTORY* exp = (IMAGE_EXPORT_DIRECTORY*)(base + exp_rva);

    // Step 3: get the three arrays
    DWORD* names    = (DWORD*)(base + exp->AddressOfNames);
    WORD*  ordinals = (WORD*) (base + exp->AddressOfNameOrdinals);
    DWORD* funcs    = (DWORD*)(base + exp->AddressOfFunctions);

    // Step 4: walk the name array looking for our function
    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        char* name = (char*)(base + names[i]);

        // Simple strcmp (no CRT)
        const char* a = name;
        const char* b = func_name;
        while (*a && *a == *b) { a++; b++; }
        if (*a != *b) continue;  // not a match

        // Step 5: found — look up via ordinal
        WORD  ord      = ordinals[i];
        DWORD func_rva = funcs[ord];

        // Watch for forwarded exports (RVA falls inside export directory)
        // For now we skip them — kernel32 functions we care about aren't forwarded
        return (void*)(base + func_rva);
    }

    return NULL;  // not found
}

The Complete Bootstrap: From Zero to Any API

Combining the PEB walk and export directory parser gives you a complete bootstrap sequence. From here, you can reach any function in any loaded DLL, and from kernel32 you can call LoadLibraryA to bring in any other DLL you need.

// bootstrap.c — full working example: PEB walk → kernel32 → any API
// No imports required. Zero strings in the binary (we hardcode wide string inline).

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "peb.h"    // our structure definitions from earlier

// ── Inline assembly PEB access ─────────────────────────────────────
static MY_PEB* get_peb(void) {
    MY_PEB* p;
    __asm__ volatile ("mov %%gs:0x60, %0" : "=r"(p));
    return p;
}

// ── Case-insensitive wide string compare ───────────────────────────
static int wstr_iequal(WCHAR* a, USHORT bytes, const WCHAR* b) {
    USHORT i = 0, n = bytes / sizeof(WCHAR);
    while (i < n && b[i]) {
        WCHAR ca = a[i], cb = b[i];
        if (ca >= L'a' && ca <= L'z') ca -= 32;
        if (cb >= L'a' && cb <= L'z') cb -= 32;
        if (ca != cb) return 0;
        i++;
    }
    return (i == n) && !b[i];
}

// ── ASCII strcmp (no CRT) ──────────────────────────────────────────
static int astr_equal(const char* a, const char* b) {
    while (*a && *a == *b) { a++; b++; }
    return *a == *b;
}

// ── PEB walk: find module by wide name ─────────────────────────────
static void* find_module(const WCHAR* name) {
    MY_LIST_ENTRY* head = &get_peb()->Ldr->InMemoryOrderModuleList;
    MY_LIST_ENTRY* curr = head->Flink;
    while (curr != head) {
        MY_LDR_DATA_TABLE_ENTRY* e = (MY_LDR_DATA_TABLE_ENTRY*)(
            (BYTE*)curr - offsetof(MY_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks));
        if (e->BaseDllName.Buffer &&
            wstr_iequal(e->BaseDllName.Buffer, e->BaseDllName.Length, name))
            return e->DllBase;
        curr = curr->Flink;
    }
    return NULL;
}

// ── Export directory walk: find function by ASCII name ─────────────
static void* get_export(void* base, const char* func) {
    BYTE* b = (BYTE*)base;
    IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(b +
        ((IMAGE_DOS_HEADER*)b)->e_lfanew);
    DWORD erva = nt->OptionalHeader.DataDirectory[0].VirtualAddress;
    if (!erva) return NULL;
    IMAGE_EXPORT_DIRECTORY* ed = (IMAGE_EXPORT_DIRECTORY*)(b + erva);
    DWORD* names = (DWORD*)(b + ed->AddressOfNames);
    WORD*  ords  = (WORD*) (b + ed->AddressOfNameOrdinals);
    DWORD* funcs = (DWORD*)(b + ed->AddressOfFunctions);
    for (DWORD i = 0; i < ed->NumberOfNames; i++) {
        if (astr_equal((char*)(b + names[i]), func))
            return (void*)(b + funcs[ords[i]]);
    }
    return NULL;
}

// ── Function pointer types ─────────────────────────────────────────
typedef HMODULE (WINAPI *fn_LoadLibraryA)  (LPCSTR);
typedef FARPROC (WINAPI *fn_GetProcAddress)(HMODULE, LPCSTR);
typedef int     (WINAPI *fn_MessageBoxA)   (HWND, LPCSTR, LPCSTR, UINT);
typedef void    (WINAPI *fn_ExitProcess)   (UINT);

// ── Entry point ────────────────────────────────────────────────────
void _start(void) {
    // 1. Find kernel32.dll via PEB walk — zero API calls
    void* k32 = find_module(L"kernel32.dll");

    // 2. Find LoadLibrary and GetProcAddress by parsing kernel32's exports
    fn_LoadLibraryA   pLoadLibrary   = (fn_LoadLibraryA)  get_export(k32, "LoadLibraryA");
    fn_GetProcAddress pGetProcAddress= (fn_GetProcAddress) get_export(k32, "GetProcAddress");
    fn_ExitProcess    pExitProcess   = (fn_ExitProcess)    get_export(k32, "ExitProcess");

    // 3. Now use LoadLibrary to bring in any DLL we need
    HMODULE user32 = pLoadLibrary("user32.dll");

    // 4. Resolve MessageBoxA from user32 via GetProcAddress
    fn_MessageBoxA pMessageBox = (fn_MessageBoxA)
        pGetProcAddress(user32, "MessageBoxA");

    // 5. Call it — this works from shellcode with ZERO imports
    pMessageBox(NULL, "PEB walk succeeded.", "Bootstrap", MB_OK);

    pExitProcess(0);
}

This program has no imports in its PE import directory. It finds and calls MessageBoxA — from a completely different DLL than kernel32 — entirely through runtime resolution. This is the foundation that every technique in this book builds on.

What PE-bear shows for this binary
  IMPORTS:   (empty — no import directory)
  EXPORTS:   (empty)
  SECTIONS:  .text  .rdata
  SIZE:      ~4 KB

  At runtime, the process memory looks like:
  ┌──────────────────────────────────────┐
  │  our shellcode/PE                    │
  │  [runs, walks GS:0x60 → PEB → Ldr]  │
  ├──────────────────────────────────────┤
  │  ntdll.dll   (always loaded by OS)   │
  ├──────────────────────────────────────┤
  │  kernel32.dll (always loaded by OS)  │  ← we find this via PEB
  ├──────────────────────────────────────┤
  │  user32.dll  (loaded by us)          │  ← we load this via LoadLibrary
  └──────────────────────────────────────┘
        

Module Load Order and Why Position Is Fragile

You may have read that kernel32.dll is "always the third entry" in the module list, or that ntdll.dll is "always first." This is approximately true for a vanilla Windows process — but it is fragile:

Hardcoding a position index (entry = list[2]) breaks on any of these cases. Comparing by name (as the implementation above does) is the correct approach. The name comparison overhead is negligible — the list typically has fewer than 30 entries.

What About ntdll.dll?

ntdll.dll is also always in the module list and is actually more useful than kernel32.dll for many purposes — it contains the actual syscall stubs (Parts 5 and 6 will use these heavily), the Rtl* utility functions, and Ldr* functions for manual library loading. You find it exactly the same way: find_module(L"ntdll.dll").

Many production implants resolve their entire API set from ntdll.dll alone, bypassing kernel32.dll entirely, because ntdll sits closer to the kernel and its functions are hooked less aggressively by most EDRs. You'll see this pattern throughout Parts 4 and 5.

Questions & Answers

Is GS:[0x60] documented? Will Microsoft ever change it?

It is not documented as a public API, but it is a de facto ABI that Microsoft cannot change without breaking every piece of software that uses it — including their own. The Windows loader itself uses this offset. The CRT startup code uses this offset. Compilers targeting Windows assume this offset. It has been stable since Windows XP and will remain stable indefinitely. The same guarantee applies to PEB.Ldr at offset 0x18 and the module list structure. These are effectively permanent.

Why not just call GetModuleHandle from a standard import?

In normal CRT-free code (Chapter 2) you can — import three functions from kernel32 and use GetModuleHandleA and GetProcAddress to resolve everything else. That's completely valid and is what most Windows programs do. The PEB walk becomes essential in two specific scenarios: (1) shellcode, where you have literally zero imports and the PE has no import directory at all, and (2) when you want to bypass EDR hooks that sit on top of GetModuleHandleA and GetProcAddress. By going directly to the PEB, you bypass those hooks entirely. Part 3 (injection) will use the PEB walk for exactly this reason.

What happens if the DLL I'm looking for isn't loaded yet?

find_module returns NULL. You can't walk the export directory of a DLL that hasn't been loaded. The solution depends on context: if you have kernel32.dll's base and can resolve LoadLibraryA, call it to load the DLL before searching for it. If you're in a restricted context where even LoadLibraryA isn't available (deep shellcode before any API resolution), you need to use LdrLoadDll from ntdll, which is always present. Part 2 (shellcode) shows the full self-contained version of this problem.

The export walk is O(n) — is that a performance problem?

No, not in practice. kernel32.dll exports around 1,500 functions. A linear scan comparing ASCII strings costs microseconds — immeasurable at human scale. The real cost would be if you called this function repeatedly in a hot loop, which you shouldn't — resolve your function pointers once at startup and cache them in local variables. That's what every real implant does: a short initialization phase that resolves all the APIs it needs, stores them in a struct or global function pointers, then uses those pointers for the rest of its lifetime.

What are forwarded exports and how do I handle them?

A forwarded export is when a DLL exports a name that actually redirects to a function in a different DLL. For example, kernel32.dll forwards HeapAlloc to ntdll.dll!RtlAllocateHeap on some Windows versions. You can detect a forwarded export because the function RVA falls within the export directory's own RVA range (not in the code section). When detected, you'd need to parse the forwarding string (it's in the format "dllname.FunctionName") and recursively resolve it. For the APIs you need early in bootstrap (LoadLibraryA, GetProcAddress, VirtualAlloc), this is not an issue — they are not forwarded. When building a general-purpose resolver (for a full reflective DLL loader in Part 3), you'll want to handle forwarding.