//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 }