# winpwn roadmap Status and planned work, organized by feature area. Each item names the pwntools feature it mirrors, where one exists. Scope: winpwn is built for a local Windows-only CTF. The goal is to let a player who understands an exploit technique (overflow, UAF, IOCTL abuse, token stealing) work at that level instead of on WinAPI struct layouts and Go plumbing. Legend: [+] done · [+-] partial · [-] planned. ## Core ergonomics — [+] - `Context` ([context.go](context.go)): `Arch` (x86/x64), `LogLevel`, `Timeout`, `Newline`. Note: `P16/P32/P64` are little-endian only and do not read endianness from `Context.Arch` — every Windows target is little-endian. - Logging ([log.go](log.go)): `Info`/`Success`/`Warn`/`Error`, gated by `Context.LogLevel`. Every `Tube` method returns `error` rather than exiting. - Timeouts ([tube.go](tube.go)): `(*Tube).SetTimeout` overrides `Context.Timeout` (0 = block forever). `Recv*`/`Send*` race the call against the deadline via goroutine + `select`. A timed-out call's goroutine is not killed; it runs until the underlying blocking I/O completes (Go has no portable deadline for an arbitrary pipe/socket reader). - `cyclic` ([cyclic.go](cyclic.go)): `Cyclic`/`CyclicN`, `CyclicFind`/ `CyclicFindN`. Generator is lazy (`deBruijnEach`) and stops at the requested length. Verified against pwntools' `cyclic(20)` output. - `fiddling` ([fiddling.go](fiddling.go)): `Hexdump`, `Enhex`/`Unhex`, `Xor`. - Packing ([packing.go](packing.go)): `P16`/`P32`/`P64`, `U16`/`U32`/`U64`. - Tube API ([tube.go](tube.go)): `Send*`/`Recv*`/`RecvRegex`/`RecvPred`/ `Interactive`/`Close`. ## PE tooling — [+] - Parsing ([pe.go](pe.go), [sections.go](sections.go)): `ImageBase`, `EntryPoint`, PE32 and PE32+ via a unified `header()` helper, per-section `Entropy`/`IsLikelyPacked`, `IsReadable`/`Writable`/`Executable`/`IsRWX`. - Checksec ([checksec.go](checksec.go)): ASLR/`DYNAMIC_BASE`, `HighEntropyVA`, DEP/`NX_COMPAT`, CFG (cross-checked against Load Config `GuardFlags`), SafeSEH (x86 only, flagged `SEHApplicable`), GS heuristic, Authenticode presence, .NET. Signature verification via `WinVerifyTrust` in [authenticode_windows.go](authenticode_windows.go). - IAT/EAT ([exports.go](exports.go), [imports.go](imports.go)): `ListExports`/ `GetExport` with forwarder resolution, `ListImports`/`FindImport` walking the thunk arrays (ordinal-or-name, 32/64-bit thunk width). - ROP ([gadgets.go](gadgets.go), [rop.go](rop.go)): `NewROP` shells out to `rp-win.exe` (rp++ build), resolved from `RP_WIN_EXE` or `C:\tools\rp-win\rp-win.exe`, run with `--allow-branches`. `NewROPExternal` takes an explicit tool path. `Find`/`Search`/`SearchRegex` filter the results; `Disassemble` decodes natively via `x86asm` for chain verification. - Patching ([patch.go](patch.go)): `OpenPEForWrite`, `SetSectionCharacteristics`/`MakeSectionExecutable`/`MakeSectionWritable`, `DisableTLSCallbacks`, `PatchBytes`/`PatchBytesAtOffset`, `RecalculateChecksum`. Deferred: - [-] `AddSection`: append a new section for payload injection. Requires growing the header area and recomputing `SizeOfImage`/`SizeOfHeaders`; needs round-trip tests against real binaries. - [-] `DynPE` (analogue of `DynELF`): given one memory-read primitive, walk a loaded module's export directory at runtime to resolve symbols. ## Live-process introspection — [+] Reads a PE inside a running process, resolving a module base via the PEB. - `PEFile` is backed by `io.ReaderAt`/`io.WriterAt` ([pe.go](pe.go)), so a `ReadProcessMemory`-backed implementation gets every PE accessor for free. - `OpenPEFromProcess(pid, base)` ([procmem_windows.go](procmem_windows.go)): live-memory entry point over `ProcessMemory`. In this mode `RVAToFileOffset` is the identity function (a loaded RVA is a read offset from the module base). - `ResolveModuleBase(pid, name)`: walks `PEB->Ldr->InMemoryOrderModuleList`, matched case-insensitively by base name. - `SpawnSuspended`/`ResumeMainThread`: launch with `CREATE_SUSPENDED`. `ResolveModuleBase` returns null on a still-suspended process — the loader (`ntdll!LdrInitializeThunk`) has not populated `PEB->Ldr` yet. `SpawnSuspended` is for attaching a debugger before the loader/entry point run, not for pre-resume base resolution. - `ProcessSymbols` ([symbols_windows.go](symbols_windows.go)): `Base`, `Symbol`, `Modules`, `AllSymbols`; caches one `PEFile` per DLL. `SymbolVA`/ `ListLoadedModules` for one-off lookups. (Analogue of `p.libs`/`p.symbols`.) ## Minidump — [+] [minidump.go](minidump.go): `OpenMinidump` parses `MINIDUMP_HEADER`/ `MINIDUMP_DIRECTORY` and decodes `ModuleListStream` (`Modules()`) and `ExceptionStream` (`Exception()`) from the public struct layouts, without `dbghelp.dll`. `RawStream(type)` returns any other stream undecoded. Register context (`CONTEXT` blob) is not decoded — its layout is arch-specific with XSAVE padding; use `RawStream`. Works without `GOOS=windows`. ## Shellcode & encoding — [+-] - [+] `ShellcodeWinExec(cmd)` ([shellcraft.go](shellcraft.go), [shellcode/asm/winexec_x64.asm](shellcode/asm/winexec_x64.asm)): position-independent x64, resolves kernel32 via the PEB (`shellcode/asm/resolver.inc`), calls `WinExec` by name. Assembled ahead of time with NASM; `.asm` source kept alongside the `.bin`. - [+] `ExecuteShellcode` ([shellcode_exec_windows.go](shellcode_exec_windows.go)): runs a template locally for validation. - [-] More templates on the same resolver base: `MessageBoxA`, reverse shell via raw `WS2_32`, a standalone `LoadLibraryA`+`GetProcAddress` primitive (current `find_export` only walks an already-loaded module). - [-] Token-stealing shellcode (see Driver/LPE). - [-] Encoders: alphanumeric and XOR bad-character avoidance. - [-] `asm`/assemble direction. Disassembly exists (`ROP.Disassemble` via `x86asm`); assembling would use cgo bindings to the `keystone/` engine (walled off by its own `go.mod`, [keystone/go.mod](keystone/go.mod)). ## Debugger — [+] [debugger_windows.go](debugger_windows.go) wraps the Windows Debug API (`DebugActiveProcess`/`WaitForDebugEvent`/`ContinueDebugEvent`/ `Get/SetThreadContext`), resolved via `LazyDLL`. No GUI-debugger integration. - `Attach(pid)`: the only entry point. The OS ties the debug session to the thread that called `DebugActiveProcess`, so `Attach` pins a dedicated goroutine with `runtime.LockOSThread` and runs the entire event loop there. Compose with `SpawnSuspended`/`ResumeMainThread` to debug from the first instruction. - `Events() <-chan DebugEvent`: decodes `EXCEPTION_DEBUG_EVENT`/ `CREATE_PROCESS_DEBUG_EVENT`/`LOAD_DLL_DEBUG_EVENT`/`EXIT_PROCESS_DEBUG_EVENT`/ etc. `DEBUG_EVENT` union payloads are read via `unsafe.Pointer`. - `SetBreakpoint`/`RemoveBreakpoint`: software INT3. `Continue` restores the original byte, single-steps, and re-arms so a breakpoint persists across hits. `Rip` is rewound past the trap before the event reaches the caller. - `GetContext`/`SetContext`: a `Registers` struct (Rax..R15/Rip/EFlags) over the x64 `CONTEXT`. `ReadMemory`/`WriteMemory` over the debuggee's address space. - Hardware breakpoints (debug registers) are not implemented; software breakpoints plus `Step`/`GetContext`/`SetContext` cover the common case. - `Step()` in response to an `EventBreakpoint` behaves like `Continue` (resuming past a software breakpoint already requires an internal single-step). ## Networking & transports — [+-] - [+] Named pipes ([pipe_windows.go](pipe_windows.go)): `ServePipe(name)`/ `DialPipe(name)`, both returning a `*Tube`. `ServePipe` uses `PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED`; each `ReadFile`/`WriteFile` blocks on `GetOverlappedResult`, so the handle behaves as a blocking `io.ReadWriteCloser`. `pipeConn.Close()` is idempotent (`sync.Once`) because the duplex handle is used as both `Tube.stdin` and `Tube.stdout`. - [-] `Listen`: TCP listener tube for reverse shells (`net.Listen`). - [-] TLS transport (`Remote` with `tls.Dial`). - [-] SSH transport (`golang.org/x/crypto/ssh`). - [-] Process-tree cleanup: `Spawn` should kill the whole tree on `Close()` via a Windows Job Object. ## CLI — [+-] [cmd/winpwn/main.go](cmd/winpwn/main.go), additive to the library. Done: `checksec`, `cyclic` (`-l` accepts literal bytes or a `0x...` packed integer), `hex`/`unhex`/`hexdump`, `rop` (`-search`/`-regex`; no unfiltered dump mode), `disasm`, `exports`, `imports`, `heap`. Open: - [-] `winpwn asm`: blocked on the Keystone integration. - [-] `winpwn template`: scaffold a new solve script (partly covered by [cmd/pwninit](cmd/pwninit/main.go)). ## Driver / LPE (Ring 0) — [-] - [-] Device handle + IOCTL wrapper: `OpenDevice(name)` over `CreateFileA`, `(*Device).IOCTL(code, in)` over `DeviceIoControl`, handling output-buffer sizing and error mapping. - [-] Token-stealing shellcode (x86 and x64), parameterized by a `KernelOffsets{Process, ActiveProcessLinks, Token}` struct. - [-] Kernel info-leak helpers over the common `NtQuerySystemInformation` classes. ## Heap exploitation — [+-] Heap struct-parsing layer. All decoders work against any `io.ReaderAt`; only finding a heap address in a live process needs Windows syscalls. - [+] NT Heap foundation ([heap.go](heap.go)): `_HEAP`/`_HEAP_SEGMENT`/ `_HEAP_ENTRY` + `DecodeHeapEntry` (XOR against `_HEAP.Encoding`). Offsets from `dt ntdll!_HEAP` on build 10.0.26100; decode validated against `!heap -a` for three allocations (see `TestDecodeHeapEntryMatchesLiveGroundTruth`). `WalkAllHeapEntries`, `SummariseEntries`, `EntriesInRange`, `EntriesWithUserData`, `AdjacentBusyPairs`. `UserSize` = `Size*HeapEntrySize - UnusedBytes` (no separate header subtraction — `UnusedBytes` already accounts for the header). - [+] `ListProcessHeaps(pid)` ([heap_windows.go](heap_windows.go)): PEB walk via `NtQueryInformationProcess(ProcessBasicInformation)`. - [+] NT Heap LFH ([heap_lfh.go](heap_lfh.go)): `ReadLFHBuckets`, `FindLFHBucket` (searches `BlockSize >= wantSize + 16`; LFH blocks include the 16-byte header), `ActiveSubsegment`, `AllSubsegments`, `ReadLFHSubsegment` (BusyBitmap validated), `CalibrateLFHFirstBlockOffset`, `BlockAddress`, `SlotOf`. Caveats: a bucket warms up after ≈19 same-size requests before `SegmentInfoArrays[bucket]` is populated (build-specific); `_HEAP_USERDATA_HEADER.EncodedOffsets` is obfuscated — `CalibrateLFHFirstBlockOffset` uses a known-address calibration instead. - [+] Segment Heap outer layer ([heap_segment.go](heap_segment.go)): `ReadSegmentHeap` (Signature/GlobalFlags + VS/LFH summaries), VS subsegment walk, LFH bucket enumeration. VS `_HEAP_VS_CHUNK_HEADER.Sizes` and Segment LFH `BlockOffsets.EncodedData` are XOR-encoded and not decoded. Adjacency is handled by `AdjacentAddressPairs`/`FindAdjacentPair` on leaked addresses, which needs no chunk-header decode. - [+] `winpwn heap [-walk]` CLI subcommand. - [-] BSTR/client-spray generator (lower priority; `SprayAndFind` covers the generic retry/search loop). ## Go-native additions — [-] - [-] `Pool`: fan out one exploit across N parallel connections via a bounded goroutine pool. - [-] Race-condition primitives: fire N goroutines at a target behind a barrier. - [-] Cancellable `Interactive`: propagate a `context.Context` into both copy goroutines so the stdin-forwarding goroutine does not outlive the tube. ## Testing - Unit tests: [cyclic_test.go](cyclic_test.go), [fiddling_test.go](fiddling_test.go), [packing_test.go](packing_test.go) (includes a pin against pwntools' `cyclic(20)`). - [tube_test.go](tube_test.go): `Send`/`Recv*`/`SendAfter`/timeouts/`Close` against an `io.Pipe`-backed transport. - [gadgets_test.go](gadgets_test.go), [pe_test.go](pe_test.go): fixture-based, against `examples/bof_basic/bof_win.c.exe`. Skip (do not fail) when the fixture or `rp-win.exe` is absent. - [heap_test.go](heap_test.go), [heap_lfh_test.go](heap_lfh_test.go), [heap_segment_test.go](heap_segment_test.go), [spray_test.go](spray_test.go): synthetic buffers and captured live-run data. Not covered: - Live-process paths (`OpenPEFromProcess`, `ResolveModuleBase`, `SpawnSuspended`/`ResumeMainThread`) — need a real target PID. - `shellcraft.go`/`shellcode_exec_windows.go` execution. - `patch.go` round-trip (patch a real binary, recompute checksum, compare). Note: the fixture binary referenced by the PE/gadget tests is not present in the tree; those tests currently skip. A green `go test` therefore does not exercise the PE, gadget, or heap paths until the fixture is restored.