140 lines
7.4 KiB
Python
140 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Bind the source AES subclass to measured, unchanged original ARM64 methods."""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / 'kits/XnuPrelink'))
|
|
import native_prelink as np
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from kernel_target import TRIPLE
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--objects-dir', type=Path, default=ROOT / 'build/software-aes')
|
|
args = parser.parse_args()
|
|
|
|
audit_path = ROOT / 'build/image4-ignition/aes-interface-audit.json'
|
|
recorded = ROOT / 'scripts/data/native-aes-original-bindings.S'
|
|
if not audit_path.is_file() and recorded.is_file():
|
|
# Without the qualification evidence of the original kernelcache (kernel candidate, KDK annotations, live metadata),
|
|
# bind with the recorded result of that audit. The addresses belong to the kernelcache pinned in board.toml.
|
|
out = args.objects_dir
|
|
subprocess.run(['xcrun', 'clang', '-target', TRIPLE, '-Werror', '-Wno-incompatible-sysroot', '-c', recorded,
|
|
'-o', out / 'original-bindings.o'], check=True)
|
|
archive = out / 'libSoftwareAES.a'
|
|
archive.unlink(missing_ok=True)
|
|
subprocess.run(['xcrun', 'ar', 'rcs', archive, out / 'NativeAES.o', out / 'original-bindings.o'], check=True)
|
|
print('PASS: native AES bound with the recorded original-kernelcache addresses')
|
|
raise SystemExit(0)
|
|
audit = json.loads(audit_path.read_text())
|
|
obj_path = args.objects_dir / 'NativeAES.o'
|
|
obj = obj_path.read_bytes()
|
|
obj_image = np.macho(obj)
|
|
symcmd = next(offset for command, offset, length in obj_image['commands'] if command == 2)
|
|
symoff, symcount, stroff, _ = struct.unpack_from('<4I', obj, symcmd + 8)
|
|
object_symbols = []
|
|
for index in range(symcount):
|
|
strx, kind, _, _, value = struct.unpack_from('<IBBHQ', obj, symoff + index * 16)
|
|
name = obj[stroff + strx:obj.index(b'\0', stroff + strx)].decode()
|
|
object_symbols.append((name, value, kind))
|
|
own_vtable = next(value for name, value, kind in object_symbols if name == '__ZTV9NativeAES')
|
|
relocations = {}
|
|
for segment in obj_image['segments']:
|
|
for section in segment['sections']:
|
|
if section['va'] <= own_vtable < section['va'] + section['size']:
|
|
reloc_offset, reloc_count = struct.unpack_from('<II', obj, section['command_offset'] + 56)
|
|
for index in range(reloc_count):
|
|
address, word = struct.unpack_from('<II', obj, reloc_offset + index * 8)
|
|
if word >> 27 & 1 and word >> 28 == 0 and word >> 25 & 3 == 3:
|
|
relocations[section['va'] + address] = object_symbols[word & 0xffffff][0]
|
|
overrides = {0, 1, 7, 86, 111, 117, 168, 169, 170, 171, 172}
|
|
for row in audit['methods']:
|
|
names = row['reference_names']
|
|
if row['slot'] in overrides:
|
|
names = [n.replace('16IOAESAccelerator', '9NativeAES') for n in names]
|
|
if relocations.get(own_vtable + 16 + row['slot'] * 8) not in names:
|
|
raise SystemExit(f'Compiled native subclass dispatch mismatch at slot {row["slot"]}')
|
|
if own_vtable + 16 + len(audit['methods']) * 8 in relocations:
|
|
raise SystemExit('Unexpected additional native subclass method')
|
|
kernel = (ROOT / 'build/xnu-prelink/native-defaults-candidate.macho').read_bytes()
|
|
if hashlib.sha256(kernel).hexdigest() != audit['candidate_sha256'] or not audit['live_native_4k']:
|
|
raise SystemExit('First qualify the original AES interface and live metadata')
|
|
image = np.macho(kernel)
|
|
plan = json.loads((ROOT / 'build/xnu-prelink/native-defaults-candidate.json').read_text())
|
|
text = next(e for e in plan['extents'] if e['module'] == 'com.apple.iokit.IOCryptoAcceleratorFamily' and e['segment'] == '__TEXT_EXEC')
|
|
|
|
|
|
def read(va, length):
|
|
at = np.backed_offset(image['segments'], va, length, len(kernel))
|
|
return kernel[at:at + length]
|
|
|
|
|
|
def branch(va):
|
|
word = struct.unpack('<I', read(va, 4))[0]
|
|
if word & 0xfc000000 != 0x14000000:
|
|
raise SystemExit('Expected a genuine one-instruction destructor tail call')
|
|
immediate = word & 0x3ffffff
|
|
if immediate & 0x2000000:
|
|
immediate -= 0x4000000
|
|
return va + immediate * 4
|
|
|
|
|
|
vptr = int(audit['vtable'], 16) + 16
|
|
constructors = []
|
|
for va in range(text['va'], text['va'] + text['size'] - 32, 4):
|
|
a = struct.unpack('<8I', read(va, 32))
|
|
if (a[0:2] != (0xa9bf7bfd, 0x910003fd) or a[2] & 0xfc000000 != 0x94000000 or
|
|
a[3] & 0x9f00001f != 0x90000008 or a[4] & 0xffc003ff != 0x91000108 or
|
|
a[5:] != (0xf9000008, 0xa8c17bfd, 0xd65f03c0)):
|
|
continue
|
|
immediate = ((a[3] >> 5 & 0x7ffff) << 2) | (a[3] >> 29 & 3)
|
|
if immediate & 0x100000:
|
|
immediate -= 0x200000
|
|
address = ((va + 12) & ~4095) + immediate * 4096 + (a[4] >> 10 & 4095)
|
|
if address == vptr:
|
|
constructors.append(va)
|
|
if len(constructors) != 2:
|
|
raise SystemExit('Cannot uniquely qualify the two original protected constructors')
|
|
# Identical x0/x1 forwarding, same BL destination and same vptr store.
|
|
for left, right in zip(struct.unpack('<8I', read(constructors[0], 32)),
|
|
struct.unpack('<8I', read(constructors[1], 32))):
|
|
if left != right and left & 0xfc000000 != 0x94000000:
|
|
raise SystemExit('Original constructor forms differ beyond their relative call')
|
|
calls = []
|
|
for va in constructors:
|
|
word = struct.unpack('<I', read(va + 8, 4))[0]
|
|
immediate = word & 0x3ffffff
|
|
if immediate & 0x2000000:
|
|
immediate -= 0x4000000
|
|
calls.append(va + 8 + immediate * 4)
|
|
if len(set(calls)) != 1 or not text['va'] <= calls[0] < text['va'] + text['size']:
|
|
raise SystemExit('Original constructors do not call the same genuine crypto parent')
|
|
destructor = int(audit['methods'][0]['candidate_target'], 16)
|
|
if branch(destructor - 4) != branch(destructor):
|
|
raise SystemExit('Original D1/D2 destructor semantics differ')
|
|
bindings = {name: int(row['candidate_target'], 16) for row in audit['methods']
|
|
if not row['kernel_names'] for name in row['reference_names']}
|
|
bindings.update({'__ZN16IOAESAcceleratorC2EPK11OSMetaClass': constructors[0],
|
|
'__ZN16IOAESAcceleratorD2Ev': destructor - 4,
|
|
'__ZN16IOAESAccelerator10gMetaClassE': int(audit['live_metaclasses']['IOAESAccelerator'], 16)})
|
|
out = args.objects_dir
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
source = out / 'original-bindings.S'
|
|
source.write_text(''.join(f'.globl {name}\n.set {name}, {address:#x}\n' for name, address in sorted(bindings.items())))
|
|
subprocess.run(['xcrun', 'clang', '-target', TRIPLE, '-Werror', '-Wno-incompatible-sysroot', '-c', source,
|
|
'-o', out / 'original-bindings.o'], check=True)
|
|
archive = out / 'libSoftwareAES.a'
|
|
archive.unlink(missing_ok=True)
|
|
subprocess.run(['xcrun', 'ar', 'rcs', archive, out / 'NativeAES.o', out / 'original-bindings.o'], check=True)
|
|
(out / 'bindings.json').write_text(json.dumps(dict(original_sha256=audit['original_sha256'],
|
|
ordinary_candidate_sha256=audit['candidate_sha256'], original_slide=plan['slide'],
|
|
protected_constructor_forms=[hex(x) for x in constructors], original_crypto_constructor=hex(calls[0]),
|
|
bindings={name: hex(value) for name, value in bindings.items()}, original_instructions_modified=False,
|
|
compiled_subclass_slots_checked=len(audit['methods']), compiled_subclass_sha256=hashlib.sha256(obj).hexdigest()), indent=2) + '\n')
|
|
print(f'PASS: {len(bindings)} native bindings to actual original metaclass/methods; no replacement code')
|