161 lines
7.6 KiB
Python
161 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Audit the unchanged target AMFI -> PMGR boundary; no hardware access."""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import struct
|
|
import sys
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / 'kits/XnuPrelink'))
|
|
from native_prelink import backed_offset, exports, macho
|
|
|
|
|
|
def require(ok, message):
|
|
if not ok:
|
|
raise SystemExit(message)
|
|
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--candidate', type=Path, default=ROOT /
|
|
'build/xnu-prelink/native-owner-ufs-rootauth3-candidate.macho')
|
|
args = parser.parse_args()
|
|
if not args.candidate.exists():
|
|
# Build products are pruned: a later candidate built from the same original kernelcache carries the same original
|
|
# AppleMobileFileIntegrity/ApplePMGR instructions; the instruction contracts below are checked against it.
|
|
later = sorted((ROOT / 'build/xnu-prelink').glob('native-owner-ufs-cpu-desktop-*-candidate.macho'), key=lambda f: f.stat().st_mtime)
|
|
require(later, 'No reference candidate available')
|
|
args.candidate = later[-1]
|
|
data = args.candidate.read_bytes()
|
|
plan = json.loads(args.candidate.with_suffix('.json').read_text())
|
|
meta = macho(data)
|
|
symbols = exports(data, meta)
|
|
|
|
|
|
def read(va, size):
|
|
at = backed_offset(meta['segments'], va, size, len(data))
|
|
return data[at:at + size]
|
|
|
|
|
|
def words(va, expected):
|
|
require(struct.unpack('<' + 'I' * len(expected), read(va, 4 * len(expected))) ==
|
|
expected, f'Instruction contract changed at {va:#x}')
|
|
|
|
|
|
def pointer(va):
|
|
return struct.unpack('<Q', read(va, 8))[0]
|
|
|
|
|
|
hashes = {
|
|
'com.apple.driver.AppleMobileFileIntegrity':
|
|
'a1088aaabc259418ed503d08df29b6146470b8c962696c688122a1f60a141db6',
|
|
'com.apple.driver.ApplePMGR':
|
|
'2c433279d004803b7ef02948e550ad6b77aa3f84b02a06a357e00fa738310a99',
|
|
}
|
|
for module, digest in hashes.items():
|
|
extent, = [e for e in plan['extents'] if e['module'] == module and
|
|
e['segment'] == '__TEXT_EXEC']
|
|
require(hashlib.sha256(read(extent['va'], extent['size'])).hexdigest() ==
|
|
plan['original_executable_sha256'][module] == digest,
|
|
f'Original executable differs: {module}')
|
|
|
|
# Check actual indirect imports, rather than treating a diagnostic string as an ABI.
|
|
for slot, name in (
|
|
(0xfffffff005cfad48, '__ZN9IOService15serviceMatchingEPKcP12OSDictionary'),
|
|
(0xfffffff005cfad50, '__ZN9IOService22waitForMatchingServiceEP12OSDictionaryy'),
|
|
(0xfffffff005cfac68, '__ZN15OSMetaClassBase12safeMetaCastEPKS_PK11OSMetaClass'),
|
|
):
|
|
require(name in symbols.get(pointer(slot), []), 'Native IOKit import differs')
|
|
require(read(0xfffffff0042dde02, 13) == b'ApplePMGRNub\0', 'Matching class differs')
|
|
require(pointer(0xfffffff005cfac08) == 0xfffffff004a24adc and
|
|
pointer(0xfffffff005cfac10) == 0xfffffff004a230ec, 'PMGR imports differ')
|
|
|
|
# AMFI: read, OR bit 2, write. The imported methods dispatch through the
|
|
# provider stored at nub+0x88; the slots belong to that provider, not the nub.
|
|
words(0xfffffff004aaeb40, (0x94006c46, 0x321e0015, 0xaa1403e0,
|
|
0xaa1503e1, 0x94006c45))
|
|
words(0xfffffff004a24adc, (0xf9404400, 0xb4000080, 0xf9400008,
|
|
0xf9450101, 0xd61f0020, 0x12800000, 0xd65f03c0))
|
|
words(0xfffffff004a230ec, (0xf9404400, 0xb4000080, 0xf9400008,
|
|
0xf9450502, 0xd61f0040, 0xd65f03c0))
|
|
|
|
|
|
def constructor_vptr(reader, pc):
|
|
first, second, store = struct.unpack('<3I', reader(pc, 12))
|
|
require(first & 0x9f00001f == 0x90000008 and
|
|
second & 0xffc003ff == 0x91000108 and store == 0xf9000008,
|
|
'Constructor must form x8 with ADRP/ADD and store it at this+0')
|
|
imm = ((first >> 5) & 0x7ffff) << 2 | ((first >> 29) & 3)
|
|
if imm & (1 << 20):
|
|
imm -= 1 << 21
|
|
return (pc & ~4095) + (imm << 12) + ((second >> 10) & 4095)
|
|
|
|
|
|
base_vptr = constructor_vptr(read, 0xfffffff0049f0058)
|
|
require(base_vptr == 0xfffffff005ce26d8 and
|
|
struct.unpack('<2Q', read(base_vptr + 320 * 8, 16)) ==
|
|
(0xfffffff004a20fbc, 0xfffffff004a20fc4), 'Base provider slots differ')
|
|
words(0xfffffff004a20fbc, (0x12800000, 0xd65f03c0, 0xd65f03c0))
|
|
|
|
# The target-specific driver is inspected in the original supplied cache; it
|
|
# is not in this native candidate. Do not infer hardware semantics from its name.
|
|
original = (ROOT / '.cache/firmware/kernelcache.macho/kernelcache.release.ipad7.decompressed').read_bytes()
|
|
original_meta = macho(original)
|
|
|
|
|
|
def original_read(va, size):
|
|
at = backed_offset(original_meta['segments'], va, size, len(original))
|
|
return original[at:at + size]
|
|
|
|
|
|
header = backed_offset(original_meta['segments'], 0xfffffff00584f680, 32, len(original))
|
|
target = macho(original, header)
|
|
target_hashes = {
|
|
'__TEXT': 'bd4be83b7c24b18d4796be180e8bdaf96a2a4670407e85101164c991cd9d408c',
|
|
'__TEXT_EXEC': '59d36c869db750dcde3ec1ab87d26619beef7c12d929d674e0b3d38357ad5b22',
|
|
'__DATA_CONST': '429c542da926dfa6376ba1dc6e622f2a3cb6afa3c0bfade43d4954c2c63a2a07',
|
|
}
|
|
for name, digest in target_hashes.items():
|
|
segment, = [s for s in target['segments'] if s['name'] == name]
|
|
require(hashlib.sha256(original_read(segment['va'], segment['filesize'])).hexdigest() == digest,
|
|
f'Target AppleT8011PMGR {name} differs')
|
|
require(original_read(0xfffffff005852125, 15) == b'AppleT8011PMGR\0', 'Target class name differs')
|
|
target_vptr = constructor_vptr(original_read, 0xfffffff006a55fb0)
|
|
require(target_vptr == 0xfffffff006f9da90, 'Target constructor vptr differs')
|
|
target_slots = struct.unpack('<2Q', original_read(target_vptr + 320 * 8, 16))
|
|
require(target_slots == (0xfffffff006a577e8, 0xfffffff006a577f0), 'Target PMGR slots differ')
|
|
require(struct.unpack('<2I', original_read(target_slots[0], 8)) == (0x12800000, 0xd65f03c0) and
|
|
struct.unpack('<I', original_read(target_slots[1], 4)) == (0xd65f03c0,),
|
|
'Target must return 0xffffffff on read and immediately return on write')
|
|
|
|
report = {
|
|
'evidence': 'static analysis of original executable bytes and rebound imports',
|
|
'original_executable_sha256': hashes,
|
|
'service_class': 'ApplePMGRNub', 'provider_field_offset': '0x88',
|
|
'provider_read_vtable_byte_offset': '0xa00', 'provider_read_slot': 320,
|
|
'provider_write_vtable_byte_offset': '0xa08', 'provider_write_slot': 321,
|
|
'amfi_value_update': '32-bit read result OR 0x4, then passed to write',
|
|
'null_provider': 'read returns 0xffffffff; write returns without an operation',
|
|
'target_provider': {
|
|
'class': 'AppleT8011PMGR', 'original_segment_sha256': target_hashes,
|
|
'constructor_vptr': hex(target_vptr), 'slot_targets': [hex(v) for v in target_slots],
|
|
'read': 'returns 0xffffffff', 'write': 'returns immediately without MMIO',
|
|
'runtime_invocation_observed': False,
|
|
'included_in_native_candidate': 'com.apple.driver.AppleT8011PMGR' in plan['components'],
|
|
},
|
|
'limitations': [
|
|
'Target constant-read/no-op-write behavior is static evidence, not runtime service qualification',
|
|
'No Snapdragon provider lifecycle, service matching or platform power operations qualified',
|
|
'The PMGRAON log is not an independent latch readback',
|
|
'No replacement nub, MMIO access, VM declaration or security bypass installed',
|
|
'No QEMU execution or hardware observation is produced by this audit',
|
|
],
|
|
}
|
|
out = ROOT / 'build/amfi-pmgr-contract/report.json'
|
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
out.write_text(json.dumps(report, indent=2) + '\n')
|
|
print('PASS: unchanged AMFI read/OR/write and original PMGR provider dispatch')
|
|
print('PASS: actual target AppleT8011PMGR slots read 0xffffffff and perform no write')
|
|
print('Static evidence only; native platform service integration remains unresolved')
|