v0.1 - initial commit

This commit is contained in:
2026-07-18 21:37:15 +03:00
commit 9b89f4cb8e
153 changed files with 22887 additions and 0 deletions
+833
View File
@@ -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
}