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