Files
go_pwner/gadgets_test.go
T
2026-07-18 21:37:15 +03:00

164 lines
4.9 KiB
Go

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