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