PEB

The Process Environment Block (PEB) is a process’s user-mode representation. It has the highest-level knowledge of a process in kernel mode and the lowest-level in user mode. The PEB is created by the kernel but is mostly operated on from user mode. If a (system) process has no user-mode footprint, it has no PEB. If only in principle, if anything about a process is shared with kernel mode but can be properly managed in user mode without needing a transition to kernel mode, it goes in the PEB. If anything about a process might usefully be shared between user-mode modules, then it’s at least a candidate for going in the PEB for easy access. Very much more in principle than in practice, data may go into the PEB for sharing between processes more easily than by any formal inter-process communication.

Access

User-mode code can easily find its own process’s PEB, albeit only by using undocumented or semi-documented behaviour. While a thread executes in user mode, its fs or gs register, for 32-bit and 64-bit code respectively, is loaded with a selector for a segment whose base address is that of the thread’s TEB. That structure’s ProcessEnvironmentBlock member holds the address of the current process’s PEB. In NTDLL version 5.1 and higher, this simple work is available more neatly as an exported function, named RtlGetCurrentPeb, but it too is undocumented. Its implementation is something very like

PEB *RtlGetCurrentPeb (VOID)
{
    return NtCurrentTeb () -> ProcessEnvironmentBlock;
}

For its own low-level user-mode programming, Microsoft has long had a macro or inline function, apparently named NtCurrentPeb, which reads directly from fs or gs, e.g.,

PEB *NtCurrentPeb (VOID)
{
    return (PEB *) __readfsdword (FIELD_OFFSET (TEB, ProcessEnvironmentBlock));
}

The difference scarcely matters at run time but has forensic significance because use of the latter in a high-level module, e.g., for MSHTML.DLL from Internet Explorer 6, not only shows that the programmers had undocumented knowledge of the PEB and TEB but also suggests they had access to otherwise private headers (if not to use them in their build, then at least to reproduce from them).

Other Processes

User-mode code can less easily access the PEB of any process for which it has a handle and sufficient access rights. The gatekeeper is the NtQueryInformationProcess function. This is exported by NTDLL in all known Windows versions. Its ProcessBasicInformation case fills a PROCESS_BASIC_INFORMATION structure whose member named PebBaseAddress is, unsurprisingly, the address of the queried process’s PEB. Of course, the address thus obtained is not directly usable. It is meaningful in the queried process’s address space. Even just to read that process’s PEB then requires such functions as ReadProcessMemory and the corresponding permission. To do much with what’s read may require synchronisation with or defence against changes being made by the queried process’s own threads—and writing to the queried process’s PEB certainly requires such synchronisation. In consequence, safe use of another process’s PEB is beyond many programers who attempt it, e.g., for malware and more notably for some of what gets foisted onto consumers as anti-malware or merely recommended to them as supposedly helpful system tools.

Documentation Status

In an ideal world, the PEB might be opaque outside the kernel and a few low-level user-mode modules such as NTDLL and KERNEL32. But, as noted in remarks above about forsensic signfiicance, various high-level modules supplied with Windows over the years have used a few members of the PEB, and this eventually had to be disclosed. A new header, named WINTERNL.H, for previously internal APIs was added to the Software Development Kit (SDK) apparently in 2002, and remains to this day. It originally presented a modified PEB that has just the BeingDebugged and SessionId members, plus padding that gets these members to the same offsets as in the true structure. More members have been included in this modified PEB over the years: Ldr, ProcessParameters and PostProcessInitRoutine in the SDK for Windows 7; and AtlThunkSListPtr and AtlThunkSListPtr32 in the SDK for Windows 8. Notwithstanding the header’s warnings, it seems unlikely that Microsoft will change the PEB in any way that moves any of these members.

Layout

Indeed, the PEB is highly stable across Windows versions. When members fall out of use the space they occupied tends to be left in place, often to be reused eventually, but without shifting other members. Many members that are useful—to know about not just when debugging but also when studying malware—have kept their positions through all the known history. The PEB has grown mostly by adding new members at its end. The following sizes are known (with caveats that follow the table):

Version Size (x86) Size (x64)
3.10 to 3.50 0x70  
3.51 0x98  
4.0 0x0150  
5.0 0x01E8  
5.1 0x0210  
5.2 0x0230 0x0358
6.0 0x0238 0x0368
6.1 0x0248 0x0380
6.2 to 10.0 0x0250 0x0388

These sizes, and the offsets, types and names in the tables that follow, are from Microsoft’s symbol files for the kernel starting with Windows 2000 SP3 and for NTDLL starting with Windows XP, but are something of a guess for earlier versions since the symbol files for these do not contain type information for the PEB. What’s known of Microsoft’s names and types for earlier versions is instead inferred from what use NTOSKRNL and various low-level user-mode modules such as NTDLL are seen to make of the PEB. Exhaustively tracking down all such use would be difficult, if not impossible, even with source code.

There is in addition the difficulty that my holdings of the earliest versions are incomplete. Not only do I miss some service packs before Windows NT 4.0 SP3 but in the only copy I have obtained of any service pack of Windows NT 3.50 several DLLs, including NTDLL.DLL, are actually version 3.10.

Original (More or Less)

The very first member is arguably too much overlooked, given that so many programmers with backgrounds in Unix seem to think that assessment of Windows as an operating system begins and ends with whether Windows truly can fork a process.

No use is known of the other bytes at the start in version 3.10. They are here thought to have been unlabelled alignment space until whichever of versions 3.50 and 3.51 defined the next two booleans. The (documented) KERNEL32 function IsDebuggerPresent does nothing more than read BeingDebugged from the current PEB. Whether the byte at offset 0x03 was labelled explicitly as spare concurrently with definition of the two at offsets 0x01 and 0x02 is not certain but is at least plausible. It anyway never was used as a boolean but started getting used as bit fields in the build of version 5.2 that first put the CPU’s support for large pages to use as an efficiency for executable images. The individual bits are presented separately, description being complicated because Windows 8.1 deleted one of them (IsLegacyProcess) and thus changed the masks for accessing the others.

Offset (x86) Offset (x64) Definition Versions
0x00 0x00
BOOLEAN InheritedAddressSpace;
all
0x01 0x01
BOOLEAN ReadImageFileExecOptions;
3.51 and higher
0x02 0x02
BOOLEAN BeingDebugged;
3.51 and higher
0x03 0x03
BOOLEAN SpareBool;
3.51 to early 5.2
union {
    UCHAR BitField;
    struct {
        /*  bit fields, follow link  */
    };
};
late 5.2 and higher
0x04 0x08
PVOID Mutant;
all
0x08 0x10
PVOID ImageBaseAddress;
all
0x0C 0x18
PEB_LDR_DATA *Ldr;
all
0x10 0x20
RTL_USER_PROCESS_PARAMETERS *ProcessParameters;
all
0x14 0x28
PVOID SubSystemData;
all
0x18 0x30
HANDLE ProcessHeap;
all

Of the original PEB members, Ldr and ProcessParameters are arguably the most used by Microsoft’s higher-level modules and Microsoft eventually included them in the reduced PEB that’s published in WINTERN.H for all the world to know about. The ProcessHeap can’t be far behind, however: the ancient (documented) KERNEL32 function GetProcessHeap has always done nothing more than read ProcessHeap from the current PEB, but very many Microsoft programs and DLLs instead read ProcessHeap by themselves (as if GetProcessHeap in inlined for their use).

At the other extreme, the SubSystemData is about as obscure as anything gets in Windows programming for ordinary purposes. As its name suggests, it is intended for subsystems that don’t have enough of Microsoft’s attention to justify defining their own members in the PEB itself. A subsystem, such as supported by PSXDLL.DLL, can point SubSystemData at its own collection of per-process data.

Offset (x86) Offset (x64) Definition Versions
0x1C 0x38
PVOID FastPebLock;
3.10 to 5.0
RTL_CRITICAL_SECTION *FastPebLock;
5.1 and higher
0x20 0x40
PVOID FastPebLockRoutine;
3.10 to 5.1
PVOID SparePtr1;
early 5.2 only
PVOID AtlThunkSListPtr;
late 5.2 and higher
0x24 0x48
PVOID FastPebUnlockRoutine;
3.10 to 5.1
PVOID SparePtr2;
5.2 only
PVOID IFEOKey;
6.0 and higher

In early versions, NTDLL supports its exported (undocumented) RtlAcquirePebLock and RtlReleasePebLock functions by storing in the PEB the addresses not just of a FastPebLock variable in the NTDLL data but of two routines for acquiring and releasing whatever is the lock. Though it does happen that the lock is a critical section and the routines are just the expected RtlEnterCriticalSection and RtlLeaveCriticalSection, not until version 5.1 is the lock’s nature formalised in the PEB and not until version 5.2 does NTDLL stop saving the routines’ addresses in the PEB

You might wonder why they ever were saved in the PEB. After all, the RtlAcquirePebLock and RtlReleasePebLock functions ought to suffice for Microsoft’s user-mode code that’s outside NTDLL and wants to synchronise its access to the PEB with access by other threads in the same process. What fascinates me, and prompts this digression, is that the only use I know of FastPebLock from outside NTDLL is in kernel mode. Moreover, it also uses the long-gone FastPebLockRoutine and FastPebUnlockRoutine members. Go back far enough and this is done by linking the exact same implementations of the RtlAcquirePebLock and RtlReleasePebLock functions into both NTDLL and the kernel—yes, with the kernel finding the PEB from the TEB, in turn found from the fs register as described above. Version 5.1 re-implemented so that the kernel instead progresses through structures that have no user-mode susceptibility, thus from the fs register to the KPCR to the KTHREAD to the EPROCESS for its pointer to the PEB. If this change was motivated by thoughts of security, it was worse than pointless because the kernel does not just follow the FastPebLockRoutine and FastPebUnlockRoutine pointers in the PEB but calls through them to execute (what is hoped to be) NTDLL code at its user-mode address. Do not miss that whatever is there gets executed with ring 0 privilege.

This trick that is plainly too clever for anyone’s good had extensive use in the very earliest versions. Among the reasons the kernel would access the PEB in ways that needed synchronisation with access by other threads (most likely in user mode) were such things as the kernel allocating from and freeing to the process heap. Even as late as version 5.1, this execution of user-mode code with kernel-mode prvilege was still being done for the exported (and documented) function RtlQueryRegistryValues to expand environment variables whose names are found between percent signs in registry data that has the REG_EXPAND_SZ type.

Offset (x86) Offset (x64) Definition Versions
0x28 0x50
ULONG EnvironmentUpdateCount;
3.51 to 5.2
union {
    ULONG CrossProcessFlags;
    struct {
        /*  bit fields, follow link  */
    };
};
6.0 and higher
0x2C 0x58
PVOID KernelCallbackTable;
3.51 to 5.2
union {
    PVOID KernelCallbackTable;
    PVOID UserSharedInfoPtr;
};
6.0 and higher
0x28 (3.10 to 3.50);
0x30
0x60
ULONG SystemReserved [4];
3.10 to 3.50
ULONG SystemReserved [2];
3.51 to 5.0
ULONG SystemReserved [1];
5.1 and higher
0x34 0x64
struct {
    ULONG ExecuteOptions : 2;
    ULONG SpareBits : 30;
};
early 5.1;
early 5.2
ULONG SpareUlong;
late 5.2 to 6.0
ULONG AtlThunkSListPtr32;
late 5.1;
6.1 and higher

In those versions that have it, the EnvironmentUpdateCount is incremented when an attempt to set the current directory gets as far as NTDLL’s RtlSetCurrentDirectory_U function. What this has to do with any sort of environment is not known. Windows Vista anyway replaced this counter with a set of flags.

What KernelCallbackTable points to is an array of function pointers to support the exported (undocumented) KiUserCallbackDispatcher function. This is one of the relatively few functions that NTDLL exports not to be imported by other user-mode modules but to be found by the kernel. The function is called by the kernel when a driver, typically WIN32K.SYS, calls the kernel export KeUserModeCallback. Of course, the NTDLL function is not actually called by the kernel. It instead becomes the target address for the kernel’s exit from ring 0 to ring 3. Still, KiUserCallbackDispatcher perceives that it has been called and that among its arguments is an index into the KernelCallbackTable. This selects where further to dispatch the execution deeper into user mode. Getting back to kernel mode with the appearance of returning from a call to user mode is important enough to have a dedicated interrupt number, 0x2B.

The array of function pointers that is the KernelCallbackTable is set into place by USER32.DLL during its initialisation, but not until after USER32 connects to the CSRSS server. Starting with version 6.0, if the process is a so-called protected process, the KernelCallbackTable pointer is first put to double duty as the UserSharedInfoPtr. Just while connecting, it becomes a side-channel for receiving a SHAREDINFO structure directly from WIN32K.SYS.

For neither EnvironmentUpdateCount nor KernelCallbackTable is any earlier use yet known. Earlier use of KernelCallbackTable would have to be very different since the kernel has no KeUserModeCallback function before version 3.51. It therefore seems more than merely plausible that the explicit reservation of two dwords immediately after these members, as known from symbol files for late service packs of Windows 2000, started as four.

Windows XP and Windows Server 2003 got into some sort of tussle about using the last of the previously reserved dwords. The ExecuteOptions certainly are used in the early releases of both. These two bits do not, however, have the same meaning as later flags for the Data Execution Prevention (DEP) that came with the late builds of these versions. They are concerned instead with checking for stack overflow.

Offset (x86) Offset (x64) Definition Versions
0x38 0x68
PEB_FREE_BLOCK *FreeList;
3.10 to early 6.0
ULONG SparePebPtr0;
late 6.0 only
PVOID ApiSetMap;
6.1 and higher

The PEB_FREE_BLOCK is simply a pointer to the Next of its type, presumably to make a single-linked list, and a 32-bit unsigned Size. The suggestion is of caching freed memory, but although FreeList is defined in symbol files, no use is known of it in any version. The ApiSetMap that replaces it is the process’s pointer to the kernel’s representation of the API Set Schema of redirections that NTDLL is to apply when loading DLLs. What the kernel points ApiSetMap to is a read-only mapping into the process’s address space. Pointing ApiSetMap elsewhere would seem to be not just possible but attractive, whether for mischief or for the supposedly well-intentioned intrusiveness of security tools as an alternative to hooking API functions by such techniques as patching code.

All the remaining members that are shown for version 3.10 and higher certainly were in use from the beginning.

Offset (x86) Offset (x64) Definition Versions
0x3C 0x70
ULONG TlsExpansionCounter;
all
0x40 0x78
PVOID TlsBitmap;
all
0x44 0x80
ULONG TlsBitmapBits [2];
all
0x4C 0x88
PVOID ReadOnlySharedMemoryBase;
all
0x50 0x90
PVOID ReadOnlySharedMemoryHeap;
3.10 to 5.2
PVOID HotpatchInformation;
6.0 and higher
0x54 0x98
PVOID *ReadOnlyStaticServerData;
all
0x58 0xA0
PVOID AnsiCodePageData;
all
0x5C 0xA8
PVOID OemCodePageData;
all
0x60 0xB0
PVOID UnicodeCaseTableData;
all
0x64 0xB8
ULONG NumberOfProcessors;
3.51 and higher
0x68 0xBC
ULONG NtGlobalFlag;
3.51 and higher
0x68 (3.10 to 3.50);
0x70
0xC0
LARGE_INTEGER CriticalSectionTimeout;
all

See that version 3.51 didn’t just append new members but instead inserted two, such that CriticalSectionTimeout becomes the first known case of any PEB member shifting between versions.

Appended for Windows NT 3.51

Offset (x86) Offset (x64) Definition Versions
0x78 0xC8
ULONG_PTR HeapSegmentReserve;
3.51 and higher
0x7C 0xD0
ULONG_PTR HeapSegmentCommit;
3.51 and higher
0x80 0xD8
ULONG_PTR HeapDeCommitTotalFreeThreshold;
3.51 and higher
0x84 0xE0
ULONG_PTR HeapDeCommitFreeBlockThreshold;
3.51 and higher
0x88 0xE8
ULONG NumberOfHeaps;
3.51 and higher
0x8C 0xEC
ULONG MaximumNumberOfHeaps;
3.51 and higher
0x90 0xF0
PVOID *ProcessHeaps;
3.51 and higher
0x94 0xF8
PVOID GdiSharedHandleTable;
3.51 and higher

Appended for Windows NT 4.0

Offset (x86) Offset (x64) Definition Versions
0x98 0x0100
PVOID ProcessStarterHelper;
4.0 and higher
0x9C 0x0108
ULONG GdiDCAttributeList;
4.0 and higher
0xA0 0x0110
PVOID LoaderLock;
4.0 to 5.1
RTL_CRITICAL_SECTION *LoaderLock;
5.2 and higher
0xA4 0x0118
ULONG OSMajorVersion;
4.0 and higher
0xA8 0x011C
ULONG OSMinorVersion;
4.0 and higher
0xAC 0x0120
USHORT OSBuildNumber;
4.0 and higher
0xAE 0x0122
USHORT OSCSDVersion;
4.0 and higher
0xB0 0x0124
ULONG OSPlatformId;
4.0 and higher
0xB4 0x0128
ULONG ImageSubsystem;
4.0 and higher
0xB8 0x012C
ULONG ImageSubsystemMajorVersion;
4.0 and higher
0xBC 0x0130
ULONG ImageSubsystemMinorVersion;
4.0 and higher
0xC0 0x0138
KAFFINITY ImageProcessAffinityMask;
4.0 to early 6.0
KAFFINITY ActiveProcessAffinityMask;
late 6.0 and higher
0xC4 0x0140
ULONG GdiHandleBuffer [0x22];
4.0 and higher (x86)
ULONG GdiHandleBuffer [0x3C];
4.0 and higher (x64)
0x014C 0x0230
VOID (*PostProcessInitRoutine) (VOID);
4.0 and higher

Appended for Windows 2000

Offset (x86) Offset (x64) Definition Versions
0x0150 0x0238
PVOID TlsExpansionBitmap;
5.0 and higher
0x0154 0x0240
ULONG TlsExpansionBitmapBits [0x20];
5.0 and higher
0x01D4 0x02C0
ULONG SessionId;
5.0 and higher

The SessionId is one of the two PEB members that Microsoft documented when required to disclose use of internal APIs by so-called middleware.

Insertion of the next three members for Windows XP produces the last known case of members whose offset varies between versions. Don’t miss the irony that this was done in the name of application compatibility.

Offset (x86) Offset (x64) Definition Versions
0x01D8 0x02C8
ULARGE_INTEGER AppCompatFlags;
5.1 and higher
0x01E0 0x02D0
ULARGE_INTEGER AppCompatFlagsUser;
5.1 and higher
0x01E8 0x02D8
PVOID pShimData;
5.1 and higher
0x01D8 (5.0);
0x01EC
0x02E0
PVOID AppCompatInfo;
5.0 and higher
0x01DC (5.0);
0x01F0
0x02E8
UNICODE_STRING CSDVersion;
5.0 and higher

Appended for Windows XP

Offset (x86) Offset (x64) Definition Versions
0x01F8 0x02F8
ACTIVATION_CONTEXT_DATA const *ActivationContextData;
5.1 and higher
0x01FC 0x0300
ASSEMBLY_STORAGE_MAP *ProcessAssemblyStorageMap;
5.1 and higher
0x0200 0x0308
ACTIVATION_CONTEXT_DATA const *SystemDefaultActivationContextData;
5.1 and higher
0x0204 0x0310
ASSEMBLY_STORAGE_MAP *SystemAssemblyStorageMap;
5.1 and higher
0x0208 0x0318
ULONG MinimumStackCommit;
5.1 and higher

Appended for Windows Server 2003

Offset (x86) Offset (x64) Definition Versions
0x020C 0x0320
FLS_CALLBACK_INFO *FlsCallback;
5.2 and higher
0x0210 0x0328
LIST_ENTRY FlsListHead;
5.2 and higher
0x0218 0x0338
PVOID FlsBitmap;
5.2 and higher
0x021C 0x0340
ULONG FlsBitmapBits [4];
5.2 and higher
0x022C 0x0350
ULONG FlsHighIndex;
5.2 and higher

Appended for Windows Vista

Offset (x86) Offset (x64) Definition Versions
0x0230 0x0358
PVOID WerRegistrationData;
6.0 and higher
0x0234 0x0360
PVOID WerShipAssertPtr;
6.0 and higher

Appended for Windows 7

Offset (x86) Offset (x64) Definition Versions
0x0238 0x0368
PVOID pContextData;
6.1 only
PVOID pUnused;
6.2 and higher
0x023C 0x0370
PVOID pImageHeaderHash;
6.1 and higher
0x0240 0x0378
union {
    ULONG TracingFlags;
    struct {
        /*  bit fields, follow link  */
    };
};
6.1 and higher

Appended for Windows 8

Offset (x86) Offset (x64) Definition Versions
0x0248 0x0380
ULONGLONG CsrServerReadOnlySharedMemoryBase;
6.2 and higher