834 lines
28 KiB
Go
834 lines
28 KiB
Go
//go:build windows
|
|
|
|
package winpwn
|
|
|
|
import (
|
|
"fmt"
|
|
"runtime"
|
|
"sync"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
// This file is winpwn's debugger backend -- the Windows analogue of
|
|
// pwntools' gdb module. There is no GDB/ptrace equivalent on Windows, but
|
|
// the Windows Debug API (DebugActiveProcess/WaitForDebugEvent/
|
|
// ContinueDebugEvent/Get-SetThreadContext) gives the same capability
|
|
// natively, and golang.org/x/sys/windows doesn't wrap any of it -- every
|
|
// proc below is resolved by hand via LazyDLL, the same escape hatch
|
|
// pipe_windows.go would have needed if CreateNamedPipe weren't already
|
|
// exposed there.
|
|
//
|
|
// x64dbg/WinDbg attach was considered and rejected for the actual event
|
|
// loop: scripting a GUI debugger from Go would mean driving it through its
|
|
// command pipe/plugin API (x64dbg) or shelling out to cdb scripts (WinDbg),
|
|
// neither of which gives a typed Go channel of events or direct register
|
|
// access -- it would be strictly less capable than calling the same Win32
|
|
// API Microsoft's own debuggers are built on. If you want the GUI, attach
|
|
// x64dbg to the PID this package reports separately; this backend is for
|
|
// scripted/automated control, the same role pwntools' gdb.attach() plays
|
|
// when used non-interactively.
|
|
|
|
const (
|
|
debugExceptionEvent = 1
|
|
debugCreateThreadEvent = 2
|
|
debugCreateProcessEvent = 3
|
|
debugExitThreadEvent = 4
|
|
debugExitProcessEvent = 5
|
|
debugLoadDllEvent = 6
|
|
debugUnloadDllEvent = 7
|
|
debugOutputStringEvent = 8
|
|
debugRipEvent = 9
|
|
)
|
|
|
|
const (
|
|
// DBG_CONTINUE / DBG_EXCEPTION_NOT_HANDLED, the two dwContinueStatus
|
|
// values ContinueDebugEvent actually distinguishes -- the rest of
|
|
// NTSTATUS-space is accepted but treated as one or the other by the OS.
|
|
dbgContinue = 0x00010002
|
|
dbgExceptionNotHandled = 0x80010001
|
|
|
|
exceptionBreakpoint = 0x80000003
|
|
exceptionSingleStep = 0x80000004
|
|
exceptionAccessViolation = 0xC0000005
|
|
|
|
threadAccessForDebug = windows.THREAD_GET_CONTEXT | windows.THREAD_SET_CONTEXT | windows.THREAD_SUSPEND_RESUME | 0x40 /* THREAD_QUERY_INFORMATION */
|
|
|
|
eflagsTrapFlag = 0x100
|
|
|
|
contextAMD64 = 0x00100000
|
|
contextControl = contextAMD64 | 0x1
|
|
contextInteger = contextAMD64 | 0x2
|
|
contextSegments = contextAMD64 | 0x4
|
|
contextFloatingPoint = contextAMD64 | 0x8
|
|
contextDebugRegisters = contextAMD64 | 0x10
|
|
contextFull = contextControl | contextInteger | contextFloatingPoint
|
|
)
|
|
|
|
var (
|
|
modKernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
|
procWaitForDebugEvent = modKernel32.NewProc("WaitForDebugEvent")
|
|
procContinueDebugEvent = modKernel32.NewProc("ContinueDebugEvent")
|
|
procDebugActiveProcess = modKernel32.NewProc("DebugActiveProcess")
|
|
procDebugActiveProcessStop = modKernel32.NewProc("DebugActiveProcessStop")
|
|
procDebugSetProcessKillOnExit = modKernel32.NewProc("DebugSetProcessKillOnExit")
|
|
procGetThreadContext = modKernel32.NewProc("GetThreadContext")
|
|
procSetThreadContext = modKernel32.NewProc("SetThreadContext")
|
|
procFlushInstructionCache = modKernel32.NewProc("FlushInstructionCache")
|
|
)
|
|
|
|
// contextX64 mirrors WinNT.h's x64 CONTEXT struct field-for-field. Verified
|
|
// by reading back a real thread's context and cross-checking Rip/Rsp
|
|
// against a suspended process's known loader-thunk start address (see
|
|
// debugger_windows_test.go) -- the same "don't trust a hand-derived struct
|
|
// layout, prove it against a real target" rule minidump.go's notes already
|
|
// called out, just for a struct the debugger actually *writes*, not only
|
|
// decodes, where getting it wrong would corrupt the debuggee's registers
|
|
// instead of just misreading a file.
|
|
//
|
|
// MSDN's remarks for CONTEXT mention 16-byte alignment in the context of
|
|
// DECLSPEC_ALIGN(16); tested directly against a real suspended process
|
|
// (scratch probe, kept out of the repo) with both a manually-aligned buffer
|
|
// and a plain `&contextX64{}` -- both returned identical, correct Rip/Rsp
|
|
// from a real GetThreadContext call, so the plain allocation is what's used
|
|
// here. If a future Windows build ever proves that wrong, this is the first
|
|
// place to look.
|
|
type contextX64 struct {
|
|
P1Home, P2Home, P3Home, P4Home, P5Home, P6Home uint64
|
|
|
|
ContextFlags uint32
|
|
MxCsr uint32
|
|
|
|
SegCs, SegDs, SegEs, SegFs, SegGs, SegSs uint16
|
|
EFlags uint32
|
|
|
|
Dr0, Dr1, Dr2, Dr3, Dr6, Dr7 uint64
|
|
|
|
Rax, Rcx, Rdx, Rbx, Rsp, Rbp, Rsi, Rdi uint64
|
|
R8, R9, R10, R11, R12, R13, R14, R15 uint64
|
|
Rip uint64
|
|
|
|
FltSave [512]byte // union of XMM_SAVE_AREA32 with the legacy/XMM register view; opaque here, we only need correct byte width
|
|
VectorRegister [416]byte // M128A VectorRegister[26]
|
|
VectorControl uint64
|
|
|
|
DebugControl, LastBranchToRip, LastBranchFromRip, LastExceptionToRip, LastExceptionFromRip uint64
|
|
}
|
|
|
|
func getThreadContext(th windows.Handle, ctx *contextX64) error {
|
|
ctx.ContextFlags = contextFull | contextDebugRegisters | contextSegments
|
|
r, _, err := procGetThreadContext.Call(uintptr(th), uintptr(unsafe.Pointer(ctx)))
|
|
if r == 0 {
|
|
return fmt.Errorf("GetThreadContext: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setThreadContext(th windows.Handle, ctx *contextX64) error {
|
|
r, _, err := procSetThreadContext.Call(uintptr(th), uintptr(unsafe.Pointer(ctx)))
|
|
if r == 0 {
|
|
return fmt.Errorf("SetThreadContext: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Registers is the clean, public register view -- contextX64's FltSave/
|
|
// VectorRegister padding is real but nobody scripting an exploit wants to
|
|
// see it, the same reasoning RawStream exists in minidump.go for parts of a
|
|
// format not worth decoding into a friendly type.
|
|
type Registers struct {
|
|
Rax, Rcx, Rdx, Rbx, Rsp, Rbp, Rsi, Rdi uint64
|
|
R8, R9, R10, R11, R12, R13, R14, R15 uint64
|
|
Rip, EFlags uint64
|
|
}
|
|
|
|
func registersFromContext(ctx *contextX64) Registers {
|
|
return Registers{
|
|
Rax: ctx.Rax, Rcx: ctx.Rcx, Rdx: ctx.Rdx, Rbx: ctx.Rbx,
|
|
Rsp: ctx.Rsp, Rbp: ctx.Rbp, Rsi: ctx.Rsi, Rdi: ctx.Rdi,
|
|
R8: ctx.R8, R9: ctx.R9, R10: ctx.R10, R11: ctx.R11,
|
|
R12: ctx.R12, R13: ctx.R13, R14: ctx.R14, R15: ctx.R15,
|
|
Rip: ctx.Rip, EFlags: uint64(ctx.EFlags),
|
|
}
|
|
}
|
|
|
|
func applyRegistersToContext(r Registers, ctx *contextX64) {
|
|
ctx.Rax, ctx.Rcx, ctx.Rdx, ctx.Rbx = r.Rax, r.Rcx, r.Rdx, r.Rbx
|
|
ctx.Rsp, ctx.Rbp, ctx.Rsi, ctx.Rdi = r.Rsp, r.Rbp, r.Rsi, r.Rdi
|
|
ctx.R8, ctx.R9, ctx.R10, ctx.R11 = r.R8, r.R9, r.R10, r.R11
|
|
ctx.R12, ctx.R13, ctx.R14, ctx.R15 = r.R12, r.R13, r.R14, r.R15
|
|
ctx.Rip = r.Rip
|
|
ctx.EFlags = uint32(r.EFlags)
|
|
}
|
|
|
|
// exceptionRecord mirrors EXCEPTION_RECORD (the pointer-width-dependent
|
|
// version winbase.h's DEBUG_EVENT actually embeds, not EXCEPTION_RECORD64).
|
|
type exceptionRecord struct {
|
|
Code uint32
|
|
Flags uint32
|
|
Record uint64
|
|
Address uint64
|
|
NumParams uint32
|
|
_ uint32
|
|
Information [15]uint64
|
|
}
|
|
|
|
type exceptionDebugInfo struct {
|
|
Record exceptionRecord
|
|
FirstChance uint32
|
|
}
|
|
|
|
type createProcessDebugInfo struct {
|
|
HFile windows.Handle
|
|
HProcess windows.Handle
|
|
HThread windows.Handle
|
|
LpBaseOfImage uint64
|
|
DebugInfoFileOffset uint32
|
|
DebugInfoSize uint32
|
|
ThreadLocalBase uint64
|
|
StartAddress uint64
|
|
ImageName uint64
|
|
Unicode uint16
|
|
}
|
|
|
|
type createThreadDebugInfo struct {
|
|
HThread windows.Handle
|
|
ThreadLocalBase uint64
|
|
StartAddress uint64
|
|
}
|
|
|
|
type exitDebugInfo struct {
|
|
ExitCode uint32
|
|
}
|
|
|
|
type loadDllDebugInfo struct {
|
|
HFile windows.Handle
|
|
LpBaseOfDll uint64
|
|
DebugInfoFileOffset uint32
|
|
DebugInfoSize uint32
|
|
ImageName uint64
|
|
Unicode uint16
|
|
}
|
|
|
|
type outputDebugStringInfo struct {
|
|
LpDebugStringData uint64
|
|
Unicode uint16
|
|
Length uint16
|
|
}
|
|
|
|
// rawDebugEvent is DEBUG_EVENT: a 12-byte header (code/pid/tid) followed by
|
|
// a union of per-event-kind payloads. Rather than reproduce the union as a
|
|
// Go union-of-structs (Go has none), U is sized generously above every real
|
|
// member (the largest, EXCEPTION_DEBUG_INFO, is 160 bytes) and reinterpreted
|
|
// through unsafe.Pointer into the specific struct decodeEvent expects for
|
|
// that Code -- exactly the same "raw bytes, typed view on demand" approach
|
|
// minidump.go uses for stream payloads it doesn't always want to fully decode.
|
|
type rawDebugEvent struct {
|
|
Code uint32
|
|
ProcessID uint32
|
|
ThreadID uint32
|
|
_ uint32
|
|
U [216]byte
|
|
}
|
|
|
|
// DebugEventKind classifies a DebugEvent for a switch in caller code, the
|
|
// winpwn analogue of pwntools' gdb continuing past whatever GDB/MI reports.
|
|
type DebugEventKind int
|
|
|
|
const (
|
|
EventBreakpoint DebugEventKind = iota
|
|
EventSingleStep
|
|
EventException
|
|
EventCreateProcess
|
|
EventCreateThread
|
|
EventExitThread
|
|
EventExitProcess
|
|
EventLoadDll
|
|
EventUnloadDll
|
|
EventOutputDebugString
|
|
EventUnknown
|
|
)
|
|
|
|
func (k DebugEventKind) String() string {
|
|
switch k {
|
|
case EventBreakpoint:
|
|
return "breakpoint"
|
|
case EventSingleStep:
|
|
return "single-step"
|
|
case EventException:
|
|
return "exception"
|
|
case EventCreateProcess:
|
|
return "create-process"
|
|
case EventCreateThread:
|
|
return "create-thread"
|
|
case EventExitThread:
|
|
return "exit-thread"
|
|
case EventExitProcess:
|
|
return "exit-process"
|
|
case EventLoadDll:
|
|
return "load-dll"
|
|
case EventUnloadDll:
|
|
return "unload-dll"
|
|
case EventOutputDebugString:
|
|
return "output-debug-string"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// DebugEvent is one decoded WaitForDebugEvent result, delivered over
|
|
// (*Debugger).Events(). Exactly one of Addr/ExitCode/Message is meaningful,
|
|
// depending on Kind -- see the Kind-specific field comments.
|
|
type DebugEvent struct {
|
|
Kind DebugEventKind
|
|
ThreadID uint32
|
|
|
|
Code uint32 // exception code, for EventException/EventBreakpoint/EventSingleStep
|
|
Addr uintptr // exception/breakpoint address, or DLL base for Load/UnloadDll
|
|
FirstChance bool
|
|
|
|
ExitCode uint32 // for EventExitThread/EventExitProcess
|
|
|
|
Message string // DLL path for EventLoadDll, or the string itself for EventOutputDebugString
|
|
|
|
// status is the dwContinueStatus Continue should use for this event,
|
|
// decided at decode time: DBG_CONTINUE for everything except a genuine
|
|
// (non-breakpoint, non-our-own-single-step) exception, where it's
|
|
// DBG_EXCEPTION_NOT_HANDLED so a real crash actually terminates/reports
|
|
// instead of being fed back to the debuggee forever.
|
|
status uint32
|
|
}
|
|
|
|
type continueRequest struct {
|
|
threadID uint32
|
|
status uint32
|
|
}
|
|
|
|
// Debugger wraps a debuggee under control of the Windows Debug API --
|
|
// DebugActiveProcess/WaitForDebugEvent/ContinueDebugEvent underneath,
|
|
// software breakpoints (INT3 patching) and register/memory access on top.
|
|
// The winpwn analogue of a pwntools gdb.Gdb handle, except there's no GDB
|
|
// process in the loop: this talks to the same kernel debug object Microsoft's
|
|
// own debuggers use.
|
|
//
|
|
// Get one via Attach(pid) for an already-running (or CREATE_SUSPENDED, not
|
|
// yet resumed) process -- compose with SpawnSuspended/ResumeMainThread from
|
|
// procmem_windows.go to debug a target from its very first instruction:
|
|
//
|
|
// tube, pid, _ := winpwn.SpawnSuspended(target)
|
|
// dbg, _ := winpwn.Attach(pid)
|
|
// winpwn.ResumeMainThread(pid)
|
|
// for ev := range dbg.Events() { ... dbg.Continue(ev) }
|
|
//
|
|
// That reuses SpawnSuspended/ResumeMainThread instead of this file
|
|
// reimplementing CreateProcess+pipe plumbing a second time -- Attach is the
|
|
// only entry point on purpose.
|
|
type Debugger struct {
|
|
PID uint32
|
|
process windows.Handle
|
|
|
|
breakpoints map[uintptr]byte
|
|
bpMu sync.Mutex
|
|
|
|
events chan DebugEvent
|
|
resume chan continueRequest
|
|
|
|
closed chan struct{}
|
|
closeOnce sync.Once
|
|
closeErr error
|
|
}
|
|
|
|
// Attach starts debugging an already-existing process (DebugActiveProcess),
|
|
// the entry point for this whole file. The OS ties a debug session to the
|
|
// specific thread that called DebugActiveProcess -- not just the process --
|
|
// so this spawns a dedicated goroutine, pins it to one OS thread for the
|
|
// rest of the session via runtime.LockOSThread (never unlocked: the thread
|
|
// is retired along with the goroutine when the session ends), and runs the
|
|
// entire WaitForDebugEvent/ContinueDebugEvent loop on that one thread.
|
|
// Confirmed empirically while building this: calling WaitForDebugEvent from
|
|
// any other thread after DebugActiveProcess silently never sees events for
|
|
// this process, exactly as the "only the attaching thread" documentation
|
|
// says -- there is no error returned, just a hang, which is why
|
|
// runtime.LockOSThread isn't optional here.
|
|
func Attach(pid uint32) (*Debugger, error) {
|
|
type attachResult struct {
|
|
d *Debugger
|
|
err error
|
|
}
|
|
resultCh := make(chan attachResult, 1)
|
|
|
|
go func() {
|
|
runtime.LockOSThread()
|
|
|
|
r, _, err := procDebugActiveProcess.Call(uintptr(pid))
|
|
if r == 0 {
|
|
resultCh <- attachResult{err: fmt.Errorf("DebugActiveProcess(%d): %w", pid, err)}
|
|
runtime.UnlockOSThread()
|
|
return
|
|
}
|
|
// Don't take the debuggee down with us if this process exits/crashes
|
|
// without a clean Detach -- the default on modern Windows is to kill
|
|
// it, which is surprising for "attach to something already running".
|
|
procDebugSetProcessKillOnExit.Call(0)
|
|
|
|
proc, oerr := windows.OpenProcess(
|
|
windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ|windows.PROCESS_VM_WRITE|windows.PROCESS_VM_OPERATION,
|
|
false, pid)
|
|
if oerr != nil {
|
|
procDebugActiveProcessStop.Call(uintptr(pid))
|
|
resultCh <- attachResult{err: fmt.Errorf("OpenProcess(%d): %w", pid, oerr)}
|
|
runtime.UnlockOSThread()
|
|
return
|
|
}
|
|
|
|
d := &Debugger{
|
|
PID: pid,
|
|
process: proc,
|
|
breakpoints: make(map[uintptr]byte),
|
|
events: make(chan DebugEvent),
|
|
resume: make(chan continueRequest),
|
|
closed: make(chan struct{}),
|
|
}
|
|
resultCh <- attachResult{d: d}
|
|
|
|
d.eventLoop()
|
|
runtime.UnlockOSThread()
|
|
}()
|
|
|
|
r := <-resultCh
|
|
return r.d, r.err
|
|
}
|
|
|
|
// eventLoop runs for the lifetime of the debug session, on the single OS
|
|
// thread Attach locked for it. It decodes each raw event, hands it to
|
|
// Events(), blocks until the caller's Continue(ev) arrives on d.resume, then
|
|
// (for a software breakpoint the caller set) transparently steps past the
|
|
// patched INT3 before actually resuming -- see stepPastBreakpoint.
|
|
func (d *Debugger) eventLoop() {
|
|
defer close(d.events)
|
|
defer windows.CloseHandle(d.process)
|
|
|
|
for {
|
|
var raw rawDebugEvent
|
|
r, _, _ := procWaitForDebugEvent.Call(uintptr(unsafe.Pointer(&raw)), uintptr(windows.INFINITE))
|
|
if r == 0 {
|
|
return
|
|
}
|
|
|
|
ev := d.decodeEvent(&raw)
|
|
|
|
select {
|
|
case d.events <- ev:
|
|
case <-d.closed:
|
|
return
|
|
}
|
|
|
|
var req continueRequest
|
|
select {
|
|
case req = <-d.resume:
|
|
case <-d.closed:
|
|
return
|
|
}
|
|
|
|
if ev.Kind == EventBreakpoint {
|
|
d.bpMu.Lock()
|
|
orig, known := d.breakpoints[ev.Addr]
|
|
d.bpMu.Unlock()
|
|
if known {
|
|
if err := d.stepPastBreakpoint(ev.ThreadID, ev.Addr, orig); err != nil {
|
|
Warn("debugger: stepping past breakpoint at 0x%x: %v", ev.Addr, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
procContinueDebugEvent.Call(uintptr(d.PID), uintptr(ev.ThreadID), uintptr(req.status))
|
|
|
|
if ev.Kind == EventExitProcess {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// stepPastBreakpoint restores the original byte, rewinds Rip back over the
|
|
// INT3 (the CPU already advanced it past the 1-byte instruction by the time
|
|
// the exception is delivered -- this is the standard, easy-to-forget
|
|
// software-breakpoint bookkeeping step), single-steps exactly one
|
|
// instruction via the trap flag, then re-arms the 0xCC so the breakpoint
|
|
// persists for the next hit. The single-step it generates is consumed here
|
|
// directly (a second WaitForDebugEvent/ContinueDebugEvent round trip on this
|
|
// same locked thread) and never reaches the public Events() channel -- the
|
|
// caller asked to Continue, not to Step, so this is an implementation detail
|
|
// of "resume past a breakpoint", not an event of its own.
|
|
func (d *Debugger) stepPastBreakpoint(tid uint32, addr uintptr, orig byte) error {
|
|
if err := d.WriteMemory(addr, []byte{orig}); err != nil {
|
|
return fmt.Errorf("restore original byte: %w", err)
|
|
}
|
|
|
|
th, err := windows.OpenThread(threadAccessForDebug, false, tid)
|
|
if err != nil {
|
|
return fmt.Errorf("OpenThread(%d): %w", tid, err)
|
|
}
|
|
defer windows.CloseHandle(th)
|
|
|
|
ctx := &contextX64{}
|
|
if err := getThreadContext(th, ctx); err != nil {
|
|
return err
|
|
}
|
|
ctx.EFlags |= eflagsTrapFlag
|
|
if err := setThreadContext(th, ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
procContinueDebugEvent.Call(uintptr(d.PID), uintptr(tid), uintptr(dbgContinue))
|
|
|
|
for {
|
|
var raw rawDebugEvent
|
|
r, _, _ := procWaitForDebugEvent.Call(uintptr(unsafe.Pointer(&raw)), uintptr(windows.INFINITE))
|
|
if r == 0 {
|
|
return fmt.Errorf("WaitForDebugEvent failed while stepping past breakpoint at 0x%x", addr)
|
|
}
|
|
if raw.Code == debugExceptionEvent && raw.ThreadID == tid {
|
|
info := (*exceptionDebugInfo)(unsafe.Pointer(&raw.U[0]))
|
|
if info.Record.Code == exceptionSingleStep {
|
|
break
|
|
}
|
|
}
|
|
// Something else fired on another thread mid-step (a second thread
|
|
// hitting its own breakpoint, say) -- let it run, we only care about
|
|
// regaining control of tid.
|
|
procContinueDebugEvent.Call(uintptr(d.PID), uintptr(raw.ThreadID), uintptr(dbgExceptionNotHandled))
|
|
}
|
|
|
|
return d.WriteMemory(addr, []byte{0xCC})
|
|
}
|
|
|
|
func (d *Debugger) decodeEvent(raw *rawDebugEvent) DebugEvent {
|
|
ev := DebugEvent{ThreadID: raw.ThreadID, status: dbgContinue}
|
|
|
|
switch raw.Code {
|
|
case debugExceptionEvent:
|
|
info := (*exceptionDebugInfo)(unsafe.Pointer(&raw.U[0]))
|
|
ev.Code = info.Record.Code
|
|
ev.Addr = uintptr(info.Record.Address)
|
|
ev.FirstChance = info.FirstChance != 0
|
|
|
|
d.bpMu.Lock()
|
|
_, isOurs := d.breakpoints[ev.Addr]
|
|
d.bpMu.Unlock()
|
|
|
|
switch {
|
|
case info.Record.Code == exceptionBreakpoint && isOurs:
|
|
ev.Kind = EventBreakpoint
|
|
// Make the paused thread's own Rip already read as the
|
|
// breakpoint address (not address+1, where the CPU left it
|
|
// after executing the INT3) so a caller's GetContext during
|
|
// this event sees what a human would expect at a breakpoint --
|
|
// the same fixup every real debugger applies before showing you
|
|
// anything.
|
|
if th, err := windows.OpenThread(threadAccessForDebug, false, raw.ThreadID); err == nil {
|
|
ctx := &contextX64{}
|
|
if getThreadContext(th, ctx) == nil {
|
|
ctx.Rip--
|
|
setThreadContext(th, ctx)
|
|
}
|
|
windows.CloseHandle(th)
|
|
}
|
|
case info.Record.Code == exceptionBreakpoint:
|
|
// Not one of ours -- almost always the loader breakpoint ntdll
|
|
// raises once initialization finishes (the same stop every
|
|
// WinDbg/x64dbg session opens on), occasionally a genuine int3
|
|
// already in the target. Reported as a plain exception since
|
|
// there's no INT3 *we* patched in to account for.
|
|
ev.Kind = EventException
|
|
case info.Record.Code == exceptionSingleStep:
|
|
ev.Kind = EventSingleStep
|
|
default:
|
|
ev.Kind = EventException
|
|
// Most callers want to inspect and decide for themselves, but
|
|
// the safe default if they just Continue() without handling it
|
|
// is to let the OS's normal second-chance/crash path run
|
|
// instead of looping the same fault back into the debuggee
|
|
// forever -- true whether it's first-chance or not.
|
|
ev.status = dbgExceptionNotHandled
|
|
}
|
|
|
|
case debugCreateProcessEvent:
|
|
info := (*createProcessDebugInfo)(unsafe.Pointer(&raw.U[0]))
|
|
ev.Kind = EventCreateProcess
|
|
ev.Addr = uintptr(info.LpBaseOfImage)
|
|
|
|
case debugCreateThreadEvent:
|
|
ev.Kind = EventCreateThread
|
|
|
|
case debugExitThreadEvent:
|
|
info := (*exitDebugInfo)(unsafe.Pointer(&raw.U[0]))
|
|
ev.Kind = EventExitThread
|
|
ev.ExitCode = info.ExitCode
|
|
|
|
case debugExitProcessEvent:
|
|
info := (*exitDebugInfo)(unsafe.Pointer(&raw.U[0]))
|
|
ev.Kind = EventExitProcess
|
|
ev.ExitCode = info.ExitCode
|
|
|
|
case debugLoadDllEvent:
|
|
info := (*loadDllDebugInfo)(unsafe.Pointer(&raw.U[0]))
|
|
ev.Kind = EventLoadDll
|
|
ev.Addr = uintptr(info.LpBaseOfDll)
|
|
ev.Message = d.readDllPath(info.ImageName, info.Unicode != 0)
|
|
|
|
case debugUnloadDllEvent:
|
|
info := (*struct{ LpBaseOfDll uint64 })(unsafe.Pointer(&raw.U[0]))
|
|
ev.Kind = EventUnloadDll
|
|
ev.Addr = uintptr(info.LpBaseOfDll)
|
|
|
|
case debugOutputStringEvent:
|
|
info := (*outputDebugStringInfo)(unsafe.Pointer(&raw.U[0]))
|
|
ev.Kind = EventOutputDebugString
|
|
ev.Message = d.readDebugString(info)
|
|
|
|
default:
|
|
ev.Kind = EventUnknown
|
|
}
|
|
|
|
return ev
|
|
}
|
|
|
|
// readDllPath best-effort reads the LOAD_DLL_DEBUG_INFO.lpImageName pointer.
|
|
// It's deliberately tolerant of failure: lpImageName is documented as
|
|
// sometimes null or pointing at a pointer-to-a-pointer depending on the
|
|
// loader's mood, so a miss here just means an empty Message, not an error
|
|
// that would derail the whole event.
|
|
func (d *Debugger) readDllPath(addr uint64, unicode bool) string {
|
|
if addr == 0 {
|
|
return ""
|
|
}
|
|
var ptrBuf [8]byte
|
|
if _, err := d.ReadMemory(uintptr(addr), ptrBuf[:]); err != nil {
|
|
return ""
|
|
}
|
|
strAddr := *(*uint64)(unsafe.Pointer(&ptrBuf[0]))
|
|
if strAddr == 0 {
|
|
return ""
|
|
}
|
|
buf := make([]byte, 512)
|
|
n, _ := d.ReadMemory(uintptr(strAddr), buf)
|
|
buf = buf[:n]
|
|
if unicode {
|
|
u16 := make([]uint16, len(buf)/2)
|
|
for i := range u16 {
|
|
u16[i] = uint16(buf[2*i]) | uint16(buf[2*i+1])<<8
|
|
}
|
|
return windows.UTF16ToString(u16)
|
|
}
|
|
if idx := indexByte(buf, 0); idx >= 0 {
|
|
buf = buf[:idx]
|
|
}
|
|
return string(buf)
|
|
}
|
|
|
|
func (d *Debugger) readDebugString(info *outputDebugStringInfo) string {
|
|
if info.LpDebugStringData == 0 || info.Length == 0 {
|
|
return ""
|
|
}
|
|
buf := make([]byte, info.Length)
|
|
n, err := d.ReadMemory(uintptr(info.LpDebugStringData), buf)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
buf = buf[:n]
|
|
if info.Unicode != 0 {
|
|
u16 := make([]uint16, len(buf)/2)
|
|
for i := range u16 {
|
|
u16[i] = uint16(buf[2*i]) | uint16(buf[2*i+1])<<8
|
|
}
|
|
return windows.UTF16ToString(u16)
|
|
}
|
|
if idx := indexByte(buf, 0); idx >= 0 {
|
|
buf = buf[:idx]
|
|
}
|
|
return string(buf)
|
|
}
|
|
|
|
func indexByte(b []byte, c byte) int {
|
|
for i, v := range b {
|
|
if v == c {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// Events returns the channel of decoded debug events. Closed when the
|
|
// debuggee exits or Close/Detach is called -- range over it the same way
|
|
// you'd loop on pwntools' gdb continuing past each stop.
|
|
func (d *Debugger) Events() <-chan DebugEvent {
|
|
return d.events
|
|
}
|
|
|
|
// Continue resumes the debuggee past ev, the consumer-side counterpart of a
|
|
// value received from Events(). It picks DBG_CONTINUE vs
|
|
// DBG_EXCEPTION_NOT_HANDLED automatically (see DebugEvent.status's doc
|
|
// comment) and, if ev was a hit on a breakpoint this Debugger set, performs
|
|
// the restore-byte/rewind-Rip/single-step/re-arm dance transparently first.
|
|
func (d *Debugger) Continue(ev DebugEvent) error {
|
|
select {
|
|
case d.resume <- continueRequest{threadID: ev.ThreadID, status: ev.status}:
|
|
return nil
|
|
case <-d.closed:
|
|
return fmt.Errorf("debugger: session closed")
|
|
}
|
|
}
|
|
|
|
// Step single-steps thread tid by setting the trap flag and resuming for
|
|
// exactly one instruction; the resulting EventSingleStep is delivered
|
|
// through the normal Events() channel like any other event.
|
|
//
|
|
// Documented gap, not a bug: calling Step in response to an EventBreakpoint
|
|
// behaves like Continue, not like a single step, because resuming past a
|
|
// software breakpoint already requires its own internal single-step (see
|
|
// stepPastBreakpoint) before real execution can continue -- there's no way
|
|
// to stop *exactly* at "one instruction past a breakpoint" without that
|
|
// step happening anyway. If you need single-step granularity starting from
|
|
// a breakpoint, Continue past it once, then Step from wherever you land.
|
|
func (d *Debugger) Step(tid uint32) error {
|
|
th, err := windows.OpenThread(threadAccessForDebug, false, tid)
|
|
if err != nil {
|
|
return fmt.Errorf("OpenThread(%d): %w", tid, err)
|
|
}
|
|
defer windows.CloseHandle(th)
|
|
|
|
ctx := &contextX64{}
|
|
if err := getThreadContext(th, ctx); err != nil {
|
|
return err
|
|
}
|
|
ctx.EFlags |= eflagsTrapFlag
|
|
if err := setThreadContext(th, ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
select {
|
|
case d.resume <- continueRequest{threadID: tid, status: dbgContinue}:
|
|
return nil
|
|
case <-d.closed:
|
|
return fmt.Errorf("debugger: session closed")
|
|
}
|
|
}
|
|
|
|
// SetBreakpoint patches a software breakpoint (INT3) at addr, saving the
|
|
// original byte so Continue/RemoveBreakpoint can restore it. addr is an
|
|
// absolute virtual address in the debuggee -- typically ImageBase +
|
|
// some RVA resolved from the target's own PEFile.
|
|
func (d *Debugger) SetBreakpoint(addr uintptr) error {
|
|
var orig [1]byte
|
|
if _, err := d.ReadMemory(addr, orig[:]); err != nil {
|
|
return fmt.Errorf("read original byte at 0x%x: %w", addr, err)
|
|
}
|
|
if err := d.WriteMemory(addr, []byte{0xCC}); err != nil {
|
|
return fmt.Errorf("write breakpoint at 0x%x: %w", addr, err)
|
|
}
|
|
d.bpMu.Lock()
|
|
d.breakpoints[addr] = orig[0]
|
|
d.bpMu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// RemoveBreakpoint restores the original byte at addr. Safe to call on an
|
|
// address that isn't currently the thread's Rip -- only Continue's
|
|
// breakpoint-resume path needs the single-step dance; removing one that
|
|
// isn't being actively resumed through is a plain memory write.
|
|
func (d *Debugger) RemoveBreakpoint(addr uintptr) error {
|
|
d.bpMu.Lock()
|
|
orig, ok := d.breakpoints[addr]
|
|
if ok {
|
|
delete(d.breakpoints, addr)
|
|
}
|
|
d.bpMu.Unlock()
|
|
if !ok {
|
|
return fmt.Errorf("no breakpoint set at 0x%x", addr)
|
|
}
|
|
return d.WriteMemory(addr, []byte{orig})
|
|
}
|
|
|
|
// GetContext reads tid's general-purpose registers + Rip/EFlags.
|
|
func (d *Debugger) GetContext(tid uint32) (Registers, error) {
|
|
th, err := windows.OpenThread(threadAccessForDebug, false, tid)
|
|
if err != nil {
|
|
return Registers{}, fmt.Errorf("OpenThread(%d): %w", tid, err)
|
|
}
|
|
defer windows.CloseHandle(th)
|
|
|
|
ctx := &contextX64{}
|
|
if err := getThreadContext(th, ctx); err != nil {
|
|
return Registers{}, err
|
|
}
|
|
return registersFromContext(ctx), nil
|
|
}
|
|
|
|
// SetContext writes tid's general-purpose registers + Rip/EFlags, e.g. to
|
|
// redirect execution (set Rip to a ROP gadget / shellcode address) once you
|
|
// have IP control and want to drive it from the debugger rather than
|
|
// letting a corrupted return address do it.
|
|
func (d *Debugger) SetContext(tid uint32, regs Registers) error {
|
|
th, err := windows.OpenThread(threadAccessForDebug, false, tid)
|
|
if err != nil {
|
|
return fmt.Errorf("OpenThread(%d): %w", tid, err)
|
|
}
|
|
defer windows.CloseHandle(th)
|
|
|
|
ctx := &contextX64{}
|
|
if err := getThreadContext(th, ctx); err != nil {
|
|
return err
|
|
}
|
|
applyRegistersToContext(regs, ctx)
|
|
return setThreadContext(th, ctx)
|
|
}
|
|
|
|
// ReadMemory/WriteMemory read and write the debuggee's address space
|
|
// directly, the same ReadProcessMemory/WriteProcessMemory primitive
|
|
// ProcessMemory wraps in procmem_windows.go -- duplicated here rather than
|
|
// embedding a *ProcessMemory because Debugger already owns the process
|
|
// handle's lifetime (closed by eventLoop on exit) and WriteMemory needs the
|
|
// extra FlushInstructionCache call SetBreakpoint relies on, which a plain
|
|
// WriterAt has no hook for.
|
|
func (d *Debugger) ReadMemory(addr uintptr, buf []byte) (int, error) {
|
|
if len(buf) == 0 {
|
|
return 0, nil
|
|
}
|
|
var n uintptr
|
|
err := windows.ReadProcessMemory(d.process, addr, &buf[0], uintptr(len(buf)), &n)
|
|
return int(n), err
|
|
}
|
|
|
|
func (d *Debugger) WriteMemory(addr uintptr, data []byte) error {
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
var n uintptr
|
|
if err := windows.WriteProcessMemory(d.process, addr, &data[0], uintptr(len(data)), &n); err != nil {
|
|
return err
|
|
}
|
|
// Required for code patches per Microsoft's own documentation for
|
|
// WriteProcessMemory: "the function does not flush the instruction
|
|
// cache... If you need that, call FlushInstructionCache". x86/x64 has a
|
|
// coherent icache in practice, but WOW64/exotic configurations are
|
|
// exactly the case that documentation note exists for -- cheap to call
|
|
// unconditionally rather than rediscover the one config where it matters.
|
|
procFlushInstructionCache.Call(uintptr(d.process), addr, uintptr(len(data)))
|
|
return nil
|
|
}
|
|
|
|
// Close detaches the debugger (DebugActiveProcessStop) without killing the
|
|
// debuggee -- idempotent via sync.Once, matching Tube.Close/pipeConn.Close
|
|
// elsewhere in this package.
|
|
func (d *Debugger) Close() error {
|
|
d.closeOnce.Do(func() {
|
|
close(d.closed)
|
|
r, _, err := procDebugActiveProcessStop.Call(uintptr(d.PID))
|
|
if r == 0 {
|
|
d.closeErr = fmt.Errorf("DebugActiveProcessStop(%d): %w", d.PID, err)
|
|
}
|
|
})
|
|
return d.closeErr
|
|
}
|