396 lines
14 KiB
Markdown
396 lines
14 KiB
Markdown
# winpwn — краткий справочник
|
||
|
||
## Установка CLI (winpwn, pwninit)
|
||
|
||
```bash
|
||
cd C:\tools\go_pwner
|
||
go install ./cmd/winpwn
|
||
go install ./cmd/pwninit
|
||
```
|
||
|
||
Кладёт `winpwn.exe`/`pwninit.exe` в `%USERPROFILE%\go\bin` (в PATH). После этого
|
||
`winpwn`/`pwninit` работают из любой директории.
|
||
|
||
Если `go build`/`go install`/`go test` внутри `go_pwner\` падают с
|
||
`directory outside module roots` — `C:\tools\go.work` не перечисляет `./go_pwner`
|
||
в `use (...)`. Отредактируй файл, не удаляй его (`workspace\` от него зависит).
|
||
|
||
Solve-скрипты (`import "winpwn"`) требуют `go.mod` с
|
||
`replace winpwn => C:/tools/go_pwner` в каждой задаче — Go не видит модуль вне
|
||
`go.work`/`replace`. `pwninit` генерирует это автоматически (см. ниже).
|
||
|
||
## Где писать solve-скрипты
|
||
|
||
Не в `go_pwner\` (исходник библиотеки). В `C:\tools\workspace\`:
|
||
|
||
```
|
||
C:\tools\
|
||
├── go_pwner\ ← библиотека
|
||
└── workspace\ ← скрипты
|
||
├── 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 .
|
||
# main.go, затем:
|
||
go run .
|
||
```
|
||
|
||
Шаблон `main.go` — в `workspace\template\main.go`. Задачи и решения — в
|
||
`workspace\` (бинари, `src\`, solve-скрипты, флаги).
|
||
|
||
## CLI — быстрые ответы без скрипта
|
||
|
||
```
|
||
winpwn checksec chal.exe # митигации + секции (VA/Offset/Size/entropy) + импортированные DLL с live base
|
||
winpwn exports chal.exe # таблица экспортов
|
||
winpwn imports chal.exe # IAT
|
||
winpwn rop chal.exe -search "pop rcx ; ret" # поиск гаджетов (ret-, jmp-, call-терминированных)
|
||
winpwn rop chal.exe -regex "^pop r.* ; ret$"
|
||
winpwn bytes chal.exe ebfe # все VA байт-паттерна (jmp $ = EB FE); "ebfe"/"EB FE"/"\xeb\xfe"
|
||
winpwn disasm chal.exe 0x140001538 5 # дизасм N инструкций с адреса
|
||
winpwn cyclic 200 # de Bruijn паттерн
|
||
winpwn cyclic -l aaab # оффсет по подстроке
|
||
winpwn hexdump chal.exe
|
||
winpwn hex # stdin → hex
|
||
winpwn unhex # hex → raw bytes
|
||
winpwn heap <PID> # все кучи в живом процессе
|
||
winpwn heap <PID> -walk # + обход записей NT Heap
|
||
```
|
||
|
||
Гаджет-поиск (`rop`/`NewROP`) работает через `rp-win.exe` (rp++): по умолчанию
|
||
`C:\tools\rp-win\rp-win.exe`, переопределяется `RP_WIN_EXE`. Запуск с
|
||
`--allow-branches` — находит `ret`-терминированные цепочки и JOP-транзит через
|
||
`jmp reg`/`call reg`.
|
||
|
||
### pwninit — начать новую задачу
|
||
|
||
```bash
|
||
cd C:\tools\workspace
|
||
mkdir mytask && cd mytask
|
||
copy path\to\chal.exe .
|
||
pwninit # автоопределяет chal.exe, если он один в директории
|
||
```
|
||
|
||
Печатает recon (arch, checksec, секции R/W/X + Offset + entropy, импортированные
|
||
DLL с live base), затем пишет `go.mod` (`replace winpwn => ...`, путь из
|
||
`WINPWN_HOME` или `C:\tools\go_pwner`) и минимальный `main.go` (`Spawn` +
|
||
`Interactive`). Не перезатирает существующие файлы без `-force`.
|
||
|
||
## Tube — I/O с процессом
|
||
|
||
```go
|
||
t, err := winpwn.Spawn("chal.exe") // локальный процесс
|
||
t, err := winpwn.Remote("host", 1337) // TCP
|
||
|
||
t.PID() // PID локального процесса (0 для remote)
|
||
|
||
t.Send(data)
|
||
t.SendLine(data) // + \n
|
||
t.SendAfter(delim, data)
|
||
t.SendLineAfter(delim, data)
|
||
|
||
data, err := t.Recv(64)
|
||
data, err := t.RecvUntil(delim) // включая delim
|
||
data, err := t.RecvLine()
|
||
data, err := t.RecvRegex(re)
|
||
|
||
t.Interactive() // передать stdin/stdout в терминал
|
||
t.Close()
|
||
```
|
||
|
||
## PE — анализ бинаря
|
||
|
||
```go
|
||
pf, _ := winpwn.OpenPE("chal.exe")
|
||
defer pf.Close()
|
||
|
||
base, _ := pf.ImageBase()
|
||
entry, _ := pf.EntryPoint()
|
||
r, _ := pf.Checksec()
|
||
// r.ASLR, r.DEP, r.CFG, r.SafeSEH, r.GSHeuristic
|
||
|
||
winRVA, _ := pf.GetProcAddress("win")
|
||
winAddr := base + winRVA
|
||
|
||
exports, _ := pf.ListExports() // []Export{Name, Ordinal, RVA}
|
||
imports, _ := pf.ListImports() // []Import{DLL, Name, Ordinal, IATRVA}
|
||
|
||
// sec.VirtualAddress (RVA в памяти) и sec.Offset (PointerToRawData, оффсет в
|
||
// файле) — разные числа, оба нужны
|
||
for _, sec := range pf.Sections() {
|
||
_ = sec.VirtualAddress
|
||
_ = sec.Offset
|
||
}
|
||
|
||
// image base импортированных DLL в этом процессе (LoadLibrary). Системные DLL
|
||
// рандомизируются раз за перезагрузку, не на процесс — значение верно для любого
|
||
// процесса до ребута, без запуска таргета.
|
||
libs, _ := pf.ImportedLibs() // []ImportedLib{Name, Base, Err}
|
||
|
||
offsets, _ := pf.SearchBytes([]byte("cmd.exe\x00"))
|
||
|
||
// PE из памяти живого процесса
|
||
memBase, _ := winpwn.ResolveModuleBase(pid, "kernel32.dll")
|
||
pf2, _ := winpwn.OpenPEFromProcess(pid, memBase)
|
||
```
|
||
|
||
## Символы живого процесса
|
||
|
||
Аналог pwntools: `p.libs["kernel32.dll"]` и `p.symbols["kernel32.dll"]["WinExec"]`.
|
||
|
||
```go
|
||
tube, _ := winpwn.Spawn("chal.exe")
|
||
sym := winpwn.NewProcessSymbols(tube.PID())
|
||
defer sym.Close()
|
||
|
||
// база DLL (p.libs["..."])
|
||
k32, _ := sym.Base("kernel32.dll")
|
||
ntdll, _ := sym.Base("ntdll.dll")
|
||
|
||
// абсолютный VA экспорта (p.symbols["..."]["..."])
|
||
winexec, _ := sym.Symbol("kernel32.dll", "WinExec")
|
||
ntAlloc, _ := sym.Symbol("ntdll.dll", "NtAllocateVirtualMemory")
|
||
|
||
// все загруженные модули (p.libs целиком)
|
||
mods, _ := sym.Modules() // map[string]uint64
|
||
|
||
// все экспорты одной DLL
|
||
all, _ := sym.AllSymbols("kernel32.dll") // map[string]uint64
|
||
|
||
// один вызов без struct
|
||
va, _ := winpwn.SymbolVA(pid, "kernel32.dll", "WinExec")
|
||
libs, _ := winpwn.ListLoadedModules(pid) // map[string]uintptr
|
||
```
|
||
|
||
`ProcessSymbols` кеширует PEFile на DLL — повторные `Symbol` для одной DLL не
|
||
переоткрывают файл.
|
||
|
||
## ROP
|
||
|
||
Бэкенд — `rp-win.exe` (по умолчанию `C:\tools\rp-win\rp-win.exe`, переопределяется
|
||
через `RP_WIN_EXE`). `NewROPExternal(path, toolPath)` — явный путь до другого
|
||
билда rp++.
|
||
|
||
```go
|
||
rop, _ := winpwn.NewROP("chal.exe")
|
||
defer rop.Close()
|
||
|
||
// find["pop rcx ; ret"][0] в pwntools-стиле: одно выражение. Ранжировано так,
|
||
// что [0] — самый чистый вариант (точное совпадение и более короткие инструкции
|
||
// приоритетнее гаджета с той же подстрокой по меньшему адресу с мусорным
|
||
// префиксом вроде "ror ... ; pop rcx ; ret").
|
||
addr := rop.Find("pop rcx ; ret")[0].Address
|
||
|
||
gadgets, _ := rop.Search("pop rcx ; ret") // тот же поиск, (results, error)
|
||
gadgets, _ := rop.SearchRegex(`^pop r\w+ ; ret$`)
|
||
lines, _ := rop.Disassemble(gadgets[0].Address, 3) // ["pop rcx", "ret"]
|
||
```
|
||
|
||
## Packing
|
||
|
||
```go
|
||
winpwn.P64(addr) // uint64 → []byte LE
|
||
winpwn.P32(addr)
|
||
winpwn.P16(addr)
|
||
winpwn.U64(buf) // []byte LE → uint64
|
||
winpwn.U32(buf)
|
||
|
||
winpwn.Enhex(data) // []byte → "aabbcc..."
|
||
winpwn.Unhex(hexstr) // "aabbcc..." → []byte, error
|
||
winpwn.Xor(data, key) // XOR, ключ циклится
|
||
winpwn.Hexdump(data) // hex+ASCII дамп, строка
|
||
```
|
||
|
||
## Cyclic patterns
|
||
|
||
```go
|
||
pattern := winpwn.Cyclic(200) // de Bruijn n=4
|
||
pattern8 := winpwn.CyclicN(200, 8) // n=8 для 64-битных указателей
|
||
|
||
offset := winpwn.CyclicFind(crashedRIPBytes)
|
||
offset8 := winpwn.CyclicFindN(crashedRIPBytes, 8)
|
||
```
|
||
|
||
## Heap API
|
||
|
||
Все функции работают с любым `io.ReaderAt` (`*ProcessMemory`, `*Debugger`, буфер
|
||
в тесте). Только `ListProcessHeaps` требует живой процесс.
|
||
|
||
### Найти кучи
|
||
|
||
```go
|
||
heaps, _ := winpwn.ListProcessHeaps(pid) // читает PEB.ProcessHeaps
|
||
mem, _ := winpwn.OpenProcessMemory(pid, 0)
|
||
kind, _ := winpwn.DetectHeapKind(mem, heapAddr) // HeapKindNT / HeapKindSegment
|
||
```
|
||
|
||
### NT Heap
|
||
|
||
```go
|
||
h, _ := winpwn.ReadHeap(mem, heapAddr)
|
||
// h.Flags, h.FrontEndHeapType, h.FrontEndHeap, h.EncodingActive()
|
||
|
||
entries, _ := h.WalkAllHeapEntries(mem) // все сегменты
|
||
entries, _ := h.WalkSegment0(mem) // только Segment0
|
||
|
||
// Поля записи
|
||
e.Addr; e.BlockSize(); e.UserSize(); e.UserData()
|
||
e.Busy(); e.LastEntry()
|
||
|
||
// Анализ
|
||
stats := winpwn.SummariseEntries(entries)
|
||
// stats.BusyEntries, stats.FreeEntries, stats.BusyBytes, stats.FreeBytes
|
||
pairs := winpwn.AdjacentBusyPairs(entries) // [][2]HeapEntry — смежные busy записи
|
||
hits := winpwn.EntriesWithUserData(entries, leakedAddr) // обратный поиск по адресу
|
||
```
|
||
|
||
### NT Heap LFH
|
||
|
||
```go
|
||
buckets, _ := winpwn.ReadLFHBuckets(mem, h.FrontEndHeap)
|
||
bucket, _ := winpwn.FindLFHBucket(buckets, 32)
|
||
// ищет BlockSize >= 32+16 (LFH блоки включают 16-байтный заголовок)
|
||
|
||
subsegAddr, _ := winpwn.ActiveSubsegment(mem, h.FrontEndHeap, bucket.Index)
|
||
subseg, _ := winpwn.ReadLFHSubsegment(mem, subsegAddr)
|
||
// subseg.BlockSize, subseg.BlockCount
|
||
// subseg.Busy — []bool, Busy[i] == true → слот занят
|
||
|
||
// Адрес слота по индексу — нужен один известный адрес для калибровки
|
||
off := winpwn.CalibrateLFHFirstBlockOffset(subseg, knownAddr)
|
||
addr := subseg.BlockAddress(off, i)
|
||
idx, ok := subseg.SlotOf(off, knownAddr)
|
||
```
|
||
|
||
Build 10.0.26100:
|
||
- LFH активируется на бакет после ≈19 аллокаций одного размера.
|
||
- `_HEAP_USERDATA_HEADER.EncodedOffsets` зашифрован — используй калибровку.
|
||
- XOR-ключ LFH-блоков не восстановлен (нужен отдельный проход).
|
||
|
||
### Segment Heap
|
||
|
||
```go
|
||
sh, _ := winpwn.ReadSegmentHeap(mem, heapAddr)
|
||
// sh.VS.CommittedUnits, sh.VS.FreeUnits, sh.VS.SubsegmentCount
|
||
// sh.LFH.ActiveBuckets — []SegmentLFHBucket{Index, TotalBlockCount}
|
||
|
||
// Смежная пара из набора утечённых адресов (без парсинга заголовков)
|
||
lo, hi, found := winpwn.FindAdjacentPair(addrs, 32) // первая пара
|
||
pairs := winpwn.AdjacentAddressPairs(addrs, 32) // все пары
|
||
```
|
||
|
||
`heap_handle + 0x2c0` → адрес внутри ntdll (build 10.0.26100). Leak ntdll base из
|
||
утечённого адреса кучи.
|
||
|
||
### Обход памяти процесса
|
||
|
||
```go
|
||
mem, _ := winpwn.OpenProcessMemory(pid, 0)
|
||
defer mem.Close()
|
||
|
||
n, err := mem.ReadAt(buf, int64(addr))
|
||
n, err := mem.WriteAt(data, int64(addr))
|
||
```
|
||
|
||
## Debugger
|
||
|
||
```go
|
||
tube, pid, _ := winpwn.SpawnSuspended("chal.exe")
|
||
dbg, _ := winpwn.Attach(pid) // до ResumeMainThread
|
||
winpwn.ResumeMainThread(pid)
|
||
|
||
for ev := range dbg.Events() {
|
||
// ev.Kind: EventBreakpoint, EventException, EventCreateProcess,
|
||
// EventLoadDll, EventExitProcess, ...
|
||
if ev.Kind == winpwn.EventBreakpoint {
|
||
regs, _ := dbg.GetContext(ev.ThreadID)
|
||
fmt.Printf("RIP=0x%x RSP=0x%x\n", regs.Rip, regs.Rsp)
|
||
}
|
||
dbg.Continue(ev) // обязательно для каждого события
|
||
}
|
||
|
||
dbg.SetBreakpoint(addr)
|
||
dbg.RemoveBreakpoint(addr)
|
||
dbg.ReadMemory(addr, 32)
|
||
dbg.WriteMemory(addr, data)
|
||
dbg.Close() // detach без убийства процесса
|
||
```
|
||
|
||
## SprayAndFind — примитив для heap grooming
|
||
|
||
```go
|
||
older, newer, attempts, ok, err := winpwn.SprayAndFind(
|
||
seed, // []winpwn.SprayResult[K] — известные цели, или nil
|
||
maxAttempts,
|
||
func(attempt int) (winpwn.SprayResult[K], error) {
|
||
// один шаг спрея — вернуть (id, утечённое значение)
|
||
},
|
||
func(a, b K) bool {
|
||
// условие совпадения: равенство, расстояние, что угодно
|
||
},
|
||
)
|
||
```
|
||
|
||
UAF (равенство адресов):
|
||
|
||
```go
|
||
winpwn.SprayAndFind(
|
||
[]winpwn.SprayResult[uint64]{{ID: victimID, Key: victimAddr}},
|
||
64,
|
||
func(i int) (winpwn.SprayResult[uint64], error) { /* один B-буфер */ },
|
||
func(a, b uint64) bool { return a == b },
|
||
)
|
||
```
|
||
|
||
Heap overflow (смежность, расстояние ровно 32):
|
||
|
||
```go
|
||
winpwn.SprayAndFind(nil, 20,
|
||
func(i int) (winpwn.SprayResult[uint64], error) { /* один A-объект */ },
|
||
func(x, y uint64) bool { d := int64(y)-int64(x); return d == 32 || d == -32 },
|
||
)
|
||
```
|
||
|
||
## Shellcode
|
||
|
||
```go
|
||
code, _ := winpwn.ShellcodeWinExec("calc.exe") // PIC x64, находит kernel32 через PEB
|
||
winpwn.ExecuteShellcode(code) // запустить локально для проверки
|
||
```
|
||
|
||
## Minidump
|
||
|
||
```go
|
||
m, _ := winpwn.OpenMinidump("crash.dmp")
|
||
defer m.Close()
|
||
|
||
mods, _ := m.Modules()
|
||
exc, _ := m.Exception() // ExceptionCode, ExceptionAddress
|
||
raw, _ := m.RawStream(winpwn.StreamSystemInfo)
|
||
```
|
||
|
||
## Примеры задач (с решениями)
|
||
|
||
| Задача | Техника | ASLR | Файл |
|
||
|--------|---------|------|------|
|
||
| `task1_leak` | info leak + redirect | Нет | `workspace/task1_leak/` |
|
||
| `task2_rop` | buffer overflow + ROP + DEP | Нет | `workspace/task2_rop/` |
|
||
| `task3_fmtstr` | format string | Нет | `workspace/task3_fmtstr/` |
|
||
| `bof_basic` | stack overflow | Нет | `workspace/bof_basic/` |
|
||
| `heap_lfh` | UAF + LFH grooming | Нет | `workspace/heap_lfh/` |
|
||
| `heap_segment` | overflow + Segment Heap | Нет | `workspace/heap_segment/` |
|
||
| `heap_typemix` | UAF type confusion, без LFH | Нет | `workspace/heap_typemix/` |
|
||
| `heap_overflow` | adjacent chunk overflow, NT Heap | Нет | `workspace/heap_overflow/` |
|
||
| `heap_info_leak` | OOB read → утечка адреса → UAF | **Да** | `workspace/heap_info_leak/` |
|
||
|
||
```bash
|
||
cd C:\tools\workspace\heap_lfh
|
||
go run .
|
||
```
|