hv.py: Now gets pretty far into XNU boot

Next up: mmiotrace

Signed-off-by: Hector Martin <marcan@marcan.st>
This commit is contained in:
Hector Martin 2021-05-13 19:02:35 +09:00
parent 9efa91bf87
commit aebda5213a
2 changed files with 427 additions and 27 deletions

View file

@ -1,16 +1,20 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
import sys
import sys, traceback, struct, array, bisect
from asm import ARMAsm
from tgtypes import *
from proxy import IODEV, START, EXC, EXC_RET, ExcInfo
from utils import *
from sysreg import *
from macho import MachO
from adt import load_adt
import xnutools
import shell
PAC_MASK = 0xfffff00000000000
class HV:
PTE_VALID = 1 << 0
@ -23,10 +27,38 @@ class HV:
SPTE_MAP = 0 << 50
SPTE_HOOK = 1 << 50
MSR_REDIRECTS = {
SCTLR_EL1: SCTLR_EL12,
TTBR0_EL1: TTBR0_EL12,
TTBR1_EL1: TTBR1_EL12,
TCR_EL1: TCR_EL12,
ESR_EL1: ESR_EL12,
FAR_EL1: FAR_EL12,
AFSR0_EL1: AFSR0_EL12,
AFSR1_EL1: AFSR1_EL12,
MAIR_EL1: MAIR_EL12,
AMAIR_EL1: AMAIR_EL12,
CONTEXTIDR_EL1: CONTEXTIDR_EL12,
ACTLR_EL1: ACTLR_EL12,
AMX_CTL_EL1: AMX_CTL_EL12,
SPRR_CONFIG_EL1: SPRR_CONFIG_EL12,
SPRR_PERM_EL1: SPRR_PERM_EL12,
SPRR_PERM_EL0: SPRR_PERM_EL02,
SPRR_UNK1_EL1: SPRR_UNK1_EL12,
SPRR_UNK2_EL1: SPRR_UNK2_EL12,
}
def __init__(self, iface, proxy, utils):
self.iface = iface
self.p = proxy
self.u = utils
self.vbar_el1 = None
self.want_vbar = None
self.vectors = [None]
self.step = False
self.sym_offset = 0
self.symbols = []
self.sysreg = {}
def unmap(self, ipa, size):
assert self.p.hv_map(ipa, 0, size, 0) >= 0
@ -37,29 +69,232 @@ class HV:
def map_sw(self, ipa, pa, size):
assert self.p.hv_map(ipa, pa | self.SPTE_MAP, size, 1) >= 0
def addr(self, addr):
unslid_addr = addr + self.sym_offset
if addr < self.tba.virt_base or unslid_addr < self.macho.vmin:
return f"0x{addr:x}"
saddr, name = self.sym(addr)
if name is None:
return f"0x{addr:x} (0x{unslid_addr:x})"
return f"0x{addr:x} ({name}+0x{unslid_addr - saddr:x})"
def sym(self, addr):
unslid_addr = addr + self.sym_offset
if addr < self.tba.virt_base or unslid_addr < self.macho.vmin:
return None, None
idx = bisect.bisect_left(self.symbols, (unslid_addr + 1, "")) - 1
if idx < 0 or idx >= len(self.symbols):
return f"0x{addr:x} (0x{unslid_addr:x})"
return self.symbols[idx]
def handle_msr(self, ctx, iss=None):
if iss is None:
iss = ctx.esr.ISS
iss = ESR_ISS_MSR(iss)
enc = iss.Op0, iss.Op1, iss.CRn, iss.CRm, iss.Op2
if enc in sysreg_rev:
name = sysreg_rev[enc]
else:
name = f"s{iss.Op0}_{iss.Op1}_c{iss.CRn}_c{iss.CRm}_{iss.Op2}"
skip = set()
shadow = {
#SPRR_CONFIG_EL1,
#SPRR_PERM_EL0,
#SPRR_PERM_EL1,
VMSA_LOCK_EL1,
#SPRR_UNK1_EL1,
#SPRR_UNK2_EL1,
}
ro = {
ACC_CFG_EL1,
CYC_OVRD_EL1,
ACC_OVRD_EL1,
}
value = 0
if enc in shadow:
if iss.DIR == MSR_DIR.READ:
value = self.sysreg.setdefault(enc, 0)
print(f"Shadow: mrs x{iss.Rt}, {name} = {value:x}")
if iss.Rt != 31:
ctx.regs[iss.Rt] = value
else:
if iss.Rt != 31:
value = ctx.regs[iss.Rt]
print(f"Shadow: msr {name}, x{iss.Rt} = {value:x}")
self.sysreg[enc] = value
elif enc in skip or (enc in ro and iss.DIR == MSR_DIR.WRITE):
if iss.DIR == MSR_DIR.READ:
print(f"Skip: mrs x{iss.Rt}, {name} = 0")
if iss.Rt != 31:
ctx.regs[iss.Rt] = 0
else:
value = ctx.regs[iss.Rt]
print(f"Skip: msr {name}, x{iss.Rt} = {value:x}")
else:
if iss.DIR == MSR_DIR.READ:
print(f"Pass: mrs x{iss.Rt}, {name}", end=" ")
sys.stdout.flush()
value = self.u.mrs(self.MSR_REDIRECTS.get(enc, enc))
print(f"= {value:x}")
if iss.Rt != 31:
ctx.regs[iss.Rt] = value
else:
if iss.Rt != 31:
value = ctx.regs[iss.Rt]
print(f"Pass: msr {name}, x{iss.Rt} = {value:x}", end=" ")
sys.stdout.flush()
self.u.msr(self.MSR_REDIRECTS.get(enc, enc), value, call=self.p.gl2_call)
print("(OK)")
ctx.elr += 4
self.patch_exception_handling()
return True
def handle_impdef(self, ctx):
if ctx.esr.ISS == 0x20:
return self.handle_msr(ctx, self.u.mrs(AFSR1_EL1))
start = ctx.elr_phys
code = struct.unpack("<I", self.iface.readmem(ctx.elr_phys, 4))
c = ARMAsm(".inst " + ",".join(str(i) for i in code), ctx.elr_phys)
insn = "; ".join(c.disassemble())
print(f"IMPDEF exception on: {insn}")
return False
def handle_hvc(self, ctx):
idx = ctx.esr.ISS
if idx == 0:
return False
vector, target = self.vectors[idx]
if target is None:
print(f"EL1: Exception #{vector} with no target")
target = 0
ok = False
else:
ctx.elr = target
ctx.elr_phys = self.p.hv_translate(target, False, False)
ok = True
if (vector & 3) == EXC.SYNC:
spsr = SPSR(self.u.mrs(SPSR_EL12))
esr = ESR(self.u.mrs(ESR_EL12))
elr = self.u.mrs(ELR_EL12)
elr_phys = self.p.hv_translate(elr, False, False)
sp_el1 = self.u.mrs(SP_EL1)
sp_el0 = self.u.mrs(SP_EL0)
far = None
if esr.EC == ESR_EC.DABORT or esr.EC == ESR_EC.IABORT:
far = self.u.mrs(FAR_EL12)
if self.sym(elr)[1] != "com.apple.kernel:_panic_trap_to_debugger":
print("Page fault")
return ok
print(f"EL1: Exception #{vector} ({esr.EC!s}) to {self.addr(target)} from {spsr.M.name}")
print(f" ELR={self.addr(elr)} (0x{elr_phys:x})")
print(f" SP_EL1=0x{sp_el1:x} SP_EL0=0x{sp_el0:x}")
if far is not None:
print(f" FAR={self.addr(far)}")
if elr_phys:
self.u.disassemble_at(elr_phys - 4 * 4, 9 * 4, elr_phys)
if self.sym(elr)[1] == "com.apple.kernel:_panic_trap_to_debugger":
print("Panic! Trying to decode panic...")
try:
self.decode_panic_call()
except:
print("Error decoding panic.")
try:
self.bt()
except:
pass
return False
if esr.EC == ESR_EC.UNKNOWN:
instr = self.p.read32(elr_phys)
if instr == 0xe7ffdeff:
print("Debugger break! Trying to decode panic...")
try:
self.decode_dbg_panic()
except:
print("Error decoding panic.")
try:
self.bt()
except:
pass
return False
return False
else:
elr = self.u.mrs(ELR_EL12)
print(f"Guest: {str(EXC(vector & 3))} at {self.addr(elr)}")
return ok
def handle_sync(self, ctx):
if ctx.esr.EC == ESR_EC.MSR:
return self.handle_msr(ctx)
if ctx.esr.EC == ESR_EC.IMPDEF:
return self.handle_impdef(ctx)
if ctx.esr.EC == ESR_EC.HVC:
return self.handle_hvc(ctx)
def handle_exception(self, reason, code, info):
self.ctx = ctx = ExcInfo.parse(self.iface.readmem(info, ExcInfo.sizeof()))
print(f"Guest exception: {code.name}")
handled = False
self.u.print_exception(code, ctx)
try:
if code == EXC.SYNC:
handled = self.handle_sync(ctx)
elif code == EXC.FIQ:
self.u.msr(CNTV_CTL_EL0, 0)
self.u.print_exception(code, ctx)
handled = True
except Exception as e:
print(f"Python exception while handling guest exception:")
traceback.print_exc()
locals = {
"hv": self,
"iface": self.iface,
"p": self.p,
"u": self.u,
}
if handled:
ret = EXC_RET.HANDLED
else:
print(f"Guest exception: {code.name}")
for attr in dir(self):
locals[attr] = getattr(self, attr)
self.u.print_exception(code, ctx)
ret = shell.run_shell(locals, "Entering debug shell", "Returning from exception")
locals = {
"hv": self,
"iface": self.iface,
"p": self.p,
"u": self.u,
}
if ret is None:
ret = EXC_RET.EXIT_GUEST
for attr in dir(self):
a = getattr(self, attr)
if callable(a):
locals[attr] = getattr(self, attr)
ret = shell.run_shell(locals, "Entering debug shell", "Returning from exception")
if ret is None:
ret = EXC_RET.EXIT_GUEST
self.iface.writemem(info, ExcInfo.build(self.ctx))
if ret == EXC_RET.HANDLED and self.step:
ret = EXC_RET.STEP
self.p.exit(ret)
def skip(self):
@ -72,6 +307,94 @@ class HV:
def exit(self):
raise shell.ExitConsole(EXC_RET.EXIT_GUEST)
def hvc(self, arg):
assert 0 <= arg <= 0xffff
return 0xd4000002 | (arg << 5)
def decode_dbg_panic(self):
xnutools.decode_debugger_state(self.u, self.ctx)
def decode_panic_call(self):
xnutools.decode_panic_call(self.u, self.ctx)
def bt(self, frame=None, lr=None):
if frame is None:
frame = self.ctx.regs[29]
if lr is None:
lr = self.ctx.regs[30] | PAC_MASK
print("Stack trace:")
while frame:
print(f" - {self.addr(lr - 4)}")
lrp = self.p.hv_translate(frame + 8)
fpp = self.p.hv_translate(frame)
if not fpp:
break
lr = self.p.read64(lrp) | PAC_MASK
frame = self.p.read64(fpp)
def patch_exception_handling(self):
if self.want_vbar is not None:
vbar = self.want_vbar
else:
vbar = self.u.mrs(VBAR_EL12)
if vbar == self.vbar_el1:
return
if vbar == 0:
return
if self.u.mrs(SCTLR_EL12) & 1:
vbar_phys = self.p.hv_translate(vbar, False, False)
if vbar_phys == 0:
print(f"VBAR vaddr 0x{vbar:x} translation failed!")
if self.vbar_el1 is not None:
self.want_vbar = vbar
self.u.msr(VBAR_EL12, self.vbar_el1)
return
else:
if vbar & (1 << 63):
print(f"VBAR vaddr 0x{vbar:x} without translation enabled")
if self.vbar_el1 is not None:
self.want_vbar = vbar
self.u.msr(VBAR_EL12, self.vbar_el1)
return
vbar_phys = vbar
if self.want_vbar is not None:
self.want_vbar = None
self.u.msr(VBAR_EL12, vbar)
print(f"New VBAR paddr: 0x{vbar_phys:x}")
#for i in range(16):
for i in [0, 3, 4, 7, 8, 11, 12, 15]:
idx = 0
addr = vbar_phys + 0x80 * i
orig = self.p.read32(addr)
if (orig & 0xfc000000) != 0x14000000:
print(f"Unknown vector #{i}:\n")
self.u.disassemble_at(addr, 16)
else:
idx = len(self.vectors)
delta = orig & 0x3ffffff
if delta == 0:
target = None
print(f"Vector #{i}: Loop\n")
else:
target = (delta << 2) + vbar + 0x80 * i
print(f"Vector #{i}: 0x{target:x}\n")
self.vectors.append((i, target))
self.u.disassemble_at(addr, 16)
self.p.write32(addr, self.hvc(idx))
self.p.dc_cvau(vbar_phys, 0x800)
self.p.ic_ivau(vbar_phys, 0x800)
self.vbar_el1 = vbar
def init(self):
self.adt = load_adt(self.u.get_adt())
self.iodev = self.p.iodev_whoami()
@ -87,7 +410,33 @@ class HV:
self.map_hw(0x2_00000000, 0x2_00000000, 0x5_00000000)
self.p.hv_map_vuart(0x2_35200000)
hcr = HCR(self.u.mrs(HCR_EL2))
hcr.TACR = 1
hcr.TIDCP = 0
hcr.TVM = 0
hcr.FMO = 0
hcr.IMO = 0
self.u.msr(HCR_EL2, hcr.value)
hacr = HACR(0)
hacr.TRAP_CPU_EXT = 1
#hacr.TRAP_SPRR = 1
#hacr.TRAP_GXF = 1
hacr.TRAP_CTRR = 1
hacr.TRAP_EHID = 1
hacr.TRAP_HID = 1
hacr.TRAP_ACC = 1
self.u.msr(HACR_EL2, hacr.value)
amx_ctl = AMX_CTL(self.u.mrs(AMX_CTL_EL1))
amx_ctl.EN_EL1 = 1
self.u.msr(AMX_CTL_EL1, amx_ctl.value)
#self.p.hv_map_vuart(0x2_35200000)
actlr = ACTLR(self.u.mrs(ACTLR_EL12))
actlr.EnMDSB = 1
self.u.msr(ACTLR_EL12, actlr.value)
self.setup_adt()
@ -102,13 +451,13 @@ class HV:
except KeyError:
pass
for cpu in list(self.adt["cpus"]):
if cpu.name != "cpu0":
print(f"Removing ADT node {cpu._path}")
try:
del self.adt["cpus"][cpu.name]
except KeyError:
pass
#for cpu in list(self.adt["cpus"]):
#if cpu.name != "cpu0":
#print(f"Removing ADT node {cpu._path}")
#try:
#del self.adt["cpus"][cpu.name]
#except KeyError:
#pass
def set_bootargs(self, boot_args):
if "-v" in boot_args.split():
@ -118,11 +467,49 @@ class HV:
print(f"Setting boot arguments to {boot_args!r}")
self.tba.cmdline = boot_args
def load_macho(self, data):
def load_macho(self, data, symfile=None):
if isinstance(data, str):
data = open(data, "rb").read()
data = open(data, "rb")
macho = MachO(data)
self.macho = macho = MachO(data)
symbols = None
if symfile is not None:
if isinstance(symfile, str):
symfile = open(symfile, "rb")
syms = MachO(symfile)
macho.add_symbols("com.apple.kernel", syms)
self.symbols = [(v, k) for k, v in macho.symbols.items()]
self.symbols.sort()
def load_hook(data, segname, size, fileoff, dest):
if segname != "__TEXT_EXEC":
return data
print(f"Patching segment {segname}...")
a = array.array("I", data)
output = []
p = 0
while (p := data.find(b"\x20\x00", p)) != -1:
if (p & 3) != 2:
p += 1
continue
opcode = a[p // 4]
inst = self.hvc((opcode & 0xffff))
off = fileoff + (p & ~3)
if off >= 0xbfcfc0:
print(f" 0x{off:x}: 0x{opcode:04x} -> hvc 0x{opcode:x} (0x{inst:x})")
a[p // 4] = inst
p += 4
print("Done.")
return a.tobytes()
#image = macho.prepare_image(load_hook)
image = macho.prepare_image()
sepfw_start, sepfw_length = self.u.adt["chosen"]["memory-map"].SEPFW
tc_start, tc_size = self.u.adt["chosen"]["memory-map"].TrustCache
@ -137,6 +524,7 @@ class HV:
print(f"Total region size: 0x{image_size:x} bytes")
self.phys_base = phys_base = guest_base = self.u.heap_top
guest_base += 16 << 20 # ensure guest starts within a 16MB aligned region of mapped RAM
adt_base = guest_base
guest_base += align(self.u.ba.devtree_size)
tc_base = guest_base
@ -180,7 +568,9 @@ class HV:
self.tba.phys_base = phys_base
self.tba.virt_base = 0xfffffe0010000000 + (phys_base & (32 * 1024 * 1024 - 1))
self.tba.devtree = adt_base - phys_base + self.tba.virt_base
self.tba.top_of_kdata = guest_base + image_size
self.tba.top_of_kernel_data = guest_base + image_size
self.sym_offset = macho.vmin - guest_base + self.tba.phys_base - self.tba.virt_base
self.iface.writemem(guest_base + self.bootargs_off, BootArgs.build(self.tba))
@ -194,6 +584,12 @@ class HV:
print(f"Shutting down framebuffer...")
self.p.fb_shutdown()
print(f"Enabling SPRR...")
self.u.msr(SPRR_CONFIG_EL1, 1)
print(f"Enabling GXF...")
self.u.msr(GXF_CONFIG_EL1, 1)
print(f"Jumping to entrypoint at 0x{self.entry:x}")
self.iface.dev.timeout = None

View file

@ -3,6 +3,7 @@
import argparse, pathlib
parser = argparse.ArgumentParser(description='Run a Mach-O payload under the hypervisor')
parser.add_argument('-s', '--symbols', type=pathlib.Path)
parser.add_argument('payload', type=pathlib.Path)
parser.add_argument('boot_args', default=[], nargs="*")
args = parser.parse_args()
@ -25,5 +26,8 @@ if len(args.boot_args) > 0:
boot_args = " ".join(args.boot_args)
hv.set_bootargs(boot_args)
hv.load_macho(args.payload.read_bytes())
symfile = None
if args.symbols:
symfile = args.symbols.open("rb")
hv.load_macho(args.payload.open("rb"), symfile=symfile)
hv.start()