mirror of
https://github.com/mirkobrombin/raven.git
synced 2026-10-30 21:35:06 +00:00
427 lines
13 KiB
Go
427 lines
13 KiB
Go
//go:build guest_windows
|
|
|
|
package ntapi
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/mirkobrombin/Raven/pkg/mmu"
|
|
"github.com/mirkobrombin/Raven/pkg/x64"
|
|
)
|
|
|
|
func TestUserCallbackManagerPE32(t *testing.T) {
|
|
mem, cpu := newUserCallbackTestCPU(t, true, 0x500000)
|
|
manager := new(UserCallbackManager)
|
|
before := cpu.Snapshot()
|
|
|
|
frame, err := manager.Begin(cpu, mem, 0x401000, 3, []byte{1, 2, 3})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cpu.GetPC() != 0x401000 || cpu.GetSP() != frame.StackPointer {
|
|
t.Fatalf("callback control state PC=0x%x SP=0x%x, want PC=0x401000 SP=0x%x", cpu.GetPC(), cpu.GetSP(), frame.StackPointer)
|
|
}
|
|
if manager.Depth(cpu) != 1 {
|
|
t.Fatalf("callback depth = %d, want 1", manager.Depth(cpu))
|
|
}
|
|
|
|
mutateUserCallbackCPU(cpu, 0x1111)
|
|
got, err := manager.NtCallbackReturn(cpu, 0xAAAAAAAA12345678, 0xBBBBBBBB87654321, 0xCCCCCCCC80000005)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := UserCallbackReturn{Address: 0x12345678, Length: 0x87654321, Status: 0x80000005}
|
|
if got != want {
|
|
t.Fatalf("callback return = %+v, want %+v", got, want)
|
|
}
|
|
if restored := cpu.Snapshot(); restored != before {
|
|
t.Fatalf("PE32 CPU snapshot was not restored")
|
|
}
|
|
if manager.Depth(cpu) != 0 {
|
|
t.Fatalf("callback depth = %d, want 0", manager.Depth(cpu))
|
|
}
|
|
}
|
|
|
|
func TestUserCallbackManagerPE64(t *testing.T) {
|
|
mem, cpu := newUserCallbackTestCPU(t, false, 0x600000)
|
|
manager := new(UserCallbackManager)
|
|
before := cpu.Snapshot()
|
|
returnPC := cpu.GetPC()
|
|
|
|
frame, err := manager.Begin(cpu, mem, 0x7FF612340000, 9, []byte{4, 5, 6, 7})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertRead64(t, mem, frame.StackPointer+userCallbackFrame64RipOffset, returnPC)
|
|
|
|
mutateUserCallbackCPU(cpu, 0x2222)
|
|
got, err := manager.NtCallbackReturn(cpu, 0x123456789ABCDEF0, 0xAAAAAAAA11223344, 0xBBBBBBBBC000000D)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := UserCallbackReturn{Address: 0x123456789ABCDEF0, Length: 0x11223344, Status: 0xC000000D}
|
|
if got != want {
|
|
t.Fatalf("callback return = %+v, want %+v", got, want)
|
|
}
|
|
if restored := cpu.Snapshot(); restored != before {
|
|
t.Fatalf("PE64 CPU snapshot was not restored")
|
|
}
|
|
}
|
|
|
|
func TestUserCallbackManagerNestedLIFO(t *testing.T) {
|
|
for _, mode32 := range []bool{false, true} {
|
|
t.Run(fmt.Sprintf("mode32=%t", mode32), func(t *testing.T) {
|
|
mem, cpu := newUserCallbackTestCPU(t, mode32, 0x700000)
|
|
manager := new(UserCallbackManager)
|
|
original := cpu.Snapshot()
|
|
|
|
outerFrame, err := manager.Begin(cpu, mem, 0x710000, 1, []byte{1})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mutateUserCallbackCPU(cpu, 0x3333)
|
|
cpu.Mode32 = mode32
|
|
cpu.SetSP(outerFrame.StackPointer - 0x100)
|
|
outer := cpu.Snapshot()
|
|
if _, err := manager.Begin(cpu, mem, 0x720000, 2, []byte{2}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mutateUserCallbackCPU(cpu, 0x4444)
|
|
|
|
innerReturn, err := manager.NtCallbackReturn(cpu, 0xAAAAAAAA00001111, 0xBBBBBBBB00000002, uint64(StatusSuccess))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantInnerAddress := uint64(0xAAAAAAAA00001111)
|
|
if mode32 {
|
|
wantInnerAddress = 0x1111
|
|
}
|
|
if innerReturn != (UserCallbackReturn{Address: wantInnerAddress, Length: 2, Status: StatusSuccess}) {
|
|
t.Fatalf("inner callback return = %+v", innerReturn)
|
|
}
|
|
if restored := cpu.Snapshot(); restored != outer {
|
|
t.Fatalf("inner return did not restore the outer callback state")
|
|
}
|
|
if manager.Depth(cpu) != 1 {
|
|
t.Fatalf("callback depth after inner return = %d, want 1", manager.Depth(cpu))
|
|
}
|
|
|
|
outerReturn, err := manager.NtCallbackReturn(cpu, 0xCCCCCCCC00002222, 0xDDDDDDDD00000004, uint64(StatusBufferOverflow))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantOuterAddress := uint64(0xCCCCCCCC00002222)
|
|
if mode32 {
|
|
wantOuterAddress = 0x2222
|
|
}
|
|
if outerReturn != (UserCallbackReturn{Address: wantOuterAddress, Length: 4, Status: StatusBufferOverflow}) {
|
|
t.Fatalf("outer callback return = %+v", outerReturn)
|
|
}
|
|
if restored := cpu.Snapshot(); restored != original {
|
|
t.Fatalf("outer return did not restore the original CPU state")
|
|
}
|
|
|
|
first, ok := manager.TakeReturn(cpu)
|
|
if !ok || first != innerReturn {
|
|
t.Fatalf("first queued return = %+v, %t; want inner return", first, ok)
|
|
}
|
|
second, ok := manager.TakeReturn(cpu)
|
|
if !ok || second != outerReturn {
|
|
t.Fatalf("second queued return = %+v, %t; want outer return", second, ok)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserCallbackManagerRejectsWrongCPUAndEmptyStack(t *testing.T) {
|
|
memA, cpuA := newUserCallbackTestCPU(t, false, 0x800000)
|
|
_, cpuB := newUserCallbackTestCPU(t, false, 0x900000)
|
|
manager := new(UserCallbackManager)
|
|
|
|
if _, err := manager.NtCallbackReturn(cpuA, 0, 0, 0); !errors.Is(err, ErrNoActiveUserCallback) {
|
|
t.Fatalf("empty return error = %v, want %v", err, ErrNoActiveUserCallback)
|
|
}
|
|
if _, err := manager.Begin(cpuA, memA, 0x810000, 1, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := manager.NtCallbackReturn(cpuB, 0, 0, 0); !errors.Is(err, ErrUserCallbackWrongCPU) {
|
|
t.Fatalf("wrong CPU error = %v, want %v", err, ErrUserCallbackWrongCPU)
|
|
}
|
|
if manager.Depth(cpuA) != 1 {
|
|
t.Fatalf("wrong CPU return consumed callback, depth = %d", manager.Depth(cpuA))
|
|
}
|
|
}
|
|
|
|
func TestUserCallbackManagerBeginFailureIsAtomic(t *testing.T) {
|
|
_, cpu := newUserCallbackTestCPU(t, false, 0xA00000)
|
|
manager := new(UserCallbackManager)
|
|
before := cpu.Snapshot()
|
|
|
|
if _, err := manager.Begin(cpu, mmu.New(), 0xA10000, 1, []byte{1}); err == nil {
|
|
t.Fatal("Begin succeeded with an unmapped callback stack")
|
|
}
|
|
if got := cpu.Snapshot(); got != before {
|
|
t.Fatal("failed Begin changed CPU state")
|
|
}
|
|
if manager.Depth(cpu) != 0 {
|
|
t.Fatalf("failed Begin left callback depth %d", manager.Depth(cpu))
|
|
}
|
|
}
|
|
|
|
func TestUserCallbackManagerConcurrentCPUs(t *testing.T) {
|
|
manager := new(UserCallbackManager)
|
|
const workers = 16
|
|
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, workers)
|
|
for i := 0; i < workers; i++ {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
base := uint64(0x1000000 + i*0x10000)
|
|
mem, cpu := newUserCallbackTestCPU(t, i%2 == 0, base)
|
|
before := cpu.Snapshot()
|
|
if _, err := manager.Begin(cpu, mem, base+0x8000, uint32(i), []byte{byte(i)}); err != nil {
|
|
errs <- err
|
|
return
|
|
}
|
|
got, err := manager.NtCallbackReturn(cpu, uint64(i), uint64(i+1), uint64(StatusSuccess))
|
|
if err != nil {
|
|
errs <- err
|
|
return
|
|
}
|
|
wantAddress := uint64(i)
|
|
if cpu.Mode32 {
|
|
wantAddress = uint64(uint32(i))
|
|
}
|
|
if got != (UserCallbackReturn{Address: wantAddress, Length: uint32(i + 1), Status: StatusSuccess}) {
|
|
errs <- fmt.Errorf("worker %d return = %+v", i, got)
|
|
return
|
|
}
|
|
if cpu.Snapshot() != before {
|
|
errs <- fmt.Errorf("worker %d CPU state was not restored", i)
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
for err := range errs {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestUserCallbackManagerConcurrentSameCPU(t *testing.T) {
|
|
mem, cpu := newUserCallbackTestCPU(t, false, 0x2000000)
|
|
manager := new(UserCallbackManager)
|
|
const workers = 16
|
|
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, workers)
|
|
for i := 0; i < workers; i++ {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
if _, err := manager.Begin(cpu, mem, 0x2100000+uint64(i), uint32(i), []byte{byte(i)}); err != nil {
|
|
errs <- err
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
if depth := manager.Depth(cpu); depth != workers {
|
|
t.Fatalf("concurrent callback depth = %d, want %d", depth, workers)
|
|
}
|
|
|
|
for i := 0; i < workers; i++ {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
if _, err := manager.NtCallbackReturn(cpu, uint64(i), uint64(i), uint64(StatusSuccess)); err != nil {
|
|
errs <- err
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
for err := range errs {
|
|
t.Error(err)
|
|
}
|
|
if depth := manager.Depth(cpu); depth != 0 {
|
|
t.Fatalf("completed callback depth = %d, want 0", depth)
|
|
}
|
|
}
|
|
|
|
func TestUserCallbackManagerSerializesSameCPUThroughCompletion(t *testing.T) {
|
|
memA, cpuA := newUserCallbackTestCPU(t, false, 0x2200000)
|
|
memB, cpuB := newUserCallbackTestCPU(t, false, 0x2300000)
|
|
manager := new(UserCallbackManager)
|
|
completionStarted := make(chan struct{})
|
|
releaseCompletion := make(chan struct{})
|
|
|
|
if _, err := manager.BeginWithCompletion(cpuA, memA, 0x2210000, 1, nil,
|
|
func(cpu *x64.CPU, _ UserCallbackReturn) {
|
|
close(completionStarted)
|
|
<-releaseCompletion
|
|
cpu.SetSyscallReturn(0x12345678)
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
returnDone := make(chan error, 1)
|
|
go func() {
|
|
_, err := manager.NtCallbackReturn(cpuA, 0, 0, uint64(StatusSuccess))
|
|
returnDone <- err
|
|
}()
|
|
<-completionStarted
|
|
|
|
serializerValue, ok := manager.cpuSerializers.Load(cpuA)
|
|
if !ok {
|
|
t.Fatal("same-CPU serializer is missing during completion")
|
|
}
|
|
serializer := serializerValue.(*sync.Mutex)
|
|
if serializer.TryLock() {
|
|
serializer.Unlock()
|
|
t.Fatal("same-CPU serializer was released before completion returned")
|
|
}
|
|
|
|
otherCPUStarted := make(chan error, 1)
|
|
go func() {
|
|
_, err := manager.Begin(cpuB, memB, 0x2310000, 2, nil)
|
|
otherCPUStarted <- err
|
|
}()
|
|
if err := <-otherCPUStarted; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sameCPUAttempted := make(chan struct{})
|
|
sameCPUStarted := make(chan error, 1)
|
|
go func() {
|
|
close(sameCPUAttempted)
|
|
_, err := manager.Begin(cpuA, memA, 0x2220000, 3, nil)
|
|
sameCPUStarted <- err
|
|
}()
|
|
<-sameCPUAttempted
|
|
select {
|
|
case err := <-sameCPUStarted:
|
|
t.Fatalf("same-CPU Begin completed before completion returned: %v", err)
|
|
default:
|
|
}
|
|
close(releaseCompletion)
|
|
if err := <-returnDone; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := <-sameCPUStarted; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if callbackReturn, ok := manager.TakeReturn(cpuA); ok {
|
|
t.Fatalf("completion callback also queued return %+v", callbackReturn)
|
|
}
|
|
|
|
if _, err := manager.NtCallbackReturn(cpuA, 0, 0, uint64(StatusSuccess)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := cpuA.Regs[x64.RAX]; got != 0x12345678 {
|
|
t.Fatalf("restored syscall return = 0x%x, want 0x12345678", got)
|
|
}
|
|
}
|
|
|
|
func TestDispatcherNtCallbackReturnContract(t *testing.T) {
|
|
for _, mode32 := range []bool{false, true} {
|
|
t.Run(fmt.Sprintf("mode32=%t", mode32), func(t *testing.T) {
|
|
base := uint64(0x3000000)
|
|
mem, cpu := newUserCallbackTestCPU(t, mode32, base)
|
|
d := &Dispatcher{
|
|
handlers: make(map[uint32]TrapHandler),
|
|
names: make(map[uint32]string),
|
|
cdeclIDs: make(map[uint32]bool),
|
|
stdcallArgs: make(map[uint32]int),
|
|
ctx: &TrapContext{
|
|
Mem: mem,
|
|
State: &ProcessState{Is32: mode32},
|
|
},
|
|
}
|
|
d.ctx.Disp = d
|
|
const syscallID = uint32(0x1234)
|
|
d.register(syscallID, "NtCallbackReturn", d.handleNtCallbackReturn)
|
|
|
|
before := cpu.Snapshot()
|
|
if _, err := d.BeginUserCallback(cpu, base+0x8000, 7, []byte{1, 2}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if mode32 {
|
|
sp := cpu.GetSP() - 16
|
|
if err := mem.Write32(sp, 0xDEADBEEF); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := mem.Write32(sp+4, 0x12345678); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := mem.Write32(sp+8, 0x11223344); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := mem.Write32(sp+12, StatusBufferOverflow); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cpu.SetSP(sp)
|
|
} else {
|
|
cpu.Regs[x64.RCX] = 0x123456789ABCDEF0
|
|
cpu.Regs[x64.RDX] = 0x11223344
|
|
cpu.Regs[x64.R8] = uint64(StatusBufferOverflow)
|
|
}
|
|
cpu.Regs[x64.RAX] = uint64(syscallID)
|
|
d.Handle(cpu)
|
|
|
|
if restored := cpu.Snapshot(); restored != before {
|
|
t.Fatal("dispatcher Handle did not preserve the restored CPU state")
|
|
}
|
|
got, ok := d.TakeUserCallbackReturn(cpu)
|
|
wantAddress := uint64(0x123456789ABCDEF0)
|
|
if mode32 {
|
|
wantAddress = 0x12345678
|
|
}
|
|
want := UserCallbackReturn{Address: wantAddress, Length: 0x11223344, Status: StatusBufferOverflow}
|
|
if !ok || got != want {
|
|
t.Fatalf("dispatcher return = %+v, %t; want %+v, true", got, ok, want)
|
|
}
|
|
|
|
emptyCtx := &TrapContext{CPU: cpu, Mem: mem, Disp: d, maxArgIdx: -1}
|
|
if status := d.handleNtCallbackReturn(emptyCtx); status != StatusNoCallbackActive {
|
|
t.Fatalf("empty handler status = 0x%x, want 0x%x", status, StatusNoCallbackActive)
|
|
}
|
|
if emptyCtx.PCRedirected || emptyCtx.ReturnOverridden {
|
|
t.Fatalf("failed handler changed control-flow flags")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func newUserCallbackTestCPU(t *testing.T, mode32 bool, base uint64) (*mmu.MMU, *x64.CPU) {
|
|
t.Helper()
|
|
mem := mmu.New()
|
|
mem.MapRange(base, 4*mmu.PageSize, mmu.PermRead|mmu.PermWrite)
|
|
cpu := x64.New(mem)
|
|
cpu.Mode32 = mode32
|
|
cpu.SetSP(base + 3*mmu.PageSize)
|
|
cpu.SetPC(base + 0x1234)
|
|
cpu.Regs[x64.RAX] = base + 1
|
|
cpu.Rflags = 0x246
|
|
cpu.XMM[0] = [2]uint64{base + 2, base + 3}
|
|
cpu.YMM_hi[0] = [2]uint64{base + 4, base + 5}
|
|
cpu.MXCSR = 0x1F80
|
|
return mem, cpu
|
|
}
|
|
|
|
func mutateUserCallbackCPU(cpu *x64.CPU, value uint64) {
|
|
cpu.SetPC(value)
|
|
cpu.SetSP(value + 1)
|
|
cpu.Regs[x64.RAX] = value + 2
|
|
cpu.Rflags = value + 3
|
|
cpu.Mode32 = !cpu.Mode32
|
|
cpu.XMM[0] = [2]uint64{value + 4, value + 5}
|
|
cpu.YMM_hi[0] = [2]uint64{value + 6, value + 7}
|
|
cpu.MXCSR = uint32(value)
|
|
}
|