PROCESS STRUCTURES
PEB
— Process Environment Block
USERMODE
HIGH-VALUE TARGET
The PEB is a user-mode structure maintained by the OS for each process. It sits at a
fixed, discoverable address and contains almost everything the loader, heap manager, and runtime need —
which is exactly why malware reads it constantly. Located at
fs:[0x30] (x86) or
gs:[0x60] (x64).
| OFFSET (x64) | FIELD | TYPE | DESCRIPTION |
|---|---|---|---|
| +0x000 | InheritedAddressSpace | BYTE | Non-zero if process was spawned from another process's address space |
| +0x002 | BeingDebugged | BYTE | 1 if a debugger is attached — classic anti-debug check point |
| +0x010 | ImageBaseAddress | PVOID | Base address where the main executable was loaded in memory |
| +0x018 | Ldr | PPEB_LDR_DATA | Pointer to loader data — contains linked lists of all loaded modules (DLLs). Core of manual DLL walking. |
| +0x020 | ProcessParameters | PRTL_USER_PROCESS_PARAMETERS | Command line, image path, environment variables, current directory |
| +0x030 | ProcessHeap | PVOID | Pointer to the default process heap |
| +0x058 | NtGlobalFlag | ULONG | Set to 0x70 when running under debugger with heap debugging — another anti-debug check target |
| +0x068 | NumberOfHeaps | ULONG | Count of heaps in the process |
| +0x078 | OSMajorVersion / OSMinorVersion | ULONG | OS version — some malware checks this to choose code paths |
| +0x2EC | ImageSubsystem | ULONG | Console (3) vs GUI (2) — IMAGE_SUBSYSTEM_* |
// ANALYST TIP
- In x64dbg:
View → PEBshows a parsed view. In WinDbg:!pebordt ntdll!_PEB @$peb PEB.Ldris your fastest path to enumerate loaded modules without calling any API — every reflective loader abuses this.- Check
BeingDebuggedandNtGlobalFlagfor anti-debug patching. Flip both to 0 to bypass basic checks.
// ATTACKER ABUSE
- API hashing / reflective loaders walk
Ldr.InMemoryOrderModuleListto findkernel32.dllandntdll.dllbase addresses without callingLoadLibrary - Module stomping overwrites a legitimate DLL's memory region discovered via
Ldr - Anti-debug via
BeingDebugged,NtGlobalFlag, andNtQueryInformationProcess(ProcessDebugPort) - Process hollowing reads
ImageBaseAddressto locate and overwrite the host process image
; Classic 64-bit API resolution stub (shellcode)
; Get kernel32.dll base via PEB → Ldr → InMemoryOrderModuleList
mov rax, qword ptr
gs:[0x60] ; rax = PEB
mov rax, [rax + 0x18] ; rax = PEB.Ldr
mov rax, [rax + 0x20] ; rax = Ldr.InMemoryOrderModuleList.Flink
mov rax, [rax] ; skip ntdll (first entry)
mov rax, [rax + 0x20] ; DllBase of kernel32.dll
TEB
— Thread Environment Block
PER-THREADSHELLCODE START
The TEB (also called NT_TIB) is a per-thread structure in user space. Each thread has
its own TEB. It's always accessible via
fs:[0] (x86) or gs:[0] (x64) — no API
call needed. It's the first thing shellcode reads to bootstrap itself.
| OFFSET (x64) | FIELD | TYPE | DESCRIPTION |
|---|---|---|---|
| +0x000 | NtTib.ExceptionList | PEXCEPTION_REGISTRATION | SEH chain pointer (x86 only — in x64 exceptions use table-based dispatch) |
| +0x008 | NtTib.StackBase | PVOID | Top of the thread's stack (highest address) |
| +0x010 | NtTib.StackLimit | PVOID | Bottom of committed stack. Stack grows down toward this. |
| +0x030 | NtTib.Self | PNT_TIB | Self-pointer — reading gs:[0x30] always gives the TEB address itself |
| +0x060 | ProcessEnvironmentBlock | PPEB | Pointer back to the PEB of this process |
| +0x068 | LastErrorValue | ULONG | Per-thread last error (what GetLastError() returns) |
| +0x0C0 | ThreadLocalStorage | PVOID[64] | TLS slots — used by some malware for per-thread state hiding |
| +0x100 | RealClientId.UniqueProcess | HANDLE | PID of owning process |
| +0x108 | RealClientId.UniqueThread | HANDLE | TID of this thread |
| +0x1480 | DeallocationStack | PVOID | Base of the memory allocation that backs this thread's stack |
// ANALYST TIP
- WinDbg:
dt ntdll!_TEB @$teb| x64dbg: no native view, usegs:[0x30]as pointer and follow in memory dump - Reading
gs:[0x60](offset of PEB inside TEB) is the standard shellcode bootstrap to get PEB without any API call - Check
LastErrorValue— can confirm whether a suspicious API call succeeded or failed
EPROCESS
— Executive Process Object (Kernel)
KERNELDKOM TARGET
EPROCESS is the kernel-mode process object. Every running process has one. It's
allocated in the kernel pool and linked into a doubly-linked list (
ActiveProcessLinks).
Rootkits manipulate this list to hide processes (DKOM — Direct Kernel Object Manipulation). Offsets vary
by Windows build — always verify with dt nt!_EPROCESS.
| OFFSET (Win10 22H2 x64 ~) | FIELD | TYPE | DESCRIPTION |
|---|---|---|---|
| +0x000 | Pcb (KPROCESS) | KPROCESS | Embedded kernel process control block — scheduler uses this |
| +0x2E0 | ActiveProcessLinks | LIST_ENTRY | Doubly-linked list of all processes. Rootkits unlink here to hide (DKOM) |
| +0x2E8 | Quota | PEPROCESS_QUOTA_BLOCK | Pool quota tracking |
| +0x358 | UniqueProcessId | PVOID | The PID. Cast to ULONG_PTR. |
| +0x448 | ImageFileName | CHAR[15] | Process image name (max 14 chars, null-term). Truncated — use full path from SeAuditProcessCreationInfo for full name. |
| +0x550 | Token | EX_FAST_REF | Security token. Token swapping (steal SYSTEM token) targets this offset. |
| +0x4B0 | InheritedFromUniqueProcessId | PVOID | Parent PID — useful in forensics to reconstruct process tree |
| +0x578 | Protection | PS_PROTECTION | PPL (Protected Process Light) flags — EDRs use this to protect their processes |
| +0x828 | VadRoot | RTL_AVL_TREE | Root of Virtual Address Descriptor tree — describes all memory regions |
| +0x8B8 | Peb | PPEB | Pointer to user-mode PEB |
// ANALYST TIP
- WinDbg:
!process 0 0to list all,dt nt!_EPROCESS <addr>to dump one - Volatility:
vol.py -f mem.dmp windows.pslist(walks ActiveProcessLinks),windows.psscan(pool-scan, finds unlinked processes) - If pslist and psscan disagree → process is likely DKOM-hidden (rootkit present)
- Token offset varies per build — always run
dt nt!_EPROCESSto confirm on target system
// ATTACKER ABUSE
- DKOM process hiding: unlink
ActiveProcessLinksFlink/Blink — process disappears from Task Manager and most tools - Token theft: find SYSTEM's EPROCESS, copy its Token to attacker process for privilege escalation (e.g. used by EternalBlue shellcode)
- PPL bypass: modify
Protectionto remove PPL flags on EDR processes (e.g. GhostWrite exploit)
ETHREAD
— Executive Thread Object (Kernel)
KERNEL
Each thread has a kernel-mode ETHREAD structure linked to its parent EPROCESS. Key for
understanding thread injection, start address spoofing, and kernel-level thread analysis.
| OFFSET (Win10 x64 ~) | FIELD | TYPE | DESCRIPTION |
|---|---|---|---|
| +0x000 | Tcb (KTHREAD) | KTHREAD | Embedded kernel thread block — contains stack pointer, state, scheduling info |
| +0x428 | StartAddress | PVOID | Original thread start address — injection sometimes overwrites this. Compare to Win32StartAddress. |
| +0x4B8 | Win32StartAddress | PVOID | User-mode start address passed to CreateThread. If different from StartAddress, possible injection. |
| +0x510 | ThreadListEntry | LIST_ENTRY | Links this thread into the owning EPROCESS thread list |
| +0x600 | CrossThreadFlags | ULONG | Includes Terminated, HideFromDebugger, SystemThread flags |
| +0x6C0 | Cid.UniqueProcess | HANDLE | Owning process PID |
| +0x6C8 | Cid.UniqueThread | HANDLE | This thread's TID |
// ANALYST TIP
- WinDbg:
!thread <addr>ordt nt!_ETHREAD <addr> - Discrepancy between
Win32StartAddressandStartAddress, or start address pointing inside a non-image (anonymous) region = strong injection indicator - Volatility:
windows.cmdline,windows.threads
KPCR / KPRCB
— Processor Control Region / Block
KERNELPER-CPU
The KPCR is a per-processor structure in kernel space. On x64,
gs in
kernel mode points to KPCR (while in user mode gs points to TEB). It contains the current
IRQL, IDT pointer, and the embedded KPRCB which holds the current thread pointer and CPU scheduling
state.
| OFFSET | FIELD | DESCRIPTION |
|---|---|---|
| +0x000 | NtTib | NT_TIB — mirrors TEB layout for compatibility |
| +0x180 | Prcb (KPRCB) | Embedded Processor Control Block — holds CurrentThread, IdleThread, scheduler queues |
| +0x180+0x008 | Prcb.CurrentThread | Pointer to the KTHREAD currently running on this CPU |
| +0x180+0x0C8 | Prcb.ProcessorNumber | Which logical CPU this KPCR belongs to |
// ANALYST TIP
- WinDbg kernel:
!pcrordt nt!_KPCR @$pcr - Getting current ETHREAD from KPCR:
gs:[0x188]→ KTHREAD → back-pointer to ETHREAD - Rootkits manipulating the IDT (at
KPCR.IdtBase) can intercept hardware interrupts
MEMORY STRUCTURES
VAD
— Virtual Address Descriptor Tree
MEMORY MAPINJECTION DETECTION
The VAD tree is a balanced AVL tree rooted at
EPROCESS.VadRoot. Each node
(MMVAD) describes one virtual memory region — its start/end page, protection flags, and
whether it's backed by a file or is private (anonymous). It's the kernel's record of what the process's
virtual address space looks like.
| FIELD | TYPE | DESCRIPTION |
|---|---|---|
| StartingVpn | ULONG_PTR | Starting virtual page number (multiply by 0x1000 to get address) |
| EndingVpn | ULONG_PTR | Ending virtual page number (inclusive) |
| u.VadFlags.Protection | ULONG:5 | MM_PROTECT flags — 0x6 = PAGE_EXECUTE_READ_WRITE (major red flag) |
| u.VadFlags.PrivateMemory | ULONG:1 | 1 = private (no backing file). RWX private region in the middle of process space = shellcode injection. |
| u.VadFlags.MemCommit | ULONG:1 | 1 = memory is committed, not just reserved |
| Subsection | PSUBSECTION | For mapped files, points to the section object with file name — null for private memory |
| FileObject | PFILE_OBJECT | Backing file object. If present, you can extract the DLL/EXE path. If absent but region is executable → suspicious. |
// ANALYST TIP
- Volatility:
windows.vadinfo— dumps VAD tree with file paths and protection flags - WinDbg:
!vador!vadexfor extended info - Look for: RWX private regions (no backing file, executable, writable) — classic sign of code injection or reflective loading
- Look for: RX private regions in unexpected locations — could be PE loaded reflectively without going through the loader
- Check if a region that looks like a legitimate DLL path actually matches the file on disk (VAD path ≠ disk content = module stomping)
// ATTACKER ABUSE
- Process injection detection: injected shellcode shows as a private RWX or RX region with no backing file
- Reflective DLL injection: entire DLL loaded into private memory — no VAD entry with a file path, but contains a full PE image
- VAD manipulation (rootkit): some rootkits delete or modify VAD nodes to hide memory regions from kernel-level scanners
Process Memory Layout
— Windows x64
LAYOUT
Visual map of a typical 64-bit Windows process address space. Regions are
approximate — ASLR randomises base addresses.
FFFF…0000
Kernel Space
EPROCESS, ETHREAD, kernel heap, drivers — inaccessible from user mode
7FFF…0000
ntdll.dll / kernel32.dll
Always highest user-mode DLLs. ntdll is the gateway to syscalls.
~7FF…
Other loaded DLLs
DLLs mapped by the loader — visible in VAD tree with file paths
~7FFE0000
KUSER_SHARED_DATA
Read-only page shared between kernel and all user processes — system time,
version, etc.
variable
TEB (per thread)
One per thread. Accessible via gs:[0x30] → self pointer.
variable
PEB
One per process. ASLR'd base but reachable from TEB at gs:[0x60].
variable
Heap(s)
Default process heap + additional heaps. Private, RW. Where malloc/HeapAlloc go.
variable
Stack (per thread)
Grows down. Typically 1–8 MB reserved, committed on demand. Private, RW.
ASLR base
Main EXE image
Loaded at ASLR'd base. PEB.ImageBaseAddress points here.
0x0000…
NULL page (no access)
First 64KB not accessible. NULL dereference crashes here.
// ANALYST TIP
- Any executable private region not backed by a file is the biggest injection red flag in memory forensics
- DLLs loaded via
LoadLibraryappear with file paths in VAD. Reflectively loaded DLLs do not. - Volatility
windows.malfindautomates the search for private executable regions
Heap Internals
— NT Heap / Segment Heap
HEAPEXPLOIT TARGET
Windows uses two heap implementations: the legacy NT Heap (used by most processes) and
the newer Segment Heap (used by UWP and some system processes). Both are relevant for
understanding exploit primitives and heap-based shellcode execution.
| FIELD (NT Heap) | TYPE | DESCRIPTION |
|---|---|---|
| _HEAP.Signature | ULONG | 0xEEFFEEFF = valid NT heap. Corruption of this = heap is compromised. |
| _HEAP.FrontEndHeap | PVOID | Pointer to LFH (Low Fragmentation Heap) or NULL if disabled |
| _HEAP_ENTRY.Size | USHORT | Chunk size in 8/16-byte granules. Encoded with a key (XOR) since Win Vista to resist overwrites. |
| _HEAP_ENTRY.Flags | BYTE | HEAP_ENTRY_BUSY (0x01), HEAP_ENTRY_EXTRA_PRESENT (0x02), etc. |
| _HEAP_FREE_ENTRY.Flink/Blink | LIST_ENTRY | Free list links — classic target for freelist unlink attacks (mostly mitigated post-Vista) |
// ANALYST TIP
- WinDbg:
!heap -a(all heaps),!heap -h <addr>(specific heap),!heap -p -a <addr>(allocation at address) - For page heap / heap debugging:
gflags.exe /i <process> +hpa— enables full page heap (every allocation gets a guard page) - Heap spraying detection: look for large blocks of repeated bytes (shellcode NOP sleds) via Volatility or raw memory search
PE FORMAT — PORTABLE EXECUTABLE
IMAGE_DOS_HEADER
— DOS Stub + e_lfanew
PE ENTRY
Every PE file starts with the DOS header at offset 0. The only field that truly
matters for modern PE parsing is
e_magic (the MZ signature) and e_lfanew — the
offset to the NT Headers.| OFFSET | FIELD | TYPE | DESCRIPTION |
|---|---|---|---|
| +0x000 | e_magic | WORD | 0x5A4D = "MZ" — PE signature. Every PE file starts with this. |
| +0x002–0x003A | e_cblp … e_oemid | WORD[…] | Legacy DOS fields — usually irrelevant, sometimes used to hide data |
| +0x03C | e_lfanew | LONG | File offset of the NT Headers (IMAGE_NT_HEADERS). Jump here to start real PE parsing. |
| +0x040–~0x0C0 | DOS Stub | BYTE[] | "This program cannot be run in DOS mode" — sometimes replaced with hidden data or zeroed by packers |
// ANALYST TIP
- Quick check: first two bytes of any PE file =
4D 5A(MZ). When carving PE files from memory dumps, search for this signature. - Malware sometimes modifies the DOS stub or uses the region between DOS stub and NT Headers to hide shellcode or a config blob
e_lfanewvalue > 0x200 is suspicious — large gap to NT headers may contain hidden data
IMAGE_NT_HEADERS
— PE Signature + File/Optional Headers
CORE
Located at
ImageBase + e_lfanew. Contains the PE signature, then the
File Header and Optional Header inline.| OFFSET | FIELD | TYPE | DESCRIPTION |
|---|---|---|---|
| +0x000 | Signature | DWORD | 0x00004550 = "PE\0\0". Must be present or loader rejects file. |
| +0x004 | FileHeader | IMAGE_FILE_HEADER | Machine type, section count, timestamp, characteristics |
| +0x018 | OptionalHeader | IMAGE_OPTIONAL_HEADER | Entry point, image base, section alignment, data directories |
IMAGE_FILE_HEADER
— COFF Header
PE
| OFFSET | FIELD | TYPE | DESCRIPTION |
|---|---|---|---|
| +0x000 | Machine | WORD | 0x014C = x86 (IMAGE_FILE_MACHINE_I386) · 0x8664 = x64 (AMD64) · 0xAA64 = ARM64 |
| +0x002 | NumberOfSections | WORD | Count of section headers that follow the Optional Header. Validate against actual size. |
| +0x004 | TimeDateStamp | DWORD | Unix timestamp of compilation. Often 0 or faked by malware. Zeroed by some linkers. |
| +0x008 | PointerToSymbolTable | DWORD | Usually 0 in modern PE (debug symbols stripped) |
| +0x010 | SizeOfOptionalHeader | WORD | Must match expected size. Corrupt value = parser bypass technique. |
| +0x012 | Characteristics | WORD | 0x0002 = Executable · 0x2000 = DLL · 0x0100 = 32-bit · 0x0020 = Large Address Aware |
// ANALYST TIP
- Timestamp 0 or a future date = likely packed, compiled by a tool that strips timestamps, or deliberately tampered
- Tools:
pefilePython lib,CFF Explorer,PE-bear,pecheck.py
IMAGE_OPTIONAL_HEADER
— Entry Point, Base, Data Directories
CRITICALPACKER INDICATOR
Despite the name, this header is mandatory. It's larger for x64 (PE32+). The
Magic field differentiates: 0x010B = PE32, 0x020B = PE32+.
| OFFSET (PE32+) | FIELD | TYPE | DESCRIPTION |
|---|---|---|---|
| +0x000 | Magic | WORD | 0x010B = PE32 (x86) · 0x020B = PE32+ (x64) |
| +0x002 | MajorLinkerVersion | BYTE | Linker version — can hint at compiler/packer |
| +0x008 | SizeOfCode | DWORD | Total size of all code sections. Should roughly match .text section size. |
| +0x010 | AddressOfEntryPoint | DWORD | RVA of where execution begins. If this points into a section other than .text, likely packed/shellcode. |
| +0x018 | ImageBase | ULONGLONG | Preferred load address. EXEs default 0x140000000 (x64), DLLs 0x180000000. ASLR changes actual load address. |
| +0x020 | SectionAlignment | DWORD | Alignment of sections in memory. Usually 0x1000 (page size). |
| +0x024 | FileAlignment | DWORD | Alignment of sections in the file. Usually 0x200 (512 bytes). |
| +0x038 | SizeOfImage | DWORD | Total size of PE in memory (must be multiple of SectionAlignment). Used by loader to allocate space. |
| +0x040 | SizeOfHeaders | DWORD | Combined size of DOS header, NT headers, section headers — rounded to FileAlignment |
| +0x044 | CheckSum | DWORD | PE checksum. Most EXEs have 0 (not verified). Drivers and DLLs in %system32% are verified. |
| +0x04C | Subsystem | WORD | 2 = GUI (Windows) · 3 = Console (CUI) · 1 = Native (driver-like) |
| +0x04E | DllCharacteristics | WORD | 0x0040 = DYNAMIC_BASE (ASLR) · 0x0100 = NX_COMPAT (DEP) · 0x4000 = GUARD_CF (CFG) |
| +0x090 | DataDirectory[16] | IMAGE_DATA_DIRECTORY[16] | Array of RVA+Size pairs pointing to: Import Table [1], Export Table [0], Resource [2], Exception [3], Security [4], Reloc [5], Debug [6], TLS [9], IAT [12], Delay Import [13] |
// ANALYST TIP
- Packer indicators: AddressOfEntryPoint in a non-.text section, high entropy in
code sections, SizeOfCode vs actual section sizes mismatch, section names like
.UPX0,.themida,.MPRESS - Missing DllCharacteristics flags (no ASLR, no DEP, no CFG) on a 2020+ binary = suspicious (legitimate modern compilers enable all three by default)
- Native subsystem (1) on a non-driver EXE = very suspicious — runs before the Win32 subsystem initialises
IMAGE_SECTION_HEADER
— Section Metadata
PEPACKER INDICATOR
| OFFSET | FIELD | TYPE | DESCRIPTION |
|---|---|---|---|
| +0x000 | Name | BYTE[8] | Section name (not null-terminated if exactly 8 chars). .text · .data · .rdata · .rsrc · .reloc · .tls |
| +0x008 | VirtualSize | DWORD | Actual size of section data in memory |
| +0x00C | VirtualAddress | DWORD | RVA of section start in memory |
| +0x010 | SizeOfRawData | DWORD | Size of section in file (aligned to FileAlignment) |
| +0x014 | PointerToRawData | DWORD | File offset to section data |
| +0x024 | Characteristics | DWORD | 0x20 = code · 0x40 = initialized data · 0x80 = uninitialized data · 0x20000000 = executable · 0x40000000 = readable · 0x80000000 = writable |
// ANALYST TIP — Common sections
.text
CODE · RX
Executable code. High entropy here = likely packed.
.data
DATA · RW
Initialised global/static variables.
.rdata
DATA · R
Read-only data: strings, import/export tables, debug info.
.rsrc
RESOURCE · R
Icons, version info, manifest, embedded files.
.reloc
DATA · R
Base relocation table — needed when ASLR changes ImageBase.
.tls
DATA · RW
Thread-local storage — TLS callbacks run BEFORE OEP, used as anti-debug.
- Writable + Executable section (W+X) = self-modifying code or packer stub. Major red flag.
- High entropy (>7.0) in any section = likely encrypted/compressed payload (packed)
- VirtualSize >> SizeOfRawData = unpacking into zero-filled memory at runtime
Import Table (IAT / IDT)
— IMAGE_IMPORT_DESCRIPTOR
IMPORTSIAT HOOK
The Import Directory Table (IDT) is an array of
IMAGE_IMPORT_DESCRIPTOR
structs (one per DLL), terminated by a null entry. Each descriptor links to the IAT (Import
Address Table) which the loader fills with actual function addresses at load time. This is
what malware hooks, patches, and walks to resolve APIs.
| OFFSET | FIELD | TYPE | DESCRIPTION |
|---|---|---|---|
| +0x000 | OriginalFirstThunk | DWORD (RVA) | RVA to INT (Import Name Table) — array of IMAGE_THUNK_DATA. Preserved after load — use this to see what was imported. |
| +0x004 | TimeDateStamp | DWORD | 0 before binding, -1 (0xFFFFFFFF) after bound import. Bound imports = IAT pre-patched with hardcoded addresses. |
| +0x00C | Name | DWORD (RVA) | RVA to null-terminated DLL name string (e.g. "kernel32.dll") |
| +0x010 | FirstThunk | DWORD (RVA) | RVA to IAT — loader overwrites this with actual function addresses. This is what code calls at runtime. |
// ATTACKER ABUSE
- IAT hook: overwrite entries in the IAT (FirstThunk array) to redirect API calls to malicious code. Cheap and easy, detected by comparing IAT values to actual module addresses.
- Import-free malware: no IDT entries at all — uses PEB walking + API hashing to resolve everything at runtime. Extremely common in shellcode and advanced loaders.
- Delay imports: suspicious DLLs imported via delay-load table (DataDirectory[13]) — only resolved on first call, evades static import analysis
// HIGH-VALUE IMPORT WATCHLIST
VirtualAlloc / VirtualAllocEx
Memory allocation for injection
WriteProcessMemory
Remote memory write = injection
CreateRemoteThread / NtCreateThreadEx
Remote thread = injection
SetWindowsHookEx
Keylogger / DLL injection via hook
OpenProcess
Accessing other processes
CryptEncrypt / CryptDecrypt
Crypto — ransomware, comms
RegSetValueEx
Registry persistence
WinExec / CreateProcess / ShellExecute
Execution
InternetOpenUrl / HttpSendRequest
C2 communication
IsDebuggerPresent / CheckRemoteDebuggerPresent
Anti-debug
Export Table
— IMAGE_EXPORT_DIRECTORY
DLL EXPORTS
Present in DLLs (and some EXEs). The export directory lets other modules find
functions by name or ordinal. Reflective loaders walk this to find
GetProcAddress and
LoadLibraryA in kernel32 before any other API is available.
| OFFSET | FIELD | TYPE | DESCRIPTION |
|---|---|---|---|
| +0x000 | Characteristics | DWORD | Reserved, always 0 |
| +0x004 | TimeDateStamp | DWORD | Timestamp — same caveats as File Header timestamp |
| +0x00C | Name | DWORD (RVA) | RVA to DLL name string — the name the DLL calls itself (can differ from filename!) |
| +0x010 | Base | DWORD | Starting ordinal number (usually 1) |
| +0x014 | NumberOfFunctions | DWORD | Total exported functions (including ordinal-only, no name) |
| +0x018 | NumberOfNames | DWORD | Number of named exports |
| +0x01C | AddressOfFunctions | DWORD (RVA) | Array of RVAs to exported functions, indexed by ordinal |
| +0x020 | AddressOfNames | DWORD (RVA) | Array of RVAs to function name strings |
| +0x024 | AddressOfNameOrdinals | DWORD (RVA) | Array of WORDs — maps name index → ordinal. Use: Names[i] → NameOrdinals[i] → Functions[ordinal] |
// ANALYST TIP
- Export forwarding: when AddressOfFunctions[i] points inside the export directory itself, it's a forwarded export (string like "NTDLL.RtlAllocateHeap")
- Malicious DLLs often export functions named to match legitimate DLLs (DLL hijacking via export forwarding or proxy DLL)
- DLL Name field mismatch (DLL calls itself "kernel32.dll" but is loaded as "evil.dll") → strong indicator of DLL impersonation
SECURITY STRUCTURES
Access Token
— _TOKEN (Kernel)
PRIVESCTOKEN THEFT
Every process and thread has an Access Token — the OS's proof of identity. It contains
the user's SID, group SIDs, privileges, and integrity level. This is what determines what a process can
access. Token manipulation is the core of Windows privilege escalation.
| FIELD | DESCRIPTION |
|---|---|
| UserAndGroups | Array of SID_AND_ATTRIBUTES — user SID + all group SIDs the token belongs to |
| Privileges | Array of LUID_AND_ATTRIBUTES — privileges held (e.g. SeDebugPrivilege, SeImpersonatePrivilege). Enabled vs just present. |
| IntegrityLevel | Low (0x1000) · Medium (0x2000) · High (0x3000) · System (0x4000). Mandatory Integrity Control (MIC). |
| TokenType | TokenPrimary (1) = process token · TokenImpersonation (2) = thread impersonation token |
| ImpersonationLevel | SecurityAnonymous / Identification / Impersonation / Delegation — how much identity is delegated |
| SessionId | Logon session — used by UAC to detect console vs remote sessions |
// ATTACKER ABUSE
- Token theft / impersonation: OpenProcessToken on a SYSTEM process → DuplicateToken → ImpersonateLoggedOnUser → attacker code runs as SYSTEM
- Token manipulation in kernel: directly overwrite EPROCESS.Token pointer with address of SYSTEM process token (used by kernel exploits like EternalBlue)
- SeImpersonatePrivilege abuse: service accounts with this privilege are vulnerable to potato-class attacks (PrintSpoofer, RoguePotato, GodPotato)
- UAC bypass: elevate token from Medium → High integrity without a UAC prompt using auto-elevated COM objects
// ANALYST TIP
- WinDbg:
!token <addr>ordt nt!_TOKEN <addr> - Check if a process running as a non-admin user has
SeDebugPrivilegeenabled — should only be Administrators - Volatility:
windows.privileges— lists privileges per process including whether they're enabled
PEB_LDR_DATA / LDR_DATA_TABLE_ENTRY
— Module Loader Lists
API RESOLUTION
PEB.Ldr points to PEB_LDR_DATA which contains three doubly-linked lists of
LDR_DATA_TABLE_ENTRY — one in load order, one in memory order, one in init order. These are
the foundation of all shellcode API resolution.
| OFFSET (x64) | FIELD | DESCRIPTION |
|---|---|---|
| +0x000 | InLoadOrderLinks | LIST_ENTRY — load order list (first = main EXE, second = ntdll, third = kernel32) |
| +0x010 | InMemoryOrderLinks | LIST_ENTRY — most common list walked by shellcode. Second entry is ntdll, third is kernel32. |
| +0x020 | DllBase | Base address where this module is loaded in memory |
| +0x028 | EntryPoint | DLL entry point (DllMain) address |
| +0x030 | SizeOfImage | Size of the module in memory |
| +0x038 | FullDllName | UNICODE_STRING — full path to the DLL on disk |
| +0x048 | BaseDllName | UNICODE_STRING — just the filename (e.g. "kernel32.dll") — what shellcode compares to find target DLL |
// ANALYST TIP
- When a DLL is loaded normally: it appears in all three lists AND has a VAD entry with a file path. Manually loaded (reflective) DLLs may be absent from these lists.
- Volatility:
windows.dlllist(walks LDR lists) vswindows.ldrmodules(cross-references LDR with VAD — discrepancy = hidden module) - Unlinking from LDR lists (while keeping the VAD entry) hides a DLL from most API-based enumeration but not from VAD inspection
Handle Table
— HANDLE_TABLE / Object Manager
KERNEL
Every process has a private handle table managed by the Object Manager. Handles are integer indices into
this table. Each entry holds a pointer to the kernel object and the access rights granted. Understanding
handle tables helps understand object access, inheritance, and handle-based C2 techniques.
// ANALYST TIP
- WinDbg:
!handle 0 0(all handles in current process),!handle <value> f(details) - Volatility:
windows.handles— list all handles per process including type (File, Process, Thread, Key, Event…) - A process holding handles to other processes (type = Process) is often performing injection or monitoring
- Ransomware often opens handles to many files before encrypting — bulk file handles are an indicator
NETWORK
TCP Endpoint / _TCP_ENDPOINT
— tcpip.sys Kernel Structure
C2 DETECTION
Network connections are tracked in kernel by
tcpip.sys in a pool of
_TCP_ENDPOINT objects (tagged TcpE in the pool). Volatility scans pool memory
for these tags to find all connections — including those hidden by rootkits from usermode APIs like
netstat.
| KEY FIELD | DESCRIPTION |
|---|---|
| State | TCP state: ESTABLISHED, LISTENING, CLOSE_WAIT, TIME_WAIT… |
| LocalAddress / LocalPort | Source IP:port of this endpoint |
| RemoteAddress / RemotePort | Destination IP:port — your C2 pivot point |
| OwningProcess | Pointer back to the EPROCESS that owns this socket |
| CreateTime | Timestamp of connection creation — valuable for timeline correlation |
// ANALYST TIP
- Volatility:
windows.netstat— walks pool to find TCP/UDP endpoints including closed ones still in memory - WinDbg: search pool for tag
TcpE—!poolfind TcpE - ESTABLISHED connections from unexpected processes (svchost → unusual remote port 443/80 to rare IP) = strong C2 indicator
- Pool scanner finds connections that rootkits hid from the OS's live APIs
QUICK REFERENCE
Key Registers & Segment Shortcuts
x86 / x64
x86 — TEB
fs:[0x18]
Self-pointer to TEB
x86 — PEB
fs:[0x30]
PEB from TEB
x64 — TEB
gs:[0x30]
Self-pointer to TEB
x64 — PEB
gs:[0x60]
PEB from TEB
x64 — KPCR (kernel)
gs:[0x00]
gs in kernel mode → KPCR
x64 — CurrentThread
gs:[0x188]
KTHREAD of running thread (kernel)
x86 — ExceptionList
fs:[0x00]
SEH chain head
x64 — LastError
gs:[0x68]
Per-thread last error value
Key Syscalls & NT APIs
— Malware-Relevant
EVASION / INJECTION
| NT API | WIN32 WRAPPER | USAGE BY MALWARE |
|---|---|---|
| NtAllocateVirtualMemory | VirtualAllocEx | Allocate RWX memory in target process for injection |
| NtWriteVirtualMemory | WriteProcessMemory | Write shellcode/DLL into allocated region |
| NtCreateThreadEx | CreateRemoteThread | Execute injected code in target process |
| NtMapViewOfSection | MapViewOfFile | Map shared section into target process (process doppelgänging, mapping injection) |
| NtUnmapViewOfSection | UnmapViewOfFile | Process hollowing — unmap legitimate image before replacing with malware |
| NtSetContextThread | SetThreadContext | Process hollowing — redirect entry point to malicious code |
| NtQuerySystemInformation | — | Enumerate processes, modules, handles — many EDR queries and malware recon use this |
| NtQueryInformationProcess | — | Anti-debug via ProcessDebugPort (class 7), ProcessDebugFlags (31), check for debugger |
| NtProtectVirtualMemory | VirtualProtect | Change page permissions — make shellcode executable after writing it as RW |
| NtOpenProcess | OpenProcess | Get handle to target process — first step in all injection chains |
| NtQueueApcThread | QueueUserAPC | APC injection — queue malicious APC to alertable thread |
// ANALYST TIP — Direct Syscall Evasion
Advanced malware bypasses EDR usermode hooks by calling NT
syscalls directly (e.g. via SysWhispers2/3, HellsGate,
TartarusGate). Look for syscall instructions outside of ntdll.dll in a
disassembler or dynamic trace — execution that goes directly to kernel without passing through ntdll
is a strong indicator of hook bypass.
Structure → MITRE Technique Map
MITRE ATT&CK
| STRUCTURE / CONCEPT | TECHNIQUE ID | NAME |
|---|---|---|
| PEB.Ldr walking | T1027 | Obfuscated Files or Information — API hiding via PEB walk + hashing |
| PEB.BeingDebugged | T1497.001 | Virtualization/Sandbox Evasion: System Checks |
| IAT Hooking | T1055.012 | Process Injection: Process Hollowing (often combined) |
| VAD private RWX injection | T1055.001 | Process Injection: DLL Injection |
| NtCreateThreadEx remote | T1055.003 | Process Injection: Thread Execution Hijacking |
| NtMapViewOfSection injection | T1055.015 | Process Injection: ListPlanting / Section injection |
| NtUnmapViewOfSection + replace | T1055.012 | Process Injection: Process Hollowing |
| EPROCESS.ActiveProcessLinks unlink | T1014 | Rootkit — process hiding via DKOM |
| EPROCESS.Token steal | T1134.001 | Access Token Manipulation: Token Impersonation/Theft |
| LDR list manipulation | T1055 | Process Injection — module hiding |
| TLS callback (PE .tls section) | T1497.001 | Anti-debug — code before OEP via TLS callback |
| NtQueueApcThread | T1055.004 | Process Injection: Asynchronous Procedure Call |
| Direct syscall (SysWhispers) | T1562.001 | Impair Defenses: Disable or Modify Tools — EDR hook bypass |