14 KiB
winpwn — краткий справочник
Установка CLI (winpwn, pwninit)
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
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 — начать новую задачу
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 с процессом
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 — анализ бинаря
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"].
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++.
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
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
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 требует живой процесс.
Найти кучи
heaps, _ := winpwn.ListProcessHeaps(pid) // читает PEB.ProcessHeaps
mem, _ := winpwn.OpenProcessMemory(pid, 0)
kind, _ := winpwn.DetectHeapKind(mem, heapAddr) // HeapKindNT / HeapKindSegment
NT Heap
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
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
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 из
утечённого адреса кучи.
Обход памяти процесса
mem, _ := winpwn.OpenProcessMemory(pid, 0)
defer mem.Close()
n, err := mem.ReadAt(buf, int64(addr))
n, err := mem.WriteAt(data, int64(addr))
Debugger
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
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 (равенство адресов):
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):
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
code, _ := winpwn.ShellcodeWinExec("calc.exe") // PIC x64, находит kernel32 через PEB
winpwn.ExecuteShellcode(code) // запустить локально для проверки
Minidump
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/ |
cd C:\tools\workspace\heap_lfh
go run .