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