Parsing the PEB Without windows.h

Walking the Process Environment Block by hand to resolve exports — the foundational technique behind shellcode and reflective loaders.

Parsing the PEB Without windows.h

When writing shellcode or a reflective DLL loader, you can’t rely on windows.h or the linker resolving your import table. You need to locate kernel32 and ntdll yourself — and the canonical approach is walking the PEB.

What the PEB Is

The Process Environment Block is a user-mode structure Windows populates for every process. It lives at a well-known offset from the Thread Environment Block (TEB), which is always reachable via the GS segment register on x64:

// x64: GS:[0x60] points to the PEB
PPEB pPEB = (PPEB)__readgsqword(0x60);

Inside the PEB, Ldr points to a PEB_LDR_DATA structure containing three doubly-linked lists of all loaded modules. We care about InMemoryOrderModuleList.

Walking the Module List

typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID          DllBase;
    PVOID          EntryPoint;
    ULONG          SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
    // ...
} LDR_DATA_TABLE_ENTRY;

PLIST_ENTRY head = &pPEB->Ldr->InMemoryOrderModuleList;
PLIST_ENTRY curr = head->Flink;

while (curr != head) {
    PLDR_DATA_TABLE_ENTRY entry = CONTAINING_RECORD(
        curr,
        LDR_DATA_TABLE_ENTRY,
        InMemoryOrderLinks
    );
    // entry->DllBase  = base address of this module
    // entry->BaseDllName = module name as UNICODE_STRING
    curr = curr->Flink;
}

Hashing Module Names

Comparing wide-character strings directly would require a wcscmp equivalent — which you haven’t resolved yet. The standard trick is to pre-compute a hash and compare hashes as you walk:

DWORD djb2_wide(PWSTR str) {
    DWORD hash = 5381;
    while (*str) {
        WCHAR c = *str++;
        if (c >= L'A' && c <= L'Z') c |= 0x20; // tolower
        hash = ((hash << 5) + hash) ^ (DWORD)c;
    }
    return hash;
}

Once you’ve located a module’s base address, parse its PE export directory to resolve function pointers — but that’s a topic for the next post.

Why This Matters

Every modern shellcode loader, reflective injector, and position-independent implant starts here. Understanding the PEB walk reveals how injected code bootstraps itself before it can call a single Windows API function.