Files
UniXNU/scripts/inspect-prelink-callsite.py

89 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""Decode an observed prelinked callsite and resolve real kernel/GOT targets."""
import argparse
from pathlib import Path
import shutil
import struct
import subprocess
import sys
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'kits/XnuPrelink'))
from native_prelink import backed_offset, exports, macho
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('candidate', type=Path)
parser.add_argument('--at', required=True, type=lambda value: int(value, 0))
parser.add_argument('--bytes', type=lambda value: int(value, 0), default=256)
args = parser.parse_args()
if args.at % 4 or args.bytes % 4 or not 0 < args.bytes <= 4096:
parser.error('Use aligned AArch64 instructions and at most 4096 bytes')
tool = shutil.which('llvm-mc')
if not tool:
raise SystemExit('llvm-mc is required on PATH')
data = args.candidate.read_bytes()
image = macho(data)
symbols = exports(data, image)
def read(va, size):
at = backed_offset(image['segments'], va, size, len(data))
return data[at:at + size]
def signed(value, bits):
return value - (1 << bits) if value & (1 << (bits - 1)) else value
def adrp(word, pc):
return (pc & ~4095) + (signed(((word >> 5) & 0x7ffff) << 2 | ((word >> 29) & 3), 21) << 12)
def literal(va):
for segment in image['segments']:
if segment['va'] <= va < segment['va'] + segment['filesize']:
raw = read(va, min(180, segment['va'] + segment['filesize'] - va)).split(b'\0', 1)[0]
if raw and all(byte in (9, 10, 13) or 32 <= byte < 127 for byte in raw):
return repr(raw.decode())
return ''
def branch_target(va):
if va in symbols:
return ','.join(symbols[va])
try:
first, second, third = struct.unpack('<3I', read(va, 12))
if (first & 0x9f000000 == 0x90000000 and second & 0xffc00000 == 0xf9400000
and third & 0xfffffc1f == 0xd61f0000 and first & 31 == (second >> 5) & 31
and second & 31 == (third >> 5) & 31):
slot = adrp(first, va) + ((second >> 10) & 0xfff) * 8
target, = struct.unpack('<Q', read(slot, 8))
return f'GOT {slot:#x} -> {target:#x} ' + ','.join(symbols.get(target, []))
except ValueError:
pass
return ''
blob = read(args.at, args.bytes)
text = '\n'.join(' '.join(f'0x{x:02x}' for x in blob[i:i + 4]) for i in range(0, len(blob), 4))
result = subprocess.run([tool, '--disassemble', '--triple=aarch64-apple-darwin'],
input=text, capture_output=True, text=True, check=True)
lines = [line.strip() for line in result.stdout.splitlines() if line.strip() and line.strip() != '.text']
if len(lines) != len(blob) // 4:
raise SystemExit('Decoder omitted an instruction; refuse to mislabel addresses')
previous = None
for index, line in enumerate(lines):
va = args.at + index * 4
word, = struct.unpack_from('<I', blob, index * 4)
note = ''
if word & 0x7c000000 == 0x14000000:
target = va + (signed(word & 0x3ffffff, 26) << 2)
note = f' -> {target:#x} ' + branch_target(target)
if word & 0x7f000000 == 0x11000000 and previous is not None:
prev_word, prev_pc = previous
if prev_word & 0x9f000000 == 0x90000000 and prev_word & 31 == (word >> 5) & 31:
target = adrp(prev_word, prev_pc) + (((word >> 10) & 0xfff) << (12 if word & (1 << 22) else 0))
note = f' -> {target:#x} ' + literal(target)
print(f'{va:#018x} {line}{note}')
previous = (word, va)