DragonFlyBSD Kernel Audit
DF-1870 / harness.c
← back to finding ↓ download raw
/*
 * harness.c -- userspace proof of the OOB read in iscsi_r2t()
 *              (sys/dev/disk/iscsi/initiator/iscsi_subr.c:60-138)
 *
 * The kernel allocates csio->data_ptr as a buffer of edtlen bytes
 * (the SCSI WRITE transfer length). iscsi_r2t() then trusts r2t->ddtl
 * (attacker-controlled from the wire) as the loop bound and walks bp
 * forward, shipping `bs`-sized chunks of kernel memory to the malicious
 * iSCSI target via isc_qout().
 *
 * This harness reproduces that loop verbatim, modelling:
 *   - csio->data_ptr as a heap allocation of size edtlen (filled with a
 *     recognisable pattern)
 *   - the adjacent kernel heap as 'OTHERHEAP' bytes (so we can see what
 *     gets leaked)
 *   - isc_qout() as a sink that just records what the attacker would see
 *
 * Build:  cc -O2 -Wall -o harness harness.c
 * Run:    ./harness
 *
 * Expected output (bug present):
 *   edtlen=512  ddtl=1048576  -> leak=1047552 bytes past buffer end
 *   [OOB READ CONFIRMED] 1st leaked byte past buffer = 0xDE (sent to attacker)
 *   ... (printed only the first few leaked bytes from each Data-Out PDU)
 *
 * Expected output (after fix):
 *   [REJECTED by bounds check] bo=0 ddtl=1048576 edtl=512
 *   no leak
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>

#define OTHERHEAP  0x100000        /* pretend adjacent kernel heap region */

/* model of isc_qout's network sink: what the attacker target receives */
static size_t  g_leaked_bytes_total = 0;
static int     g_first_oob_byte     = -1;
static int     g_oob_observed       = 0;

static void isc_qout_sink(const void *ds, size_t len,
			  const void *buf_start, size_t buf_len)
{
	const unsigned char *p   = ds;
	const unsigned char *end = p + len;
	const unsigned char *bufend = (const unsigned char *)buf_start + buf_len;

	for (; p < end; p++) {
		if (p >= bufend) {
			/* this byte is past the legitimate buffer */
			if (!g_oob_observed) {
				g_first_oob_byte = *p;
				g_oob_observed   = 1;
			}
			g_leaked_bytes_total++;
		}
	}
}

/*
 * Verbatim port of iscsi_r2t() (iscsi_subr.c:60-138), with the kernel
 * plumbing (pdu_alloc, sessions, mbufs) elided. The control flow and
 * the *missing* bounds check are preserved exactly.
 *
 * Returns 0 normally; with the fix, returns -1 when the bounds check
 * rejects the R2T.
 */
static int iscsi_r2t_MODEL(unsigned char *csio_data_ptr, /* edtlen-sized buf */
			   size_t         edtlen,         /* sizeof csio->data_ptr */
			   uint32_t       bo_wire,        /* r2t->bo, attacker */
			   uint32_t       ddtl_wire,      /* r2t->ddtl, attacker */
			   size_t         maxXmitDS,      /* sp->opt.maxXmitDataSegmentLength */
			   int            apply_fix)
{
	unsigned char *bp = csio_data_ptr;   /* L82 */
	uint32_t       bo = bo_wire;          /* L84 */
	uint32_t       ddtl = ddtl_wire;      /* L79 */
	uint32_t       edtl = (uint32_t)edtlen;
	uint32_t       bleft, bs, dsn;

	/* THE FIX -- not present upstream */
	if (apply_fix) {
		if (bo > edtl || ddtl > edtl - bo) {
			fprintf(stderr,
				"[REJECTED by bounds check] bo=%u ddtl=%u edtl=%u\n",
				bo, ddtl, edtl);
			return -1;
		}
	}

	bleft = ddtl;                          /* L85 */
	if (maxXmitDS > 0)
		bs = (maxXmitDS < ddtl) ? maxXmitDS : ddtl; /* L88 */
	else
		bs = ddtl;                              /* L90 */
	dsn  = 0;

	while (bleft > 0) {                    /* L94 */
		uint32_t this_bs = (bs < bleft) ? bs : bleft; /* L116/L117 */

		/* wpq->pdu.ds = bp;  isc_qout(sp, wpq);  -- L120/L122 */
		isc_qout_sink(bp, this_bs, csio_data_ptr, edtlen);

		bo    += this_bs;             /* L126 */
		bp    += this_bs;             /* L127 */
		bleft -= this_bs;             /* L128 */
		dsn++;
	}
	return 0;
}

int main(int argc, char **argv)
{
	size_t   edtlen   = 512;        /* e.g. SCSI WRITE of one 512B sector */
	uint32_t ddtl     = 0x100000;   /* attacker: 1 MiB */
	uint32_t bo       = 0;
	size_t   maxXmitDS = 64 * 1024; /* typical default */

	if (argc > 1) edtlen   = (size_t)strtoul(argv[1], 0, 0);
	if (argc > 2) ddtl     = (uint32_t)strtoul(argv[2], 0, 0);
	if (argc > 3) maxXmitDS= (size_t)strtoul(argv[3], 0, 0);

	/* Allocate a buffer that is edtlen bytes (the legitimate CCB data
	 * buffer) PLUS a tail of "adjacent kernel heap" so the OOB walk has
	 * something readable to model the leak into. */
	unsigned char *heap = calloc(1, edtlen + OTHERHEAP);
	assert(heap);

	/* fill legitimate buffer with 0xAA; fill adjacent "heap" with 0xDE
	 * to model recognizable kernel memory that should NOT be sent. */
	memset(heap,                 0xAA, edtlen);
	memset(heap + edtlen,        0xDE, OTHERHEAP);

	/* ---- BUG PRESENT (no fix) ---- */
	fprintf(stderr, "=== iscsi_r2t() WITHOUT fix ===\n");
	fprintf(stderr, "edtlen=%zu  ddtl=%u  maxXmitDS=%zu\n",
		edtlen, ddtl, maxXmitDS);
	iscsi_r2t_MODEL(heap, edtlen, bo, ddtl, maxXmitDS, /*apply_fix=*/0);

	if (g_oob_observed) {
		size_t leaked = g_leaked_bytes_total;
		fprintf(stderr,
			"[OOB READ CONFIRMED] attacker received %zu bytes "
			"PAST the %zu-byte CCB buffer\n",
			leaked, edtlen);
		fprintf(stderr,
			"first leaked byte past buffer = 0x%02X (was 0xDE in our "
			"model of adjacent kernel heap)\n",
			g_first_oob_byte);
		if (leaked > 0) {
			fprintf(stderr,
				"VERDICT: iscsi_r2t() walks csio->data_ptr past "
				"its allocation -> kernel heap info leak.\n");
		}
	} else {
		fprintf(stderr, "no OOB (unexpected for bug-present case)\n");
	}

	/* ---- BUG FIXED (bounds check) ---- */
	g_leaked_bytes_total = 0;
	g_oob_observed       = 0;
	g_first_oob_byte     = -1;
	fprintf(stderr, "\n=== iscsi_r2t() WITH proposed fix ===\n");
	iscsi_r2t_MODEL(heap, edtlen, bo, ddtl, maxXmitDS, /*apply_fix=*/1);
	if (!g_oob_observed) {
		fprintf(stderr,
			"VERDICT: bounds check rejects the over-long R2T; "
			"0 bytes leaked past buffer.\n");
	}

	free(heap);
	return g_oob_observed ? 0 : 1;
}