# 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 # все кучи в живом процессе winpwn heap -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 . ```