231 lines
12 KiB
Markdown
231 lines
12 KiB
Markdown
# 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.
|