← Back to writeups Windows Internals

Why Understanding Beats Obfuscation

The difference between using tools and knowing why they work

// What is this?
Why understanding Windows from the kernel up makes you a better defender — not just a better attacker.

Most security tooling operates at a high abstraction layer: file hashes, process names, alert rules. Attackers who understand Windows internals (how tokens flow through handles, what syscalls expose, how the kernel resolves names) can stay below that visibility. The same knowledge in a defender's hands means hunting at the layer where evasion actually happens — not the abstracted view your SIEM shows.

PowerShell — System Internals
# Process with modules from user paths
Get-Process | ForEach-Object {
  $mods = $_.Modules | Where-Object { $_.FileName -match "Users|AppData|Temp" }
  if ($mods) { [PSCustomObject]@{Process=$_.Name; Modules=($mods.FileName -join ",")} }
} -ErrorAction SilentlyContinue

# AMSI status
try { [Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").GetField("amsiInitFailed","NonPublic,Static").GetValue($null) }
catch { "Cannot check AMSI" }

# ETW providers (detection infrastructure)
logman query providers | Select-String "Windows-Security|PowerShell|Sysmon"
If this flags: Modules from user paths are injected DLLs. AMSI True = bypassed. ETW providers show what's being logged - attackers disable these.

The Three Levels

Level Characteristic Limitation
Downloads tools Runs exploits from GitHub, uses Metasploit modules Stops when the tool doesn't work
Builds tools Writes custom implants, modifies existing code Copies techniques without understanding why
Understands systems Knows why exploits work at the OS level Can find new vulnerabilities, adapt to any defense

Most people get stuck between levels 1 and 2. They can Base64 encode payloads, chain tools together, and follow tutorials. But when something breaks - when the technique stops working - they're stuck.

The Obfuscation Trap

It's tempting to think evasion is about obfuscation:

This works against static signatures. It fails against behavioral analysis. Modern EDR doesn't care what your code looks like - it watches what it does.

Living off the land beats obfuscation. Understanding the system beats both.

What to Actually Learn

If you want to move beyond tool user:

PE Format

Windows executables have structure: headers, sections, imports, exports. Understanding PE format means understanding:

Virtual Memory

Every process has its own virtual address space. Understanding memory means understanding:

Token Structures

SeDebugPrivilege isn't magic - it's a bit in a token structure. Understanding tokens means understanding:

Handle Tables

Processes reference kernel objects through handles. Understanding handles means understanding:

ALPC/RPC

Windows services communicate through these mechanisms. Understanding them means understanding:

Essential Reading

Projects That Build Understanding

Reading isn't enough. Build these:

  1. Write a debugger - Not WinDbg scripts. Actually implement DebugActiveProcess, breakpoints, memory reading.
  2. Parse PE files manually - Don't use libraries. Read the bytes, decode the headers, resolve imports.
  3. Implement injection without CreateRemoteThread - APC injection, thread hijacking, callback abuse.
  4. Token manipulation without OpenProcessToken - Use NtQuerySystemInformation to enumerate handles.

The Real Test

Can you explain why your technique works? Not how - why.

When you understand why, you can:

Real Example: PEB Walking

Here's what "understanding" looks like in practice. EDR hooks GetModuleHandle and GetProcAddress to detect malware resolving APIs. But if you understand how Windows actually tracks loaded modules, you can walk the data structures yourself:

class LDR_DATA_TABLE_ENTRY(ctypes.Structure):
    """Describes a loaded module (DLL) in the process."""
    _fields_ = [
        ("InLoadOrderLinks", LIST_ENTRY),
        ("InMemoryOrderLinks", LIST_ENTRY),
        ("InInitializationOrderLinks", LIST_ENTRY),
        ("DllBase", ctypes.c_void_p),
        ("EntryPoint", ctypes.c_void_p),
        ("SizeOfImage", wintypes.ULONG),
        ("FullDllName", UNICODE_STRING),
        ("BaseDllName", UNICODE_STRING),
    ]

def find_module(name):
    """Find module by walking PEB - no GetModuleHandle call."""
    peb = get_peb()
    ldr = peb.Ldr.contents
    head = ctypes.addressof(ldr.InLoadOrderModuleList)
    entry_ptr = ldr.InLoadOrderModuleList.Flink

    while ctypes.addressof(entry_ptr.contents) != head:
        entry = ctypes.cast(entry_ptr,
            ctypes.POINTER(LDR_DATA_TABLE_ENTRY)).contents
        if entry.BaseDllName.Buffer.lower() == name.lower():
            return entry.DllBase
        entry_ptr = entry.InLoadOrderLinks.Flink
    return None

This finds any loaded DLL without calling monitored APIs. The technique extends to parsing export tables for function addresses - completely invisible to userland hooks.

The person who memorized "use PEB walking to evade hooks" knows what to do. The person who understands doubly-linked lists and Windows loader internals can implement it from scratch when their tool breaks.

Reference: Windows Internals by Mark Russinovich, David Solomon, Alex Ionescu