109 lines
3.0 KiB
Go
109 lines
3.0 KiB
Go
//go:build windows
|
|
|
|
package winpwn
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestDebuggerBreakpointAtEntry composes SpawnSuspended + Attach +
|
|
// ResumeMainThread (the pattern documented on Debugger) to break at the
|
|
// real entry point of the checked-in fixture binary, confirming against a
|
|
// live process that: the loader/CreateProcess/LoadDll/CreateThread events
|
|
// decode without error, ResolveModuleBase eventually succeeds once the
|
|
// loader has run, SetBreakpoint+the breakpoint-hit path leaves the
|
|
// thread's Rip exactly equal to the breakpoint address (the INT3
|
|
// rewind-by-one fixup), and Continue can step back past a still-armed
|
|
// breakpoint without hanging.
|
|
//
|
|
// This exercises the exact sequence validated manually against a live
|
|
// process while building debugger_windows.go (see ROADMAP.md's Phase 4
|
|
// notes) -- a hermetic version of that same proof, run by `go test`.
|
|
func TestDebuggerBreakpointAtEntry(t *testing.T) {
|
|
requireFixturePE(t)
|
|
|
|
diskPE, err := OpenPE(testFixturePE)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
diskBase, err := diskPE.ImageBase()
|
|
if err != nil {
|
|
diskPE.Close()
|
|
t.Fatal(err)
|
|
}
|
|
diskEntry, err := diskPE.EntryPoint()
|
|
if err != nil {
|
|
diskPE.Close()
|
|
t.Fatal(err)
|
|
}
|
|
diskPE.Close()
|
|
entryRVA := diskEntry - diskBase
|
|
|
|
tube, pid, err := SpawnSuspended(testFixturePE)
|
|
if err != nil {
|
|
t.Fatalf("SpawnSuspended: %v", err)
|
|
}
|
|
defer tube.Close()
|
|
|
|
dbg, err := Attach(pid)
|
|
if err != nil {
|
|
t.Fatalf("Attach: %v", err)
|
|
}
|
|
defer dbg.Close()
|
|
|
|
if err := ResumeMainThread(pid); err != nil {
|
|
t.Fatalf("ResumeMainThread: %v", err)
|
|
}
|
|
|
|
var bpAddr uintptr
|
|
var entrySet, hit bool
|
|
|
|
deadline := time.After(10 * time.Second)
|
|
for !hit {
|
|
select {
|
|
case ev, ok := <-dbg.Events():
|
|
if !ok {
|
|
t.Fatal("debugger event channel closed before hitting the entry breakpoint")
|
|
}
|
|
|
|
if !entrySet {
|
|
if base, rerr := ResolveModuleBase(pid, "bof_win.c.exe"); rerr == nil {
|
|
bpAddr = uintptr(base) + uintptr(entryRVA)
|
|
if err := dbg.SetBreakpoint(bpAddr); err != nil {
|
|
t.Fatalf("SetBreakpoint(0x%x): %v", bpAddr, err)
|
|
}
|
|
entrySet = true
|
|
}
|
|
// Not yet resolvable (loader hasn't populated PEB.Ldr yet) --
|
|
// expected on the first couple of events, see
|
|
// ResolveModuleBase's doc comment in procmem_windows.go.
|
|
}
|
|
|
|
if ev.Kind == EventBreakpoint {
|
|
regs, gerr := dbg.GetContext(ev.ThreadID)
|
|
if gerr != nil {
|
|
t.Fatalf("GetContext(%d): %v", ev.ThreadID, gerr)
|
|
}
|
|
if regs.Rip != uint64(ev.Addr) {
|
|
t.Errorf("Rip = 0x%x, want 0x%x (breakpoint address, after the INT3 rewind)", regs.Rip, ev.Addr)
|
|
}
|
|
if ev.Addr != bpAddr {
|
|
t.Errorf("breakpoint fired at 0x%x, want entry point 0x%x", ev.Addr, bpAddr)
|
|
}
|
|
hit = true
|
|
}
|
|
|
|
// Continue every event, including the still-armed breakpoint --
|
|
// proves Continue's internal restore/rewind/single-step/re-arm
|
|
// dance (stepPastBreakpoint) completes without hanging.
|
|
if err := dbg.Continue(ev); err != nil {
|
|
t.Fatalf("Continue: %v", err)
|
|
}
|
|
|
|
case <-deadline:
|
|
t.Fatal("timed out waiting for the entry breakpoint to fire")
|
|
}
|
|
}
|
|
}
|