v0.1 - initial commit
This commit is contained in:
@@ -0,0 +1,833 @@
|
||||
//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
|
||||
}
|
||||
Reference in New Issue
Block a user