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

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.

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.

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.