ᛉ Yggdrasil

Malware development

PEB & TEB Internals

15 Oct 2026 · 10,051 words · 46 min

Contents

Legend: [STRUCT] = Data structure | [ASM] = Assembly snippet | [C] = C/C++ code | [PY] = Python code | [WDB] = WinDbg command | [OFFENS] = Offensive use | [ANTIDEBUG] = Anti-debug technique | [BYPASS] = Bypass/evasion | [OPSEC] = Operational Security note | [DETECT] = Detection vector | [NOTE] = Important note


1. Why PEB and TEB Matter for Red Teamers

What it means: PEB (Process Environment Block) and TEB (Thread Environment Block) are fundamental Windows kernel data structures mapped into every process's user-mode address space. Every running process has exactly one PEB, and every thread has its own TEB. They are the bridge between kernel-mode OS internals and user-mode code.

For red teamers, shellcode authors, and malware developers, they are critically important because:

Use case in offensive security:

1. Position-Independent Shellcode (PIC)
   → Shellcode has no Import Address Table (IAT)
   → It cannot call LoadLibrary / GetProcAddress like a normal PE
   → Instead: walk PEB → find loaded DLLs → parse EAT → resolve API addresses
   → This is the backbone of every msfvenom/Cobalt Strike/Havoc shellcode stub

2. Anti-Debug Detection
   → PEB.BeingDebugged   → is a debugger attached?
   → PEB.NtGlobalFlag    → are heap debug checks active?
   → PEB.ProcessHeap     → are heap ForceFlags set by debugger?
   → Read these in shellcode to detect sandbox / analyst environment

3. Anti-Debug Bypass
   → Patch PEB.BeingDebugged = 0 in memory → IsDebuggerPresent() returns false
   → Patch NtGlobalFlag = 0 → no heap debug indicators
   → Cheap evasion, works against many automated analysis tools

4. Process Environment Access
   → PEB.ProcessParameters → CommandLine, ImagePathName, Environment variables
   → Shellcode can read its own command-line args, hostname, username at runtime
   → BOFs (Beacon Object Files) in Cobalt Strike do this constantly

5. Remote Process Enumeration
   → Query another process's PEB via NtQueryInformationProcess + ReadProcessMemory
   → Enumerate loaded DLLs in remote process (process hollowing, injection targeting)

6. Thread Local Storage (TLS) / Fiber abuse
   → TEB.TlsSlots / TEB.TlsExpansionSlots
   → Used for storing per-thread shellcode state
   → TEB.NtTib.FiberData → fiber-based shellcode execution techniques

2. Memory Layout — Where They Live

2.1 Segment Registers — The Entry Point

The kernel maps TEB into the process's address space at process creation.
The segment register FS (x86) or GS (x64) is set to point to the TEB.
This is guaranteed on every Windows NT version from Win2000 onward.

x86 (32-bit):
  FS register → base address of TEB
  FS:[0x00]  → NT_TIB (starts with SEH chain)
  FS:[0x18]  → Self-pointer (FS base address itself, i.e., &TEB)
  FS:[0x30]  → PEB address  ← the key offset everyone uses

x64 (64-bit):
  GS register → base address of TEB
  GS:[0x00]  → NT_TIB (first field)
  GS:[0x30]  → Self-pointer (GS base address itself, i.e., &TEB)
  GS:[0x60]  → PEB address  ← the key offset for 64-bit

2.2 Address Space Position

Virtual address space of a 64-bit Windows process (simplified):

0x000000000000  ── User-mode low
                   [code / stack / heap / mapped DLLs]
                   ...
                   TEB → lives here (per-thread, kernel allocates it)
                   PEB → lives here (per-process, near TEB range)
                   ...
0x00007FFFFFFFFFFF ── User-mode high
─────────────────── Kernel boundary ───────────────────────────────
0xFFFF800000000000  ── Kernel space (EPROCESS, KTHREAD live here)
0xFFFFFFFFFFFFFFFF

2.3 Relationship Diagram

                    ┌───────────────────────────────────────┐
                    │            EPROCESS (kernel)          │
                    │  PebBaseAddress ──────────────────┐   │
                    └───────────────────────────────────┼───┘
                                                        ↓
                    ┌───────────────────────────────────────┐
                    │               PEB (user)              │
                    │  Ldr ─────────────────────────────┐   │
                    │  ProcessParameters ───────────┐   │   │
                    │  ImageBaseAddress             │   │   │
                    │  BeingDebugged                │   │   │
                    │  NtGlobalFlag                 │   │   │
                    │  ProcessHeap                  │   │   │
                    └───────────────────────────────┬───┴───┘   
                                                    │↓
                    ┌────────────────────────────────────────┐
   GS:[0x60]──→     │  TEB (Thread Environment Block)        │
   FS:[0x30]  (x86) │  NtTib (NT_TIB inner struct)           │
                    │    ├─ ExceptionList (SEH chain)        │
                    │    ├─ StackBase                        │
                    │    ├─ StackLimit                       │
                    │    └─ Self pointer                     │
                    │  ProcessEnvironmentBlock ──→ [PEB]     │
                    │  ThreadLocalStoragePointer             │
                    │  LastErrorValue                        │
                    │  TlsSlots[64]                          │
                    │  TlsExpansionSlots                     │
                    └────────────────────────────────────────┘

            Each thread has its OWN TEB — one PEB per process, shared.

3. NT_TIB — Thread Information Block (Inner Structure)

What it means: NT_TIB (Thread Information Block) is the very first structure embedded at offset 0 inside the TEB. It's the oldest part — present since Win9x. It holds the SEH (Structured Exception Handling) chain, stack bounds, and a self-pointer. Every thread has one. It was the original "thread info" structure before TEB expanded around it.

3.1 NT_TIB Structure

// [STRUCT] NT_TIB — Thread Information Block (first member of TEB)

typedef struct _NT_TIB {
    /* +0x00 */ PEXCEPTION_REGISTRATION_RECORD  ExceptionList;
                // x86: pointer to head of SEH chain (linked list of EXCEPTION_REGISTRATION_RECORD)
                // x64: NOT used for SEH (x64 uses table-based exception handling in PE metadata)
                //      Set to 0xFFFFFFFFFFFFFFFF (-1) for the last frame (sentinel)

    /* +0x08 */ PVOID  StackBase;
                // Top of the stack (highest address — stacks grow DOWN on x86/x64)
                // TEB.NtTib.StackBase > TEB.NtTib.StackLimit

    /* +0x10 */ PVOID  StackLimit;
                // Bottom of the accessible stack (lowest currently committed address)
                // The OS grows the stack downward on demand (guard pages)

    /* +0x18 */ PVOID  SubSystemTib;
                // Legacy subsystem pointer (rarely used, set to NULL in Win32 processes)

    /* +0x20 */ union {
                    PVOID  FiberData;
                    DWORD  Version;  // used in TIB version 1 (non-fiber)
                };
                // FiberData: pointer to current fiber data when thread uses fibers
                // [OFFENS] Fiber-based shellcode execution uses this field to store
                //          a pointer to the fiber context. Fiber stomping technique.

    /* +0x28 */ PVOID  ArbitraryUserPointer;
                // Available for application use (OS never touches this)
                // [OFFENS] Occasionally abused for stealthy pointer storage

    /* +0x30 */ struct _NT_TIB* Self;
                // Pointer back to THIS NT_TIB (= GS:[0x30] on x64, FS:[0x18] on x86)
                // Key: this IS the GS/FS segment base. Reading GS:[0x30] gives you
                //      the TEB address itself.
                // [OPSEC] Confirm you have the right TEB address: TEB.NtTib.Self == &TEB

} NT_TIB, *PNT_TIB;

// Offsets summary:
//               x86 offset    x64 offset
// ExceptionList    +0x00         +0x00
// StackBase        +0x04         +0x08
// StackLimit       +0x08         +0x10
// SubSystemTib     +0x0C         +0x18
// FiberData        +0x10         +0x20
// ArbitraryUser    +0x14         +0x28
// Self             +0x18         +0x30

3.2 Accessing NT_TIB in C

#include <windows.h>
#include <winternl.h>

void inspect_nt_tib() {
    // Get pointer to current thread's TEB
    PTEB pTeb = NtCurrentTeb();
    PNT_TIB pTib = &pTeb->NtTib;

    printf("[+] NT_TIB fields:\n");
    printf("    ExceptionList:    %p\n", pTib->ExceptionList);
    printf("    StackBase:        %p  (top of stack)\n",   pTib->StackBase);
    printf("    StackLimit:       %p  (bottom of stack)\n", pTib->StackLimit);
    printf("    FiberData:        %p\n", pTib->FiberData);
    printf("    Self (TEB addr):  %p\n", pTib->Self);

    // Compute current stack usage
    ULONG_PTR stackSize = (ULONG_PTR)pTib->StackBase - (ULONG_PTR)pTib->StackLimit;
    printf("    Stack committed:  %zu KB\n", stackSize / 1024);

    // [NOTE] StackBase is always > StackLimit — stacks grow DOWN
    // The actual RSP/ESP will be between StackLimit and StackBase
}

4. TEB — Thread Environment Block

What it means: The TEB is the per-thread data structure that the kernel creates and maps into user space for every thread. It begins with NT_TIB and extends much further. The OS, ntdll, and Win32 subsystem use it constantly — GetLastError() reads TEB.LastErrorValue, GetCurrentThreadId() reads TEB.ClientId.UniqueThread, TlsGetValue() reads from TEB.TlsSlots.

4.1 TEB Structure — Key Fields

// [STRUCT] TEB — Thread Environment Block (simplified, key fields only)
// Full structure: use `dt ntdll!_TEB` in WinDbg

typedef struct _TEB {
    //─── NT_TIB (first and foremost) ─────────────────────────────────
    /* x86:+0x000  x64:+0x000 */ NT_TIB NtTib;
    // Contains SEH chain, StackBase, StackLimit, Self pointer
    // sizeof(NT_TIB) = 0x1C (x86) or 0x38 (x64)

    //─── Pointers and IDs ────────────────────────────────────────────
    /* x86:+0x01C  x64:+0x038 */ PVOID  EnvironmentPointer;
    // [NOTE] Rarely used directly; legacy field

    /* x86:+0x020  x64:+0x040 */ CLIENT_ID ClientId;
    // ClientId.UniqueProcess = PID (Process ID)
    // ClientId.UniqueThread  = TID (Thread ID)
    // [OFFENS] Shellcode reads this instead of calling GetCurrentProcessId()

    /* x86:+0x028  x64:+0x050 */ PVOID  ActiveRpcHandle;
    /* x86:+0x02C  x64:+0x058 */ PVOID  ThreadLocalStoragePointer;
    // Pointer to the per-thread TLS array (different from TlsSlots below)

    /* x86:+0x030  x64:+0x060 */ PPEB   ProcessEnvironmentBlock;
    // ★ THE KEY FIELD — pointer to the process PEB
    // [ASM] x86: mov eax, fs:[0x30]   → PEB address
    // [ASM] x64: mov rax, gs:[0x60]   → PEB address

    /* x86:+0x034  x64:+0x068 */ DWORD  LastErrorValue;
    // [OFFENS] GetLastError() just reads this: return (DWORD)TEB.LastErrorValue
    // [OFFENS] SetLastError(0) just writes 0 here — no syscall needed
    // Shellcode can save/restore this to avoid detection via error code changes

    /* x86:+0x038  x64:+0x06C */ DWORD  CountOfOwnedCriticalSections;

    /* x86:+0x03C  x64:+0x070 */ PVOID  CsrClientThread;
    /* x86:+0x040  x64:+0x078 */ PVOID  Win32ThreadInfo;

    //─── Stack Information ────────────────────────────────────────────
    // (via NtTib.StackBase and NtTib.StackLimit above)
    /* x86:+0x0DC  x64:+0x1748 */ DWORD  GuaranteedStackBytes;
    // Minimum committed stack bytes (used with /F linker option)

    //─── Locale and Code Page ─────────────────────────────────────────
    /* x86:+0x0C4  x64:+0x108 */ DWORD  CurrentLocale;

    //─── Exception Handling ───────────────────────────────────────────
    /* x86:+0x1A4  x64:+0x2C0 */ LONG   ExceptionCode;
    // Set by the OS when an exception occurs in this thread

    //─── WOW64 ────────────────────────────────────────────────────────
    /* x86:+0x0C0  x64:+0x100 */ PVOID  WOW32Reserved;
    // WOW64 transition stub — used when 32-bit code runs on 64-bit OS

    //─── Thread Local Storage ────────────────────────────────────────
    /* x86:+0xE10  x64:+0x1480 */ PVOID  TlsSlots[64];
    // First 64 TLS indices stored here directly
    // [OFFENS] Legitimate TLS storage channel for shellcode state

    /* x86:+0xF10  x64:+0x1680 */ LIST_ENTRY TlsLinks;
    // Doubly-linked list connecting all TEB TLS entries

    /* x86:+0xF94  x64:+0x1780 */ PVOID  TlsExpansionSlots;
    // Pointer to heap-allocated array for TLS indices 64-1088

    //─── Fiber ─────────────────────────────────────────────────────
    // NtTib.FiberData (in NT_TIB at +0x20 x64 / +0x10 x86) — fiber pointer

    //─── GDI (rarely relevant to red teamers) ────────────────────────
    /* large GDI batch structures ... */

    //─── Activation Context ──────────────────────────────────────────
    /* x86:+0x1A8  x64:+0x2C8 */ ACTIVATION_CONTEXT_STACK ActivationContextStackPointer;

    //─── SameTebFlags (debugging visibility bits) ─────────────────────
    /* x64: +0x17EE */ WORD SameTebFlags;
    // Bits include: SafeThunkCall, InDebugPrint, HasFiberData,
    //               SkipThreadAttach, DisableUserStackWalk,
    //               RtlExceptionAttached, InitialThread

} TEB, *PTEB;

4.2 Accessing TEB in Assembly

; ─── x86: Get TEB address ─────────────────────────────────────────
mov eax, fs:[0x18]          ; FS:[0x18] = NT_TIB.Self = TEB base address

; ─── x86: Get PEB address directly ───────────────────────────────
mov eax, fs:[0x30]          ; FS:[0x30] = TEB.ProcessEnvironmentBlock = PEB

; ─── x86: Get PID / TID ──────────────────────────────────────────
mov eax, fs:[0x20]          ; PID = TEB.ClientId.UniqueProcess
mov eax, fs:[0x24]          ; TID = TEB.ClientId.UniqueThread

; ─── x86: Get LastError ──────────────────────────────────────────
mov eax, fs:[0x34]          ; TEB.LastErrorValue

; ─── x64: Get TEB address ─────────────────────────────────────────
mov rax, gs:[0x30]          ; GS:[0x30] = NT_TIB.Self = TEB base address

; ─── x64: Get PEB address directly ───────────────────────────────
mov rax, gs:[0x60]          ; GS:[0x60] = TEB.ProcessEnvironmentBlock = PEB

; ─── x64: Get PID / TID ──────────────────────────────────────────
mov rax, gs:[0x40]          ; TEB.ClientId.UniqueProcess (PID)
mov rax, gs:[0x48]          ; TEB.ClientId.UniqueThread  (TID)

; ─── x64: Get LastError ──────────────────────────────────────────
mov eax, gs:[0x68]          ; TEB.LastErrorValue (DWORD at +0x068)

; ─── x64: Get StackBase / StackLimit (from NT_TIB) ────────────────
mov rax, gs:[0x08]          ; TEB.NtTib.StackBase  (top of stack)
mov rax, gs:[0x10]          ; TEB.NtTib.StackLimit (bottom of committed stack)

4.3 Accessing TEB in C

#include <windows.h>
#include <winternl.h>

void inspect_teb() {
    // NtCurrentTeb() is a compiler intrinsic — no imports needed
    PTEB pTeb = NtCurrentTeb();

    printf("[+] TEB at: %p\n", pTeb);
    printf("    NtTib.StackBase:        %p\n", pTeb->NtTib.StackBase);
    printf("    NtTib.StackLimit:       %p\n", pTeb->NtTib.StackLimit);
    printf("    PID:                    %llu\n", (ULONG64)pTeb->ClientId.UniqueProcess);
    printf("    TID:                    %llu\n", (ULONG64)pTeb->ClientId.UniqueThread);
    printf("    PEB at:                 %p\n",  pTeb->ProcessEnvironmentBlock);
    printf("    LastErrorValue:         %lu\n",  pTeb->LastErrorValue);
    printf("    CurrentLocale:          0x%08X\n", pTeb->CurrentLocale);

    // [NOTE] NtCurrentTeb() compiles to: mov rax, gs:[0x30] (x64)
    //                                 or mov eax, fs:[0x18] (x86)
    // It is an INTRINSIC — zero overhead, no function call
}

4.4 Thread Hiding (Anti-Debug via TEB)

// [ANTIDEBUG] Hide thread from debugger using NtSetInformationThread
// When a thread has the ThreadHideFromDebugger flag set,
// the OS stops sending debug events for it — debugger "loses" the thread

#include <windows.h>

typedef NTSTATUS (WINAPI *NtSetInformationThread_t)(
    HANDLE ThreadHandle,
    ULONG  ThreadInformationClass,
    PVOID  ThreadInformation,
    ULONG  ThreadInformationLength
);

void hide_thread_from_debugger() {
    HMODULE ntdll = GetModuleHandleA("ntdll.dll");

    NtSetInformationThread_t pNtSetInformationThread =
        (NtSetInformationThread_t)GetProcAddress(ntdll, "NtSetInformationThread");

    if (pNtSetInformationThread) {
        // ThreadHideFromDebugger = 0x11 (17)
        NTSTATUS status = pNtSetInformationThread(
            GetCurrentThread(),
            0x11,    // ThreadHideFromDebugger
            NULL,
            0
        );
        if (status == 0) {
            printf("[+] Thread hidden from debugger\n");
        }
    }
}
// [BYPASS] Hypervisor-based tools (HyperHide, ScyllaHide) intercept this
//          and send fake debug contexts — bypasses this technique

5. PEB — Process Environment Block

What it means: The PEB is the user-mode representation of the process. Created once per process by the kernel. It holds everything from the image base address to loaded DLL list, heap handles, command line parameters, and several OS-level configuration flags. For shellcode and malware, it is the starting point for everything — every position-independent payload begins by reading GS:[0x60] to get the PEB.

5.1 PEB Structure — Key Fields

// [STRUCT] PEB — Process Environment Block (key fields)
// Full structure: dt ntdll!_PEB in WinDbg

typedef struct _PEB {
    //─── Basic flags ─────────────────────────────────────────────────
    /* +0x000 */ BYTE  InheritedAddressSpace;
    /* +0x001 */ BYTE  ReadImageFileExecOptions;

    /* +0x002 */ BYTE  BeingDebugged;
    // ★ CRITICAL for anti-debug.
    // Set to 1 by the kernel when a debugger attaches via DebugActiveProcess
    // kernel32!IsDebuggerPresent() just returns this byte
    // [ANTIDEBUG] Check this first in shellcode/malware
    // [BYPASS]    Overwrite with 0x00 to fool IsDebuggerPresent()

    /* +0x003 */ union {
                    BYTE BitField;
                    struct {
                        BYTE ImageUsesLargePages       : 1;
                        BYTE IsProtectedProcess        : 1;
                        BYTE IsImageDynamicallyRelocated : 1;
                        BYTE SkipPatchingUser32Forwarders : 1;
                        BYTE IsPackagedProcess         : 1;
                        BYTE IsAppContainer            : 1;
                        BYTE IsProtectedProcessLight   : 1;
                        BYTE IsLongPathAwareProcess    : 1;
                    };
                };
    // [NOTE] IsPackagedProcess / IsAppContainer useful for sandbox detection
    //        Sandboxes rarely run packaged/UWP apps

    //─── Image base ──────────────────────────────────────────────────
    /* x86:+0x008  x64:+0x010 */ PVOID  ImageBaseAddress;
    // Base address where the main EXE was loaded (= HMODULE of the EXE)
    // [OFFENS] Shellcode uses this to find the host process's PE headers

    //─── Loaded Module List ──────────────────────────────────────────
    /* x86:+0x00C  x64:+0x018 */ PPEB_LDR_DATA  Ldr;
    // ★ CRITICAL for PEB walk
    // Pointer to PEB_LDR_DATA, which contains three doubly-linked lists
    // of all loaded DLLs. This is how shellcode finds kernel32/ntdll base addresses.

    //─── Process parameters ──────────────────────────────────────────
    /* x86:+0x010  x64:+0x020 */ PRTL_USER_PROCESS_PARAMETERS  ProcessParameters;
    // ★ IMPORTANT — command line, image path, environment variables, CWD, window title
    // [OFFENS] Read this to get CommandLine, ImagePath, Environment strings
    //          without calling any Win32 API

    /* x86:+0x014  x64:+0x028 */ PVOID  SubSystemData;

    //─── Heap ────────────────────────────────────────────────────────
    /* x86:+0x018  x64:+0x030 */ PVOID  ProcessHeap;
    // Handle to the process's default heap (from HeapCreate(0,0,0) at process start)
    // [ANTIDEBUG] Heap flags at ProcessHeap+0x40 (x86) / +0x70 (x64)
    //             reveal debugger presence via HEAP_GROWABLE check

    /* x86:+0x01C  x64:+0x038 */ PRTL_CRITICAL_SECTION  FastPebLock;
    // Critical section protecting PEB modifications (loader lock related)

    //─── ATL thunk ───────────────────────────────────────────────────
    /* x86:+0x020  x64:+0x040 */ PVOID  AtlThunkSListPtr;
    /* x86:+0x024  x64:+0x048 */ PVOID  IFEOKey;

    //─── Cross-process flags ─────────────────────────────────────────
    /* x86:+0x028  x64:+0x050 */ union {
                                     DWORD CrossProcessFlags;
                                     struct {
                                         DWORD ProcessInJob           : 1;
                                         DWORD ProcessInitializing    : 1;
                                         DWORD ProcessUsingVEH        : 1;
                                         DWORD ProcessUsingVCH        : 1;
                                         DWORD ProcessUsingFTH        : 1;
                                         DWORD ProcessPreviouslyThrottled : 1;
                                         DWORD ProcessCurrentlyThrottled  : 1;
                                         DWORD ProcessImagesHotPatched    : 1;
                                         DWORD ReservedBits0         : 24;
                                     };
                                 };
    // [NOTE] ProcessInJob — useful for sandbox detection (many sandboxes run inside jobs)

    //─── KernelCallbackTable ─────────────────────────────────────────
    /* x86:+0x02C  x64:+0x058 */ union {
                                     PVOID KernelCallbackTable;
                                     PVOID UserSharedInfoPtr;
                                 };
    // [OFFENS] KernelCallbackTable hijacking — a stealthy code injection technique
    //          Replace __fnCOPYDATA entry with shellcode address → triggered by SendMessage

    //─── System call table cache ─────────────────────────────────────
    /* x86:+0x034  x64:+0x068 */ DWORD SystemReserved;
    /* x86:+0x038  x64:+0x06C */ DWORD AtlThunkSListPtr32;

    //─── API Set Schema ──────────────────────────────────────────────
    /* x86:+0x03C  x64:+0x070 */ PVOID ApiSetMap;
    // Pointer to the API Set schema (used by the loader to resolve api-ms-win-* DLL names)

    //─── NT Global Flags ─────────────────────────────────────────────
    /* x86:+0x068  x64:+0x0BC */ DWORD NtGlobalFlag;
    // ★ CRITICAL for anti-debug
    // Under a debugger, set to 0x70:
    //   FLG_HEAP_ENABLE_TAIL_CHECK   = 0x10
    //   FLG_HEAP_ENABLE_FREE_CHECK   = 0x20
    //   FLG_HEAP_VALIDATE_PARAMETERS = 0x40
    //   Combined: 0x70
    // Normal execution: 0x00
    // [ANTIDEBUG] cmp NtGlobalFlag, 0x70 → je DebuggerDetected
    // [BYPASS] Write 0x00 to this address to clear debug flags

    //─── Heap limits ─────────────────────────────────────────────────
    /* x86:+0x070  x64:+0x0C8 */ LARGE_INTEGER CriticalSectionTimeout;
    /* x86:+0x078  x64:+0x0D0 */ SIZE_T HeapSegmentReserve;
    /* x86:+0x07C  x64:+0x0D8 */ SIZE_T HeapSegmentCommit;
    /* x86:+0x080  x64:+0x0E0 */ SIZE_T HeapDeCommitTotalFreeThreshold;
    /* x86:+0x084  x64:+0x0E8 */ SIZE_T HeapDeCommitFreeBlockThreshold;

    //─── Heap list ───────────────────────────────────────────────────
    /* x86:+0x088  x64:+0x0F0 */ DWORD  NumberOfHeaps;
    /* x86:+0x08C  x64:+0x0F4 */ DWORD  MaximumNumberOfHeaps;
    /* x86:+0x090  x64:+0x0F8 */ PVOID* ProcessHeaps;
    // Array of heap handles for all heaps in this process

    //─── GDI ─────────────────────────────────────────────────────────
    /* x86:+0x094  x64:+0x100 */ PVOID  GdiSharedHandleTable;
    /* x86:+0x098  x64:+0x108 */ PVOID  ProcessStarterHelper;
    /* x86:+0x09C  x64:+0x110 */ DWORD  GdiDCAttributeList;

    //─── Loader lock ─────────────────────────────────────────────────
    /* x86:+0x0A0  x64:+0x118 */ PRTL_CRITICAL_SECTION LoaderLock;
    // The "loader lock" — held while DLLs are being loaded/unloaded
    // [NOTE] Calling LoadLibrary from inside DllMain risks deadlock here

    //─── OS version ──────────────────────────────────────────────────
    /* x86:+0x0A4  x64:+0x11C */ DWORD  OSMajorVersion;   // e.g., 10
    /* x86:+0x0A8  x64:+0x120 */ DWORD  OSMinorVersion;   // e.g., 0
    /* x86:+0x0AC  x64:+0x124 */ WORD   OSBuildNumber;    // e.g., 19041
    /* x86:+0x0AE  x64:+0x126 */ WORD   OSCSDVersion;
    /* x86:+0x0B0  x64:+0x128 */ DWORD  OSPlatformId;
    // [OFFENS] Read OS version without calling VerifyVersionInfo() or RtlGetVersion()

    //─── Image subsystem ─────────────────────────────────────────────
    /* x86:+0x0B4  x64:+0x12C */ DWORD  ImageSubsystem;
    // IMAGE_SUBSYSTEM_WINDOWS_GUI = 2
    // IMAGE_SUBSYSTEM_WINDOWS_CUI = 3
    /* x86:+0x0B8  x64:+0x130 */ DWORD  ImageSubsystemMajorVersion;
    /* x86:+0x0BC  x64:+0x134 */ DWORD  ImageSubsystemMinorVersion;

    //─── CPU count ───────────────────────────────────────────────────
    /* x86:+0x064  x64:+0x0B8 */ DWORD  NumberOfProcessors;
    // [OFFENS] Sandbox detection: real machines have ≥ 4 cores
    //          if (NumberOfProcessors < 4) → likely sandbox/VM

    //─── Misc ────────────────────────────────────────────────────────
    /* x86:+0x1D4  x64:+0x280 */ PVOID  PostProcessInitRoutine;
    // Called after process initialization if non-NULL (rarely used)

    /* x86:+0x1DC  x64:+0x290 */ PVOID  TlsBitmap;
    /* x86:+0x1E0  x64:+0x298 */ DWORD  TlsBitmapBits[2];

    /* x86:+0x1E8  x64:+0x2A8 */ PVOID  ReadOnlySharedMemoryBase;
    /* x86:+0x1F0  x64:+0x2B8 */ PVOID  SharedData;
    /* x86:+0x1F4  x64:+0x2C0 */ PVOID  ReadOnlyStaticServerData;

    /* x86:+0x1F8  x64:+0x2C8 */ PVOID  AnsiCodePageData;
    /* x86:+0x1FC  x64:+0x2D0 */ PVOID  OemCodePageData;
    /* x86:+0x200  x64:+0x2D8 */ PVOID  UnicodeCaseTableData;

} PEB, *PPEB;

5.2 Accessing PEB in C

#include <windows.h>
#include <winternl.h>

// winternl.h has a PARTIAL PEB definition.
// For the full struct, use the definition above or include phnt headers.

void inspect_peb() {
    PPEB pPeb = NtCurrentTeb()->ProcessEnvironmentBlock;
    // or equivalently:
    // PPEB pPeb = (PPEB)__readgsqword(0x60); // x64 intrinsic
    // PPEB pPeb = (PPEB)__readfsdword(0x30); // x86 intrinsic

    printf("[+] PEB at:              %p\n", pPeb);
    printf("    BeingDebugged:       %d\n",  pPeb->BeingDebugged);
    printf("    ImageBaseAddress:    %p\n",  pPeb->Reserved3[1]); // depends on SDK
    printf("    Ldr:                 %p\n",  pPeb->Ldr);
    printf("    ProcessParameters:   %p\n",  pPeb->ProcessParameters);

    // Read NtGlobalFlag (not in winternl.h PPEB — need full struct or offset)
    // For x64: NtGlobalFlag is at PEB + 0xBC
    DWORD ntGlobalFlag = *(DWORD*)((BYTE*)pPeb + 0xBC);
    printf("    NtGlobalFlag:        0x%08X (%s)\n",
           ntGlobalFlag,
           ntGlobalFlag == 0x70 ? "DEBUGGER PRESENT" : "clean");

    // Read NumberOfProcessors (at PEB + 0xB8 on x64)
    DWORD cpuCount = *(DWORD*)((BYTE*)pPeb + 0xB8);
    printf("    NumberOfProcessors:  %d\n", cpuCount);

    // Read OS version (x64 offsets)
    DWORD osMajor = *(DWORD*)((BYTE*)pPeb + 0x11C);
    DWORD osMinor = *(DWORD*)((BYTE*)pPeb + 0x120);
    WORD  osBuild = *(WORD*) ((BYTE*)pPeb + 0x124);
    printf("    OS Version:          %d.%d Build %d\n", osMajor, osMinor, osBuild);
}

5.3 PEB in Shellcode (x64 Assembly)

; ─── x64 shellcode: get PEB address ──────────────────────────────
xor    rax, rax
mov    rax, gs:[rax+0x60]   ; RAX = PEB address (avoids null byte in 0x60)
; [NOTE] Using gs:[0x60] directly produces 65 48 8B 04 25 60 00 00 00
;        Using xor + offset avoids the large null bytes in some contexts

; Alternative (also avoids nulls):
xor    r10, r10
mul    r10                  ; RAX = RDX = 0
add    al, 0x60             ; RAX = 0x60
mov    rbx, gs:[rax]        ; RBX = PEB

; ─── Access PEB.BeingDebugged ────────────────────────────────────
movzx  ecx, byte [rbx+0x02] ; ECX = BeingDebugged (BYTE at PEB+0x02)
test   ecx, ecx
jnz    .debugger_present    ; if BeingDebugged != 0, debugger is attached

; ─── Access PEB.Ldr ──────────────────────────────────────────────
mov    rdx, [rbx+0x18]      ; RDX = PEB.Ldr (pointer to PEB_LDR_DATA)

; ─── Access PEB.ProcessParameters ───────────────────────────────
mov    r8,  [rbx+0x20]      ; R8 = PEB.ProcessParameters

6. PEB_LDR_DATA — Loaded Module Lists

What it means: PEB_LDR_DATA is pointed to by PEB.Ldr. It is the loader's bookkeeping structure containing three doubly-linked lists of all DLLs currently loaded in the process. Each entry in these lists is an LDR_DATA_TABLE_ENTRY that holds the DLL's base address, name, size, and entry point. This is what shellcode walks to find kernel32.dll without calling any APIs.

6.1 PEB_LDR_DATA Structure

// [STRUCT] PEB_LDR_DATA
typedef struct _PEB_LDR_DATA {
    /* +0x00 */ DWORD     Length;               // Size of structure
    /* +0x04 */ BOOLEAN   Initialized;          // TRUE after loader init
    /* +0x08 */ PVOID     SsHandle;             // Server silo handle (usually NULL)
    /* +0x10 */ LIST_ENTRY InLoadOrderModuleList;
    // Doubly-linked list in the order DLLs were loaded
    // Order: [0] = main EXE, [1] = ntdll.dll, [2] = kernel32.dll, [3] = ...
    // [OFFENS] ★ Most common list for PEB walk — kernel32 at index 2 (zero-based)

    /* +0x20 */ LIST_ENTRY InMemoryOrderModuleList;
    // Doubly-linked list sorted by base address (DllBase)
    // [OFFENS] Used in some shellcode variants

    /* +0x30 */ LIST_ENTRY InInitializationOrderModuleList;
    // Doubly-linked list in the order DllMain(DLL_PROCESS_ATTACH) was called
    // Order: [0] = ntdll.dll, [1] = kernel32.dll, [2] = ...
    // [OFFENS] Classic MSDN example uses this list — kernel32 at index 1 (zero-based)
    // [NOTE] This list does NOT include the main EXE!

    /* +0x40 */ PVOID      EntryInProgress;     // DLL currently initializing
    /* +0x48 */ BOOLEAN    ShutdownInProgress;
    /* +0x50 */ PVOID      ShutdownThreadId;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

// [NOTE] LIST_ENTRY is a circular doubly-linked list:
// typedef struct _LIST_ENTRY {
//     struct _LIST_ENTRY* Flink;  // Forward link (next)
//     struct _LIST_ENTRY* Blink;  // Backward link (prev)
// } LIST_ENTRY;
// The list "ends" when Flink wraps back to the list head

6.2 Module List Visual — InLoadOrderModuleList

PEB.Ldr.InLoadOrderModuleList (circular doubly-linked):

  HEAD (at PEB_LDR_DATA+0x10)
    ↕ Flink/Blink
  LDR_DATA_TABLE_ENTRY [0]  ← main executable (e.g., notepad.exe)
    ↕
  LDR_DATA_TABLE_ENTRY [1]  ← ntdll.dll
    ↕
  LDR_DATA_TABLE_ENTRY [2]  ← KERNEL32.DLL  ← shellcode targets this
    ↕
  LDR_DATA_TABLE_ENTRY [3]  ← KERNELBASE.DLL
    ↕
  LDR_DATA_TABLE_ENTRY [4]  ← user32.dll (if loaded)
    ↕
  ... more DLLs ...
    ↕
  (Flink of last entry → points back to HEAD, closing the loop)

[NOTE] InInitializationOrderModuleList starts from ntdll.dll (index 0),
       so kernel32.dll is at index 1 in THAT list.
       The main EXE never appears in InInitializationOrderModuleList.

7. LDR_DATA_TABLE_ENTRY — Per-Module Entry

What it means: Each DLL in the process gets one LDR_DATA_TABLE_ENTRY. This structure holds the DLL's base address (DllBase), its full path, its name, and its size. Shellcode extracts DllBase to get the starting address of kernel32.dll, then parses the PE export table from that base.

7.1 LDR_DATA_TABLE_ENTRY Structure

// [STRUCT] LDR_DATA_TABLE_ENTRY
typedef struct _LDR_DATA_TABLE_ENTRY {
    /* x86:+0x00  x64:+0x00 */ LIST_ENTRY InLoadOrderLinks;
    // Links for the InLoadOrderModuleList (8 bytes x86 / 16 bytes x64)

    /* x86:+0x08  x64:+0x10 */ LIST_ENTRY InMemoryOrderLinks;
    // Links for the InMemoryOrderModuleList

    /* x86:+0x10  x64:+0x20 */ LIST_ENTRY InInitializationOrderLinks;
    // Links for the InInitializationOrderModuleList

    /* x86:+0x18  x64:+0x30 */ PVOID  DllBase;
    // ★ BASE ADDRESS of the loaded DLL
    // This IS the HMODULE — pointer to the PE header in memory
    // [OFFENS] THIS is what shellcode needs to parse the export table

    /* x86:+0x1C  x64:+0x38 */ PVOID  EntryPoint;
    // Address of DllMain (or NULL if no entry point)

    /* x86:+0x20  x64:+0x40 */ DWORD  SizeOfImage;
    // Size of the DLL in memory (from IMAGE_OPTIONAL_HEADER.SizeOfImage)

    /* x86:+0x24  x64:+0x48 */ UNICODE_STRING FullDllName;
    // Full path: L"C:\\Windows\\System32\\kernel32.dll"
    // UNICODE_STRING: { USHORT Length, MaxLength; PWSTR Buffer; }

    /* x86:+0x2C  x64:+0x58 */ UNICODE_STRING BaseDllName;
    // Short name: L"kernel32.dll"
    // ★ Shellcode compares this to find the right module
    // [NOTE] Must be case-insensitive comparison!

    /* x86:+0x34  x64:+0x68 */ union {
                                   DWORD Flags;
                                   struct {
                                       DWORD PackagedBinary       : 1;
                                       DWORD MarkedForRemoval     : 1;
                                       DWORD ImageDll             : 1;
                                       DWORD LoadNotificationsSent : 1;
                                       DWORD TelemetryEntryProcessed : 1;
                                       DWORD ProcessStaticImport  : 1;
                                       DWORD InLegacyLists        : 1;
                                       DWORD InIndexes            : 1;
                                       DWORD ShimDll              : 1;
                                       DWORD InExceptionTable     : 1;
                                       DWORD ReservedFlags1       : 2;
                                       DWORD LoadInProgress       : 1;
                                       DWORD LoadConfigProcessed  : 1;
                                       DWORD EntryProcessed       : 1;
                                       DWORD ProtectDelayLoad     : 1;
                                   };
                               };

    /* x86:+0x38  x64:+0x6C */ WORD   LoadCount;  // Reference count (or ObsoleteLoadCount)
    /* x86:+0x3A  x64:+0x6E */ WORD   TlsIndex;   // TLS index for this DLL

    /* x86:+0x3C  x64:+0x70 */ LIST_ENTRY HashLinks;
    /* x86:+0x44  x64:+0x80 */ ULONG  TimeDateStamp;  // From PE header
    /* x86:+0x48  x64:+0x88 */ PVOID  EntryPointActivationContext;
    /* x86:+0x4C  x64:+0x90 */ PVOID  Lock;
    /* x86:+0x50  x64:+0x98 */ PVOID  DdagNode;       // DLL dependency graph node
    /* x86:+0x54  x64:+0xA0 */ LIST_ENTRY NodeModuleLink;
    /* x86:+0x5C  x64:+0xB0 */ PVOID  LoadContext;    // loader context
    /* x86:+0x60  x64:+0xB8 */ PVOID  ParentDllBase;

} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

// Critical offsets for shellcode (x64):
//   InLoadOrderLinks  +0x00  (Flink at +0x00, Blink at +0x08)
//   DllBase           +0x30
//   BaseDllName       +0x58  (UNICODE_STRING: Length at +0x58, Buffer at +0x60)

8. PEB Walk — Dynamic API Resolution (Core Shellcode Technique)

What it means: The PEB walk is the foundational technique used by virtually ALL position-independent shellcode. Because shellcode has no Import Address Table (IAT), it must find Windows API addresses at runtime. The chain is:

TEB (GS:[0x60]) → PEB → PEB.Ldr → InLoadOrderModuleList → find kernel32.dll entry → DllBase
  → parse PE export table → find function name → get function address → call it

8.1 PEB Walk — x64 Assembly (Classic Approach)

; ─── x64 PEB Walk: Find kernel32.dll base address ────────────────
; Goal: Walk InLoadOrderModuleList to find kernel32.dll DllBase
;
; Register usage:
;   RBX = PEB pointer
;   RCX = current LDR_DATA_TABLE_ENTRY pointer
;   RDX = PEB_LDR_DATA pointer
;   RSI = module list head
;   R8  = BaseDllName.Buffer

section .text
find_kernel32_base:
    ; Step 1: Get PEB via GS register
    xor    rax, rax
    mov    rbx, gs:[rax+0x60]   ; RBX = PEB

    ; Step 2: Get Ldr (PEB_LDR_DATA) from PEB
    mov    rdx, [rbx+0x18]      ; RDX = PEB.Ldr (at +0x18 on x64)

    ; Step 3: Get InLoadOrderModuleList head
    lea    rsi, [rdx+0x10]      ; RSI = &Ldr.InLoadOrderModuleList (head sentinel)
    mov    rcx, [rsi]           ; RCX = first Flink → first LDR_DATA_TABLE_ENTRY

.next_module:
    ; Step 4: Get BaseDllName (UNICODE_STRING at +0x58 from InLoadOrderLinks)
    ; LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer is at +0x60 (two QWORDs into BaseDllName)
    mov    r8,  [rcx+0x60]      ; R8 = BaseDllName.Buffer (PWSTR)
    test   r8, r8
    jz     .skip_module         ; skip if name is NULL

    ; Step 5: Compare to "KERNEL32.DLL" (case-insensitive, Unicode)
    ; "K" = 0x004B  "E" = 0x0045  "R" = 0x0052  "N" = 0x004E
    ; Simple comparison for fixed known string:
    mov    ax,  [r8]            ; First wide char
    cmp    ax,  'K'             ; 'K' in Unicode = 0x004B
    je     .check_k32
    cmp    ax,  'k'             ; lowercase 'k'
    jne    .skip_module

.check_k32:
    ; Compare full name "kernel32.dll" or "KERNEL32.DLL"
    ; In practice shellcode uses a hash comparison (see §10)
    ; For illustration, checking known offset of 'e' in "kernel32"
    ; ... (real shellcode uses hashing or tolower loop)

    ; Step 6: Found kernel32 — get DllBase (at +0x30 from InLoadOrderLinks)
    mov    rax, [rcx+0x30]      ; RAX = LDR_DATA_TABLE_ENTRY.DllBase = kernel32.dll base
    ret

.skip_module:
    ; Step 7: Advance to next module in list
    mov    rcx, [rcx]           ; RCX = Flink of InLoadOrderLinks → next entry
    cmp    rcx, rsi             ; Did we loop back to the head? (circular list)
    jne    .next_module

    ; List exhausted, kernel32 not found
    xor    rax, rax
    ret

8.2 PEB Walk — Full C Implementation

// [C] Full PEB walk to find a DLL by name and resolve a function by name
// No imports used — pure pointer arithmetic

#include <windows.h>
#include <winternl.h>
#include <stdint.h>

// ─── Type definitions (needed because winternl.h hides these) ────
typedef struct _MY_LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID      DllBase;
    PVOID      EntryPoint;
    ULONG      SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
    ULONG      Flags;
    WORD       LoadCount;
    WORD       TlsIndex;
} MY_LDR_DATA_TABLE_ENTRY, *PMY_LDR_DATA_TABLE_ENTRY;

// ─── Helper: case-insensitive wide string comparison ─────────────
BOOL wide_str_ieq(PCWSTR a, PCWSTR b) {
    while (*a && *b) {
        WCHAR ca = (*a >= L'a' && *a <= L'z') ? (*a - 32) : *a;
        WCHAR cb = (*b >= L'a' && *b <= L'z') ? (*b - 32) : *b;
        if (ca != cb) return FALSE;
        a++; b++;
    }
    return (*a == 0 && *b == 0);
}

// ─── Step 1: Walk PEB to find DLL base address ───────────────────
PVOID peb_find_module(PCWSTR dllName) {
    // Get PEB via TEB
    PPEB pPeb = NtCurrentTeb()->ProcessEnvironmentBlock;
    PPEB_LDR_DATA pLdr = pPeb->Ldr;

    // InLoadOrderModuleList head
    PLIST_ENTRY pHead    = &pLdr->InLoadOrderModuleList;
    PLIST_ENTRY pCurrent = pHead->Flink;

    while (pCurrent != pHead) {
        // Cast list entry back to the full LDR_DATA_TABLE_ENTRY
        // InLoadOrderLinks is the FIRST member, so the cast is direct
        PMY_LDR_DATA_TABLE_ENTRY pEntry =
            (PMY_LDR_DATA_TABLE_ENTRY)pCurrent;

        if (pEntry->BaseDllName.Buffer &&
            wide_str_ieq(pEntry->BaseDllName.Buffer, dllName)) {
            return pEntry->DllBase;   // ← base address of the DLL
        }

        pCurrent = pCurrent->Flink;
    }
    return NULL;
}

// ─── Step 2: Parse PE export table to find function ──────────────
PVOID eat_find_export(PVOID moduleBase, PCSTR funcName) {
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)moduleBase;
    if (pDos->e_magic != IMAGE_DOS_SIGNATURE) return NULL;  // MZ check

    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)
        ((BYTE*)moduleBase + pDos->e_lfanew);
    if (pNt->Signature != IMAGE_NT_SIGNATURE) return NULL;  // PE check

    // Get export directory from Optional Header DataDirectory
    DWORD exportRva = pNt->OptionalHeader
        .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    if (!exportRva) return NULL;

    PIMAGE_EXPORT_DIRECTORY pExport =
        (PIMAGE_EXPORT_DIRECTORY)((BYTE*)moduleBase + exportRva);

    // Export table arrays (all RVAs — add moduleBase to dereference)
    DWORD* pFuncRvas  = (DWORD*)((BYTE*)moduleBase + pExport->AddressOfFunctions);
    DWORD* pNameRvas  = (DWORD*)((BYTE*)moduleBase + pExport->AddressOfNames);
    WORD*  pOrdinals  = (WORD*) ((BYTE*)moduleBase + pExport->AddressOfNameOrdinals);

    for (DWORD i = 0; i < pExport->NumberOfNames; i++) {
        PCSTR exportedName = (PCSTR)((BYTE*)moduleBase + pNameRvas[i]);

        if (strcmp(exportedName, funcName) == 0) {
            // pOrdinals[i] gives the index into pFuncRvas
            DWORD funcRva = pFuncRvas[pOrdinals[i]];

            // Check for forwarded export (RVA falls within export directory)
            DWORD exportDirRva = pNt->OptionalHeader
                .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
            DWORD exportDirSize = pNt->OptionalHeader
                .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;

            if (funcRva >= exportDirRva && funcRva < exportDirRva + exportDirSize) {
                // Forwarded export — handle NTDLL.RtlAllocateHeap → HeapAlloc etc.
                // In practice: parse forward string and resolve recursively
                return NULL;  // simplified
            }

            return (PVOID)((BYTE*)moduleBase + funcRva);
        }
    }
    return NULL;  // not found
}

// ─── Step 3: Full usage example ──────────────────────────────────
int shellcode_resolve_apis() {
    // Find kernel32.dll base via PEB walk
    PVOID k32Base = peb_find_module(L"KERNEL32.DLL");
    if (!k32Base) return -1;
    printf("[+] kernel32.dll base: %p\n", k32Base);

    // Resolve LoadLibraryA and GetProcAddress via EAT walk
    typedef HMODULE(WINAPI *pLoadLibraryA)(LPCSTR);
    typedef FARPROC(WINAPI *pGetProcAddress)(HMODULE, LPCSTR);

    pLoadLibraryA   LoadLib    = (pLoadLibraryA)eat_find_export(k32Base, "LoadLibraryA");
    pGetProcAddress GetProc    = (pGetProcAddress)eat_find_export(k32Base, "GetProcAddress");

    if (!LoadLib || !GetProc) return -2;
    printf("[+] LoadLibraryA:   %p\n", LoadLib);
    printf("[+] GetProcAddress: %p\n", GetProc);

    // Now we can load any DLL and resolve any function
    HMODULE hWs2  = LoadLib("ws2_32.dll");
    PVOID pWSAStartup = GetProc(hWs2, "WSAStartup");
    printf("[+] WSAStartup: %p\n", pWSAStartup);

    return 0;
}

8.3 PEB Walk — Using InInitializationOrderModuleList (Classic x86)

; ─── x86 Classic shellcode: kernel32 via InInitializationOrderModuleList ──
; This is what you see in old msfvenom stubs and SLAE shellcode exercises
; InInitializationOrder: [0]=ntdll, [1]=kernel32

find_kernel32_x86:
    xor    eax, eax
    mov    eax, [fs:eax+0x30]   ; EAX = PEB

    mov    eax, [eax+0x0C]      ; EAX = PEB.Ldr (at +0x0C on x86)
    mov    esi, [eax+0x1C]      ; ESI = InInitializationOrderModuleList.Flink
                                ;       This is the FIRST entry (ntdll.dll)

    lodsd                       ; EAX = [ESI]  → Flink of ntdll entry
                                ; ESI += 4     → now points to kernel32 entry's InInitLinks

    mov    eax, [eax+0x08]      ; EAX = DllBase of kernel32
                                ; InInitializationOrderLinks (LIST_ENTRY) = 8 bytes (x86)
                                ; DllBase is at offset +0x08 from InInitializationOrderLinks
    ret

; ─── Why this works ──────────────────────────────────────────────────
; PEB.Ldr+0x1C = &InInitializationOrderModuleList (x86 offset)
; [0] = ntdll.dll  (InInitializationOrder list EXCLUDES the main EXE)
; [1] = kernel32.dll  ← what we want
;
; After lodsd: ESI points to kernel32's InInitializationOrderLinks.Flink
; After mov eax, [eax+0x08]: EAX = DllBase of kernel32

9. Export Address Table (EAT) Walk

What it means: Once you have a DLL's base address, you parse its PE structure manually to find a specific exported function. The Export Directory in the PE Optional Header DataDirectory leads to three arrays: function RVAs, function name RVAs, and name ordinals. Walking these three arrays gives you any exported function's address.

9.1 PE Export Directory Layout

DLL in memory:

[DllBase]
  ├─ IMAGE_DOS_HEADER   (MZ header at offset 0)
  │    e_lfanew ──────────────────────────────────────────────────┐
  │                                                               ↓
  ├─ IMAGE_NT_HEADERS  (PE header at DllBase + e_lfanew)
  │    Signature = "PE\0\0"
  │    FileHeader
  │    OptionalHeader
  │        DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT] ───────────┐
  │            .VirtualAddress = RVA of export directory          │
  │            .Size           = size of export directory         │
  │                                                               ↓
  ├─ IMAGE_EXPORT_DIRECTORY  (DllBase + export RVA)
  │    Name                 → DLL name string (e.g., "KERNEL32.dll")
  │    NumberOfFunctions    → total exported function count
  │    NumberOfNames        → count of named exports (≤ NumberOfFunctions)
  │    AddressOfFunctions   → RVA of DWORD array of function RVAs
  │    AddressOfNames       → RVA of DWORD array of name RVAs
  │    AddressOfNameOrdinals→ RVA of WORD  array of name→function ordinals
  │
  │    Arrays:
  │    ┌─────────────────────────────┐  ┌──────────────────────┐
  │    │ AddressOfNames              │  │ AddressOfNameOrdinals│
  │    │ [0] → RVA of "AddAtom"      │  │ [0] → ordinal N      │
  │    │ [1] → RVA of "AddConsole.." │  │ [1] → ordinal M      │
  │    │ [i] → RVA of func name      │  │ [i] → ordinal O      │
  │    └─────────────────────────────┘  └──────────────────────┘
  │                                                        │
  │    ┌─────────────────────────────┐                     │
  │    │ AddressOfFunctions          │ ←───────────────────┘
  │    │ [ordinal O] → RVA of func   │
  │    └─────────────────────────────┘
  │    DllBase + RVA = absolute function address

9.2 EAT Walk in x64 Assembly

; ─── x64 EAT Walk: find function by hash ─────────────────────────
; Input:  RCX = module base address (e.g., kernel32.dll DllBase)
;         RDX = function name hash (see §10 for hashing)
; Output: RAX = function address (or 0 if not found)

eat_find_by_hash:
    push   rbx
    push   rsi
    push   rdi
    push   r12
    push   r13
    push   r14
    sub    rsp, 0x20             ; shadow space

    mov    r12, rcx              ; R12 = module base
    mov    r13, rdx              ; R13 = target hash

    ; Step 1: Check MZ signature
    cmp    word [r12], 0x5A4D    ; 'MZ'
    jne    .not_found

    ; Step 2: Get PE header (IMAGE_NT_HEADERS)
    movsxd rax, dword [r12+0x3C] ; RVA of NT headers from DOS header
    lea    rbx, [r12+rax]        ; RBX = IMAGE_NT_HEADERS

    ; Step 3: Check PE signature
    cmp    dword [rbx], 0x00004550  ; 'PE\0\0'
    jne    .not_found

    ; Step 4: Get export directory
    ; OptionalHeader starts at NT_HEADERS + 0x18 (after signature + FileHeader)
    ; DataDirectory[0] (IMAGE_DIRECTORY_ENTRY_EXPORT) is at OptionalHeader + 0x70 (x64)
    ; or IMAGE_NT_HEADERS + 0x18 + 0x70 = IMAGE_NT_HEADERS + 0x88
    movsxd rax, dword [rbx+0x88]  ; Export directory RVA
    test   rax, rax
    jz     .not_found
    lea    rsi, [r12+rax]         ; RSI = IMAGE_EXPORT_DIRECTORY

    ; Step 5: Get export table arrays
    movsxd rax, dword [rsi+0x20]  ; AddressOfNames RVA
    lea    rbx, [r12+rax]         ; RBX = names array (DWORD[])

    movsxd rax, dword [rsi+0x24]  ; AddressOfNameOrdinals RVA
    lea    rdi, [r12+rax]         ; RDI = ordinals array (WORD[])

    movsxd rax, dword [rsi+0x1C]  ; AddressOfFunctions RVA
    lea    r14, [r12+rax]         ; R14 = functions array (DWORD[])

    ; Step 6: Iterate over named exports
    xor    ecx, ecx
    mov    ecx, dword [rsi+0x18]  ; NumberOfNames

.loop:
    test   ecx, ecx
    jz     .not_found
    dec    ecx

    ; Get name RVA → name string
    movsxd rax, dword [rbx + rcx*4]  ; names[i] RVA
    lea    rdx, [r12+rax]             ; RDX = pointer to export name string

    ; Hash the name (call hash function, or inline it)
    ; ... (see §10 for ROR13 hash inline)
    ; Compare hash:
    ; cmp rax, r13
    ; jne .loop

    ; Found! Get function address via ordinals
    movzx  rax, word [rdi + rcx*2]   ; ordinal = ordinals[i]
    movsxd rax, dword [r14 + rax*4]  ; funcRVA = functions[ordinal]
    lea    rax, [r12+rax]             ; absolute = base + RVA
    jmp    .done

.not_found:
    xor    rax, rax
.done:
    add    rsp, 0x20
    pop    r14
    pop    r13
    pop    r12
    pop    rdi
    pop    rsi
    pop    rbx
    ret

10. API Hashing

What it means: Instead of comparing function names as strings (which leaves obvious string artifacts in the shellcode binary), shellcode computes a hash of each export name and compares it against a pre-computed hash of the target function. This removes all readable strings from the shellcode, making it harder for static analysis tools to identify which APIs the shellcode calls.

10.1 ROR13 Hash (Most Common in Shellcode)

#!/usr/bin/env python3
# ror13_hash.py — Compute ROR13 hash of Windows API function names

def ror13(value: int, count: int = 13) -> int:
    """Rotate right by 13 bits (32-bit value)."""
    return ((value >> count) | (value << (32 - count))) & 0xFFFFFFFF

def hash_ror13(name: str, include_module: bool = False) -> int:
    """
    Compute ROR13 hash of a function/module name.
    Used by msfvenom shellcode stubs for API resolution.
    """
    h = 0
    for char in name:
        # Uppercase the character (APIs are case-insensitive in some implementations)
        # Note: classic ror13 for API names is case-sensitive
        h = ror13(h)
        h = (h + ord(char)) & 0xFFFFFFFF
    return h

def hash_module_function(module: str, function: str) -> int:
    """
    Compute combined hash: hash(module_uppercase) XOR hash(function)
    OR: hash(module_uppercase) + hash(function)  (variant)
    """
    # Some shellcode variants hash module + function together
    mod_hash  = hash_ror13(module.upper() + "\x00")
    func_hash = hash_ror13(function)
    return (mod_hash + func_hash) & 0xFFFFFFFF

# ─── Pre-compute common hashes ───────────────────────────────────
APIs = [
    ("kernel32.dll", "LoadLibraryA"),
    ("kernel32.dll", "GetProcAddress"),
    ("kernel32.dll", "VirtualAlloc"),
    ("kernel32.dll", "VirtualProtect"),
    ("kernel32.dll", "CreateThread"),
    ("kernel32.dll", "WaitForSingleObject"),
    ("kernel32.dll", "ExitProcess"),
    ("kernel32.dll", "OpenProcess"),
    ("kernel32.dll", "VirtualAllocEx"),
    ("kernel32.dll", "WriteProcessMemory"),
    ("kernel32.dll", "CreateRemoteThread"),
    ("ws2_32.dll",   "WSAStartup"),
    ("ws2_32.dll",   "WSASocketA"),
    ("ws2_32.dll",   "connect"),
    ("ws2_32.dll",   "recv"),
    ("ntdll.dll",    "NtAllocateVirtualMemory"),
    ("ntdll.dll",    "NtCreateThread"),
    ("ntdll.dll",    "NtWriteVirtualMemory"),
]

print(f"{'Module':<20} {'Function':<30} {'ROR13 Hash':<14} {'Combined Hash'}")
print("-" * 85)
for mod, func in APIs:
    fhash = hash_ror13(func)
    chash = hash_module_function(mod, func)
    print(f"{mod:<20} {func:<30} 0x{fhash:08X}     0x{chash:08X}")

10.2 djb2 Hash (Alternative)

// [C] djb2 hash — simpler, used in some custom shellcode
// seed = 5381, each char: hash = hash * 33 XOR char

DWORD djb2_hash(const char* str) {
    DWORD hash = 5381;
    char c;
    while ((c = *str++)) {
        hash = ((hash << 5) + hash) ^ (DWORD)c;  // hash * 33 ^ c
    }
    return hash;
}

// Usage in API resolution:
// Instead of: if (strcmp(exportName, "VirtualAlloc") == 0)
// Use:        if (djb2_hash(exportName) == 0x97D1C069)
//             → No "VirtualAlloc" string in binary

10.3 ROR13 Hash Inline (Assembly)

; ─── ROR13 hash function (inline in shellcode) ───────────────────
; Input:  RSI = pointer to null-terminated ASCII string
; Output: EAX = ROR13 hash

hash_ror13:
    xor    eax, eax             ; EAX = hash = 0

.hash_loop:
    movzx  ecx, byte [rsi]      ; ECX = current char
    test   ecx, ecx             ; null terminator?
    jz     .hash_done           ; yes → done

    ror    eax, 13              ; rotate hash right by 13 bits
    add    eax, ecx             ; add character value
    inc    rsi                  ; advance string pointer
    jmp    .hash_loop

.hash_done:
    ret

; [NOTE] To hash a module name: make it uppercase first
;        Module names in LDR_DATA_TABLE_ENTRY are lowercase in recent Windows
;        Common approach: mask with 0xDF before adding to hash (uppercases letters)
;        ror eax, 13 / and ecx, 0xDF / add eax, ecx

11. RTL_USER_PROCESS_PARAMETERS

What it means: PEB.ProcessParameters points to RTL_USER_PROCESS_PARAMETERS, which holds the process's startup parameters — command line, image path, environment variables, current directory, and window title. This is where shellcode or BOFs go to read environment-specific information without calling any Win32 APIs like GetCommandLineW() or GetEnvironmentVariable().

11.1 RTL_USER_PROCESS_PARAMETERS Structure

// [STRUCT] RTL_USER_PROCESS_PARAMETERS
typedef struct _RTL_USER_PROCESS_PARAMETERS {
    /* +0x00 */ ULONG MaximumLength;
    /* +0x04 */ ULONG Length;
    /* +0x08 */ ULONG Flags;
    /* +0x0C */ ULONG DebugFlags;
    /* +0x10 */ PVOID ConsoleHandle;
    /* +0x18 */ ULONG ConsoleFlags;
    /* +0x20 */ PVOID StandardInput;    // HANDLE to stdin
    /* +0x28 */ PVOID StandardOutput;   // HANDLE to stdout
    /* +0x30 */ PVOID StandardError;    // HANDLE to stderr

    /* +0x38 */ CURDIR CurrentDirectory;
    // CURDIR { UNICODE_STRING DosPath; HANDLE Handle; }
    // [OFFENS] Read current working directory path

    /* +0x50 */ UNICODE_STRING DllPath;     // DLL search path

    /* +0x60 */ UNICODE_STRING ImagePathName;
    // ★ Full path to the EXE: L"C:\\Windows\\System32\\notepad.exe"
    // [OFFENS] Read to get the host process name — useful for process doppelganging,
    //          process hollowing target matching, or sandbox detection

    /* +0x70 */ UNICODE_STRING CommandLine;
    // ★ Full command line: L"C:\\Windows\\System32\\notepad.exe"
    // [OFFENS] Read without calling GetCommandLineW()
    //          Cobalt Strike BOFs use PEB.ProcessParameters.CommandLine extensively

    /* +0x80 */ PVOID Environment;
    // ★ Pointer to the process environment block (null-terminated wide key=value pairs)
    // Format: L"KEY=value\0KEY2=value2\0\0"
    // [OFFENS] Read environment variables: COMPUTERNAME, USERNAME, USERDOMAIN, TEMP, etc.

    /* +0x390 */ ULONG EnvironmentSize;
    // Size of the environment block in bytes

    /* +0x3F0 */ ULONG EnvironmentVersion;

    /* +0x88 */ ULONG StartingX, StartingY;
    /* +0x90 */ ULONG CountX, CountY;
    /* +0x98 */ ULONG CountCharsX, CountCharsY;
    /* +0xA0 */ ULONG FillAttribute;
    /* +0xA4 */ ULONG WindowFlags;
    /* +0xA8 */ ULONG ShowWindowFlags;

    /* +0xB0 */ UNICODE_STRING WindowTitle;

    /* +0xC0 */ UNICODE_STRING DesktopInfo;
    /* +0xD0 */ UNICODE_STRING ShellInfo;
    /* +0xE0 */ UNICODE_STRING RuntimeData;

    /* +0xF0 */ RTL_DRIVE_LETTER_CURDIR CurrentDirectories[0x20];

} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;

11.2 Reading Process Parameters in C

#include <windows.h>
#include <winternl.h>
#include <stdio.h>

void read_process_params() {
    PPEB pPeb = NtCurrentTeb()->ProcessEnvironmentBlock;
    PRTL_USER_PROCESS_PARAMETERS pParams = pPeb->ProcessParameters;

    // ─── Read ImagePathName ───────────────────────────────────
    // UNICODE_STRING: { USHORT Length; USHORT MaxLength; PWSTR Buffer; }
    if (pParams->ImagePathName.Buffer) {
        wprintf(L"[+] Image Path: %.*s\n",
                pParams->ImagePathName.Length / 2,  // Length is in bytes, not chars
                pParams->ImagePathName.Buffer);
    }

    // ─── Read CommandLine ─────────────────────────────────────
    if (pParams->CommandLine.Buffer) {
        wprintf(L"[+] Command Line: %.*s\n",
                pParams->CommandLine.Length / 2,
                pParams->CommandLine.Buffer);
    }

    // ─── Read environment variables ──────────────────────────
    // Environment is a pointer to a wide string block
    // Format: KEY=value\0KEY2=value2\0\0 (double null at end)
    PWSTR pEnv = (PWSTR)pParams->Environment;
    if (pEnv) {
        printf("\n[+] Environment variables:\n");
        while (*pEnv) {
            // Find the '=' separator
            PWSTR pEq = wcschr(pEnv, L'=');
            if (pEq) {
                int keyLen = (int)(pEq - pEnv);
                wprintf(L"    %-30.*s = %s\n", keyLen, pEnv, pEq + 1);
            }
            // Advance past this entry (skip to next null terminator + 1)
            pEnv += wcslen(pEnv) + 1;
        }
    }

    // ─── Read current directory ───────────────────────────────
    if (pParams->CurrentDirectory.DosPath.Buffer) {
        wprintf(L"[+] Current Dir: %.*s\n",
                pParams->CurrentDirectory.DosPath.Length / 2,
                pParams->CurrentDirectory.DosPath.Buffer);
    }
}

11.3 Reading Environment in Shellcode (x64 Assembly)

; ─── Shellcode: Get COMPUTERNAME from environment ────────────────
; Chain: GS:[0x60] → PEB → ProcessParameters → Environment
; Then walk the KEY=value\0 pairs looking for "COMPUTERNAME="

xor    rax, rax
mov    rbx, gs:[rax+0x60]   ; RBX = PEB
mov    rbx, [rbx+0x20]      ; RBX = ProcessParameters (at +0x20 on x64)
mov    rbx, [rbx+0x80]      ; RBX = Environment pointer

; RBX now points to the wide env block.
; Walk pairs until COMPUTERNAME= found or double-null reached.
; (Actual compare loop not shown for brevity — standard null-terminated loop)

12. PEB-Based Anti-Debug Techniques

What it means: The PEB and heap structures are modified by Windows when a debugger attaches. Malware and shellcode read these modified values to detect analysis environments. There are three main checks: BeingDebugged, NtGlobalFlag, and heap flags (Flags / ForceFlags).

[ANTIDEBUG]

12.1 Check 1 — PEB.BeingDebugged

// TECHNIQUE 1: BeingDebugged byte at PEB+0x02
// This is what kernel32!IsDebuggerPresent() does internally

// Method A: Win32 API (obvious — appears in IAT)
if (IsDebuggerPresent()) {
    // Debugger attached
}

// Method B: Direct PEB read (no API, harder to hook)
PPEB pPeb = NtCurrentTeb()->ProcessEnvironmentBlock;
if (pPeb->BeingDebugged != 0) {
    // Debugger attached
}

// Method C: Inline assembly (x64 — most opaque, no imports)
BOOL check_being_debugged_asm() {
    BYTE bd;
    __asm__ volatile (
        "mov %%gs:0x60, %%rax\n\t"   // RAX = PEB
        "movzb 0x02(%%rax), %0\n\t"  // %0 = BeingDebugged byte
        : "=r"(bd)
        :: "rax"
    );
    return bd != 0;
}

// Method D: NASM (position-independent shellcode)
; mov rax, gs:[0x60]          ; PEB
; movzx eax, byte [rax+0x02]  ; BeingDebugged
; test eax, eax
; jnz debugger_present

12.2 Check 2 — PEB.NtGlobalFlag

// TECHNIQUE 2: NtGlobalFlag at PEB+0xBC (x64) / PEB+0x68 (x86)
// Under debugger: flags 0x10 | 0x20 | 0x40 = 0x70 are set
//   FLG_HEAP_ENABLE_TAIL_CHECK   = 0x10 → adds extra bytes after heap allocs
//   FLG_HEAP_ENABLE_FREE_CHECK   = 0x20 → fills freed memory with 0xFEEEFEEE
//   FLG_HEAP_VALIDATE_PARAMETERS = 0x40 → validates heap params on every call

void check_ntglobalflag() {
    PPEB pPeb = NtCurrentTeb()->ProcessEnvironmentBlock;

    // Access NtGlobalFlag — not in winternl.h PPEB, use raw offset
    DWORD ntGlobalFlag = *(DWORD*)((BYTE*)pPeb + 0xBC);  // x64
    // DWORD ntGlobalFlag = *(DWORD*)((BYTE*)pPeb + 0x68);  // x86

    if (ntGlobalFlag & 0x70) {
        printf("[!] Debugger detected via NtGlobalFlag: 0x%08X\n", ntGlobalFlag);
    }
}

// Assembly version (x64):
; mov rax, gs:[0x60]              ; PEB
; mov eax, dword [rax+0xBC]       ; NtGlobalFlag
; and eax, 0x70                   ; mask debug bits
; test eax, eax
; jnz debugger_present

12.3 Check 3 — Heap Flags (ProcessHeap)

// TECHNIQUE 3: ProcessHeap.Flags and ProcessHeap.ForceFlags
// The process's default heap has debug-related flags set when a debugger is present

// PEB.ProcessHeap points to an _HEAP structure
// Within _HEAP:
//   +0x14 Flags      (x86) / +0x40 Flags      (x64)  ← DWORD
//   +0x18 ForceFlags (x86) / +0x44 ForceFlags  (x64)  ← DWORD

// Normal values:
//   Flags:      HEAP_GROWABLE = 0x00000002
//   ForceFlags: 0x00000000

// Under debugger:
//   Flags:      0x50000062 (approx — exact value depends on NtGlobalFlag)
//   ForceFlags: 0x40000060

void check_heap_flags() {
    PPEB pPeb = NtCurrentTeb()->ProcessEnvironmentBlock;
    PVOID pHeap = pPeb->ProcessHeap;

    // Read Flags and ForceFlags from heap structure
    // x64 offsets: Flags at +0x40, ForceFlags at +0x44
    DWORD heapFlags      = *(DWORD*)((BYTE*)pHeap + 0x40);  // x64
    DWORD heapForceFlags = *(DWORD*)((BYTE*)pHeap + 0x44);  // x64
    // For x86: 0x14 and 0x18 respectively

    // Normal: heapFlags = 0x02 (HEAP_GROWABLE), heapForceFlags = 0x00
    if (heapFlags != 0x00000002 || heapForceFlags != 0x00000000) {
        printf("[!] Debugger detected via heap flags!\n");
        printf("    Flags:      0x%08X (expected 0x00000002)\n", heapFlags);
        printf("    ForceFlags: 0x%08X (expected 0x00000000)\n", heapForceFlags);
    }
}

12.4 Combined Anti-Debug Scan

// Full anti-debug scanner using all three PEB-based techniques
typedef enum {
    DEBUG_NONE          = 0,
    DEBUG_BEING_DEBUGGED = 1 << 0,
    DEBUG_NTGLOBALFLAG  = 1 << 1,
    DEBUG_HEAP_FLAGS    = 1 << 2,
} DEBUG_INDICATORS;

DWORD scan_for_debugger() {
    DWORD result = DEBUG_NONE;
    PPEB pPeb = NtCurrentTeb()->ProcessEnvironmentBlock;

    // Check 1: BeingDebugged
    if (pPeb->BeingDebugged)
        result |= DEBUG_BEING_DEBUGGED;

    // Check 2: NtGlobalFlag (x64 offset 0xBC)
    DWORD ntGF = *(DWORD*)((BYTE*)pPeb + 0xBC);
    if (ntGF & 0x70)
        result |= DEBUG_NTGLOBALFLAG;

    // Check 3: Heap flags (x64 offsets)
    PVOID pHeap = pPeb->ProcessHeap;
    DWORD hf  = *(DWORD*)((BYTE*)pHeap + 0x40);
    DWORD hff = *(DWORD*)((BYTE*)pHeap + 0x44);
    if (hf != 0x02 || hff != 0x00)
        result |= DEBUG_HEAP_FLAGS;

    return result;
}

int main() {
    DWORD dbg = scan_for_debugger();

    if (dbg == DEBUG_NONE) {
        printf("[+] No debugger detected — safe to proceed\n");
    } else {
        if (dbg & DEBUG_BEING_DEBUGGED) printf("[!] BeingDebugged flag set\n");
        if (dbg & DEBUG_NTGLOBALFLAG)   printf("[!] NtGlobalFlag = 0x70\n");
        if (dbg & DEBUG_HEAP_FLAGS)     printf("[!] Heap debug flags set\n");
        // Decoy action: exit, delay, or execute benign code
        ExitProcess(0);
    }
    // ... payload continues ...
}

13. Bypassing Anti-Debug via PEB Patching

What it means: When you are reverse-engineering malware that uses PEB-based anti-debug tricks, you can patch the PEB in memory to clear the debug indicators, making the malware think it's not being debugged. This is done in WinDbg, x64dbg, or programmatically.

[BYPASS]

13.1 Manual Bypass in WinDbg

; ─── WinDbg: Bypass all three PEB anti-debug checks ──────────────

; Step 1: Locate PEB
r peb                           ; show PEB address stored in pseudo-register
dt _PEB @$peb                   ; dump PEB structure

; Step 2: Clear BeingDebugged
eb @$peb+2 0                    ; eb = edit byte; PEB+0x02 = BeingDebugged; write 0

; Step 3: Clear NtGlobalFlag (x64: PEB+0xBC)
ed @$peb+bc 0                   ; ed = edit dword; write 0x00000000

; Step 4: Clear heap flags
; First find ProcessHeap pointer (PEB+0x30 on x64)
r rax = poi(@$peb+0x30)         ; RAX = ProcessHeap address
ed rax+0x40 00000002            ; Flags = HEAP_GROWABLE (normal)
ed rax+0x44 00000000            ; ForceFlags = 0 (normal)

; Verify all cleared:
dt _PEB @$peb BeingDebugged NtGlobalFlag

13.2 Bypass in x64dbg (Script / Plugin)

; x64dbg script to bypass PEB anti-debug
; (Run via Script tab or as plugin)

; Get PEB address
mov peb, qword:[gs:0x60]    ; peb = GS:[0x60]

; Clear BeingDebugged
mov byte:[peb+0x02], 0

; Clear NtGlobalFlag (x64)
mov dword:[peb+0xBC], 0

; Clear heap flags
mov heap, qword:[peb+0x30]  ; ProcessHeap
mov dword:[heap+0x40], 2    ; Flags = HEAP_GROWABLE
mov dword:[heap+0x44], 0    ; ForceFlags = 0

; Done
msg "PEB anti-debug cleared!"

; [NOTE] ScyllaHide x64dbg plugin does all of this automatically
;        Also handles CheckRemoteDebuggerPresent, NtQueryInformationProcess, etc.

13.3 Programmatic PEB Patching (C)

// Self-patching: clear own PEB debug indicators
// Useful for shellcode that needs to evade sandbox analysis

void clear_peb_debug_indicators() {
    PPEB pPeb = NtCurrentTeb()->ProcessEnvironmentBlock;

    // 1. Clear BeingDebugged
    pPeb->BeingDebugged = 0;

    // 2. Clear NtGlobalFlag (x64: offset 0xBC)
    *(DWORD*)((BYTE*)pPeb + 0xBC) = 0;  // x64
    // *(DWORD*)((BYTE*)pPeb + 0x68) = 0;  // x86

    // 3. Clear ProcessHeap debug flags
    PVOID pHeap = pPeb->ProcessHeap;
    *(DWORD*)((BYTE*)pHeap + 0x40) = HEAP_GROWABLE; // = 0x00000002 (x64)
    *(DWORD*)((BYTE*)pHeap + 0x44) = 0x00000000;    // ForceFlags (x64)
    // x86: offsets are +0x14 and +0x18

    printf("[+] PEB debug indicators cleared\n");

    // Verify:
    // IsDebuggerPresent() will now return FALSE
    // NtGlobalFlag will be 0x00
    // Heap flags will appear normal
}

14. Remote PEB Walking

What it means: You can read another process's PEB from outside that process using NtQueryInformationProcess to get the PEB address, then ReadProcessMemory to read its contents. This lets you enumerate loaded DLLs in a remote process — useful for process injection targeting, checking if a target process has a specific DLL loaded, or verifying injection success.

14.1 Get Remote PEB Address

// [C] Get PEB address of a remote process
#include <windows.h>
#include <winternl.h>

typedef NTSTATUS (WINAPI* NtQueryInformationProcess_t)(
    HANDLE ProcessHandle,
    PROCESSINFOCLASS ProcessInformationClass,
    PVOID  ProcessInformation,
    ULONG  ProcessInformationLength,
    PULONG ReturnLength
);

PVOID get_remote_peb_address(HANDLE hProcess) {
    // Resolve NtQueryInformationProcess from ntdll (not in kernel32)
    HMODULE ntdll = GetModuleHandleA("ntdll.dll");
    NtQueryInformationProcess_t pNtQIP =
        (NtQueryInformationProcess_t)GetProcAddress(ntdll, "NtQueryInformationProcess");

    PROCESS_BASIC_INFORMATION pbi = {0};
    ULONG returnLen = 0;

    NTSTATUS status = pNtQIP(
        hProcess,
        ProcessBasicInformation,  // class 0
        &pbi,
        sizeof(pbi),
        &returnLen
    );

    if (status != 0) {
        printf("[-] NtQueryInformationProcess failed: 0x%08X\n", status);
        return NULL;
    }

    // PebBaseAddress is the remote PEB pointer
    printf("[+] Remote PEB address: %p\n", pbi.PebBaseAddress);
    return pbi.PebBaseAddress;
}

14.2 Enumerate Remote Process Modules

// [C] Read remote PEB and enumerate loaded DLLs
// (Process injection targeting, injection verification)

void enum_remote_modules(DWORD targetPid) {
    // Open target process
    HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
                                   FALSE, targetPid);
    if (!hProcess) {
        printf("[-] OpenProcess failed: %lu\n", GetLastError());
        return;
    }

    // Get remote PEB address
    PVOID remotePebAddr = get_remote_peb_address(hProcess);
    if (!remotePebAddr) { CloseHandle(hProcess); return; }

    // Read remote PEB into local buffer
    PEB localPeb = {0};
    SIZE_T bytesRead = 0;
    if (!ReadProcessMemory(hProcess, remotePebAddr, &localPeb, sizeof(PEB), &bytesRead)) {
        printf("[-] ReadProcessMemory (PEB) failed: %lu\n", GetLastError());
        CloseHandle(hProcess);
        return;
    }

    // Read remote PEB_LDR_DATA
    PEB_LDR_DATA localLdr = {0};
    ReadProcessMemory(hProcess, localPeb.Ldr, &localLdr, sizeof(localLdr), &bytesRead);

    // Walk InLoadOrderModuleList
    // The list head pointer (in our local copy) still points into the remote process
    PLIST_ENTRY pRemoteHead = &((PPEB_LDR_DATA)localPeb.Ldr)->InLoadOrderModuleList;
    PLIST_ENTRY pRemoteCurrent = localLdr.InLoadOrderModuleList.Flink;

    printf("[+] Modules in PID %lu:\n", targetPid);

    while (pRemoteCurrent != pRemoteHead) {
        // We need to use CONTAINING_RECORD logic manually
        // InLoadOrderLinks is the FIRST member of LDR_DATA_TABLE_ENTRY
        // So pRemoteCurrent IS the address of LDR_DATA_TABLE_ENTRY in remote process

        // Read the remote LDR_DATA_TABLE_ENTRY
        // (Simplified — use actual struct offsets)
        BYTE remoteEntry[256] = {0};
        ReadProcessMemory(hProcess, pRemoteCurrent, remoteEntry, sizeof(remoteEntry), &bytesRead);

        // Flink is at +0x00 (LIST_ENTRY.Flink)
        PVOID nextEntry = *(PVOID*)&remoteEntry[0x00];

        // DllBase is at +0x30 (x64 LDR_DATA_TABLE_ENTRY)
        PVOID dllBase = *(PVOID*)&remoteEntry[0x30];

        // BaseDllName is a UNICODE_STRING at +0x58
        // UNICODE_STRING.Length is USHORT at +0x58
        // UNICODE_STRING.Buffer is PWSTR at +0x60
        USHORT nameLen = *(USHORT*)&remoteEntry[0x58];
        PVOID  nameBuf = *(PVOID*)&remoteEntry[0x60];

        if (nameBuf && nameLen > 0 && nameLen < 256) {
            WCHAR moduleName[128] = {0};
            ReadProcessMemory(hProcess, nameBuf, moduleName,
                              min(nameLen, sizeof(moduleName)-2), &bytesRead);
            wprintf(L"    Base: %p  %s\n", dllBase, moduleName);
        }

        pRemoteCurrent = (PLIST_ENTRY)nextEntry;
        if (!pRemoteCurrent) break;
    }

    CloseHandle(hProcess);
}

15. WinDbg Cheatsheet — Live Inspection

; ═══════════════════════════════════════════════════════════════
; TEB Inspection
; ═══════════════════════════════════════════════════════════════

!teb                            ; Formatted TEB dump (StackBase, StackLimit, PEB ptr, TLS)
dt ntdll!_TEB @$teb             ; Raw TEB struct with all field offsets
dt ntdll!_NT_TIB @$teb          ; Just the inner NT_TIB portion
~                               ; List all threads in process
~0s                             ; Switch to thread 0
~1s                             ; Switch to thread 1
!teb                            ; Now shows THAT thread's TEB
r @$teb                         ; Show $teb pseudo-register value

; Access TEB fields directly:
dt ntdll!_TEB @$teb ClientId    ; Show PID and TID
dt ntdll!_TEB @$teb LastErrorValue
dt ntdll!_TEB @$teb TlsSlots    ; All 64 TLS slots

; ═══════════════════════════════════════════════════════════════
; PEB Inspection
; ═══════════════════════════════════════════════════════════════

!peb                            ; Formatted PEB dump (image, params, modules)
dt ntdll!_PEB @$peb             ; Raw PEB struct
r @$peb                         ; Show $peb pseudo-register value

; Key PEB fields:
dt ntdll!_PEB @$peb BeingDebugged
dt ntdll!_PEB @$peb Ldr
dt ntdll!_PEB @$peb ProcessParameters
dt ntdll!_PEB @$peb NtGlobalFlag
dt ntdll!_PEB @$peb ProcessHeap
dt ntdll!_PEB @$peb ImageBaseAddress
dt ntdll!_PEB @$peb OSMajorVersion OSMinorVersion OSBuildNumber

; ═══════════════════════════════════════════════════════════════
; Module List Inspection
; ═══════════════════════════════════════════════════════════════

!peb                            ; Shows loaded module list as part of PEB dump
lm                              ; List all loaded modules (simpler)
lmv m kernel32                  ; Verbose info on kernel32

; Walk PEB_LDR_DATA manually:
dt ntdll!_PEB_LDR_DATA poi(@$peb+0x18)
; poi() dereferences a pointer: poi(addr) = *(PVOID*)addr

; Walk InLoadOrderModuleList:
dt ntdll!_LDR_DATA_TABLE_ENTRY poi(@$peb+0x18)+0x10
; The above shows the first entry (Flink of InLoadOrderModuleList)

; Dump all LDR entries in InLoadOrderModuleList:
!list -x "dt ntdll!_LDR_DATA_TABLE_ENTRY @$extret" poi(@$peb+0x18)+0x10

; ═══════════════════════════════════════════════════════════════
; Process Parameters
; ═══════════════════════════════════════════════════════════════

dt ntdll!_RTL_USER_PROCESS_PARAMETERS poi(@$peb+0x20)
; Shows CommandLine, ImagePathName, Environment, CurrentDirectory

; Read CommandLine string:
du poi(poi(@$peb+0x20)+0x70+0x08)
; PEB → ProcessParameters (+0x20) → CommandLine (+0x70) → Buffer (+0x08)

; Read ImagePathName:
du poi(poi(@$peb+0x20)+0x60+0x08)

; ═══════════════════════════════════════════════════════════════
; Anti-Debug PEB Patching (in WinDbg)
; ═══════════════════════════════════════════════════════════════

eb @$peb+2 0                    ; Clear BeingDebugged
ed @$peb+bc 0                   ; Clear NtGlobalFlag (x64)
r rax = poi(@$peb+0x30)         ; Get ProcessHeap
ed rax+40 00000002              ; Heap.Flags = HEAP_GROWABLE
ed rax+44 00000000              ; Heap.ForceFlags = 0

; Verify:
db @$peb+2 L1                   ; Show BeingDebugged byte
dd @$peb+bc L1                  ; Show NtGlobalFlag

16. Offset Comparison Tables

16.1 TEB Key Offsets

Field x86 (FS:[]) x64 (GS:[]) Notes
NT_TIB +0x000 +0x000 First field, embedded struct
NtTib.ExceptionList +0x000 +0x000 SEH chain head
NtTib.StackBase +0x004 +0x008 Top of thread stack
NtTib.StackLimit +0x008 +0x010 Bottom of committed stack
NtTib.SubSystemTib +0x00C +0x018 Rarely used
NtTib.FiberData +0x010 +0x020 Fiber pointer (or version)
NtTib.ArbitraryUserPointer +0x014 +0x028 App-usable
NtTib.Self +0x018 +0x030 Self-pointer = TEB base
EnvironmentPointer +0x01C +0x038 Legacy
ClientId.UniqueProcess (PID) +0x020 +0x040 Process ID
ClientId.UniqueThread (TID) +0x024 +0x048 Thread ID
ActiveRpcHandle +0x028 +0x050
ThreadLocalStoragePointer +0x02C +0x058
ProcessEnvironmentBlock +0x030 +0x060 → PEB address
LastErrorValue +0x034 +0x068 GetLastError() reads this
CurrentLocale +0x0C4 +0x108 Locale ID
TlsSlots[0..63] +0xE10 +0x1480 First 64 TLS slots
TlsLinks +0xF10 +0x1680 List linking all TLS
TlsExpansionSlots +0xF94 +0x1780 Slots 64–1088

16.2 PEB Key Offsets

Field x86 x64 Notes
InheritedAddressSpace +0x000 +0x000
ReadImageFileExecOptions +0x001 +0x001
BeingDebugged +0x002 +0x002 Anti-debug flag
BitField / NtImageFlags +0x003 +0x003
ImageBaseAddress +0x008 +0x010 Host EXE base
Ldr +0x00C +0x018 → PEB_LDR_DATA
ProcessParameters +0x010 +0x020 → RTL_USER_PROCESS_PARAMETERS
SubSystemData +0x014 +0x028
ProcessHeap +0x018 +0x030 Default heap handle
FastPebLock +0x01C +0x038 Loader lock
AtlThunkSListPtr +0x020 +0x040
KernelCallbackTable +0x02C +0x058 KernelCallbackTable hijacking
NtGlobalFlag +0x068 +0x0BC Anti-debug: 0x70 = debugger
NumberOfProcessors +0x064 +0x0B8 Sandbox: < 4 → suspicious
OSMajorVersion +0x0A4 +0x11C
OSMinorVersion +0x0A8 +0x120
OSBuildNumber +0x0AC +0x124

16.3 PEB_LDR_DATA Key Offsets

Field x64 Offset Notes
Length +0x000 Struct size
Initialized +0x004 TRUE after loader init
InLoadOrderModuleList +0x010 First DLL list (EXE, ntdll, kernel32...)
InMemoryOrderModuleList +0x020 By base address
InInitializationOrderModuleList +0x030 ntdll first, then kernel32

16.4 LDR_DATA_TABLE_ENTRY Key Offsets (x64)

Field x64 Offset Notes
InLoadOrderLinks +0x000 Flink=+0x000, Blink=+0x008
InMemoryOrderLinks +0x010
InInitializationOrderLinks +0x020
DllBase +0x030 DLL base address (= HMODULE)
EntryPoint +0x038 DllMain address
SizeOfImage +0x040 Image size in bytes
FullDllName +0x048 Full path UNICODE_STRING
BaseDllName +0x058 Short name UNICODE_STRING
BaseDllName.Length +0x058 USHORT (bytes)
BaseDllName.MaxLength +0x05A USHORT
BaseDllName.Buffer +0x060 PWSTR → actual module name
Flags +0x068 DWORD flags
LoadCount +0x06C WORD
TlsIndex +0x06E WORD

16.5 ProcessHeap Debug Offsets

Field x86 Offset x64 Offset Normal Value Debugger Value
Flags +0x014 +0x040 0x00000002 0x50000062
ForceFlags +0x018 +0x044 0x00000000 0x40000060

17. Cheat Sheet

Quick Shellcode Access Reference (x64)

; ─── Entry points ─────────────────────────────────────────────────
mov rax, gs:[0x30]      ; TEB.NtTib.Self (TEB address)
mov rax, gs:[0x60]      ; TEB.ProcessEnvironmentBlock (PEB)
mov rax, gs:[0x08]      ; TEB.NtTib.StackBase
mov rax, gs:[0x10]      ; TEB.NtTib.StackLimit
mov eax, gs:[0x40]      ; TEB.ClientId.UniqueProcess (PID) - 32-bit
mov eax, gs:[0x48]      ; TEB.ClientId.UniqueThread  (TID) - 32-bit
mov eax, gs:[0x68]      ; TEB.LastErrorValue

; ─── From PEB (RBX = PEB) ──────────────────────────────────────────
movzx eax, byte [rbx+0x02]  ; PEB.BeingDebugged
mov   rcx, [rbx+0x10]       ; PEB.ImageBaseAddress
mov   rdx, [rbx+0x18]       ; PEB.Ldr
mov   r8,  [rbx+0x20]       ; PEB.ProcessParameters
mov   r9,  [rbx+0x30]       ; PEB.ProcessHeap
mov   eax, [rbx+0xB8]       ; PEB.NumberOfProcessors
mov   eax, [rbx+0xBC]       ; PEB.NtGlobalFlag

; ─── From Ldr (RDX = PEB.Ldr) ──────────────────────────────────────
lea   rsi, [rdx+0x10]        ; &InLoadOrderModuleList (list head)
mov   rcx, [rsi]             ; first Flink → first LDR_DATA_TABLE_ENTRY

; ─── From LDR_DATA_TABLE_ENTRY (RCX = entry) ──────────────────────
mov   rax, [rcx+0x30]        ; DllBase (module base address)
mov   r8,  [rcx+0x60]        ; BaseDllName.Buffer (PWSTR)
movzx edx, word [rcx+0x58]   ; BaseDllName.Length (bytes)

; ─── From ProcessParameters (R8 = ProcessParameters) ────────────
mov   rax, [r8+0x80]         ; Environment pointer (wide string block)
lea   rcx, [r8+0x60]         ; &ImagePathName (UNICODE_STRING)
lea   rcx, [r8+0x70]         ; &CommandLine (UNICODE_STRING)

PEB Walk Chain — One Liner Path

GS:[0x60]          → PEB
PEB+0x18           → Ldr (PEB_LDR_DATA)
Ldr+0x10           → InLoadOrderModuleList.Flink (first LDR_DATA_TABLE_ENTRY)
entry+0x30         → DllBase (kernel32.dll base)
DllBase+e_lfanew   → NT Headers
NTHdrs+0x88        → Export Directory RVA
ExportDir+0x20     → AddressOfNames (name RVAs)
ExportDir+0x24     → AddressOfNameOrdinals
ExportDir+0x1C     → AddressOfFunctions
Match name[i]      → ordinals[i] → functions[ordinals[i]] = function RVA
DllBase + funcRVA  → Absolute function address → CALL!

Module Order in Lists

InLoadOrderModuleList (includes main EXE):
  [0] = main executable (notepad.exe, etc.)
  [1] = ntdll.dll
  [2] = KERNEL32.DLL        ← shellcode targets [2]
  [3] = KERNELBASE.DLL
  [4] = ... additional DLLs

InInitializationOrderModuleList (excludes main EXE):
  [0] = ntdll.dll
  [1] = KERNEL32.DLL        ← shellcode targets [1] (classic x86 method)
  [2] = KERNELBASE.DLL
  [3] = ...

Anti-Debug Summary

Detection               PEB Path                Value (debugged)    Value (clean)
───────────────────────────────────────────────────────────────────────────────
BeingDebugged           PEB+0x02                0x01                0x00
NtGlobalFlag            PEB+0xBC (x64)          0x70                0x00
                        PEB+0x68 (x86)
ProcessHeap.Flags       PEB.ProcessHeap+0x40(x64) 0x50000062       0x00000002
ProcessHeap.ForceFlags  PEB.ProcessHeap+0x44(x64) 0x40000060       0x00000000

Bypass: overwrite with clean values in WinDbg, x64dbg, or from within the process

Tools for PEB/TEB Inspection

Tool Usage
WinDbg !peb, !teb, dt ntdll!_PEB, dt ntdll!_TEB
x64dbg PEB in dump: follow address gs:[60]
Process Hacker / System Informer GUI view of PEB, LDR lists, heap flags
API Monitor Catch when malware reads PEB fields (hook-based)
Frida JavaScript hooks for PEB fields at runtime
PE-bear / PE-view Browse PE export tables (EAT walk verification)
pestudio Static export table analysis
ScyllaHide Auto-patches all PEB anti-debug checks in x64dbg
HyperHide Hypervisor-level anti-anti-debug

References & Further Reading

Windows Internals Books: - Windows Internals (7th Ed) — Pavel Yosifovich, Alex Ionescu, Mark Russinovich, David Solomon - The Art of Memory Forensics — Ligh, Case, Levy, Walters (Chapter 5 — PEB/TEB)

Online References: - Geoff Chappell's PEB documentation — https://geoffchappell.com/studies/windows/win32/ntdll/structs/peb - NtDoc (phnt headers) — https://ntdoc.m417z.com - Red Teaming's Dojo: PEB — https://mohamed-fakroud.gitbook.io/red-teamings-dojo/windows-internals/peb - PEB Walk Anatomy — https://moval0x1.github.io/posts/the-peb-walk-anatomy/

SLAE / Shellcode Courses: - SecurityTube SLAE64 — Module on PEB Walk and EAT resolution - Sektor7 Malware Development Courses — PEB walk + API hashing - Maldev Academy — PEB walk, custom GetProcAddress

Anti-Debug Reference: - The "Ultimate" Anti-Debug Reference — Peter Ferrie (archived) - Checkpoint Research Anti-Debug — https://anti-debug.checkpoint.com - Art of Unpacking (BlackHat 2007) — Yason

← Malware development top ↑