PEB Internals: What the Process Environment Block Reveals and Why Defenders Care
Every process on Windows has a Process Environment Block (PEB). Most developers never interact with it directly - the Win32 API abstracts everything away. But if you’re doing malware analysis, EDR engineering, or any kind of defensive work, the PEB comes up constantly.
1. What Is the PEB?
The PEB is a user-mode structure that the Windows kernel creates for every process. It lives in the process’s own address space, readable from user mode without any system calls. This is by design - many common operations (checking if the debugger is attached, enumerating loaded modules, reading environment variables) need this data without the overhead of a syscall.
The PEB is accessible through the TEB (Thread Environment Block), which itself is pointed to by the GS segment register on x64:
// x64: TEB is at GS:[0x30], PEB is at TEB+0x60
PEB* peb = (PEB*)__readgsqword(0x60);
On x86:
// x86: TEB is at FS:[0x18], PEB is at TEB+0x30
PEB* peb = (PEB*)__readfsdword(0x30);
Why This Matters for Defense
Because the PEB is in user-mode memory, any code running in the process can read and modify it. This is both a feature and a security concern. Legitimate code reads the PEB for process information. Malware modifies the PEB to hide its tracks.
2. Key PEB Fields
The PEB is large and version-dependent. These are the fields that matter most for security analysis:
BeingDebugged (offset 0x02)
A single byte. Set to 1 when a debugger is attached via DebugActiveProcess or when the process is started under a debugger.
if (peb->BeingDebugged) {
// debugger is attached
}
This is equivalent to calling IsDebuggerPresent(), which literally just reads this byte. Malware commonly checks this field and alters its behavior. Anti-analysis code typically modifies this field to 0 to hide the debugger.
If your EDR sees a process where BeingDebugged is 0 but debug events are active on the process, something is manipulating the PEB.
Ldr (offset 0x18) - PEB_LDR_DATA
Pointer to the loader data structure, which contains three linked lists of loaded modules:
InLoadOrderModuleList- modules in load orderInMemoryOrderModuleList- modules in memory address orderInInitializationOrderModuleList- modules in initialization order
Each entry is an LDR_DATA_TABLE_ENTRY containing:
typedef struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase; // base address of the module
PVOID EntryPoint; // entry point
ULONG SizeOfImage; // size in memory
UNICODE_STRING FullDllName; // full path
UNICODE_STRING BaseDllName; // just the filename
// ... more fields
} LDR_DATA_TABLE_ENTRY;
Walking these lists is how tools like Process Explorer enumerate loaded DLLs. But malware can unlink entries from these lists to hide injected DLLs. The module is still loaded in memory, but PEB-based enumeration won’t find it.
ProcessParameters (offset 0x20) - RTL_USER_PROCESS_PARAMETERS
Contains the command line, current directory, environment variables, image path, and window information:
RTL_USER_PROCESS_PARAMETERS* params = peb->ProcessParameters;
// params->CommandLine - full command line
// params->ImagePathName - path to the executable
// params->Environment - environment variable block
// params->CurrentDirectory - working directory
Malware can modify ImagePathName to make the process appear to be running from a different location. Comparing PEB ImagePathName with the actual executable path (from kernel structures) reveals this tampering.
NtGlobalFlag (offset 0x68 on x86, 0xBC on x64)
Debug-related flags set by the OS. When a debugger creates a process, certain flags are set:
FLG_HEAP_ENABLE_TAIL_CHECK(0x10)FLG_HEAP_ENABLE_FREE_CHECK(0x20)FLG_HEAP_VALIDATE_PARAMETERS(0x40)
Combined value when debugging: 0x70.
Malware checks this: if NtGlobalFlag contains 0x70, a debugger likely started the process. This is a more subtle check than BeingDebugged and is missed by naive anti-anti-debug tools.
3. PEB-Based Module Enumeration
Walking the PEB loader lists is the standard way to enumerate modules from user mode without calling EnumProcessModules or CreateToolhelp32Snapshot - API calls that security tools monitor.
The Walk
PEB* peb = get_peb();
PEB_LDR_DATA* ldr = peb->Ldr;
LIST_ENTRY* head = &ldr->InLoadOrderModuleList;
LIST_ENTRY* current = head->Flink;
while (current != head) {
LDR_DATA_TABLE_ENTRY* entry =
CONTAINING_RECORD(current, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
// entry->BaseDllName.Buffer - module name
// entry->DllBase - base address
// entry->SizeOfImage - size
current = current->Flink;
}
Why Malware Does This
Calling GetModuleHandle or LoadLibrary goes through the Windows API, which EDR products hook. PEB walking achieves the same result (finding a module base address) without touching any hooked functions.
This is a common pattern in malware:
- Walk PEB to find
kernel32.dllbase address - Parse its export table to find
GetProcAddress - Use
GetProcAddressto resolve everything else
No API calls that an EDR can intercept in the traditional sense.
How to Catch It
Even if malware avoids API hooks, ETW providers at the kernel level still see module loads. Comparing ETW module load events with PEB module lists reveals unlinking.
You can also scan process memory for PE headers (MZ / PE signatures) and compare against the PEB module list. If there’s a PE header in memory that doesn’t appear in the loader lists, something was unlinked.
Periodically comparing PEB module lists against kernel-side structures (EPROCESS.VadRoot, kernel module lists) works too. The kernel still tracks the memory regions even after PEB unlinking.
4. PEB Manipulation Techniques and Detection
Module Unlinking
The most common PEB manipulation: removing a LDR_DATA_TABLE_ENTRY from the three loader lists. After unlinking, the module is still loaded and functional, but will not appear when enumerating modules via the PEB.
void unlink_module(LDR_DATA_TABLE_ENTRY* entry) {
entry->InLoadOrderLinks.Blink->Flink = entry->InLoadOrderLinks.Flink;
entry->InLoadOrderLinks.Flink->Blink = entry->InLoadOrderLinks.Blink;
// ... same for the other two lists
}
To catch this, compare the PEB module list against:
- VAD (Virtual Address Descriptor) tree from kernel mode - memory regions are still tracked
- PE header scanning in the process address space
- ETW module load events (the load was logged before unlinking happened)
ImagePathName Spoofing
Overwriting ProcessParameters->ImagePathName to a different path. Some security tools trust this field to identify what binary is running.
Compare with EPROCESS.ImageFileName in kernel mode, or with the QueryFullProcessImageName result, which reads from kernel structures.
BeingDebugged / NtGlobalFlag Clearing
Malware zeroes these fields to evade anti-debug checks by analysis tools or its own anti-analysis routines.
From a debugger, compare expected debug state with PEB values. Tools like ScyllaHide do the reverse (clear these fields to help analysts), which is useful during malware analysis.
CommandLine Modification
Overwriting ProcessParameters->CommandLine after process creation. Process creation events capture the original command line, but tools querying the PEB later see the modified version.
Process creation events (Sysmon Event ID 1, ETW) capture the original command line. Comparing with the PEB reveals tampering.
5. PEB and the Heap
The PEB contains pointers to process heaps:
PVOID ProcessHeap; // default process heap
ULONG NumberOfHeaps; // total heap count
ULONG MaximumNumberOfHeaps; // maximum heap count
PVOID* ProcessHeaps; // array of heap pointers
Heap-Based Debug Detection
The process heap (from GetProcessHeap() or PEB->ProcessHeap) has debug-specific flags when a debugger creates the process:
Heap->Flagsshould beHEAP_GROWABLE(0x02) normally- Under a debugger: additional flags like
HEAP_TAIL_CHECKING_ENABLED,HEAP_FREE_CHECKING_ENABLED Heap->ForceFlagsshould be 0 normally, non-zero under debugger
This is a more reliable anti-debug check than BeingDebugged because many anti-anti-debug tools forget to patch heap flags.
This one has bitten me during analysis. You patch BeingDebugged thinking you’re clean, and the malware still detects you because you forgot about the heap flags. A lot of anti-anti-debug tools miss this.
6. PEB Across Windows Versions
The PEB grows with each Windows version. Microsoft adds fields but does not remove them, maintaining backward compatibility. Key additions over time:
- Windows Vista: Added
AppCompatFlags,AppCompatFlagsUser - Windows 8: Added
AppModelPolicy - Windows 10: Added
LeapSecondData,ActiveCodePage - Windows 11: Various additions for security mitigations
Practical Implication
When writing tools that read the PEB, always verify the Windows version and use the correct structure layout. Using the wrong offsets causes silent reads of wrong fields. I’ve seen tools read garbage for months because of this.
The best approach: use NtQueryInformationProcess(ProcessBasicInformation) to get the PEB address, then read fields at known offsets rather than relying on a compiled structure definition that might not match the running OS version.
7. Defensive Tooling Using PEB Analysis
Integrity Monitoring
A lightweight detection approach: periodically snapshot PEB state and compare:
- Module list: any entries added or removed since last check?
ImagePathName: still matches the real binary path?BeingDebugged/NtGlobalFlag: consistent with actual debug state?CommandLine: matches process creation event?
Changes to any of these fields outside of expected operations are strong indicators of compromise or tampering.
EDR Integration
Modern EDRs combine PEB inspection with other telemetry:
- PEB module list + VAD scan + ETW events = comprehensive module visibility
- PEB
CommandLine+ Sysmon creation event = tamper detection - PEB heap analysis + debug state = environment fingerprinting detection
Forensic Analysis
During incident response, dumping the PEB provides immediate context:
- What modules are loaded (and what’s hiding)?
- What was the real command line?
- Is the process environment modified?
- Are there signs of debugger evasion?
Tools like Volatility can extract PEB data from memory dumps, making this analysis possible even on dead systems.
Conclusion
The PEB is small but it comes up everywhere. Attackers read it to avoid API hooks. They modify it to hide their presence. Defenders read it to catch that modification.
The big points: the PEB is user-mode writable, so always assume it can be tampered with. Module enumeration via PEB is a malware staple because it avoids hooked APIs. Defensive detection works by comparing PEB state against kernel-side ground truth. And debug detection through PEB goes well beyond IsDebuggerPresent - heap flags and NtGlobalFlag catch a lot of analysts off guard.
Every detection rule for PEB tampering starts with knowing how the tampering actually works.