package winpwn import ( "os" "testing" ) // testFixturePE is a real PE32+ checked into the repo (examples/bof_basic), // used by the ROP-level tests below to validate parsing against an actual // binary rather than only hand-built Gadget slices. const testFixturePE = "examples/bof_basic/bof_win.c.exe" func requireFixturePE(t *testing.T) { if _, err := os.Stat(testFixturePE); err != nil { t.Skipf("fixture %s not present: %v", testFixturePE, err) } } // requireRPWinTool skips a test if rp-win.exe isn't resolvable -- NewROP // shells out to it, so integration tests that build a real ROP need it // installed the same way a real target binary needs to exist. func requireRPWinTool(t *testing.T) { toolPath := os.Getenv("RP_WIN_EXE") if toolPath == "" { toolPath = defaultRPWinTool } if _, err := os.Stat(toolPath); err != nil { t.Skipf("rp-win.exe not present at %s: %v", toolPath, err) } } // The Find/SearchRegex tests below construct a *ROP directly from a // hand-built gadgets slice, bypassing NewROP entirely -- these two methods // only ever read r.gadgets, so this exercises the actual filtering/sorting // logic without needing rp-win.exe or a real PE on the test machine. func TestROPFindSortsByAddressAscendingAsTiebreak(t *testing.T) { r := &ROP{gadgets: []Gadget{ {Address: 0x3000, Instructions: "pop rcx ; ret"}, {Address: 0x1000, Instructions: "pop rcx ; ret"}, {Address: 0x2000, Instructions: "pop rcx ; ret"}, }} found := r.Find("pop rcx ; ret") if len(found) != 3 { t.Fatalf("expected 3 matches, got %d", len(found)) } for i := 1; i < len(found); i++ { if found[i-1].Address > found[i].Address { t.Fatalf("results not sorted ascending among equal-quality matches: %v", found) } } if found[0].Address != 0x1000 { t.Errorf("Find(...)[0] should be the lowest address among ties, got 0x%X", found[0].Address) } } func TestROPFindPrefersExactMatchOverDirtySubstring(t *testing.T) { // Regression test for a real bug caught against kernel32.dll: sorting // purely by address let a "dirty" longer gadget win [0] over the clean // one just because it happened to start a few bytes earlier in memory // (0x1800198B7 < 0x1800198BB numerically, even though only the latter // is a bare "pop rcx ; ret" with no side effects). r := &ROP{gadgets: []Gadget{ {Address: 0x1800198B7, Instructions: "ror byte [rax-0x1], 0x15 ; pop rcx ; ret"}, {Address: 0x1800198BB, Instructions: "pop rcx ; ret"}, }} found := r.Find("pop rcx ; ret") if len(found) != 2 { t.Fatalf("expected 2 matches, got %d", len(found)) } if found[0].Address != 0x1800198BB || found[0].Instructions != "pop rcx ; ret" { t.Errorf("Find(...)[0] should be the clean exact-match gadget, got %+v", found[0]) } } func TestROPFindSubstringMatchIsCaseInsensitive(t *testing.T) { r := &ROP{gadgets: []Gadget{ {Address: 0x1000, Instructions: "POP RCX ; RET"}, {Address: 0x2000, Instructions: "pop rdx ; ret"}, }} found := r.Find("pop rcx ; ret") if len(found) != 1 || found[0].Address != 0x1000 { t.Errorf("expected a case-insensitive match on 0x1000, got %v", found) } } func TestROPFindReturnsEmptyWhenNoMatch(t *testing.T) { r := &ROP{gadgets: []Gadget{{Address: 0x1000, Instructions: "pop rcx ; ret"}}} found := r.Find("pop rbp ; ret") if len(found) != 0 { t.Errorf("expected no matches, got %v", found) } } func TestROPFindEmptyIndexPanics(t *testing.T) { // Documented behavior: indexing an empty Find() result panics rather // than silently handing back a zero-value Gadget -- an exploit script // should fail loudly at the gadget lookup, not against a garbage // address three chain-steps later. defer func() { if recover() == nil { t.Error("expected indexing an empty Find() result to panic") } }() r := &ROP{gadgets: nil} _ = r.Find("nonexistent")[0] } func TestROPSearchRegexOnHandBuiltGadgets(t *testing.T) { r := &ROP{gadgets: []Gadget{ {Address: 0x1000, Instructions: "pop rcx ; ret"}, {Address: 0x2000, Instructions: "pop rdx ; ret"}, {Address: 0x3000, Instructions: "mov [rcx], eax ; ret"}, }} gadgets, err := r.SearchRegex(`^pop r\w+ ; ret$`) if err != nil { t.Fatal(err) } if len(gadgets) != 2 { t.Errorf("expected 2 pop-reg-then-ret gadgets, got %d: %v", len(gadgets), gadgets) } } func TestROPFindsRetGadget(t *testing.T) { requireFixturePE(t) requireRPWinTool(t) rop, err := NewROP(testFixturePE) if err != nil { t.Fatal(err) } defer rop.Close() gadgets, err := rop.Search("ret") if err != nil { t.Fatal(err) } if len(gadgets) == 0 { t.Error("expected at least one 'ret' gadget in a real x64 binary") } } func TestROPSearchRegex(t *testing.T) { requireFixturePE(t) requireRPWinTool(t) rop, err := NewROP(testFixturePE) if err != nil { t.Fatal(err) } defer rop.Close() gadgets, err := rop.SearchRegex(`^pop r\w+ ; ret$`) if err != nil { t.Fatal(err) } if len(gadgets) == 0 { t.Error("expected at least one pop-reg-then-ret gadget") } }