xref.py (4199B)
1 # Cross-reference scanner for the decompressed arm64e kernelcache. 2 # Finds code that loads a given C string (ADRP + ADD) and says whether it's passed to PE_parse_boot_argn, 3 # i.e. whether the string is a boot-arg. 4 # 5 # Usage: venv/bin/python xref.py kernelcache.macho <string> [<string> ...] 6 # Rebuild kernelcache.macho: see SETUP.md ("How the boot-arg was found"). 7 import struct 8 import sys 9 10 import numpy as np 11 from capstone import CS_ARCH_ARM64, CS_MODE_ARM, Cs 12 13 data = open(sys.argv[1] if len(sys.argv) > 1 else 'kernelcache.macho', 'rb').read() 14 md = Cs(CS_ARCH_ARM64, CS_MODE_ARM) 15 16 # Top-level LC_SEGMENT_64s of the fileset map every byte of the file. 17 segs = [] 18 ncmds, off = struct.unpack_from('<I', data, 16)[0], 32 19 for _ in range(ncmds): 20 cmd, sz = struct.unpack_from('<II', data, off) 21 if cmd == 0x19: 22 vm, vms, fo, fs = struct.unpack_from('<QQQQ', data, off + 24) 23 segs.append((vm, fo, fs)) 24 off += sz 25 26 27 def f2v(fo): 28 for vm, sfo, fs in segs: 29 if sfo <= fo < sfo + fs: 30 return vm + fo - sfo 31 32 33 def v2f(va): 34 for vm, sfo, fs in segs: 35 if vm <= va < vm + fs: 36 return sfo + va - vm 37 38 39 def cstr(va): 40 fo = v2f(va) 41 if fo is None: 42 return None 43 e = data.find(b'\0', fo, fo + 100) 44 s = data[fo:e] 45 return s.decode() if s and all(32 <= c < 127 for c in s) else None 46 47 48 # Every ADRP's target page, computed once (vectorized). 49 words = np.frombuffer(data[:len(data) // 4 * 4], dtype='<u4') 50 adrp_idx = np.nonzero((words & 0x9F000000) == 0x90000000)[0] 51 aw = words[adrp_idx].astype(np.int64) 52 imm = (((aw >> 5) & 0x7FFFF) << 2 | ((aw >> 29) & 3)) << 12 53 imm = np.where(imm & (1 << 32), imm - (1 << 33), imm) 54 fo_arr = adrp_idx.astype(np.uint64) * np.uint64(4) 55 pc_arr = np.zeros(len(adrp_idx), dtype=np.uint64) 56 valid = np.zeros(len(adrp_idx), dtype=bool) 57 for vm, sfo, fs in segs: 58 m = (fo_arr >= np.uint64(sfo)) & (fo_arr < np.uint64(sfo + fs)) 59 pc_arr[m] = np.uint64(vm) + fo_arr[m] - np.uint64(sfo) 60 valid |= m 61 target = (pc_arr & ~np.uint64(0xFFF)) + imm.astype(np.uint64) 62 63 64 def find_xrefs(s): 65 """Addresses of instructions that load the address of C string `s`.""" 66 out, start = [], 0 67 while (i := data.find(b'\0' + s.encode() + b'\0', start)) >= 0: 68 start = i + 1 69 sva = f2v(i + 1) 70 if sva is None: 71 continue 72 for wi in adrp_idx[valid & (target == np.uint64(sva & ~0xFFF))]: 73 wi = int(wi) 74 rd = int(words[wi]) & 31 75 for k in range(1, 6): 76 w2 = int(words[wi + k]) 77 if (w2 & 0xFF800000) == 0x91000000 and ((w2 >> 5) & 31) == rd and ((w2 >> 10) & 0xFFF) == (sva & 0xFFF): 78 out.append(f2v(wi * 4)) 79 break 80 return out 81 82 83 def calls_after(pc, n=8): 84 fo = v2f(pc) 85 return [int(i.op_str[1:], 16) for i in md.disasm(data[fo:fo + n * 4], pc) if i.mnemonic == 'bl'] 86 87 88 def find_boot_arg_parser(): 89 """PE_parse_boot_argn = the function called right after loading a known boot-arg string.""" 90 counts = {} 91 for s in ('amfi_get_out_of_my_way', 'debug', 'serial'): 92 for pc in find_xrefs(s): 93 for t in calls_after(pc, 6): 94 counts[t] = counts.get(t, 0) + 1 95 return max(counts, key=counts.get) 96 97 98 def disasm(va, n): 99 """Disassembly with ADRP+ADD string operands annotated.""" 100 fo, regs, lines = v2f(va), {}, [] 101 for ins in md.disasm(data[fo:fo + n * 4], va): 102 note = '' 103 if ins.mnemonic == 'adrp': 104 regs[ins.op_str.split(',')[0]] = int(ins.op_str.split('#')[1], 16) 105 elif ins.mnemonic == 'add' and '#' in ins.op_str: 106 r = [x.strip() for x in ins.op_str.split(',')] 107 if r[1] in regs: 108 s = cstr(regs[r[1]] + int(r[2][1:], 16)) 109 note = f' ; {s!r}' if s else '' 110 lines.append(f'{ins.address:#x}: {ins.mnemonic:7} {ins.op_str}{note}') 111 return lines 112 113 114 if __name__ == '__main__': 115 pb = find_boot_arg_parser() 116 print(f'PE_parse_boot_argn = {pb:#x}') 117 for s in sys.argv[2:]: 118 for pc in find_xrefs(s): 119 kind = 'BOOT-ARG' if pb in calls_after(pc, 6) else 'other' 120 print(f'{s:32} ref {pc:#x}: {kind}')