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