Files
go_pwner/ROADMAP.md
T
2026-08-06 20:19:30 +03:00

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/P64 are little-endian only and do not read endianness from Context.Arch — every Windows target is little-endian.
  • Logging (log.go): Info/Success/Warn/Error, gated by Context.LogLevel. Every Tube method returns error rather than exiting.
  • Timeouts (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/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 unified header() helper, per-section Entropy/IsLikelyPacked, IsReadable/Writable/Executable/IsRWX.
  • Checksec (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.
  • IAT/EAT (exports.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, 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): 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), so a ReadProcessMemory-backed implementation gets every PE accessor for free.
  • OpenPEFromProcess(pid, base) (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): Base, Symbol, Modules, AllSymbols; caches one PEFile per DLL. SymbolVA/ ListLoadedModules for one-off lookups. (Analogue of p.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), calls WinExec by name. Assembled ahead of time with NASM; .asm source 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 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).

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 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): 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, 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) 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/_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): PEB walk via NtQueryInformationProcess(ProcessBasicInformation).
  • [+] NT Heap LFH (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): 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 <pid> [-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

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.