OsderdaDev

Time:

UTC+3

W

Windows Dissecting the PEB in x86 and WoW64 Architectures

OsderdaDev
February 3, 2026
25 views

In the world of Windows systems programming, few structures are as critical, yet as undocumented, as the PEB (Process Environment Block). While Microsoft encourages developers to use standard Win32 APIs to get process information, there are times—during debugging, security research, or low-level optimization—when you need to go directly to the source.

However, the landscape of memory management changed drastically with the introduction of 64-bit Windows. Today, we aren't just dealing with x86 and x64; we are dealing with the hybrid beast known as WoW64 (Windows 32-bit on Windows 64-bit).

This article bridges the gap between high-level documentation and the raw memory structures that drive Windows processes.

What is the PEB?

The PEB is a data structure in user-mode memory that contains global variables used by the operating system and the loader. It includes:

* ImageBaseAddress: Where the executable is loaded in memory.

* Ldr: The Loader Data, a linked list of all loaded DLLs (modules).

* ProcessHeap: The location of the default heap.

Unlike Kernel structures (EPROCESS), the PEB resides in the address space of the application, meaning we can read it without a kernel driver.

The Entry Point: TEB to PEB

To find the PEB, we must first find the TEB (Thread Environment Block). The operating system places the TEB in a specific segment register depending on the architecture.

1. Native x86 (32-bit)

On a purely 32-bit system (or a 32-bit app), the TEB is located at FS:[0]. The pointer to the PEB is located at offset 0x30 of the TEB.

assembly
mov eax, fs:[0x30]  ; EAX now holds the pointer to the PEB

2. Native x64 (64-bit)

On a 64-bit system, the TEB is located at GS:[0]. The pointer to the PEB is located at offset 0x60 of the TEB.

assembly
mov rax, gs:[0x60]  ; RAX now holds the pointer to the PEB

The WoW64 Complexity: A Tale of Two PEBs

Here is where it gets fascinating. If you run a 32-bit application on a 64-bit version of Windows, it runs under the WoW64 subsystem.

To maintain compatibility, the OS actually creates two PEBs for the process:

  • The 32-bit PEB: Used by the 32-bit application code. This is what you find at FS:[0x30].
  • The 64-bit PEB: Used by the hidden 64-bit ntdll.dll that handles the transition (Heaven's Gate) to the 64-bit kernel.
  • If you are writing a debugger or a process inspector, grabbing the wrong PEB will lead to garbage data. For example, the 64-bit PEB contains the 64-bit modules (like the native ntdll), while the 32-bit PEB contains the modules your app actually "sees" (like kernel32.dll).

    Coding a PEB Walker in C++

    Let's write a robust C++ utility that can detect the current architecture and retrieve the ImageBaseAddress directly from the PEB, bypassing GetModuleHandle.

    We will use compiler intrinsics to read the segment registers safely.

    cpp
    #include <windows.h>
    #include <iostream>
    #include <intrin.h>
    
    // We need to define the PEB struct partially because it is opaque in standard headers.
    // Note: This is a simplified definition for demonstration.
    
    typedef struct _PEB32 {
        BYTE Reserved1[2];
        BYTE Reserved2[1];
        BYTE Reserved3[1];
        DWORD Reserved4[2];
        DWORD Ldr;
        DWORD ProcessParameters;
        DWORD Reserved5[3];
        DWORD AtlThunkSListPtr;
        DWORD Reserved6;
        DWORD Reserved7;
        DWORD ImageBaseAddress; // Offset 0x08 is usually ImageBase in simplified view, 
                                // but historically it is at 0x08 after Reserved3.
                                // Let's use the standard offsets for clarity below.
    } PEB32;
    
    // The layout changes slightly between OS versions, but the start is stable.
    // Safe offsets for ImageBaseAddress:
    // x86 PEB: Offset 0x08
    // x64 PEB: Offset 0x10
    
    void PrintPebInfo() {
        LPVOID pPeb = NULL;
        LPVOID imageBase = NULL;
        BOOL isWow64 = FALSE;
    
        // Check if we are a 32-bit process on 64-bit OS
        IsWow64Process(GetCurrentProcess(), &isWow64);
    
        #ifdef _WIN64
            // --- 64-BIT NATIVE CONTEXT ---
            // Read GS:[0x60] to get the 64-bit PEB
            pPeb = (LPVOID)__readgsqword(0x60);
            
            // In 64-bit PEB, ImageBaseAddress is at offset 0x10
            // We cast to char* for byte-wise arithmetic
            imageBase = *(LPVOID*)((char*)pPeb + 0x10);
    
            std::cout << "[x64 Mode]" << std::endl;
            std::cout << "PEB Address: " << pPeb << std::endl;
            std::cout << "Image Base:  " << imageBase << std::endl;
    
        #else
            // --- 32-BIT CONTEXT ---
            // Read FS:[0x30] to get the 32-bit PEB
            pPeb = (LPVOID)__readfsdword(0x30);
    
            // In 32-bit PEB, ImageBaseAddress is at offset 0x08
            imageBase = *(LPVOID*)((char*)pPeb + 0x08);
    
            std::cout << "[x86 Mode]" << std::endl;
            if (isWow64) {
                std::cout << "(Running under WoW64)" << std::endl;
            }
            std::cout << "PEB Address: " << pPeb << std::endl;
            std::cout << "Image Base:  " << imageBase << std::endl;
            
            // Accessing the 64-bit PEB from a 32-bit app is possible 
            // but requires querying NtQueryInformationProcess with ProcessWow64Information.
        #endif
    
        // Verify against the Win32 API
        HMODULE hMod = GetModuleHandle(NULL);
        std::cout << "Win32 API says:" << hMod << std::endl;
        
        if (hMod == imageBase) {
            std::cout << ">> SUCCESS: Manual PEB parsing matches Win32 API." << std::endl;
        } else {
            std::cout << ">> ERROR: Mismatch detected." << std::endl;
        }
    }
    
    int main() {
        PrintPebInfo();
        std::cin.get();
        return 0;
    }

    Accessing the "Other" PEB

    If you are writing a debugger, you often need to access the target process's PEB.

  • Same Architecture: Use NtQueryInformationProcess with ProcessBasicInformation. This fills a struct containing PebBaseAddress.
  • x64 Debugging x86 (WoW64): You call NtQueryInformationProcess with ProcessWow64Information. This returns the address of the 32-bit PEB inside the 64-bit address space.
  • *Note: Attempting to read the 64-bit PEB from a 32-bit process is generally blocked by the OS boundaries and requires complex "Heaven's Gate" thunking techniques, which are beyond standard software engineering practices.*

    Practical Application: Module Hiding

    Why do we care about the loader list in the PEB?

    When GetModuleHandle is called, it simply walks the linked list in the PEB (PEB->Ldr->InMemoryOrderModuleList).

    In the context of malware analysis or security engineering, a common technique called "unlink" involves manually removing a node from this linked list. The DLL remains in memory, functions work, but GetModuleHandle returns NULL, and tools like Task Manager won't list the DLL. Understanding the PEB is the first step in detecting such anomalies.

    Conclusion

    The Process Environment Block is the cornerstone of user-mode execution on Windows. By understanding the distinction between the TEB and PEB, and how the OS manages these structures across the 32-bit/64-bit divide, developers gain the ability to debug complex startup issues, perform security audits, and optimize low-level memory operations without relying solely on the higher-level Win32 API wrappers.

    Osderda

    Senior Software Engineer

    With a diverse background in Windows system programming, scalable Backend infrastructure, native Android development, and server management, I strive to bridge the gap between different technology stacks. My goal is to simplify complex coding concepts and share actionable knowledge through concise, technical documentation.

    Discussion (0)