164 lines
4.9 KiB
Go
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")
|
|
}
|
|
}
|