v0.1 - initial commit

This commit is contained in:
2026-07-18 21:37:15 +03:00
commit 9b89f4cb8e
153 changed files with 22887 additions and 0 deletions
+230
View File
@@ -0,0 +1,230 @@
# 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 <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
- 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.