Files
raven/pkg/ntapi/seh_runtime.go
T

1137 lines
31 KiB
Go

//go:build guest_windows
package ntapi
import (
"fmt"
"sort"
"sync"
"github.com/mirkobrombin/Raven/pkg/mmu"
ravelog "github.com/mirkobrombin/Raven/pkg/log"
)
const (
ExceptionDispositionContinueExecution = 0
ExceptionDispositionContinueSearch = 1
ExceptionDispositionCollidedUnwind = 3
ExceptionContinueExecution = 0xFFFFFFFF
ExceptionContinueSearch = 0
ExceptionExecuteHandler = 1
ExceptionFlagUnwinding = 0x02
ExceptionFlagExitUnwind = 0x04
ExceptionFlagTargetUnwind = 0x20
ExceptionFlagCollidedUnwind = 0x40
StatusUnwind = 0xC0000027
StatusInvalidDisposition = 0xC0000026
StatusBadStack = 0xC0000028
StatusInvalidUnwindTarget = 0xC0000029
StatusBadFunctionTable = 0xC00000FF
StatusUnwindConsolidate = 0x80000029
)
type functionTableEntry struct {
baseAddr uint64
endAddr uint64
tableAddr uint64
identifier uint64
entries []RuntimeFunction
count int
callback uint64
context uint64
callbackLength uint64
}
var (
sehFunctionTableMu sync.RWMutex
staticFunctionTables []functionTableEntry
dynamicFunctionTables []functionTableEntry
)
type AMD64Context struct {
Regs [16]uint64
XMM [16][2]uint64
Rflags uint64
Rip uint64
}
type UnwindError struct {
Status uint32
Err error
}
func (e *UnwindError) Error() string {
if e == nil {
return ""
}
if e.Err == nil {
return fmt.Sprintf("unwind failed with NTSTATUS 0x%08x", e.Status)
}
return fmt.Sprintf("unwind failed with NTSTATUS 0x%08x: %v", e.Status, e.Err)
}
func (e *UnwindError) Unwrap() error {
if e == nil {
return nil
}
return e.Err
}
type DispatcherContext struct {
ControlPc uint64
ImageBase uint64
FunctionEntry uint64
EstablisherFrame uint64
TargetIp uint64
ContextRecord uint64
LanguageHandler uint64
HandlerData uint64
HistoryTable uint64
ScopeIndex uint32
}
func ReadRuntimeFunctionEntry(mem *mmu.MMU, entryAddr uint64) *RuntimeFunction {
if entryAddr == 0 {
return nil
}
begin, err := mem.Read32(entryAddr)
if err != nil {
return nil
}
end, err := mem.Read32(entryAddr + 4)
if err != nil {
return nil
}
unwind, err := mem.Read32(entryAddr + 8)
if err != nil {
return nil
}
return &RuntimeFunction{
BeginAddress: begin,
EndAddress: end,
UnwindData: unwind,
EntryAddr: entryAddr,
}
}
func parseRuntimeFunctionTable(mem *mmu.MMU, tableAddr uint64, count uint32) []RuntimeFunction {
entries := make([]RuntimeFunction, 0, count)
for i := uint32(0); i < count; i++ {
entryAddr := tableAddr + uint64(i)*12
rf := ReadRuntimeFunctionEntry(mem, entryAddr)
if rf == nil || rf.BeginAddress == 0 || rf.EndAddress <= rf.BeginAddress {
continue
}
entries = append(entries, *rf)
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].BeginAddress < entries[j].BeginAddress
})
return entries
}
func RegisterExceptionTable(imageBase, imageSize uint64, table *ExceptionTable) {
if table == nil || len(table.Entries) == 0 || imageBase == 0 {
return
}
if imageSize == 0 {
for _, entry := range table.Entries {
if end := uint64(entry.EndAddress); end > imageSize {
imageSize = end
}
}
}
table.ImageBase = imageBase
entry := functionTableEntry{
baseAddr: imageBase,
endAddr: imageBase + imageSize,
tableAddr: table.Entries[0].EntryAddr,
entries: append([]RuntimeFunction(nil), table.Entries...),
count: len(table.Entries),
}
sehFunctionTableMu.Lock()
defer sehFunctionTableMu.Unlock()
for i := range staticFunctionTables {
if staticFunctionTables[i].baseAddr == imageBase {
staticFunctionTables[i] = entry
return
}
}
staticFunctionTables = append(staticFunctionTables, entry)
}
func AddDynamicFunctionTable(mem *mmu.MMU, tableAddr uint64, count uint32, baseAddr uint64) bool {
if mem == nil || tableAddr == 0 || count == 0 || baseAddr == 0 {
return false
}
entries := parseRuntimeFunctionTable(mem, tableAddr, count)
if len(entries) == 0 {
return false
}
endAddr := baseAddr
for _, entry := range entries {
if end := baseAddr + uint64(entry.EndAddress); end > endAddr {
endAddr = end
}
}
sehFunctionTableMu.Lock()
defer sehFunctionTableMu.Unlock()
for i := range dynamicFunctionTables {
if dynamicFunctionTables[i].tableAddr == tableAddr {
dynamicFunctionTables[i] = functionTableEntry{
baseAddr: baseAddr,
endAddr: endAddr,
tableAddr: tableAddr,
entries: entries,
count: len(entries),
}
return true
}
}
dynamicFunctionTables = append(dynamicFunctionTables, functionTableEntry{
baseAddr: baseAddr,
endAddr: endAddr,
tableAddr: tableAddr,
entries: entries,
count: len(entries),
})
return true
}
func DeleteDynamicFunctionTable(tableAddr uint64) bool {
if tableAddr == 0 {
return false
}
sehFunctionTableMu.Lock()
defer sehFunctionTableMu.Unlock()
for i := range dynamicFunctionTables {
if dynamicFunctionTables[i].tableAddr == tableAddr || dynamicFunctionTables[i].identifier == tableAddr {
dynamicFunctionTables = append(dynamicFunctionTables[:i], dynamicFunctionTables[i+1:]...)
return true
}
}
return false
}
func InstallDynamicFunctionTableCallback(identifier, baseAddr, length, callback, context uint64) bool {
if identifier == 0 || baseAddr == 0 || length == 0 || callback == 0 {
return false
}
entry := functionTableEntry{
baseAddr: baseAddr,
endAddr: baseAddr + length,
tableAddr: identifier,
identifier: identifier,
callback: callback,
context: context,
callbackLength: length,
}
sehFunctionTableMu.Lock()
defer sehFunctionTableMu.Unlock()
for i := range dynamicFunctionTables {
if dynamicFunctionTables[i].identifier == identifier {
dynamicFunctionTables[i] = entry
return true
}
}
dynamicFunctionTables = append(dynamicFunctionTables, entry)
return true
}
func lookupRuntimeFunctionInEntries(entries []RuntimeFunction, rva uint32) *RuntimeFunction {
if len(entries) == 0 {
return nil
}
i := sort.Search(len(entries), func(i int) bool {
return entries[i].EndAddress > rva
})
if i < len(entries) && entries[i].BeginAddress <= rva && rva < entries[i].EndAddress {
return &entries[i]
}
return nil
}
func LookupFunctionEntry(mem *mmu.MMU, controlPC uint64, guestCall GuestCallFunc) (*RuntimeFunction, uint64, uint64) {
sehFunctionTableMu.RLock()
for i := range staticFunctionTables {
table := &staticFunctionTables[i]
if controlPC < table.baseAddr || controlPC >= table.endAddr {
continue
}
rva := uint32(controlPC - table.baseAddr)
if entry := lookupRuntimeFunctionInEntries(table.entries, rva); entry != nil {
result := *entry
imageBase := table.baseAddr
sehFunctionTableMu.RUnlock()
return &result, imageBase, result.EntryAddr
}
}
candidates := make([]functionTableEntry, 0, len(dynamicFunctionTables))
for i := range dynamicFunctionTables {
table := &dynamicFunctionTables[i]
if controlPC < table.baseAddr || controlPC >= table.endAddr {
continue
}
candidate := *table
candidate.entries = append([]RuntimeFunction(nil), table.entries...)
candidates = append(candidates, candidate)
}
sehFunctionTableMu.RUnlock()
for i := range candidates {
table := &candidates[i]
if table.callback != 0 && guestCall != nil && mem != nil {
entryAddr, err := guestCall(table.callback, []uint64{controlPC, table.context})
if err == nil && entryAddr != 0 {
if rf := ReadRuntimeFunctionEntry(mem, entryAddr); rf != nil {
return rf, table.baseAddr, entryAddr
}
}
continue
}
rva := uint32(controlPC - table.baseAddr)
if entry := lookupRuntimeFunctionInEntries(table.entries, rva); entry != nil {
return entry, table.baseAddr, entry.EntryAddr
}
}
return nil, 0, 0
}
func ReadAMD64Context(mem *mmu.MMU, ctxAddr uint64) (*AMD64Context, error) {
if ctxAddr == 0 {
return nil, fmt.Errorf("nil CONTEXT")
}
read64 := func(off uint64) (uint64, error) {
return mem.Read64(ctxAddr + off)
}
ctx := &AMD64Context{}
var err error
eflags, err := mem.Read32(ctxAddr + 0x44)
if err != nil {
return nil, err
}
ctx.Rflags = uint64(eflags)
offsets := []uint64{0x78, 0x80, 0x88, 0x90, 0x98, 0xA0, 0xA8, 0xB0, 0xB8, 0xC0, 0xC8, 0xD0, 0xD8, 0xE0, 0xE8, 0xF0}
for i, off := range offsets {
if ctx.Regs[i], err = read64(off); err != nil {
return nil, err
}
}
if ctx.Rip, err = read64(0xF8); err != nil {
return nil, err
}
for index := range ctx.XMM {
offset := uint64(0x1A0 + index*16)
if ctx.XMM[index][0], err = read64(offset); err != nil {
return nil, err
}
if ctx.XMM[index][1], err = read64(offset + 8); err != nil {
return nil, err
}
}
return ctx, nil
}
func WriteAMD64Context(mem *mmu.MMU, ctxAddr uint64, ctx *AMD64Context) error {
if ctxAddr == 0 || ctx == nil {
return fmt.Errorf("invalid CONTEXT write")
}
mem.Write32(ctxAddr+0x30, 0x10000B)
mem.Write32(ctxAddr+0x44, uint32(ctx.Rflags))
offsets := []uint64{0x78, 0x80, 0x88, 0x90, 0x98, 0xA0, 0xA8, 0xB0, 0xB8, 0xC0, 0xC8, 0xD0, 0xD8, 0xE0, 0xE8, 0xF0}
for i, off := range offsets {
mem.Write64(ctxAddr+off, ctx.Regs[i])
}
mem.Write64(ctxAddr+0xF8, ctx.Rip)
for index, value := range ctx.XMM {
offset := uint64(0x1A0 + index*16)
mem.Write64(ctxAddr+offset, value[0])
mem.Write64(ctxAddr+offset+8, value[1])
}
return nil
}
func (c *AMD64Context) GetReg(idx uint8) uint64 {
if int(idx) < len(c.Regs) {
return c.Regs[idx]
}
return 0
}
func (c *AMD64Context) SetReg(idx int, val uint64) {
if idx >= 0 && idx < len(c.Regs) {
c.Regs[idx] = val
}
}
func getEstablisherFrame(mem *mmu.MMU, unwindInfoAddr uint64, ctx *AMD64Context,
ui *UnwindInfo, codeOffset uint32) uint64 {
if ui == nil {
return ctx.Regs[regRSP]
}
if ui.FrameRegister == 0 {
return ctx.Regs[regRSP]
}
if codeOffset >= uint32(ui.SizeOfProlog) || ui.Flags&UnwFlagChainInfo != 0 {
return ctx.GetReg(ui.FrameRegister) - uint64(ui.FrameOffset)*16
}
codesAddr := unwindInfoAddr + 4
for index := uint8(0); index < ui.CountOfCodes; {
operationOffset, err := mem.Read8(codesAddr + uint64(index)*2)
if err != nil {
break
}
operationInfo, err := mem.Read8(codesAddr + uint64(index)*2 + 1)
if err != nil {
break
}
operation := operationInfo & 0x0F
if operation == 3 && uint32(operationOffset) <= codeOffset {
return ctx.GetReg(ui.FrameRegister) - uint64(ui.FrameOffset)*16
}
index += unwindCodeSlots(operation, operationInfo>>4)
}
return ctx.Regs[regRSP]
}
func unwindCodeSlots(op, info uint8) uint8 {
switch op {
case 1:
if info == 0 {
return 2
}
return 3
case 4, 8:
return 2
case 5, 9:
return 3
default:
return 1
}
}
func applyUnwindCodes(mem *mmu.MMU, unwindInfoAddr uint64, ui *UnwindInfo,
codeOffset uint32, context *AMD64Context) (bool, error) {
codesAddr := unwindInfoAddr + 4
insideProlog := codeOffset < uint32(ui.SizeOfProlog)
for index := uint8(0); index < ui.CountOfCodes; {
codeAddr := codesAddr + uint64(index)*2
operationOffset, err := mem.Read8(codeAddr)
if err != nil {
return false, err
}
operationInfo, err := mem.Read8(codeAddr + 1)
if err != nil {
return false, err
}
operation := operationInfo & 0x0F
info := operationInfo >> 4
slots := unwindCodeSlots(operation, info)
if index+slots > ui.CountOfCodes {
return false, fmt.Errorf("truncated unwind operation %d", operation)
}
if insideProlog && uint32(operationOffset) > codeOffset {
index += slots
continue
}
switch operation {
case 0: // UWOP_PUSH_NONVOL
value, err := mem.Read64(context.Regs[regRSP])
if err != nil {
return false, err
}
context.SetReg(int(info), value)
context.Regs[regRSP] += 8
case 1: // UWOP_ALLOC_LARGE
low, err := mem.Read16(codesAddr + uint64(index+1)*2)
if err != nil {
return false, err
}
if info == 0 {
context.Regs[regRSP] += uint64(low) * 8
} else {
high, err := mem.Read16(codesAddr + uint64(index+2)*2)
if err != nil {
return false, err
}
context.Regs[regRSP] += uint64(low) | uint64(high)<<16
}
case 2: // UWOP_ALLOC_SMALL
context.Regs[regRSP] += uint64(info)*8 + 8
case 3: // UWOP_SET_FPREG
if ui.FrameRegister == 0 {
return false, fmt.Errorf("UWOP_SET_FPREG without frame register")
}
context.Regs[regRSP] = context.GetReg(ui.FrameRegister) - uint64(ui.FrameOffset)*16
case 4: // UWOP_SAVE_NONVOL
offset, err := mem.Read16(codesAddr + uint64(index+1)*2)
if err != nil {
return false, err
}
value, err := mem.Read64(context.Regs[regRSP] + uint64(offset)*8)
if err != nil {
return false, err
}
context.SetReg(int(info), value)
case 5: // UWOP_SAVE_NONVOL_FAR
low, err := mem.Read16(codesAddr + uint64(index+1)*2)
if err != nil {
return false, err
}
high, err := mem.Read16(codesAddr + uint64(index+2)*2)
if err != nil {
return false, err
}
offset := uint64(low) | uint64(high)<<16
value, err := mem.Read64(context.Regs[regRSP] + offset)
if err != nil {
return false, err
}
context.SetReg(int(info), value)
case 8: // UWOP_SAVE_XMM128
offset, err := mem.Read16(codesAddr + uint64(index+1)*2)
if err != nil {
return false, err
}
address := context.Regs[regRSP] + uint64(offset)*16
if context.XMM[info][0], err = mem.Read64(address); err != nil {
return false, err
}
if context.XMM[info][1], err = mem.Read64(address + 8); err != nil {
return false, err
}
case 9: // UWOP_SAVE_XMM128_FAR
low, err := mem.Read16(codesAddr + uint64(index+1)*2)
if err != nil {
return false, err
}
high, err := mem.Read16(codesAddr + uint64(index+2)*2)
if err != nil {
return false, err
}
address := context.Regs[regRSP] + (uint64(low) | uint64(high)<<16)
if context.XMM[info][0], err = mem.Read64(address); err != nil {
return false, err
}
if context.XMM[info][1], err = mem.Read64(address + 8); err != nil {
return false, err
}
case 10: // UWOP_PUSH_MACHFRAME
stack := context.Regs[regRSP]
ripOffset := uint64(0)
rspOffset := uint64(24)
if info != 0 {
ripOffset = 8
rspOffset = 32
}
rip, err := mem.Read64(stack + ripOffset)
if err != nil {
return false, err
}
rsp, err := mem.Read64(stack + rspOffset)
if err != nil {
return false, err
}
context.Rip = rip
context.Regs[regRSP] = rsp
return true, nil
default:
return false, fmt.Errorf("unsupported unwind operation %d", operation)
}
index += slots
}
return false, nil
}
func tryVirtualUnwindEpilog(mem *mmu.MMU, function *RuntimeFunction, imageBase, controlPC uint64,
context *AMD64Context) (bool, error) {
if function == nil || controlPC < imageBase+uint64(function.BeginAddress) ||
controlPC >= imageBase+uint64(function.EndAddress) {
return false, nil
}
trial := *context
pc := controlPC
for instruction := 0; instruction < 32; instruction++ {
opcode, err := mem.Read8(pc)
if err != nil {
return false, nil
}
switch {
case opcode == 0x90:
pc++
case opcode == 0x48 || opcode == 0x49:
second, readErr := mem.Read8(pc + 1)
if readErr != nil {
return false, nil
}
switch second {
case 0x83:
modrm, _ := mem.Read8(pc + 2)
immediate, immediateErr := mem.Read8(pc + 3)
if opcode != 0x48 || modrm != 0xC4 || immediateErr != nil {
return false, nil
}
trial.Regs[regRSP] += uint64(immediate)
pc += 4
case 0x81:
modrm, _ := mem.Read8(pc + 2)
immediate, immediateErr := mem.Read32(pc + 3)
if opcode != 0x48 || modrm != 0xC4 || immediateErr != nil {
return false, nil
}
trial.Regs[regRSP] += uint64(immediate)
pc += 7
case 0x89:
modrm, _ := mem.Read8(pc + 2)
if opcode != 0x48 || modrm != 0xEC {
return false, nil
}
trial.Regs[regRSP] = trial.Regs[regRBP]
pc += 3
case 0x8D:
modrm, _ := mem.Read8(pc + 2)
mode := modrm >> 6
if (modrm>>3)&7 != 4 || mode == 0 || mode == 3 || modrm&7 == 4 {
return false, nil
}
baseRegister := int(modrm & 7)
if opcode&1 != 0 {
baseRegister += 8
}
displacement := int64(0)
if mode == 1 {
raw, displacementErr := mem.Read8(pc + 3)
if displacementErr != nil {
return false, nil
}
displacement = int64(int8(raw))
pc += 4
} else {
raw, displacementErr := mem.Read32(pc + 3)
if displacementErr != nil {
return false, nil
}
displacement = int64(int32(raw))
pc += 7
}
trial.Regs[regRSP] = uint64(int64(trial.Regs[baseRegister]) + displacement)
default:
return false, nil
}
case opcode >= 0x58 && opcode <= 0x5F:
register := int(opcode - 0x58)
if register == regRSP {
return false, nil
}
value, readErr := mem.Read64(trial.Regs[regRSP])
if readErr != nil {
return false, readErr
}
trial.SetReg(register, value)
trial.Regs[regRSP] += 8
pc++
case opcode == 0x41:
pop, readErr := mem.Read8(pc + 1)
if readErr != nil || pop < 0x58 || pop > 0x5F {
return false, nil
}
value, readErr := mem.Read64(trial.Regs[regRSP])
if readErr != nil {
return false, readErr
}
trial.SetReg(int(pop-0x58)+8, value)
trial.Regs[regRSP] += 8
pc += 2
case opcode == 0xC3 || opcode == 0xC2:
returnAddress, readErr := mem.Read64(trial.Regs[regRSP])
if readErr != nil {
return false, readErr
}
trial.Regs[regRSP] += 8
if opcode == 0xC2 {
immediate, immediateErr := mem.Read16(pc + 1)
if immediateErr != nil {
return false, immediateErr
}
trial.Regs[regRSP] += uint64(immediate)
}
trial.Rip = returnAddress
*context = trial
return true, nil
case opcode == 0xE9:
displacement, readErr := mem.Read32(pc + 1)
if readErr != nil {
return false, readErr
}
target := uint64(int64(pc+5) + int64(int32(displacement)))
if target >= imageBase+uint64(function.BeginAddress) && target < imageBase+uint64(function.EndAddress) {
return false, nil
}
trial.Rip = target
*context = trial
return true, nil
case opcode == 0xEB:
displacement, readErr := mem.Read8(pc + 1)
if readErr != nil {
return false, readErr
}
target := uint64(int64(pc+2) + int64(int8(displacement)))
if target >= imageBase+uint64(function.BeginAddress) && target < imageBase+uint64(function.EndAddress) {
return false, nil
}
trial.Rip = target
*context = trial
return true, nil
default:
return false, nil
}
}
return false, nil
}
func VirtualUnwind(mem *mmu.MMU, handlerType uint32, imageBase, controlPC uint64, functionEntry *RuntimeFunction, ctx *AMD64Context) (uint64, uint64, uint64, error) {
if ctx == nil {
return 0, 0, 0, fmt.Errorf("nil unwind context")
}
if functionEntry == nil {
establisherFrame := ctx.Regs[regRSP]
retAddr, err := mem.Read64(establisherFrame)
if err != nil {
return 0, 0, establisherFrame, err
}
ctx.Regs[regRSP] += 8
ctx.Rip = retAddr
return 0, 0, establisherFrame, nil
}
epilogEstablisher := ctx.Regs[regRSP]
if unwound, err := tryVirtualUnwindEpilog(mem, functionEntry, imageBase, controlPC, ctx); err != nil {
return 0, 0, 0, err
} else if unwound {
return 0, 0, epilogEstablisher, nil
}
rva := uint32(0)
if controlPC >= imageBase {
rva = uint32(controlPC - imageBase)
}
ui := ParseUnwindInfo(mem, imageBase, functionEntry.UnwindData)
if ui == nil {
return 0, 0, 0, fmt.Errorf("missing unwind info")
}
establisherFrame := getEstablisherFrame(
mem,
imageBase+uint64(functionEntry.UnwindData),
ctx,
ui,
rva-functionEntry.BeginAddress,
)
currentUI := ui
currentUnwindRVA := functionEntry.UnwindData
currentCodeOffset := rva - functionEntry.BeginAddress
var handlerAddr, handlerData uint64
for {
machineFrame, err := applyUnwindCodes(
mem,
imageBase+uint64(currentUnwindRVA),
currentUI,
currentCodeOffset,
ctx,
)
if err != nil {
return 0, 0, establisherFrame, err
}
if machineFrame {
return 0, 0, establisherFrame, nil
}
if currentUI.Flags&UnwFlagChainInfo == 0 || currentUI.ChainedRF == nil {
if handlerType != 0 && uint32(currentUI.Flags)&handlerType != 0 {
handlerAddr = imageBase + uint64(currentUI.HandlerRVA)
handlerData = currentUI.HandlerData
}
break
}
currentUnwindRVA = currentUI.ChainedRF.UnwindData
currentUI = ParseUnwindInfo(mem, imageBase, currentUnwindRVA)
if currentUI == nil {
return 0, 0, establisherFrame, fmt.Errorf("missing chained unwind info")
}
currentCodeOffset = uint32(currentUI.SizeOfProlog)
}
retAddr, err := mem.Read64(ctx.Regs[regRSP])
if err != nil {
return 0, 0, establisherFrame, err
}
ctx.Regs[regRSP] += 8
ctx.Rip = retAddr
return handlerAddr, handlerData, establisherFrame, nil
}
// UnwindStack performs the x64 phase-two unwind used by RtlUnwindEx.
// The context remains at the target frame and is redirected to targetIP.
func UnwindStack(mem *mmu.MMU, exceptionRecordAddr, contextRecordAddr uint64,
targetFrame, targetIP, returnValue, historyTable uint64,
guestCall GuestCallFunc, trace bool) error {
if mem == nil || exceptionRecordAddr == 0 || contextRecordAddr == 0 {
return fmt.Errorf("invalid unwind inputs")
}
context, err := ReadAMD64Context(mem, contextRecordAddr)
if err != nil {
return err
}
exceptionCode, err := mem.Read32(exceptionRecordAddr)
if err != nil {
return err
}
flags, err := mem.Read32(exceptionRecordAddr + 4)
if err != nil {
return err
}
flags &^= ExceptionFlagTargetUnwind | ExceptionFlagCollidedUnwind
flags |= ExceptionFlagUnwinding
if targetFrame == 0 {
flags |= ExceptionFlagExitUnwind
} else {
flags &^= ExceptionFlagExitUnwind
}
if err := mem.Write32(exceptionRecordAddr+4, flags); err != nil {
return err
}
reachedTarget := false
var lastRip, lastRSP, lastFrame uint64
for depth := 0; depth < 256; depth++ {
if context.Rip == 0 || context.Regs[regRSP] == 0 {
break
}
current := *context
unwound := current
functionEntry, imageBase, entryAddr := LookupFunctionEntry(mem, current.Rip, guestCall)
handlerAddr, handlerData, establisherFrame, unwindErr := VirtualUnwind(
mem,
UnwFlagUHandler,
imageBase,
current.Rip,
functionEntry,
&unwound,
)
if unwindErr != nil {
return &UnwindError{
Status: StatusBadFunctionTable,
Err: fmt.Errorf("unwind frame at RIP 0x%x: %w", current.Rip, unwindErr),
}
}
lastRip = current.Rip
lastRSP = current.Regs[regRSP]
lastFrame = establisherFrame
atTarget := targetFrame != 0 && establisherFrame == targetFrame
if atTarget {
flags |= ExceptionFlagTargetUnwind
if err := mem.Write32(exceptionRecordAddr+4, flags); err != nil {
return err
}
}
if trace {
ravelog.Logf("[seh] unwind depth=%d rip=0x%x frame=0x%x target=%t handler=0x%x\n",
depth, current.Rip, establisherFrame, atTarget, handlerAddr)
}
if handlerAddr != 0 {
if guestCall == nil {
return &UnwindError{
Status: StatusInvalidDisposition,
Err: fmt.Errorf("unwind handler 0x%x requires a guest caller", handlerAddr),
}
}
if err := WriteAMD64Context(mem, contextRecordAddr, &unwound); err != nil {
return err
}
dispatcherContextAddr := mem.Mmap(0, 0x80, mmu.PermRead|mmu.PermWrite)
dispatcherContext := &DispatcherContext{
ControlPc: current.Rip,
ImageBase: imageBase,
FunctionEntry: entryAddr,
EstablisherFrame: establisherFrame,
TargetIp: targetIP,
ContextRecord: contextRecordAddr,
LanguageHandler: handlerAddr,
HandlerData: handlerData,
HistoryTable: historyTable,
}
if err := WriteDispatcherContext(mem, dispatcherContextAddr, dispatcherContext); err != nil {
return err
}
defer mem.Munmap(dispatcherContextAddr, 0x80)
disposition, callErr := guestCall(handlerAddr, []uint64{
exceptionRecordAddr,
establisherFrame,
contextRecordAddr,
dispatcherContextAddr,
})
if callErr != nil {
return fmt.Errorf("call unwind handler 0x%x: %w", handlerAddr, callErr)
}
switch uint32(disposition) {
case ExceptionDispositionContinueSearch:
flags &^= ExceptionFlagCollidedUnwind
if err := mem.Write32(exceptionRecordAddr+4, flags); err != nil {
return err
}
case ExceptionDispositionCollidedUnwind:
collidedDispatch, err := ReadDispatcherContext(mem, dispatcherContextAddr)
if err != nil {
return fmt.Errorf("read collided unwind context: %w", err)
}
collidedContextAddr := collidedDispatch.ContextRecord
if collidedContextAddr == 0 {
collidedContextAddr = contextRecordAddr
}
collidedContext, err := ReadAMD64Context(mem, collidedContextAddr)
if err != nil {
return fmt.Errorf("read collided unwind CPU context: %w", err)
}
collidedFunction := ReadRuntimeFunctionEntry(mem, collidedDispatch.FunctionEntry)
collidedImageBase := collidedDispatch.ImageBase
if collidedFunction == nil {
collidedFunction, collidedImageBase, _ = LookupFunctionEntry(
mem,
collidedDispatch.ControlPc,
guestCall,
)
}
if collidedFunction == nil {
return fmt.Errorf("collided unwind has no function entry at 0x%x", collidedDispatch.ControlPc)
}
if _, _, _, err := VirtualUnwind(
mem,
0,
collidedImageBase,
collidedDispatch.ControlPc,
collidedFunction,
collidedContext,
); err != nil {
return fmt.Errorf("unwind collided frame at 0x%x: %w", collidedDispatch.ControlPc, err)
}
flags |= ExceptionFlagCollidedUnwind
if err := mem.Write32(exceptionRecordAddr+4, flags); err != nil {
return err
}
*context = *collidedContext
if err := WriteAMD64Context(mem, contextRecordAddr, context); err != nil {
return err
}
continue
default:
return &UnwindError{
Status: StatusInvalidDisposition,
Err: fmt.Errorf("disposition 0x%x from handler 0x%x", disposition, handlerAddr),
}
}
}
if atTarget {
*context = current
reachedTarget = true
break
}
if unwound.Rip == current.Rip && unwound.Regs[regRSP] == current.Regs[regRSP] {
return fmt.Errorf("unwind made no progress at RIP 0x%x", current.Rip)
}
*context = unwound
if err := WriteAMD64Context(mem, contextRecordAddr, context); err != nil {
return err
}
if targetFrame == 0 && context.Rip == 0 {
break
}
if depth == 255 {
return fmt.Errorf("unwind exceeded frame limit")
}
}
if targetFrame != 0 && !reachedTarget {
return &UnwindError{
Status: StatusInvalidUnwindTarget,
Err: fmt.Errorf(
"target frame 0x%x was not found (last RIP 0x%x RSP 0x%x frame 0x%x)",
targetFrame,
lastRip,
lastRSP,
lastFrame,
),
}
}
if targetFrame == 0 {
if err := WriteAMD64Context(mem, contextRecordAddr, context); err != nil {
return err
}
return &UnwindError{
Status: StatusBadStack,
Err: fmt.Errorf("exit unwind exhausted the stack"),
}
}
context.Regs[regRAX] = returnValue
if err := WriteAMD64Context(mem, contextRecordAddr, context); err != nil {
return err
}
if exceptionCode == StatusUnwindConsolidate {
count, err := mem.Read32(exceptionRecordAddr + 0x18)
if err != nil {
return err
}
if count < 1 || guestCall == nil {
return &UnwindError{
Status: StatusInvalidDisposition,
Err: fmt.Errorf("invalid unwind consolidation record"),
}
}
callback, err := mem.Read64(exceptionRecordAddr + 0x20)
if err != nil || callback == 0 {
return &UnwindError{
Status: StatusInvalidDisposition,
Err: fmt.Errorf("invalid unwind consolidation callback"),
}
}
continuation, callErr := guestCall(callback, []uint64{exceptionRecordAddr})
if callErr != nil {
return &UnwindError{
Status: StatusInvalidDisposition,
Err: fmt.Errorf("call unwind consolidation callback 0x%x: %w", callback, callErr),
}
}
refreshed, err := ReadAMD64Context(mem, contextRecordAddr)
if err != nil {
return err
}
context = refreshed
context.Rip = continuation
} else {
context.Rip = targetIP
}
return WriteAMD64Context(mem, contextRecordAddr, context)
}
func ReadDispatcherContext(mem *mmu.MMU, addr uint64) (*DispatcherContext, error) {
if addr == 0 {
return nil, fmt.Errorf("nil dispatcher context")
}
read64 := func(off uint64) uint64 {
v, _ := mem.Read64(addr + off)
return v
}
scopeIndex, _ := mem.Read32(addr + 72)
return &DispatcherContext{
ControlPc: read64(0),
ImageBase: read64(8),
FunctionEntry: read64(16),
EstablisherFrame: read64(24),
TargetIp: read64(32),
ContextRecord: read64(40),
LanguageHandler: read64(48),
HandlerData: read64(56),
HistoryTable: read64(64),
ScopeIndex: scopeIndex,
}, nil
}
func WriteDispatcherContext(mem *mmu.MMU, addr uint64, dc *DispatcherContext) error {
if addr == 0 || dc == nil {
return fmt.Errorf("invalid dispatcher context")
}
mem.Write64(addr+0, dc.ControlPc)
mem.Write64(addr+8, dc.ImageBase)
mem.Write64(addr+16, dc.FunctionEntry)
mem.Write64(addr+24, dc.EstablisherFrame)
mem.Write64(addr+32, dc.TargetIp)
mem.Write64(addr+40, dc.ContextRecord)
mem.Write64(addr+48, dc.LanguageHandler)
mem.Write64(addr+56, dc.HandlerData)
mem.Write64(addr+64, dc.HistoryTable)
mem.Write32(addr+72, dc.ScopeIndex)
return nil
}
func DispatchException(mem *mmu.MMU, exceptionRecordAddr, contextRecordAddr uint64, guestCall GuestCallFunc, trace bool) (bool, error) {
if mem == nil || exceptionRecordAddr == 0 || contextRecordAddr == 0 {
return false, fmt.Errorf("invalid exception dispatch inputs")
}
for depth := 0; depth < 64; depth++ {
ctx, err := ReadAMD64Context(mem, contextRecordAddr)
if err != nil {
return false, err
}
functionEntry, imageBase, entryAddr := LookupFunctionEntry(mem, ctx.Rip, guestCall)
if functionEntry == nil {
retAddr, err := mem.Read64(ctx.Regs[regRSP])
if err != nil || retAddr == 0 {
return false, nil
}
ctx.Regs[regRSP] += 8
ctx.Rip = retAddr
if err := WriteAMD64Context(mem, contextRecordAddr, ctx); err != nil {
return false, err
}
continue
}
unwound := *ctx
handlerAddr, handlerData, establisherFrame, err := VirtualUnwind(mem, 1, imageBase, ctx.Rip, functionEntry, &unwound)
if err != nil {
return false, err
}
if trace {
ravelog.Logf("[seh] dispatch depth=%d rip=0x%x handler=0x%x establisher=0x%x\n", depth, ctx.Rip, handlerAddr, establisherFrame)
}
if handlerAddr != 0 && guestCall != nil {
dispatcherContextAddr := mem.Mmap(0, 0x80, mmu.PermRead|mmu.PermWrite)
dispatcherContext := &DispatcherContext{
ControlPc: ctx.Rip,
ImageBase: imageBase,
FunctionEntry: entryAddr,
EstablisherFrame: establisherFrame,
ContextRecord: contextRecordAddr,
LanguageHandler: handlerAddr,
HandlerData: handlerData,
}
_ = WriteDispatcherContext(mem, dispatcherContextAddr, dispatcherContext)
disposition, err := guestCall(handlerAddr, []uint64{
exceptionRecordAddr,
establisherFrame,
contextRecordAddr,
dispatcherContextAddr,
})
if err != nil {
return false, err
}
switch uint32(disposition) {
case ExceptionDispositionContinueExecution:
return true, nil
case ExceptionDispositionContinueSearch:
default:
if trace {
ravelog.Logf("[seh] unexpected handler disposition 0x%x at 0x%x\n", disposition, handlerAddr)
}
}
}
if err := WriteAMD64Context(mem, contextRecordAddr, &unwound); err != nil {
return false, err
}
}
return false, nil
}