DragonFlyBSD Kernel Audit
DF-0874 / harness.c
← back to finding ↓ download raw
/*
 * DF-0874 deterministic harness -- replicates the EXACT kernel walks in
 * sys/vfs/ntfs/ntfs_vnops.c:585-586 (ntfs_readdir UNBOUNDED inner loop) and
 * sys/vfs/ntfs/ntfs_subr.c:1176 (ntfs_ntreaddir BOUNDED loop), feeds them the
 * same crafted INDEX_ROOT data the live crafter produces, and proves:
 *
 *   - subr.c bounded walk stays inside [rdbuf, rdbuf+rdsize)   (safe)
 *   - vnops.c unbounded walk reads past rdbuf+rdsize / faults   (the bug)
 *
 * The harness allocates f_dirblbuf exactly as the kernel does
 * (kmalloc(max(va_datalen, ir_size)) -- here malloc(3) + a guard page), fills
 * the first va_datalen bytes with crafted index-root data, and runs BOTH
 * loops. A SIGSEGV handler reports the exact byte offset at which the unbounded
 * walk dereferences past the buffer (mirrors a kernel page-fault panic).
 *
 * Build:  cc -O2 -o harness harness.c
 * Run:    ./harness [reclen_hex] [with_last 0|1] [residue 0|1]
 *         defaults: reclen=0xFFF0  with_last=0  residue=1(poison)/0(zeros)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <signal.h>
#include <unistd.h>
#include <sys/mman.h>

/* ---- mirror of struct attr_indexentry (ntfs.h:170, natural align) ---- */
#define IE_RECLN_OFF   0x08
#define IE_FLAG_OFF    0x0C
#define IE_FNAMEN_OFF  0x50
#define IE_FNAME_OFF   0x52
#define IE_HDRSZ       82    /* fixed header up to ie_fname */
#define NTFS_IEFLAG_LAST   0x00000002

static const char *g_rdbuf;
static uint32_t    g_bufsz;
static uint32_t    g_rdsize;
static volatile sig_atomic_t g_fault_off;
static int g_faulted;

static void sigh(int s, siginfo_t *si, void *uc)
{
	(void)s; (void)uc;
	long off = (char *)si->si_addr - g_rdbuf;
	g_fault_off = (uint32_t)off;
	g_faulted = 1;
	/* The unbounded walk hit unmapped memory (guard page / past allocation):
	 * this is the userspace analogue of the kernel page-fault panic. */
	printf("  [SIGSEGV] vnops walk dereferenced rdbuf+%ld (bufsz=%u, "
	       "rdsize=%u) -> kernel would fatal-trap 12 (page fault) here.\n",
	       off, g_bufsz, g_rdsize);
	_exit(0);   /* don't return from the handler; treat as end-of-walk */
}

static const char *subr_walk(const char *rdbuf, uint32_t rdsize, uint32_t aoff,
                             uint32_t target_num, uint32_t *walked_to)
{
	/* EXACT transliteration of ntfs_subr.c:1176-1178 */
	uint32_t cnum = 0;
	const char *iep = rdbuf + aoff;
	for (;;) {
		uint32_t flag, reclen;
		/* bounded check BEFORE deref of next -- but the CURRENT iep must
		 * also be in-bounds to read flag; the kernel relies on the data
		 * being valid. We emulate the kernel's trust. */
		if (!(rdsize > aoff)) break;          /* walked past data: stop */
		memcpy(&flag, iep + IE_FLAG_OFF, 4);
		if (flag & NTFS_IEFLAG_LAST) break;
		/* body */
		if (cnum >= target_num) { *walked_to = aoff; return iep; }
		cnum++;
		memcpy(&reclen, iep + IE_RECLN_OFF, 2);
		aoff += reclen;
		iep = rdbuf + aoff;
	}
	*walked_to = aoff;
	return NULL;
}

static int vn_max_off;
static int vn_oob_derefs;
static int vn_steps;
static int vn_cap = 200000;

static void vnops_walk(const char *rdbuf, uint32_t rdsize, const char *iep)
{
	/* EXACT transliteration of ntfs_vnops.c:585-586:
	 *   for (; !(iep->ie_flag & LAST); iep = NTFS_NEXTREC(iep))
	 * with NTFS_NEXTREC(s) = (s + s->reclen). NO bounds check. */
	for (;;) {
		long off = iep - rdbuf;
		if (off > vn_max_off) vn_max_off = off;
		if ((uint32_t)off >= rdsize) vn_oob_derefs++;
		uint32_t flag;
		memcpy(&flag, iep + IE_FLAG_OFF, 4);   /* may SIGSEGV -> sigh() */
		if (flag & NTFS_IEFLAG_LAST) break;
		vn_steps++;
		uint16_t reclen;
		memcpy(&reclen, iep + IE_RECLN_OFF, 2);
		if (reclen == 0) {
			printf("  [vnops] reclen==0 at off=%ld -> kernel infinite-loop "
			       "hang (DoS)\n", off);
			return;
		}
		iep += reclen;                          /* NTFS_NEXTREC */
		if (--vn_cap <= 0) {
			printf("  [vnops] safety cap at off=%ld\n", off);
			return;
		}
	}
}

struct args { uint32_t evil_reclen; int with_last; int poison; };

static uint32_t build_index_root(char *out, uint32_t outcap, struct args a)
{
	char *p = out;
	/* 32-byte attr_indexroot header */
	uint32_t ir_size = 0x1000;
	uint32_t z[8]; memset(z, 0, sizeof(z));
	z[2] = ir_size;            /* ir_size field */
	memcpy(p, z, 32); p += 32;

	/* evil entry[0] -- exactly what craft_img.py emits */
	char e[IE_HDRSZ + 4*2];    /* header + 4 wchars */
	memset(e, 0, sizeof(e));
	uint32_t num = 0x41414141; memcpy(e+0, &num, 4);
	uint16_t rl = a.evil_reclen; memcpy(e+IE_RECLN_OFF, &rl, 2);
	uint32_t fl = 0; memcpy(e+IE_FLAG_OFF, &fl, 4);   /* NOT LAST */
	e[IE_FNAMEN_OFF] = 4;                                /* ie_fnamelen */
	e[IE_FNAMEN_OFF+1] = 1;                              /* ie_fnametype */
	uint16_t wc = 0x41; for (int i=0;i<4;i++) memcpy(e+IE_FNAME_OFF+2*i,&wc,2);
	/* entry occupies IE_HDRSZ + fnamelen*2 = 90 bytes; reclen must be >= that.
	 * The on-disk reclen is the ATTACKER value (may be larger or smaller
	 * than the real body). We write exactly the body bytes. */
	size_t body = IE_HDRSZ + 4*2;
	memcpy(p, e, body); p += body;

	if (a.with_last) {
		/* place a proper terminal LAST entry. Its on-disk offset must equal
		 * entry[0]'s reclen for a *valid* image; we honor the attacker reclen
		 * so with_last exercises the "did the LAST entry save us?" question. */
		char last[0x58]; memset(last, 0, sizeof(last));
		uint32_t lf = NTFS_IEFLAG_LAST; memcpy(last+IE_FLAG_OFF, &lf, 4);
		uint16_t lr = 0x58; memcpy(last+IE_RECLN_OFF, &lr, 2);
		memcpy(p, last, 0x58); p += 0x58;
	}
	uint32_t datalen = (uint32_t)(p - out);
	/* patch ir_datalen / ir_allocated (header+0x14 / +0x18) */
	uint32_t dl = datalen - 32;
	memcpy(out+0x14, &dl, 4);
	memcpy(out+0x18, &dl, 4);
	(void)outcap;
	return datalen;
}

int main(int argc, char **argv)
{
	struct args a;
	a.evil_reclen = (argc > 1) ? (uint32_t)strtoul(argv[1], 0, 0) : 0xFFF0;
	a.with_last   = (argc > 2) ? atoi(argv[2]) : 0;
	a.poison      = (argc > 3) ? atoi(argv[3]) : 1;

	/* SIGSEGV handler -> reports the OOB offset */
	struct sigaction sa; memset(&sa, 0, sizeof(sa));
	sa.sa_sigaction = sigh; sa.sa_flags = SA_SIGINFO;
	sigaction(SIGSEGV, &sa, NULL);

	char indexroot[8192];
	uint32_t datalen = build_index_root(indexroot, sizeof(indexroot), a);
	uint32_t ir_size = 0x1000;
	uint32_t rdsize = datalen;           /* va_datalen for INDXROOT */
	uint32_t bufsz  = (datalen > ir_size) ? datalen : ir_size;
	if (bufsz < 4096) bufsz = 4096;

	long pg = 4096;
	size_t maplen = ((bufsz + pg - 1) / pg + 1) * pg;
	char *raw = mmap(NULL, maplen + pg, PROT_READ|PROT_WRITE,
	                 MAP_PRIVATE|MAP_ANON, -1, 0);
	if (raw == MAP_FAILED) { perror("mmap"); return 2; }
	mprotect(raw + maplen, pg, PROT_NONE);              /* guard page */
	char *rdbuf = raw + (maplen - bufsz);
	memcpy(rdbuf, indexroot, datalen);
	if (a.poison)
		for (size_t i = datalen; i+3 < bufsz; i += 4) {
			rdbuf[i]=0xde; rdbuf[i+1]=0xad; rdbuf[i+2]=0xc0; rdbuf[i+3]=0xde;
		}
	else
		memset(rdbuf + datalen, 0, bufsz - datalen);    /* fresh-page zeros */

	g_rdbuf = rdbuf; g_bufsz = bufsz; g_rdsize = rdsize;

	printf("=== DF-0874 harness ===\n");
	printf("f_dirblbuf         = %u bytes (kmalloc)\n", bufsz);
	printf("INDEX_ROOT rdsize = %u bytes (valid data)\n", rdsize);
	printf("evil entry[0]     : reclen=0x%04x ie_flag=0 (NOT LAST)\n",
	       a.evil_reclen);
	printf("terminal LAST     : %s\n", a.with_last ? "present (valid image)"
	                                        : "ABSENT (crafted)");
	printf("residue [%u..%u)  : %s\n", rdsize, bufsz,
	       a.poison ? "0xdeadc0de (INVARIANTS poison)" : "0x00 (fresh page)");
	printf("\n");

	uint32_t aoff = 32;   /* sizeof(attr_indexroot) */
	uint32_t subr_walked = 0;
	const char *match = subr_walk(rdbuf, rdsize, aoff, 0, &subr_walked);
	printf("[subr.c:1176 BOUNDED walk] find entry 0:\n");
	printf("  walked to off=%u (rdsize=%u): %s\n", subr_walked, rdsize,
	       subr_walked < rdsize  ? "IN-BOUNDS (safe)" :
	       subr_walked == rdsize ? "stopped at rdsize boundary (safe)" :
	                               "PAST rdsize");
	if (!match) { printf("  no match (subr walk never returned entry)\n"); }
	else {
		uint32_t num; memcpy(&num, match, 4);
		uint16_t rl;  memcpy(&rl, match+IE_RECLN_OFF, 2);
		uint32_t fl;  memcpy(&fl, match+IE_FLAG_OFF, 4);
		printf("  returned @ off %ld: ie_number=0x%08x reclen=0x%04x "
		       "ie_flag=0x%08x\n", match-rdbuf, num, rl, fl);

		printf("\n[vnops.c:585 UNBOUNDED walk] from returned entry:\n");
		vnops_walk(rdbuf, rdsize, match);
		printf("  steps=%d  max_off=%d (bufsz=%d, rdsize=%u)  "
		       "oob_derefs=%d\n", vn_steps, vn_max_off, bufsz, rdsize,
		       vn_oob_derefs);
		if (g_faulted) {
			printf("  => BUG: walk page-faulted at rdbuf+%u (past bufsz=%u) "
			       "=> kernel panic 'fatal trap 12'.\n", g_fault_off, bufsz);
		} else if (vn_oob_derefs > 0) {
			printf("  => BUG CONFIRMED: ntfs_readdir dereferenced index "
			       "entries %d time(s) PAST the INDEX_ROOT valid data "
			       "(rdsize=%u) using attacker reclen=0x%04x as stride.\n",
			       vn_oob_derefs, rdsize, a.evil_reclen);
		} else if (vn_max_off >= (int)bufsz) {
			printf("  => BUG CONFIRMED: walk reached off=%d >= bufsz=%d.\n",
			       vn_max_off, bufsz);
		} else {
			printf("  => walk terminated at off=%d (residue byte at ie_flag "
			       "acted as LAST). The OOB deref of ie_flag STILL occurred "
			       "(read residue at rdbuf+%d). On a non-INVARIANTS kernel "
			       "(zeros) the walk continues and leaks ie_number/ie_fname.\n",
			       vn_max_off, vn_max_off + IE_FLAG_OFF);
		}
	}
	munmap(raw, maplen + pg);
	return 0;
}