Skip to main content

windows-internals-dev

Auth/lab dev: Windows internals; PE loading and CRT startup, PEB/TEB, PE/COFF, syscalls, unwinding, memory/heap, tokens, kernel objects, ETW/AMSI telemetry.

Source facts

Repository
AeonDave/malskill
Last source activity
October 20, 2026 at 20:42
Detected SKILL.md language
English
Stars
26
Forks
3

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
12 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
windows-internals-dev
description
Auth/lab dev: Windows internals; PE loading and CRT startup, PEB/TEB, PE/COFF, syscalls, unwinding, memory/heap, tokens, kernel objects, ETW/AMSI telemetry.
license
MIT
compatibility
Windows 10 1809 through Windows 11 24H2, x86-64 and ARM64; Kernel structures referenced from Windows 11 22H2/24H2 public debug symbols.
metadata
{"author":"AeonDave","version":"1.2"}
# windows-internals Foundational Windows internals for programmatic work: writing implants and loaders, reversing your own binaries, building EDR/AV tooling, or understanding what the kernel actually does under a Win32 call. This is **structural and mechanical** knowledge — if you need tool usage, look in `offensive-tools/`; if you need language-specific patterns, look in `*-patterns` skills. **What this skill gives you**: the offsets, structures, data flows, and invariants you need to touch Windows at the NTAPI/undocumented/kernel-struct level without guessing. Every claim here is either stable across the supported build range or explicitly flagged as version-dependent. **Rule of thumb**: if your code uses any Win32 API that is documented, this skill is not needed. If you are walking a PEB pointer, parsing UNWIND_INFO, resolving SSNs, spoofing a call stack, patching ETW-TI, or touching a kernel object directly — start here. --- ## When to activate - Writing an implant, loader, or BOF that cannot use traditional imports - Building indirect syscall dispatch (Hell's / Halo's / Tartarus / Recycled Gate) - Reversing your own release binary to verify ASM stubs land correctly - Tracing native execution before `main`/`WinMain` or checking PE entry, TLS, and CRT initialization against the target toolchain - Implementing call-stack spoofing (SilentMoonwalk / Draugr / CHRYSALIS) - Patching AMSI, ETW, or ETW-TI in userland; understanding what kernel telemetry remains - Writing a kernel driver that registers process/thread/image/registry callbacks - Designing around mitigations: CFG, XFG, CET shadow stack, HVCI, Credential Guard - Reasoning about detection surface when designing an offensive technique If the question is "what function do I call" — wrong skill. If the question is "what does the OS actually do when I call this, and what can I touch directly instead" — right skill. --- ## Map of the territory Everything below is a pointer into a reference file. Load only what you need. | Domain | File | Covers | |--------|------|--------| | **Process/thread state blocks** | `references/peb-teb.md` | PEB, TEB, LDR data, InMemoryOrderModuleList, ApiSetMap, module walking, hash-based resolution | | **Build survey evidence** | [references/build-survey.md](references/build-survey.md) | Load when comparing x64 Windows survey logs: per-run observations, provenance, collection status, and bounded diagnostic checks | | **PE / COFF and startup** | `references/pe-format.md` | Load for PE parsing or before-user-entry diagnosis: headers, TLS/CRT boundaries, matching source, symbols, and debugger steps | | **Syscalls** | `references/syscalls.md` | Syscall ABI (x64/ARM64), SSN resolution strategies, direct/indirect dispatch, gate variants, stub layout | | **Exception handling** | `references/exception-unwind.md` | SEH, VEH, UNWIND_INFO, RtlLookupFunctionEntry, RtlVirtualUnwind, KiUserExceptionDispatcher, call-stack spoofing frames | | **Memory** | `references/memory-management.md` | Virtual memory (NtAllocate/Protect/Write/Read), sections, VADs, NT heap vs Segment heap, LFH | | **Threads / APCs** | `references/threads-apcs.md` | Thread creation, suspension, CONTEXT struct, user/kernel/Special APCs, thread hijacking primitives | | **Tokens / privileges** | `references/tokens-privileges.md` | TOKEN struct, privileges, integrity levels, impersonation vs primary, DuplicateTokenEx semantics | | **Kernel objects** | `references/kernel-objects.md` | EPROCESS, ETHREAD, KPCR/KPRCB, handle table, object manager namespace, kernel callbacks | | **Mitigation surface** | `references/evasion-surface.md` | ETW/ETW-TI, AMSI, CFG/XFG/CET, VBS/HVCI/CG, userland hook detection/unhooking | --- ## Quick-reference: x86-64 Windows ABI Non-negotiable invariants. Violating any of these causes silent corruption that surfaces later. | Invariant | Rule | |---|---| | Stack alignment | `rsp % 16 == 0` at every `call` site | | Shadow space | 32 bytes reserved by caller before every `call` (`sub rsp, 0x28` min with alignment) | | Integer args (1–4) | `rcx`, `rdx`, `r8`, `r9` | | Integer args (5+) | Stack, caller-allocated | | Float args (1–4) | `xmm0`–`xmm3` | | Integer return | `rax` | | Callee-saved (int) | `rbx`, `rbp`, `rsi`, `rdi`, `r12`–`r15` | | Callee-saved (xmm) | `xmm6`–`xmm15` | | Syscall number | `rax` (user-mode SSN before `syscall`) | | Syscall arg 4 | `r10` — kernel clobbers `rcx`, so userland stub must `mov r10, rcx` | | TEB | `gs:[0x30]` | | PEB | `gs:[0x60]` (or `[[gs:0x30] + 0x60]`) | --- ## Quick-reference: ARM64 Windows ABI | Invariant | Rule | |---|---| | Stack alignment | `sp % 16 == 0` (hardware enforced at PSTATE transitions) | | Integer args (1–8) | `x0`–`x7` | | Float args (1–8) | `d0`–`d7` | | Integer return | `x0` | | Callee-saved (int) | `x19`–`x28`, `x29` (fp), `x30` (lr) | | Callee-saved (vec) | `d8`–`d15` | | Syscall number | `x8` | | Syscall instruction | `svc #0` | | TEB | `x18` (platform register, reserved by Windows) | | PEB | `[x18 + 0x60]` | ARM64EC (x64 emulation compatible) uses a modified register mapping — x0..x15 map to rcx,rdx,r8,r9 etc. If writing ARM64EC ASM, consult the Microsoft ARM64EC ABI doc, not this table. --- ## Common structure offsets (x64, Windows 10–11) These are the 12 offsets you will look up most often. Full layouts in `references/peb-teb.md`. | Structure | Field | Offset | Notes | |---|---|---|---| | TEB | ProcessEnvironmentBlock | 0x60 | PEB pointer | | TEB | ThreadLocalStoragePointer | 0x58 | TLS slot array | | TEB | LastErrorValue | 0x68 | GetLastError storage | | PEB | BeingDebugged | 0x02 | 1 byte | | PEB | ImageBaseAddress | 0x10 | Main .exe base | | PEB | Ldr | 0x18 | PEB_LDR_DATA pointer | | PEB | ProcessParameters | 0x20 | RTL_USER_PROCESS_PARAMETERS | | PEB | ApiSetMap | 0x68 | API_SET_NAMESPACE pointer | | PEB_LDR_DATA | InLoadOrderModuleList | 0x10 | Head of load order list | | PEB_LDR_DATA | InMemoryOrderModuleList | 0x20 | Head of memory order list | | LDR_DATA_TABLE_ENTRY | DllBase | 0x30 | Module base address | | LDR_DATA_TABLE_ENTRY | BaseDllName | 0x58 | UNICODE_STRING | > When walking `InMemoryOrderModuleList`, the `Flink` you dereference points **into** `InMemoryOrderLinks` (offset 0x10) of the next entry, not its base. Subtract 0x10 to get the entry pointer. --- ## The 10 things you will do over and over 1. **Get a module base without LoadLibrary** — walk PEB → Ldr → InMemoryOrderModuleList, hash `BaseDllName`, compare. See `peb-teb.md` §Module Walk. 2. **Resolve an export by hash** — parse target module's PE headers → `DataDirectory[0]` → iterate AddressOfNames / AddressOfOrdinals / AddressOfFunctions. See `pe-format.md` §Export Walk. 3. **Resolve an SSN** — Hell's Gate reads the function stub at the resolved Nt* address and extracts the immediate after `mov eax, imm32`. Halo's / Tartarus Gate fall back when the stub is hooked. Recycled Gate enumerates all `Zw*` exports and sorts by RVA to derive SSN from index. See `syscalls.md`. 4. **Find a `syscall; ret` gadget** — scan any Nt* stub body for the byte sequence `0F 05 C3`. Used by Recycled Gate / indirect syscall paths to route all syscalls through ntdll. See `syscalls.md` §Indirect Dispatch. 5. **Parse UNWIND_INFO for a function** — binary-search `.pdata` (`DataDirectory[3]`) for a `RUNTIME_FUNCTION` whose `[BeginAddress, EndAddress)` contains your RIP. Walk `UnwindCodes[]` to derive frame size and saved registers. See `exception-unwind.md`. 6. **Build a spoofed call stack** — find two chained unwind frames (`UWOP_SET_FPREG` terminator + `UWOP_PUSH_NONVOL(rbp)` second frame), plant fake return addresses on the real RSP matching those unwinders, then `jmp` into the target. The unwinder validates; the scanner walks the frame list and sees legitimate module addresses. See `exception-unwind.md` §Call-stack Spoofing. 7. **Open a token, check elevation, impersonate** — `NtOpenProcessToken` → `NtQueryInformationToken(TokenElevation)` / `TokenUser` → `NtDuplicateToken(SecurityImpersonation)` → `NtSetInformationThread(ThreadImpersonationToken)`. See `tokens-privileges.md`. 8. **Inject into another process** — allocate remote memory (`NtAllocateVirtualMemory` or section + `NtMapViewOfSection`), write payload, create thread (`NtCreateThreadEx`) or queue APC (`NtQueueApcThread[Ex2]`). See `threads-apcs.md`. 9. **Patch AMSI / ETW in-process** — locate `AmsiScanBuffer` (amsi.dll) or `EtwEventWrite` / `NtTraceEvent` (ntdll.dll), flip page protection RW, write a 1-byte `ret` or `mov eax, 0x80070057; ret` prologue, restore protection. See `evasion-surface.md`. 10. **Walk the PSP callback list (kernel)** — kernel shellcode iterates `PspCreateProcessNotifyRoutine` (array of 64 `EX_CALLBACK_ROUTINE_BLOCK` pointers with low-bit tagging) and nulls entries belonging to EDR drivers. See `kernel-objects.md` §Kernel Callbacks. --- ## Syscall dispatch decision tree ``` Need to invoke an Nt* function without touching ntdll's hooked stub? │ ├── Can ntdll.dll be walked? │ │ │ ├── YES → Use PEB walk to resolve Nt* address │ │ │ │ │ ├── Stub unhooked? → Hell's Gate (read SSN from stub at +4) │ │ │ │ │ └── Stub hooked? → Halo's/Tartarus Gate (walk neighbors ±1) │ │ OR Recycled Gate (sort Zw* exports by RVA) │ │ │ └── NO (unusual — stripped PEB or sandbox) → Hard-coded SSN table │ (brittle, version-specific) │ ├── Executing the syscall: │ │ │ ├── Direct → Emit `syscall` instruction in your own .text │ │ • Callstack has your module → red flag │ │ │ ├── Indirect → Emit `call <syscall;ret gadget inside ntdll>` │ │ • Callstack shows ntdll frame → clean │ │ • Requires gadget discovery pass │ │ │ └── Desync → Build spoofed 3-frame call stack, `jmp` into gadget │ • Callstack shows chain of legit frames │ • Full UNWIND_INFO math required │ └── 6+ args? → 5th arg onwards goes on stack after shadow space Stubs vary: see syscalls.md §Multi-arg variants ``` --- ## Architectural lifecycles you must understand ### DLL load (`LdrLoadDll`) For startup analysis, use the documented boundary: an image's TLS callbacks run before that image's PE entry point. With default MSVC DLL startup, the PE entry point is `_DllMainCRTStartup`, which initializes the CRT before calling the user's `DllMain`. Dependencies are initialized before the executable reaches its own PE entry point; keep exact cross-image loader event ordering build-specific. See `references/pe-format.md` for the WinDbg observation method. For `DLL_THREAD_ATTACH`, Microsoft documents that a DLL loaded with `LoadLibrary` receives notifications only for threads created after that load; existing threads do not receive a retroactive thread-attach call. Do not generalize this rule into undocumented TLS callback timing for every loader path. ### Thread creation (`NtCreateThreadEx`) 1. Kernel allocates ETHREAD, creates TEB in process VA space 2. Initial context populated from caller params (start address, parameter, stack info) 3. If `THREAD_CREATE_FLAGS_CREATE_SUSPENDED` set → thread placed in waiting state 4. Otherwise queued for execution 5. First user-mode code on thread is `LdrInitializeThunk` (unless `THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH`) — runs DLL TLS callbacks, `DllMain(DLL_THREAD_ATTACH)` for every loaded DLL, then jumps to start address **Offensive note**: `SKIP_THREAD_ATTACH` skips the entire LdrInit chain — useful for shellcode threads where you do not want `DLL_THREAD_ATTACH` to fire across every loaded DLL (which can alert hooks or crash if a DLL's DllMain mismatches). ### Exception dispatch (user-mode) 1. Hardware or software raises → kernel transfers to `KiUserExceptionDispatcher` in ntdll 2. `RtlDispatchException` walks the function table starting at faulting RIP 3. For each frame: `RtlLookupFunctionEntry` → `RtlVirtualUnwind` → check for registered handler 4. Handler decides: `ExceptionContinueSearch` / `ContinueExecution` 5. If unhandled → `UnhandledExceptionFilter` → VEH chain → process terminate via `NtTerminateProcess` **Offensive use**: VEH registration (`RtlAddVectoredExceptionHandler`) is a pre-SEH hook, runs before stack-based handlers. Common abuse: register VEH, trigger a fault, have VEH set `ContextRecord->Rip` to shellcode, return `ExceptionContinueExecution`. Leaves minimal forensic trace compared to thread creation. --- ## Detection surface to reason about When you apply any technique, the following signals are potentially visible. This is a high-level map; details in `evasion-surface.md`. | Signal | Who sees it | What triggers it | |---|---|---| | Userland hooks | EDR in-process agent | Calling `Nt*` via ntdll stub | | ETW (userland) | EDR agent or `Microsoft-Windows-*` consumers | `EtwEventWrite` inside ntdll/kernel32 wrappers | | ETW-TI (kernel) | PPL service consuming kernel events | Memory writes to remote processes, SUSPEND/RESUME, APC queue, etc. | | AMSI | Registered AMSI providers (Defender) | Script content scanned by `AmsiScanBuffer` | | Kernel callbacks | EDR driver | Process/thread/image/registry create, handle open | | Minifilter | EDR filesystem driver | File open/read/write/create | | Handle open audit | Kernel | Any `NtOpen*` on protected process (LSASS, etc.) | | CFG violation | Process | Indirect call to non-valid target | | Shadow stack mismatch | CET-enabled process | `ret` address mismatches shadow stack top | **Important reality**: modern advanced EDRs (Defender for Endpoint, Elastic, CrowdStrike recent builds) rely primarily on kernel callbacks and ETW-TI. Userland unhooking by itself does nothing against them. Plan your technique accordingly — see `evasion-surface.md` §EDR Architecture. --- ## Pitfalls and invariants **PEB walk checks** - List positions and counts describe the sampled process; do not identify modules by a fixed ordinal. For diagnostic plausibility checks, load `references/peb-teb.md`. - `BaseDllName` is a `UNICODE_STRING` (16 bytes: Length, MaximumLength, Buffer). Length is in **bytes**, not chars - Module name comparisons must be case-insensitive (use lowercase for both operands of hash) **PE parsing invariants** - Validate `e_magic == 0x5A4D` ("MZ") and `e_lfanew > 0 && e_lfanew < PE_file_size` - Validate `NT_Signature == 0x00004550` ("PE\0\0") - On 64-bit PE, `OptionalHeader.Magic == 0x20B`; export directory RVA at `OptionalHeader + 0x70` (DataDirectory[0].VirtualAddress lives at offset 0x88 of OptionalHeader on x64, or offset 0x78 of IMAGE_NT_HEADERS64 if counting from NT base) - Forwarded exports: if an export RVA is **within** `DataDirectory[0]` range, it is a forwarder string `"dll.function"` — recursively resolve **Manual mapping / reflective DLL invariants** - The OS loader normally runs TLS callbacks and calls the PE `AddressOfEntryPoint`, which is usually CRT startup before user `DllMain`. - A custom reflective loader may define a different contract: exported `DllMain` can be a minimal shellcode attach gate, followed by an explicit reflective start export. Do not replace that with `AddressOfEntryPoint` just because it resembles the OS loader; verify the payload's `reserved` semantics and runtime markers first. - For Rust cdylibs, distinguish loader lifecycle bugs from runtime-library boundary bugs. If profile decode, session encode, and DNS resolve already work, a crash in `TcpStream::connect_timeout` is more likely a socket/runtime primitive issue than an earlier PE entrypoint issue. - Generated shellcode arrays are build artifacts. After changing a reflective loader, regenerate and rebuild the final wrapper before testing; source edits alone do not change the payload under test. **Syscall invariants** - On x64, before `syscall`: `r10 = rcx`. Kernel clobbers `rcx` with return address - Wow64 processes have a 32→64 transition through `wow64cpu!CpupReturnFromSimulatedCode`; direct `syscall` from 32-bit code does **not** work — must go through the transition - SSN drift between builds: hard-coded SSNs break on the first Patch Tuesday. Always resolve at runtime - On ARM64: `svc #0` is the instruction; SSN goes in `x8` (not `w8`), args in `x0`–`x7`, then stack **Unwind invariants** - `UNWIND_INFO.CountOfCodes` is rounded up to even; iterate in pairs - `UWOP_ALLOC_SMALL`: size encoded as `OpInfo * 8 + 8` (range 8..128) - `UWOP_ALLOC_LARGE`: size in following 2 or 4 bytes; `OpInfo == 0` → 2-byte scaled by 8, `OpInfo == 1` → 4-byte raw - Chained unwind info (`UNW_FLAG_CHAININFO`): last code entry is `RUNTIME_FUNCTION` of parent, not a code — follow it - Functions without `.pdata` (leaf functions with no prologue) cannot be unwound — useless as spoof frames **Token invariants** - `NtOpenProcessToken(NtCurrentProcess(), TOKEN_QUERY, ...)` always succeeds — you own your own token - `NtOpenProcessToken` on another process requires at minimum `PROCESS_QUERY_LIMITED_INFORMATION` on the process handle and `TOKEN_DUPLICATE` for impersonation - Primary tokens cannot be assigned to threads — must `DuplicateTokenEx` with `TokenImpersonation` first, or use `CreateProcessWithTokenW` for new process - Impersonation token integrity cannot exceed thread's primary token integrity without `SeImpersonatePrivilege` **Memory invariants** - `NtProtectVirtualMemory` rounds up to page granularity — querying the old protection returns the protection of the **first** page in the range - `NtAllocateVirtualMemory` with `MEM_COMMIT` over an already-committed region succeeds (idempotent commit) and preserves contents; over a reserved-but-not-committed region zero-fills - Writing to a `PAGE_NOACCESS` guard page raises `STATUS_GUARD_PAGE_VIOLATION` once, then converts to normal access — useful for trap-then-continue primitives --- ## Resources ### Companion skills
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub