⬢ DragonFlyBSD Kernel Audit
DF-5604 / usbutil_probe.c
← back to finding ↓ download raw
/*
 * usbutil_probe.c - pass-2 (GLM 5.3) evidence harness for
 * sys/bus/u4b/usb_util.c (DragonFlyBSD u4b core utilities).
 *
 * Sysctl-driven KLD, no USB hardware required:
 *
 *   sysctl hw.usbutil_probe.case=0   usb_make_str_desc() exhaustive
 *                                    boundary matrix with canaries
 *                                    (negative evidence: the bLength /
 *                                    truncation math is safe).
 *   sysctl hw.usbutil_probe.case=1   DF-5604: post-unlock use of the
 *                                    shared per-udev scratch buffer
 *                                    (device_set_usb_desc) - race
 *                                    mechanism, control run + raced run.
 *   sysctl hw.usbutil_probe.case=2   usb_make_str_desc(buf, 64, NULL):
 *                                    missing NULL guard -> deliberate
 *                                    panic (run LAST; dirties the guest).
 *
 * NOT a driver; test-only module for the security audit evidence pack.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/module.h>
#include <sys/sysctl.h>
#include <sys/malloc.h>
#include <sys/kthread.h>
#include <sys/lock.h>
#include <sys/types.h>

/* <bus/u4b/usb_util.h> prototype - spelled out to avoid header path
 * dependencies in this standalone module. */
extern uint8_t usb_make_str_desc(void *ptr, uint16_t max_len, const char *s);

#define	UP_UDESC_STRING		0x03	/* <bus/u4b/usb.h> */

MALLOC_DEFINE(M_UPROBE, "uprobe", "usb_util.c audit probe");

/*
 * Models the tail of struct usb_device:
 *   [0,255)   == union usb_device_scratch.data (the shared scratch)
 *   [255,1024)== adjacent kernel heap after the object (marker 0x5A)
 *   zone[1023]== first zero byte found in the adjacent heap
 */
#define	UP_ZSIZE	1024
#define	UP_SCRATCH_LEN	255		/* sizeof(udev->scratch.data) */

static struct lock	up_lk;		/* models udev->enum_lock */
static volatile u_int	up_state;
static char		*up_zone;

/*--------------------------------------------------------------------
 * case 0: usb_make_str_desc() boundary matrix (negative evidence)
 *------------------------------------------------------------------*/ 
static int
up_matrix(void)
{
	static const uint16_t lens[] = { 0, 1, 2, 3, 4, 5, 254, 255, 256,
	    511, 512, 513, 1023, 1024, 32767, 32768, 65534, 65535 };
	char strs[6][1024];
	int slen[6];
	char *buf;
	int i, k, li, si;
	int fails = 0, cells = 0;

	buf = kmalloc(1024, M_UPROBE, M_INTWAIT | M_ZERO);

	/* strings: "", "A", 31, 126, 127, 1000 chars */
	slen[0] = 0;	strs[0][0] = 0;
	slen[1] = 1;	strs[1][0] = 'A'; strs[1][1] = 0;
	slen[2] = 31;	memset(strs[2], 'a', 31);  strs[2][31] = 0;
	slen[3] = 126;	memset(strs[3], 'b', 126); strs[3][126] = 0;
	slen[4] = 127;	memset(strs[4], 'c', 127); strs[4][127] = 0;
	slen[5] = 1000;	memset(strs[5], 'd', 1000); strs[5][1000] = 0;

	for (li = 0; li < (int)(sizeof(lens) / sizeof(lens[0])); li++) {
		for (si = 0; si < 6; si++) {
			uint16_t max_len = lens[li];
			uint8_t ret, exp_tot, eff;
			int exp_j;

			cells++;
			memset(buf, 0xA5, 1024);
			ret = usb_make_str_desc(buf, max_len, strs[si]);

			exp_j = (slen[si] > 126) ? 126 : slen[si];
			if (max_len < 2) {
				eff = 0;
				exp_tot = 0;
			} else {
				eff = (uint8_t)(max_len / 2 - 1);
				if (eff > exp_j)
					eff = exp_j;
				exp_tot = (uint8_t)((eff + 1) * 2);
			}

			if (ret != exp_tot) {
				kprintf("UPROBE FAIL max_len=%u slen=%d: "
				    "ret=%u expected=%u\n",
				    max_len, slen[si], ret, exp_tot);
				fails++;
				continue;
			}
			if (max_len < 2) {
				/* return 0, buffer must be untouched */
				for (i = 0; i < 1024; i++) {
					if (buf[i] != (char)0xA5) {
						kprintf("UPROBE FAIL "
						    "max_len=%u: wrote with "
						    "invalid length @%d\n",
						    max_len, i);
						fails++;
						break;
					}
				}
				continue;
			}
			if ((uint8_t)buf[0] != exp_tot ||
			    (uint8_t)buf[1] != UP_UDESC_STRING) {
				kprintf("UPROBE FAIL max_len=%u slen=%d: "
				    "hdr %02x%02x\n", max_len, slen[si],
				    (uint8_t)buf[0], (uint8_t)buf[1]);
				fails++;
				continue;
			}
			/* UTF-16LE payload */
			for (k = 0; k < eff; k++) {
				if (buf[2 + 2 * k] != strs[si][k] ||
				    buf[3 + 2 * k] != 0) {
					kprintf("UPROBE FAIL max_len=%u "
					    "slen=%d: payload @%d\n",
					    max_len, slen[si], k);
					fails++;
					break;
				}
			}
			/* canaries: nothing at or past totlen may be
			 * touched (covers the odd-max_len tail byte) */
			for (i = exp_tot; i < 1024; i++) {
				if (buf[i] != (char)0xA5) {
					kprintf("UPROBE FAIL max_len=%u "
					    "slen=%d: OOB write @%d "
					    "(totlen=%u)\n", max_len, slen[si],
					    i, exp_tot);
					fails++;
					break;
				}
			}
		}
	}

	kfree(buf, M_UPROBE);
	kprintf("UPROBE case0 matrix: %d cells, %d failures "
	    "(usb_make_str_desc write discipline)\n", cells, fails);
	return (fails);
}

/*--------------------------------------------------------------------
 * case 1: DF-5604 - scratch used after usbd_enum_unlock()
 *
 * Writer kthread = a concurrent enumeration operation on the same udev
 * running usbd_req_get_string_any(udev, NULL, udev->scratch.data, 255, i)
 * for a malicious device string (126 16-bit chars; every raw byte
 * nonzero, every char "silently skipped" by the filter).  The raw
 * descriptor copy + in-place conversion below reproduce the code shape
 * of sys/bus/u4b/usb_request.c:1129-1177 exactly.
 *
 * Reader = device_set_usb_desc() after usb_util.c:110-111 has dropped
 * the enum lock: strlen()/strcpy() of scratch as done by
 * device_set_desc_copy() (sys/kern/subr_bus.c device_set_desc_internal).
 *------------------------------------------------------------------*/ 
static void
up_writer(void *arg)
{
	uint8_t raw[254];
	int i;
	char *s;
	uint8_t *temp;
	uint16_t n;

	/* usbd_req_get_string_desc() DMA result: bLength=254,
	 * UDESC_STRING, 126 chars of U+4E2D -> raw bytes {0x2D,0x4E} */
	raw[0] = 254;
	raw[1] = UP_UDESC_STRING;
	for (i = 0; i != 126; i++) {
		raw[2 + 2 * i] = 0x2D;
		raw[3 + 2 * i] = 0x4E;
	}

	/* usbd_enum_lock() */
	lockmgr(&up_lk, LK_EXCLUSIVE);

	/* raw descriptor lands in scratch (clobbers the string the
	 * attaching driver relies on, INCLUDING its NUL terminator) */
	bcopy(raw, up_zone, 254);

	up_state = 1;
	wakeup(&up_state);

	/* in-place conversion, verbatim shape of
	 * usbd_req_get_string_any() usb_request.c:1129-1177:
	 * every char has both bytes nonzero -> skipped, s never moves */
	s = up_zone;
	temp = (uint8_t *)up_zone + 2;
	n = ((uint8_t)up_zone[0] / 2) - 1;
	for (i = 0; i != n; i++) {
		uint16_t c = temp[2 * i] | (temp[2 * i + 1] << 8);
		/* LE / BE branches both require one zero byte: skipped */
		if (((c & 0xff00) == 0) && (i < 0))
			*s++ = (char)c;
		else if (((c & 0x00ff) == 0) && (i < 0))
			*s++ = (char)(c >> 8);
		/* else: silently skip bad character */
	}
	/* models control-transfer + filter latency of the real fetch;
	 * does not change the algorithm, only widens the window */
	tsleep(&up_lk, 0, "upconv", 1);
	*s = 0;			/* usb_request.c:1176 - too late for the reader */

	/* usbd_enum_unlock() */
	lockmgr(&up_lk, LK_RELEASE);

	up_state = 2;			/* tell the reader we are done */
	wakeup(&up_state);
	kthread_exit();
}

static int
up_race(int raced)
{
	static const char iface1[] = "Generic USB interface hub";
	struct thread *td;
	char *desc;
	size_t len, oob;
	int error, i;

	up_state = 0;

	/* stale tail of scratch from an earlier (malformed bLength=255)
	 * fetch by the same malicious device: nonzero raw bytes all the
	 * way through data[254] - exactly what makes the raced strlen()
	 * leave the buffer instead of stopping inside it */
	for (i = 0; i < UP_SCRATCH_LEN; i++)
		up_zone[i] = 0x4E;
	/* adjacent heap after struct usb_device's scratch: marker bytes,
	 * first NUL far away (as in a sprayed slab neighborhood) */
	memset(up_zone + UP_SCRATCH_LEN, 0x5A, UP_ZSIZE - UP_SCRATCH_LEN);
	up_zone[UP_ZSIZE - 1] = 0;

	/* ---- attaching driver thread: device_set_usb_desc() ---- */
	/* under enum lock, fetch own iface string into scratch */
	lockmgr(&up_lk, LK_EXCLUSIVE);			/* usb_util.c:94 */
	bcopy(iface1, up_zone, sizeof(iface1));		/* fetch result */
	up_zone[sizeof(iface1) - 1] = 0;		/* *s = 0 */

	if (!raced) {
		lockmgr(&up_lk, LK_RELEASE);
	} else {
		lockmgr(&up_lk, LK_RELEASE);		/* usb_util.c:110-111 */

		/* concurrent same-udev enumeration op starts fetching a
		 * device-supplied string into the shared scratch */
		error = kthread_create(up_writer, NULL, &td, "upwriter");
		if (error) {
			kprintf("UPROBE case1: kthread_create %d\n", error);
			return (error);
		}
		/* wait until the raw descriptor copy has clobbered our
		 * string and the conversion loop is running */
		while (up_state == 0)
			tsleep(&up_state, 0, "upread", 2);
	}

	/* device_set_desc_copy() -> device_set_desc_internal():
	 * unbounded strlen() on scratch AFTER the lock was dropped */
	len = strlen(up_zone);

	/* dev->desc = kmalloc(strlen+1); strcpy() - what would get
	 * disclosed by device_printf(dev, "<%s>") */
	desc = kmalloc(len + 1, M_UPROBE, M_INTWAIT);
	strcpy(desc, up_zone);

	if (len >= UP_SCRATCH_LEN)
		oob = len - UP_SCRATCH_LEN + 1;
	else
		oob = 0;
	kprintf("UPROBE case1(%s): strlen(scratch)=%zu scratch=%d "
	    "bytes_past_boundary=%zu desc[0..25]=\"%.25s\" "
	    "desc[255..290]=\"%.36s\"\n",
	    raced ? "RACED" : "control", len, UP_SCRATCH_LEN, oob,
	    desc, desc + UP_SCRATCH_LEN);

	if (raced) {
		/* let the writer finish before we free anything */
		while (up_state != 2)
			tsleep(&up_state, 0, "upjoin", 2);
	}
	kfree(desc, M_UPROBE);
	return (0);
}

/*--------------------------------------------------------------------
 * sysctl front-end
 *------------------------------------------------------------------*/ 
static int	up_case;

static int
up_sysctl(SYSCTL_HANDLER_ARGS)
{
	int error, val = up_case;

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

	switch (val) {
	case 0:
		up_matrix();
		break;
	case 1:
		up_race(0);		/* control: not raced */
		up_race(1);		/* raced */
		break;
	case 2:
		kprintf("UPROBE case2: calling usb_make_str_desc(buf, 64, "
		    "NULL) - expect NULL-deref panic\n");
		{
			uint8_t ret = usb_make_str_desc(up_zone, 64, NULL);
			kprintf("UPROBE case2: returned %u (no panic)\n", ret);
		}
		break;
	default:
		return (EINVAL);
	}
	up_case = val;
	return (0);
}
SYSCTL_PROC(_hw, OID_AUTO, usutil_probe, CTLTYPE_INT | CTLFLAG_RW,
    &up_case, 0, up_sysctl, "I", "usb_util.c audit probe case");

static int
up_load(module_t mod, int what, void *arg)
{
	switch (what) {
	case MOD_LOAD:
		up_zone = kmalloc(UP_ZSIZE, M_UPROBE, M_INTWAIT | M_ZERO);
		if (up_zone == NULL)
			return (ENOMEM);
		lockinit(&up_lk, "upenumlk", 0, 0);
		kprintf("usbutil_probe loaded: sysctl hw.usutil_probe="
		    "0 (matrix) | 1 (DF-5604 race) | 2 (NULL panic)\n");
		up_matrix();	/* run the safe negative-evidence matrix now */
		return (0);
	case MOD_UNLOAD:
		kfree(up_zone, M_UPROBE);
		return (0);
	default:
		return (0);
	}
}

static moduledata_t up_mod = { "usbutil_probe", up_load, NULL };
DECLARE_MODULE(usbutil_probe, up_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);