深入分析Binder中的单指令竞态条件漏洞(一)

2021-01-06 8,000

在本文中,我们将为读者深入介绍Binder中的单指令竞态条件漏洞及其利用方法。

 

导言

 

在去年10月份的安卓安全公告中,漏洞CVE-2020-0423被公开,具体描述如下:

 

在binder.c的binder_release_work中,由于加锁不当,导致UAF漏洞。这可能导致内核中的本地提权漏洞,而不需要额外的执行权限。并且,利用该漏洞时无需用户交互。

 

CVE-2020-0423是Android中另一个可导致权限提升的漏洞。在这篇文章中,我们将深入分析这个漏洞,并构造一个可用于在Android设备上获取root权限的exploit。

 

CVE-2020-0423的根因分析

 

Binder概述

 

Android上的进程都是互相隔离的,也就是说,它们无法直接访问彼此的内存空间。然而,有时候它们却需要这样做,例如,无论是在客户机和服务器之间交换数据,还是仅仅在两个进程之间共享信息的时候。

 

Android上的进程间通信由Binder负责的。该内核组件提供了一个用户可访问的字符设备,可用于调用远程进程中的例程,并向其传递参数。实际上,Binder不仅充当了两个任务之间的代理,还负责在数据交换期间处理内存分配以及管理共享对象的生命周期的任务。

 

如果您不熟悉Binder的内部结构,不妨先参阅这方面的一些文章,例如Synacktiv撰写的“Binder Transactions In The Bowels of the Linux Kernel”,这对于理解这篇文章的其余部分会很有帮助。

 

补丁分析与简要说明

 

CVE-2020-0423的补丁程序于10月10日upstream至Linux内核,并带有以下提交消息:

 

1.png

 

上面的文字粗略地概述了利用这个漏洞触发Use-After-Free或UAF漏洞所需的不同步骤。这些步骤将在下一节中详细加以介绍,现在让我们先来看看补丁程序,以了解漏洞的根源在哪里。

 

本质上,这个补丁所做的事情就是将函数binder_dequeue_work_head的内容内联到binder_release_work函数中。唯一的区别是,在仍然对proc加锁的同时,读取binder_work结构体的type字段。

 

// 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

 

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);

        wtype = w ? w->type : 0;

        binder_inner_proc_unlock(proc);

        if (!w)

            return;

 

        switch (wtype) { /* <--- Use-after-free not possible anymore */

 

// [...]

 

在这个补丁之前,可以让一个binder_work结构体退出队列,释放另一个线程并重新分配,然后更改binder_release_work的控制流。下一节中,我们将更深入地解释为什么会发生这种行为,以及它是如何被任意触发的。

 

深入分析

 

在本节中,作为一个示例,让我们想象有两个进程使用binder进行通信,其中,一个进程为发送方,另一个进程为接收方。

 

此时,触发该漏洞需要三个先决条件:

 

  1. 从发送方线程调用binder_release_work

  2. 从发送方线程的todo列表中出列的binder_work结构体

  3. 从接收方线程中释放的一个binder_work结构体

 

让我们逐一进行研究,并尝试找出实现它们的方法。

 

调用binder_release_work

 

实现这个前提条件非常简单。如前所述,当使用binder处理任务时,binder_release_work是清理例程的一部分。我们可以使用ioctl命令binder_thread_exit在线程中显式调用它。

 

// Userland code from the exploit

 

int binder_fd = open("/dev/binder", O_RDWR);

// [...]

ioctl(binder_fd, BINDER_THREAD_EXIT, 0);

 

这个ioctl最终将调用位于drivers/android/binder.c文件中的内核函数binder_ioctl。

 

然后,binder_ioctl将到达BINDER_THREAD_EXIT分支,并调用binder_thread_release函数。其中,thread是一个binder_thread结构体,保存当前进行ioctl调用的线程的信息。

 

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;

 

    // [...]

 

在binder_thread_release函数的尾部,出现了对binder_release_work函数的调用。

 

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;

}

 

请注意,在调用bind_release_work时,参数list_head *list的值为&thread->todo。当我们尝试用binder_work结构体填充该列表时,将涉及下节的内容。

 

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;

 

    // [...]

 

既然我们知道了如何触发易受攻击的函数,那么让我们来确定如何用任意的binder_work结构体来填充线程的TODO列表。

 

从发送方线程中取出binder_work结构体

 

binder_work结构体被安插到thread->todo的两个位置处:

 

  •     binder_enqueue_deferred_thread_work_ilocked

 

static void

binder_enqueue_deferred_thread_work_ilocked(struct binder_thread *thread,

                        struct binder_work *work)

{

    binder_enqueue_work_ilocked(work, &thread->todo);

}

 

  •     binder_enqueue_thread_work_ilocked

 

static void

binder_enqueue_thread_work_ilocked(struct binder_thread *thread,

                   struct binder_work *work)

{

    binder_enqueue_work_ilocked(work, &thread->todo);

    thread->process_todo = true;

}

 

这些函数在代码中的不同地方都有用到,但是我们感兴趣的代码路径是从binder_translate_binder开始的那个。当线程发送包含BINDER_TYPE_BINDER或BINDER_TYPE_WEAK_BINDER的事务时将调用该函数。

 

之后,从binder对象创建一个binder节点,并令接收端进程的引用计数器加1。只要接收进程持有对该节点的引用,它就会在Binder的内存中保持活动状态。但是,如果进程释放该引用,那么该节点将被销毁,这也是我们稍后将尝试实现的触发UAF的功能。

 

首先,让我们解释一下在调用binder_inc_ref_for_node的过程中,binder节点和thread -> todo列表的关系。

 

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);

    // [...]

}

 

binding_inc_ref_for_node的参数如下所示:

 

  •      structinder_proc * proc:保存对节点的引用的进程

  •      struct binder_node * node:目标节点

  •      bool strong:true = 强引用,false = 弱引用

  •      struct list_head * target_list:如果节点增加,则使用的工作列表

  •      struct binder_ref_data * rdata:引用的ID/引用计数数据

 

当前路径中的target_list是thread->todo。该参数仅在调用binder_inc_ref_olocked的binder_inc_ref_for_node中使用。

 

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);

    // [...]

}

 

然后,binder_inc_ref_olocked会调用binder_inc_node,不管它是弱引用还是强引用。

 

static int binder_inc_ref_olocked(struct binder_ref *ref, int strong,

                  struct list_head *target_list)

{

    // [...]

            // Strong ref path

            ret = binder_inc_node(ref->node, 1, 1, target_list);

    // [...]

            // Weak ref path

            ret = binder_inc_node(ref->node, 0, 1, target_list);

    // [...]

 

}

 

binder_inc_node是binder_inc_node_nilocked的一个简单封装器,它在当前节点上持有一个锁。

 

binder_inc_node_nilocked最后将调用:

 

  •     binder_enqueue_deferred_thread_work_ilocked,如果节点上有一个强引用的话;

  •     binder_enqueue_work_ilocked,如果节点上有弱引用的话。

 

在实践中,引用是弱是强都无关紧要。

 

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);

        }

    } else {

        // [...]

        if (!node->has_weak_ref && list_empty(&node->work.entry)) {

            // [...]

            binder_enqueue_work_ilocked(&node->work, target_list);

        }

    }

    return 0;

}

 

这里需要注意的是,实际上是node->work字段被被安插在thread->todo列表中,因此,它并不是普通的binder_work结构体。这是因为binder_node嵌入了一个binder_work结构体。这意味着,要触发这个漏洞,我们要释放的不是一个单独的binder_work结构体,而是要释放整个binder_node。

 

到目前为止,我们介绍了如何填充thread->todo列表,以及如何调用易受攻击的函数binder_release_work来访问可能被释放的binder_work/binder_node结构体。剩下的唯一一步就是想办法释放我们在线程中分配的binder_node。

 

小结

在本系列文章中,我们将为读者深入介绍Binder中的单指令竞态条件漏洞及其利用方法。由于篇幅过长,我们将分多篇文章发表,更多精彩内容,敬请期待!

 

(未完待续)


本文作者:mssp299

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/150966.html

Tags:
评论  (0)
快来写下你的想法吧!

mssp299

文章数:51 积分: 662

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号