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特权。
如我们之前的文章中所述,当驱动程序在处理经过验证的binder transaction时,我们可以破坏它的某些部分,可以在两个阶段使用这些值作为攻击目标:
分析一下绑定驱动程序中的transaction缓冲区清理代码,同时考虑到我们可能损坏了transaction数据:
在[1]处,驱动程序检查当前transaction是否存在目标绑定程序节点,如果存在,则递减其引用计数,如果其引用计数达到零,它可能触发释放此节点,但是我们无法控制此指针。
在[2]中,驱动程序遍历transaction中的所有对象,并进入switch语句,在其中为每种对象类型执行所需的清除。对于类型BINDER_TYPE_BINDER和BINDER_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完全相同。但是,我们在该漏洞利用中使用的某些技术在最近的内核中不再可用。
绑定程序驱动程序的设计方式是,transaction只能发送到你从其他进程收到的句柄或上下文管理器(句柄0)。通常,当要与服务对话时,他们首先向上下文管理器 (用于当前版本的Android的三个Binder域的servicemanager,hwservicemanager或vndservicemanager)请求句柄 。
如果服务代表客户端创建子服务或对象,则该服务将发送一个句柄,以便客户端可以与新对象对话。
在某些情况下,控制通信的两端,例如对条件竞争进行更好的定时控制将是有益的。在我们的特定情况下,需要在发送transaction时知道接收方绑定程序映射的地址,以避免崩溃。另外,为了使我们拥有的损坏原语可以UAF,接收过程必须创建绑定节点,fp-> binder字段等于我们要破坏的 sg_buf值。
满足所有这些约束的最简单方法是控制transaction的发送端和接收端。在这种情况下,我们可以访问所有必需的值,而无需使用信息泄漏来从远程进程中检索它们。
但是,我们不允许通过上下文管理器 从非特权应用程序注册服务 ,因此不能走常规路线。我们 在/ dev / hwbinder域中使用了 ITokenManager服务来设置通信通道,此服务最初由Gal Beniamini在“project-zero”报告中公开使用:
为了利用自己的“进程”,我们在漏洞利用中使用了相同的机制。然而,需要注意的是“进程”在这里并不一定代表实际的进程,而是一个binder_proc关联到一个binder文件描述符结构。
这意味着我们可以打开两个binder文件描述符,通过第一个文件描述符创建一个令牌,然后从第二个文件描述符中检索它。这样,我们已经收到第一个文件描述符拥有的句柄,现在可以在两者之间发送binder transaction。
绑定程序节点由驱动程序以两种不同的方式使用:作为transaction内容的一部分,以便将它们从一个进程传递到另一个进程,或作为transaction的目标。当用作transaction的一部分时,总是从节点的rb-tree中检索这些节点,并对引用进行正确计数。当我们导致节点“UAF”时,它也会从rb-tree中删除。因此,当用作transaction目标时,我们只能悬挂指向已释放节点的指针,因为在这种情况下,驱动程序将指向实际的binder_node的指针存储在transaction-> target_node中。
binder驱动程序中有很多对target_node的引用,但是许多引用是在transaction的发送路径或调试代码中执行的。与其他相比,transaction接收路径为我们提供了一种将某些数据泄漏回用户空间的方法:
在[1],驱动程序从target_node提取两个64位值到transaction_data结构中,稍后将此结构复制到[2]的 userland 。因此,如果在释放其target_node并将其替换为另一个对象之后接收到transaction,则可以读取对应于ptr和cookie的偏移处的两个64位字段。
如果我们在gdb上查看此结构编译内核,我们可以分别在偏移量0x58和0x60处看到这些字段:
因此,我们需要找到可以随意分配和释放的对象,并且这些对象在这些偏移处包含有趣的数据。当我们最初向Google报告此漏洞时,我们编写了覆盖selinux_enforcing的漏洞利用,并且使用了kgsl_drawobj_sync,该泄漏会泄漏指向自身的指针和指向内核函数的指针。这对于漏洞验证就足够了,但对于我们在此描述的完整的root攻击来说,还不够。
对于完整的漏洞利用,我们使用了与CVE-2019-2025漏洞利用相同的对象:用于跟踪事件轮询中监视文件的Epitem 结构:
如上所示,fllink链表与泄漏字段重叠。此列表由eventpoll用于链接正在监视同一struct文件的所有Epitem结构。因此,我们可以泄漏一对内核指针。
这里有几种可能性,如果对于一个特定的struct文件只有一个这样的表位结构,那么数据结构是什么样的:
因此,我们应该泄漏fllink的内容epitem,我们发送到两个相同的指针到文件结构。现在考虑如果在同一文件上有第二个epitem会发生什么:
在这种情况下,如果我们同时从两个表位泄漏,我们将获得它们的地址以及struct文件的地址。
在我们的漏洞利用中,在将它们用于写原语之前,我们将使用这两种技巧来公开结构文件指针和已释放节点的地址。
但是请注意,为了泄漏数据,我们需要将待处理的放在队列中,直到可以触发错误并释放binder_node为止。漏洞利用是通过为每个未决事务分配专用线程,然后根据释放节点所需的transaction次数来减少引用计数来实现的。在这种情况发生之后,我们可以随时根据需要释放释放的缓冲区,与创建的待处理transaction一样多。
为了识别内存写入原语,我们转向transaction-> target_node字段的另一种用法:前面讨论的binder_transaction_buffer_release中引用计数的递减。假设已经用完全受控的对象替换了释放的节点,在这种情况下,驱动程序使用以下代码减少节点的引用计数:
我们可以设置节点数据,以便到达[1]处的else分支,并确保node-> proc为NULL。在这种情况下,我们首先在[2]到达list_empty检查。要绕过此检查,需要设置一个空列表(即next和prev指向list_head本身),这就是为什么需要首先泄漏节点地址的原因。
一旦绕过了[2]处的检查,就可以通过受控数据到达[3]处的hlist_del。该函数执行以下操作:
现在就变成了经典的unlink,其中我们可以设置 X = Y*和(Y + 8)= X。因此,拥有两个可写的内核地址,可以使用它来破坏某些数据。此外,如果将next设置为NULL*,则仅具有一个内核地址就可以执行一个8字节的NULL写入。
上面描述的获取导致内存崩溃的unlink原语的步骤假定可以用受控对象替换释放的对象。我们不需要完全控制该对象,而只需要通过所有检查并触发hlist_del原语而不会崩溃即可。
为了实现这一点,我们使用了一种众所周知的技术:通过sendmsg syscall 堆喷控制消息。该系统调用的代码如下所示:
如果请求的控制消息长度大于本地ctl缓冲区,则在[1]处将在内核堆上分配一个缓冲区。在[2]处,从用户区复制控制消息,最后在[3]处处理消息后,释放分配的缓冲区。
一旦目标套接字缓冲区已满,我们将使用阻塞调用来使系统调用阻塞,因此在点[2]和[3]之间的线程之后进行阻塞。这样,我们可以控制替换对象的生存期。
还可以利用Jann Horn在其PROCA攻击中使用的方法:让sendmsg调用完成,并立即使用signalfd文件描述符重新分配该对象,这样的好处是不需要为每个分配使用单独的线程。
在任何情况下,使用这种类型的喷射,我们都可以按照几乎完全控制的方式重新分配释放的binder节点,以便触发前面所述的写原语。
但是要注意的一件事是,如果我们的喷射失败,由于释放的内存上要执行的操作和检查量很大,最终将导致内核崩溃。但是,这种UAF的特性非常好,只要我们不触发写原语,就可以简单地关闭binder文件描述符,并且不会对内核有任何影响。
因此,在尝试触发写原语之前,我们使用泄漏原语来验证是否已成功重新分配节点。为此,只需拥有大量待处理的transaction,并在每次需要从释放的对象中泄漏一些数据时读取一个transaction。如果数据不是我们期望的,可以简单地关闭binder文件描述符,然后重试。
即使存在相对不可靠的重新分配,此属性也使漏洞利用非常可靠。
此时,我们使用与OffensiveCon 2020演讲中所述相同的任意读取技术。也就是说,我们破坏了file-> f_inode并使用以下代码 执行读取:
如果看了我们的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字段对齐,将能够执行任意读取。
下图以图形方式展示了设置:
我们破坏了基于epitem的 fllink.next 内存区域 ,由于我们获得了写原语就可以指回file->f_inode 。如果曾经使用过此字段,则可能会出现问题,但是由于我们是这些struct文件和 Epitem实例的唯一用户,因此 只需避免调用任何使用它们的API就可以了。
基于上面描述的设置,我们现在可以如下构建一个任意读取原语:
请注意,我们的数据字段设置 epitem到 地址- 24,其中24偏移的 s_blocksize的结构。同样,即使 s_blocksize原则上是64位长,但 ioctl代码仅将32位复制回用户区,因此如果要读取64位值,我们需要读取两次。
现在有了一个任意的读取原语,并且从最初的泄漏中知道了结构文件的地址,我们可以简单地读取f_op 字段来检索内核.text指针。
这会导致完全绕过KASLR:
现在我们知道了内核基地址,可以使用写原语在selinux_enforcing变量上写一个NULL qword 并将SELinux设置为许可模式。我们的漏洞利用程序在设置任意写入原语之前会执行此操作,因为我们提出的技术实际上需要禁用SELinux。
在考虑了几种选择之后,我们最终决定攻击内核用来处理/ proc / sys的sysctl表以及从那里hook所有数据。有许多描述这些变量的全局表,例如下面的kern_table:
例如,第一个变量是“ 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如下图所示:
如果查看这些节点的字母顺序,可以看到所有左子节点均按字母降序排序。实际上,这是这些树中的平衡规则:左子级必须比当前节点低,而右子级必须更高。
因此,为了确保我们保持树的平衡,请使用我们的write8 原语,向最左边的节点添加名称以“ aaa”开头的左子节点。以下代码在prev_node中找到树的最左节点,这将是我们假节点的插入点:
为了插入新节点,我们需要在内核存储器中为其找到一个位置。这是必需的,因为现代移动手机都启用了PAN(永远访问权限),可以防止内核无意中使用用户区内存。假设我们有一个任意的读取原语,可以通过解析从current-> mm-> pgd开始的进程的页表,并在physmap中找到其中一个页面的地址来解决这个问题。另外,使用我们自己的用户空间页面的physmap别名是理想的选择,因为可以轻松地编辑节点以更改要定位的数据的地址,从而为我们提供了灵活的读/写原语。
通过以下方式解析physmap别名:
请注意,我们需要读取memstart_addr的内容,以便能够在物理地址和相应的physmap地址之间进行转换。运行此代码后,在进程地址空间的0x200000处找到的数据也可以在内核域的kaddr中找到。
这样,就可以如下设置新的sysctl节点:
在/ proc / sys / aaa_ [random]中创建一个具有读/写权限的文件,并使用proc_douintvec处理读/写。此函数将数据字段作为要读取或写入的指针,并允许最多以无符号整数读取或写入max_size个字节。
这样,我们可以如下设置写原语:
一旦我们在Pixel手机上具有读/写功能,获得root访问权限就像从root任务复制凭据一样简单。由于我们早先已经禁用了SELinux,因此只需要查找初始化凭据,增加其引用计数并将它们复制到我们的进程中,如下所示:
但是,还有一些工作要完成,因为我们已经破坏了内核领域的相当多的内存,一旦退出当前进程并执行shell,事情就会崩溃。我们需要修复一些事情:
通过sendmsg重新分配了用于执行写原语的binder_node结构,但在执行写操作时再次将其释放。我们需要确保相应的线程在从sendmsg返回时不会再次释放这些对象。为此解析线程堆栈,并用ZERO_SIZE_PTR替换对这些节点的所有引用。
我们已经修改了f_inode一个的结构文件,它现在指向到一个中间epitem。解决此问题的最简单方法是简单地增加该文件的引用计数,以使永远不会调用release。
清除所有这些混乱之后,最终可以执行我们的root shell并看到uid 0而不会导致手机崩溃。
以下视频显示了使用我们刚刚描述的漏洞从adb shell root手机的过程:
可以在Blue Frost Security GitHub上找到本文和上一篇文章中描述的漏洞利用代码。自2020年2月起,该漏洞仅在使用固件的Pixel 3手机上进行了测试,并且需要针对其他固件进行调整。特别是,漏洞利用中使用了许多内核偏移,以及在内核版本之间可能有所不同的结构偏移。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课