Geoff Chappell, Software Analyst
CURRENT WORK ITEM - PREVIEW ONLY
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. If a process has no user-mode footprint, it has no PEB. If only in principle, if anything about a process can be properly managed in user mode without needing a transition to kernel mode, it goes in the PEB.
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 programming, Microsoft has all along 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 an Internet Explorer DLL, shows knowledge not just of the PEB but of the TEB too and suggests that the programmers had access to otherwise private headers (if not to use them in their build, then at least to reproduce from them).
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 ProcessBasicInformation case of the NtQueryInformationProcess function. This is exported by NTDLL in all known versions. It 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 process’s own threads—and writing to the queried process’s PEB certainly requires such synchronisation. In consequence, accessing another process’s PEB is beyond many programers who attempt it, e.g., for malware and for supposedly helpful system tools.
In an ideal world, the PEB might be opaque outside the kernel and NTDLL. 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.
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—at least to know about when debugging—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.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 NTDLL are seen to make of the PEB. Exhaustively tracking down all such use would be difficult, if not impossible, even with source code.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x00 | 0x00 |
BOOLEAN InheritedAddressSpace; |
3.51 and higher |
0x01 | 0x01 |
BOOLEAN ReadImageFileExecOptions; |
3.51 and higher |
0x02 | 0x02 |
BOOLEAN BeingDebugged; |
3.51 and higher |
Of the original members, the first to get documented was BeingDebugged. The ancient (documented) KERNEL32 function IsDebuggerPresent has always done nothing more than read this member from the current PEB.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x03 | 0x03 |
UCHAR SpareBool; |
3.51 to early 5.2 |
union { UCHAR BitField; struct { /* bit fields, see below */ }; }; |
late 5.2 and higher |
A boolean in space caused by an alignment requirement might have been left undefined but was instead explicitly labelled as spare (if not from the start, then at least by the versions for which the symbol files have type information). It 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 at the end of the article, 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 |
---|---|---|---|
0x04 | 0x08 |
PVOID Mutant; |
3.51 and higher |
0x08 | 0x10 |
PVOID ImageBaseAddress; |
3.51 and higher |
0x0C | 0x18 |
PEB_LDR_DATA *Ldr; |
3.51 and higher |
0x10 | 0x20 |
RTL_USER_PROCESS_PARAMETERS *ProcessParameters; |
3.51 and higher |
0x14 | 0x28 |
PVOID SubSystemData; |
3.51 and higher |
0x18 | 0x30 |
HANDLE ProcessHeap; |
3.51 and higher |
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 the ProcessHeap from the current PEB, but very many Microsoft programs and DLLs instead read ProcessHeap by themselves.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x1C | 0x38 |
PVOID FastPebLock; |
3.51 to 5.0 |
RTL_CRITICAL_SECTION *FastPebLock; |
5.1 and higher | ||
0x20 | 0x40 |
PVOID FastPebLockRoutine; |
3.51 to 5.1 |
PVOID SparePtr1; |
early 5.2 only | ||
PVOID AtlThunkSListPtr; |
late 5.2 and higher | ||
0x24 | 0x48 |
PVOID FastPebUnlockRoutine; |
3.51 to 5.1 |
PVOID SparePtr2; |
5.2 only | ||
PVOID IFEOKey; |
6.0 and higher |
In early versions, NTDLL supports its exported 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. The lock does happen to be a critical section and the routines are just the expected RtlEnterCriticalSection and RtlLeaveCriticalSection. Still, not until version 5.1 is the lock’s nature formalised in the PEB and not until version 5.2 does NTDLL fix on the routines so that their addresses aren’t saved in the PEB.
You might wonder why they ever were saved in the PEB. After all, the exported functions ought to suffice for user-mode code that’s outside NTDLL and wants to synchronise its access to the PEB. What fascinates me, and prompts this lengthy digression, is that only use I know of FastPebLock from outside NTDLL is in kernel mode. Moreover, it also uses the long-gone FastPebLockRoutine and FastPebUnlockRoutine members. Up to and including Windows XP, the kernel calls through these pointers—yes, such that the kernel executes NTDLL code at its user-mode address but in ring 0—to synchronise its proposed access to the PEB with access by other threads (most likely in user mode). Its only known use of this trick is when the exported (and documented) function RtlQueryRegistryValues expands 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, see below */ }; }; |
6.0 and higher | ||
0x2C | 0x58 |
PVOID KernelCallbackTable; |
3.51 to 5.2 |
union { PVOID KernelCallbackTable; PVOID UserSharedInfoPtr; }; |
6.0 and higher |
Yes, there is a plan to write something here.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x30 | 0x60 |
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 and early 5.2 |
ULONG SpareUlong; |
late 5.2 to 6.0 | ||
ULONG AtlThunkSListPtr32; |
late 5.1, and 6.1 and higher |
Yes, there is a plan to write something here.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x38 | 0x68 |
PEB_FREE_BLOCK *FreeList; |
3.51 to early 6.0 |
ULONG SparePebPtr0; |
late 6.0 only | ||
PVOID ApiSetMap; |
6.1 and higher |
Yes, there is a plan to write something here.
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x3C | 0x70 |
ULONG TlsExpansionCounter; |
3.51 and higher |
0x40 | 0x78 |
PVOID TlsBitmap; |
3.51 and higher |
0x44 | 0x80 |
ULONG TlsBitmapBits [2]; |
3.51 and higher |
0x4C | 0x88 |
PVOID ReadOnlySharedMemoryBase; |
3.51 and higher |
0x50 | 0x90 |
PVOID ReadOnlySharedMemoryHeap; |
3.51 to 5.2 |
PVOID HotpatchInformation; |
6.0 and higher | ||
0x54 | 0x98 |
PVOID *ReadOnlyStaticServerData; |
3.51 and higher |
0x58 | 0xA0 |
PVOID AnsiCodePageData; |
3.51 and higher |
0x5C | 0xA8 |
PVOID OemCodePageData; |
3.51 and higher |
0x60 | 0xB0 |
PVOID UnicodeCaseTableData; |
3.51 and higher |
0x64 | 0xB8 |
ULONG NumberOfProcessors; |
3.51 and higher |
0x68 | 0xBC |
ULONG NtGlobalFlag; |
3.51 and higher |
0x70 | 0xC0 |
LARGE_INTEGER CriticalSectionTimeout; |
3.51 and higher |
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 |
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 only |
RTL_CRITICAL_SECTION *LoaderLock; |
5.0 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 |
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 was one of the two PEB members that Microsoft documented when required to disclose use of internal APIs by so-called middleware.
Insertion of three members for Windows XP produces the first known cases of members whose offset varies between versions:
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 |
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 |
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 |
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x0230 | 0x0358 |
PVOID WerRegistrationData; |
6.0 and higher |
0x0234 | 0x0360 |
PVOID WerShipAssertPtr; |
6.0 and higher |
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, see below */ }; }; |
6.1 and higher |
Offset (x86) | Offset (x64) | Definition | Versions |
---|---|---|---|
0x0248 | 0x0380 |
ULONGLONG CsrServerReadOnlySharedMemoryBase; |
6.2 and higher |
The build of version 5.2 for Windows Server 2003 SP1 defined bit fields in what had been a SpareBool at offset 0x03. A single byte, named BitField, overlays the bits.
Mask | Definition | Versions |
---|---|---|
0x01 |
UCHAR ImageUsedLargePages : 1; |
late 5.2 and higher |
0x02 |
UCHAR IsProtectedProcess : 1; |
6.0 and higher |
0x04 (6.0 to 6.2) |
UCHAR IsLegacyProcess : 1; |
6.0 to 6.2 |
0x08 (6.0 to 6.2); 0x04 |
UCHAR IsImageDynamicallyRelocated : 1; |
6.0 and higher |
0x10 (late 6.0 to 6.2); 0x08 |
UCHAR SkipPatchingUser32Forwarders : 1; |
late 6.0 and higher |
0x20 (6.2); 0x10 |
UCHAR IsPackagedProcess : 1; |
6.2 and higher |
0x40 (6.2); 0x20 |
UCHAR IsAppContainer: 1; |
6.2 and higher |
0x40 |
UCHAR IsProtectedProcessLight : 1; |
6.3 and higher |
UCHAR SpareBits : 7; |
late 5.2 only | |
UCHAR SpareBits : 4; |
early 6.0 only | |
UCHAR SpareBits : 3; |
late 6.0 to 6.1 | |
UCHAR SpareBits : 1; |
6.2 and higher |
Mask | Definition | Versions |
---|---|---|
0x00000001 |
ULONG ProcessInJob : 1; |
6.0 and higher |
0x00000002 |
ULONG ProcessInitializing : 1; |
6.0 and higher |
0x00000004 |
ULONG ProcessUsingVEH : 1; |
6.1 and higher |
0x00000008 |
ULONG ProcessUsingVCH : 1; |
6.1 and higher |
0x00000010 |
ULONG ProcessUsingFTH : 1; |
6.1 and higher |
ULONG ReservedBits0 : 30; |
6.0 only | |
ULONG ReservedBits0 : 27; |
6.1 and higher |
Mask | Definition | Versions |
---|---|---|
0x00000001 |
ULONG HeapTracingEnabled : 1; |
6.1 and higher |
0x00000002 |
ULONG CritSecTracingEnabled : 1; |
6.1 and higher |
0x00000004 |
ULONG LibLoaderTracingEnabled : 1; |
6.2 and higher |
ULONG SpareTracingBits : 30; |
6.1 only | |
ULONG SpareTracingBits : 29; |
6.2 and higher |