Files
go_pwner/USAGE_RU.md
T
2026-07-18 21:37:15 +03:00

14 KiB
Raw Blame History

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 rootsC:\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 .