12 KiB
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):Arch(x86/x64),LogLevel,Timeout,Newline. Note:P16/P32/P64are little-endian only and do not read endianness fromContext.Arch— every Windows target is little-endian.- Logging (log.go):
Info/Success/Warn/Error, gated byContext.LogLevel. EveryTubemethod returnserrorrather than exiting. - Timeouts (tube.go):
(*Tube).SetTimeoutoverridesContext.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/CyclicN,CyclicFind/CyclicFindN. Generator is lazy (deBruijnEach) and stops at the requested length. Verified against pwntools'cyclic(20)output.fiddling(fiddling.go):Hexdump,Enhex/Unhex,Xor.- Packing (packing.go):
P16/P32/P64,U16/U32/U64. - Tube API (tube.go):
Send*/Recv*/RecvRegex/RecvPred/Interactive/Close.
PE tooling — ✅
- Parsing (pe.go, sections.go):
ImageBase,EntryPoint, PE32 and PE32+ via a unifiedheader()helper, per-sectionEntropy/IsLikelyPacked,IsReadable/Writable/Executable/IsRWX. - Checksec (checksec.go): ASLR/
DYNAMIC_BASE,HighEntropyVA, DEP/NX_COMPAT, CFG (cross-checked against Load ConfigGuardFlags), SafeSEH (x86 only, flaggedSEHApplicable), GS heuristic, Authenticode presence, .NET. Signature verification viaWinVerifyTrustin authenticode_windows.go. - IAT/EAT (exports.go, imports.go):
ListExports/GetExportwith forwarder resolution,ListImports/FindImportwalking the thunk arrays (ordinal-or-name, 32/64-bit thunk width). - ROP (gadgets.go, rop.go):
NewROPshells out torp-win.exe(rp++ build), resolved fromRP_WIN_EXEorC:\tools\rp-win\rp-win.exe, run with--allow-branches.NewROPExternaltakes an explicit tool path.Find/Search/SearchRegexfilter the results;Disassembledecodes natively viax86asmfor chain verification. - Patching (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 recomputingSizeOfImage/SizeOfHeaders; needs round-trip tests against real binaries. - ⬜
DynPE(analogue ofDynELF): 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.
PEFileis backed byio.ReaderAt/io.WriterAt(pe.go), so aReadProcessMemory-backed implementation gets every PE accessor for free.OpenPEFromProcess(pid, base)(procmem_windows.go): live-memory entry point overProcessMemory. In this modeRVAToFileOffsetis the identity function (a loaded RVA is a read offset from the module base).ResolveModuleBase(pid, name): walksPEB->Ldr->InMemoryOrderModuleList, matched case-insensitively by base name.SpawnSuspended/ResumeMainThread: launch withCREATE_SUSPENDED.ResolveModuleBasereturns null on a still-suspended process — the loader (ntdll!LdrInitializeThunk) has not populatedPEB->Ldryet.SpawnSuspendedis for attaching a debugger before the loader/entry point run, not for pre-resume base resolution.ProcessSymbols(symbols_windows.go):Base,Symbol,Modules,AllSymbols; caches onePEFileper DLL.SymbolVA/ListLoadedModulesfor one-off lookups. (Analogue ofp.libs/p.symbols.)
Minidump — ✅
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, shellcode/asm/winexec_x64.asm): position-independent x64, resolves kernel32 via the PEB (shellcode/asm/resolver.inc), callsWinExecby name. Assembled ahead of time with NASM;.asmsource kept alongside the.bin. - ✅
ExecuteShellcode(shellcode_exec_windows.go): runs a template locally for validation. - ⬜ More templates on the same resolver base:
MessageBoxA, reverse shell via rawWS2_32, a standaloneLoadLibraryA+GetProcAddressprimitive (currentfind_exportonly walks an already-loaded module). - ⬜ Token-stealing shellcode (see Driver/LPE).
- ⬜ Encoders: alphanumeric and XOR bad-character avoidance.
- ⬜
asm/assemble direction. Disassembly exists (ROP.Disassembleviax86asm); assembling would use cgo bindings to thekeystone/engine (walled off by its owngo.mod, keystone/go.mod).
Debugger — ✅
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 calledDebugActiveProcess, soAttachpins a dedicated goroutine withruntime.LockOSThreadand runs the entire event loop there. Compose withSpawnSuspended/ResumeMainThreadto debug from the first instruction.Events() <-chan DebugEvent: decodesEXCEPTION_DEBUG_EVENT/CREATE_PROCESS_DEBUG_EVENT/LOAD_DLL_DEBUG_EVENT/EXIT_PROCESS_DEBUG_EVENT/ etc.DEBUG_EVENTunion payloads are read viaunsafe.Pointer.SetBreakpoint/RemoveBreakpoint: software INT3.Continuerestores the original byte, single-steps, and re-arms so a breakpoint persists across hits.Ripis rewound past the trap before the event reaches the caller.GetContext/SetContext: aRegistersstruct (Rax..R15/Rip/EFlags) over the x64CONTEXT.ReadMemory/WriteMemoryover the debuggee's address space.- Hardware breakpoints (debug registers) are not implemented; software
breakpoints plus
Step/GetContext/SetContextcover the common case. Step()in response to anEventBreakpointbehaves likeContinue(resuming past a software breakpoint already requires an internal single-step).
Networking & transports — 🚧
- ✅ Named pipes (pipe_windows.go):
ServePipe(name)/DialPipe(name), both returning a*Tube.ServePipeusesPIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED; eachReadFile/WriteFileblocks onGetOverlappedResult, so the handle behaves as a blockingio.ReadWriteCloser.pipeConn.Close()is idempotent (sync.Once) because the duplex handle is used as bothTube.stdinandTube.stdout. - ⬜
Listen: TCP listener tube for reverse shells (net.Listen). - ⬜ TLS transport (
Remotewithtls.Dial). - ⬜ SSH transport (
golang.org/x/crypto/ssh). - ⬜ Process-tree cleanup:
Spawnshould kill the whole tree onClose()via a Windows Job Object.
CLI — 🚧
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).
Driver / LPE (Ring 0) — ⬜
- ⬜ Device handle + IOCTL wrapper:
OpenDevice(name)overCreateFileA,(*Device).IOCTL(code, in)overDeviceIoControl, 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
NtQuerySystemInformationclasses.
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/_HEAP_SEGMENT/_HEAP_ENTRY+DecodeHeapEntry(XOR against_HEAP.Encoding). Offsets fromdt ntdll!_HEAPon build 10.0.26100; decode validated against!heap -afor three allocations (seeTestDecodeHeapEntryMatchesLiveGroundTruth).WalkAllHeapEntries,SummariseEntries,EntriesInRange,EntriesWithUserData,AdjacentBusyPairs.UserSize=Size*HeapEntrySize - UnusedBytes(no separate header subtraction —UnusedBytesalready accounts for the header). - ✅
ListProcessHeaps(pid)(heap_windows.go): PEB walk viaNtQueryInformationProcess(ProcessBasicInformation). - ✅ NT Heap LFH (heap_lfh.go):
ReadLFHBuckets,FindLFHBucket(searchesBlockSize >= 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 beforeSegmentInfoArrays[bucket]is populated (build-specific);_HEAP_USERDATA_HEADER.EncodedOffsetsis obfuscated —CalibrateLFHFirstBlockOffsetuses a known-address calibration instead. - ✅ Segment Heap outer layer (heap_segment.go):
ReadSegmentHeap(Signature/GlobalFlags + VS/LFH summaries), VS subsegment walk, LFH bucket enumeration. VS_HEAP_VS_CHUNK_HEADER.Sizesand Segment LFHBlockOffsets.EncodedDataare XOR-encoded and not decoded. Adjacency is handled byAdjacentAddressPairs/FindAdjacentPairon leaked addresses, which needs no chunk-header decode. - ✅
winpwn heap <pid> [-walk]CLI subcommand. - ⬜ BSTR/client-spray generator (lower priority;
SprayAndFindcovers 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 acontext.Contextinto both copy goroutines so the stdin-forwarding goroutine does not outlive the tube.
Testing
- Unit tests: cyclic_test.go, fiddling_test.go,
packing_test.go (includes a pin against pwntools'
cyclic(20)). - tube_test.go:
Send/Recv*/SendAfter/timeouts/Closeagainst anio.Pipe-backed transport. - gadgets_test.go, pe_test.go: fixture-based,
against
examples/bof_basic/bof_win.c.exe. Skip (do not fail) when the fixture orrp-win.exeis absent. - heap_test.go, heap_lfh_test.go, heap_segment_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.goexecution.patch.goround-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.