前言
近期,我们在进行Zimperium zLabs平台的研究过程中,披露了一个缓冲区溢出漏洞,该漏洞影响到Google的多个Android DRM服务。在我们提交漏洞情况之后,Google将其定级为高危,并指定了CVE-2017-13253编号,该漏洞已经在2018年3月份的安全更新中实现了修复。
在本文,我们将介绍该漏洞的详细信息。首先会介绍相关背景信息,包括常规的Android运行机制和与该漏洞相关的机制。我们将重点介绍最近推出的treble计划以及其意义。随后,将研究如何利用某些设备上的其他故障来修复该漏洞,以此获得root权限。最后,我们将一同研究该漏洞的成因,并探究该漏洞的缓解与防范方式。尽管Google声称Treble计划能增强安全性,但我们还是看到了它的不足之处。
请读者注意,在本篇文章之中,我们将详细介绍大量与该漏洞相关的背景信息。如果您已经拥有Binder和libbinder的使用经验,可以跳过第一部分,直接从具体的漏洞分析开始阅读。
Android的Binder与安全
在包括Android在内的许多操作系统中,为了实现进程之间通信(IPC),都使用了一种常见的安全模型。借助该模型,在非特权进程中运行的不可信代码可以与特权进程(服务)进行通信,并要求它们执行操作系统允许的制定操作。该模型依赖于服务与IPC机制自身,可以正确验证来自非特权进程的每一个输入。但是反之,这也意味着这些服务中一旦出现错误,特别是输入验证部分的错误非常容易导致漏洞。
举例来说,Rani Idan在Zimperium发现的最新iOS漏洞( https://blog.zimperium.com/cve-2018-4087-poc-escaping-sandbox-misleading-bluetoothd/ )就属于上述方法。在IPS输入验证中存在一个问题,从而允许攻击者从非特权应用程序执行具有更高权限的代码。
在Android系统中,IPC机制被称为Binder。Android Binder服务的安全性一直是许多漏洞研究者的一个研究方向( https://www.blackhat.com/docs/asia-16/materials/asia-16-He-Hey-Your-Parcel-Looks-Bad-Fuzzing-And-Exploiting-Parcelization-Vulnerabilities-In-Android-wp.pdf )。Binder具有非常多有用的功能。举例来说,它允许进程之间传输文件描述符或对其他Binder服务的引用等较为复杂的对象。为了保持简洁和良好性能,Binder佳你给每个事务都限制在1MB大小之内。如果进程需要传输大量数据,可以使用共享内存来实现快速共享数据。
Binder的C++库
Android的Binder库(libbinder)为依赖于Binder的C++代码提供了很多抽象,从而可以允许调用C++类的远程实例的方法。
只要是使用了此类机制的对象,都会在预定义的结构中,实现了下面的几个类:
1、一个接口类,定义了可以通过Binder调用的对象的方法,以“I”为前缀;
2、负责序列化输入和反序列化输出的“客户端”类,以“Bp”为前缀;
3、负责反序列化输入和序列化输出的“服务器端”类,以“Bn”为前缀。
最终,在使用该对象时,几乎始终是使用接口类。这样一来,就可以以相同的方式处理对象,无论其处于同一个进程中还是处于不同的进程中。、
代码中“服务器端”部分通常都位于特权服务的内部,尽管在某些情况下角色是相反的,因此通常会负责验证输入。验证代码可以从Bn*类开始,并沿着随后调用的方法继续进行。这显然是脆弱性研究过程中最有意思的部分。
ICrypto接口和解密方法
在介绍完Binder之后,就让我们来看看与漏洞相关的具体实现。 Mediadrmserver服务(该服务负责DRM媒体)提供了一个加密对象的接口,接口名为ICrypto。 在这里需要注意,最近这一对象已经变为了CryptoHal,我们将在稍后详细讨论。 该接口通常的用途是允许非特权应用程序解密需要较高权限解密的DRM数据,比如访问TEE。受文章篇幅所限,加密相关的细节不在此次讨论范围之内,我们会将重点放在输入验证上面。
ICrypto有多种方法,但无疑最重要的方法是解密,这也正是大多数人的研究方向。
在解密签名中,最引人注意的事情之一就是输入的复杂程度。由于每个参数都通过Binder进行传输,并且需要进行验证,因此复杂的输入会导致验证代码更为复杂。也就是说,这些代码可能容易受到漏洞的影响。
我们来看一些参数:
1、Mode:这是一个控制加密模式的枚举。其中的一个模式是kMode_Unencrypted,它表示数据实际上未被加密。该模式意味着数据只会从一个地方简单地复制到另一个地方,不会涉及到任何解密。这就使得整个过程非常简单,因此从现在开始我们主要专注于这个模式。所以正如前文所说,我们不考虑加密相关的参数。
2、Source/Destination:输入和输出缓冲区。由于数据的大小可能非常大(大于1MB),实际数据就有可能通过这些对象所代表的共享内存进行传输。
3、Offset:将偏移量偏移到数据开始的输入缓冲区中。
4、subSamples/numSubSamples:一个子样本数组,其中存储与输入相关的元数据。每个子样本表示多个清空的字节,后面跟着一些加密的字节。这样一来便可以在清空和加密的输入数据之间切换。使用kMode_Unencrypted也可以简化这一部分,因为我们就只需要使用一个代表所有清空数据的子样本。
现在,我们来仔细看看源参数和目标参数的类型:
源代码请参考:http://androidxref.com/8.0.0_r4/xref/frameworks/av/include/media/ICrypto.h#51 。
这里的相关结构成员是mHeapSeqNum和两个mSharedMemory成员(DestinationBuffer的其余部分是在目标未被存储为共享内存的情况下,这种情况与此漏洞无关)。 堆name在这里用来指代实际的共享内存(也就是运行mmap后所得到的内容)。mHeapSeqNum是一个像这样的内存标识符,它以前使用称为setHeap的ICrypto共享方法。这两个mSharedMemory成员仅表示堆内缓冲区的偏移量和大小。这意味着,虽然mHeapSeqNum在源结构的内部,但它实际上与两者都相关。
值得注意的是,在参数结构中的某些部分有一些奇怪。mSharedMemory是一个IMemory,它实际上连接到它自己的堆,并且应该是表示内部的一个缓冲区。然而,这个堆却被忽略,其偏移量和大小都被用于mHeapSeqNum堆。在源结构中,还存在mHeapSeqNum,但它与源和目标都有关。上述内容,是最近代码发生更新造成的,这些代码是作为Treble项目中重要架构的一部分而创建的。
Treble项目
Treble项目是在Android 8.0版本中被引入的,其主要目标是通过在AOSP和供应商之间建立明确的分隔,从而简化系统更新的过程。Google还声称,Treble项目会通过增加更多的隔离功能,从而提高Android的安全性。
对于像mediadrmserver这样的服务,引入Treble项目后就会被隔离成多个进程。负责解密的代码属于供应商,因此它会被分成多个供应商进程,称为HAL,每个供应商都负责其自己的DRM方案。 Mediadrm服务器的作用现在只剩下在相关DRM方案的应用程序和HAL进程之间传输数据。 Mediadrmserver和HAL之间的通信也建立在Binder之上,但是在不同的域中会使用不同库的格式——libhwbinder。之前提到的从Crypto到CryptoHal的变化,其原因在于现在它是一个不同的类,其唯一目的是将数据转换为libhwbinder格式,并将其传递给HAL。
在上图中,展现了Google之所以声称Treble项目有助于提升安全性的原因。由于在不同的进程中权限被分开,每个HAL只能与自己的驱动程序通信,不受信任的应用程序不会再直接与高权限进程交互。
然而,需要注意的是,从Android 8.1开始,隔离继续变回了可选项,具体取决于供应商的设定。例如,在Nexus 5X设备中,HAL都会位于mediadrmserver进程中。数据仍然会转换成HAL格式,但不会转移到其他进程。
加密插件
之前,我提到了不同的DRM方案。在Android官方术语中,每种DRM方案的处理程序都称为是插件,或者在一些特定情况下会被称为加密插件。供应商负责提供这些插件,但是AOSP中还有一些提供给销售商使用的有效代码。例如,AOSP包含ClearKey DRM方案插件的完整开源实现方式。通常,设备将具有开源的ClearKey插件和闭源的Widevine插件(例如Nexus/Pixel设备)。
上述Treble项目发生变化导致的一个问题是,目前插件接收了HAL格式的数据。为了让转换过程更为简单,我们并不需要对每个插件都进行更新,默认的CryptoPlugin实现已经添加到AOSP中,可以让供应商使用。这一实现会将数据从HAL格式转换为传统的格式,并将其传递给原始插件代码。如果不出所料的话,这一解决方案只是暂时性的,后续还会对插件进行更新。否则,系统将会出现冗余格式转换的问题。
针对源代码的研究
在介绍ICrypto解密方法的一般过程之后,我们来仔细看看共享内存缓冲区的验证代码。正如读者可能已经猜到的那样,这正是发现漏洞的地方。
如前所述,验证通常从Bn*类开始,在我们的例子中就是ICrypto接口的“服务器端”BnCrypto。
首先,代码会检查子采样大小的总和是否有效,并确保其不会溢出。请注意,在这里是要复制的数据的大小。
它还会检查这一总和是否与totalSize匹配。通过Binder传递的另一个参数非常多余,我们可以完全通过子样本的总和来得知总大小。
接下来的检查是数据大小不能超过源缓冲区的大小。
最后,它还会检查数据大小加上偏移量之后是否仍然不超过源缓冲区。
在这里,CryptoHal会将数据转换为HAL格式并发送给相关的插件,这里并没有有趣的验证代码。
接下来,默认的Crypto Plugin实现会将数据转换回传统格式,并继续对其进行验证。
通过仔细阅读这个代码,我发现代码中有一些混乱。其中有多个“dest”和“source”变量,但实质上sourceBase与destBase是完全相同的,并且没有任何注释能帮助我们理解。考虑到Android 8.0中进行了相应更新,所以我非常怀疑是这一更新导致了该漏洞的存在,使得整个验证代码工作更为完整。
在这里,首先要检查偏移量与缓冲区大小的总和应该不超过堆大小。SourceBase是堆,而源是之前的source.mSharedMemory。如果读者对两个偏移量比较困惑,记住mSharedMemory包含一个偏移量,并且解密方法也有一个不同的偏移量参数。
其他的检查与上述类似,但是是在目标缓冲区上进行。destBuffer的堆与destination.mSharedMemory相同,只是这次不涉及到偏移量参数。
在最后,每个缓冲区都会简化为一个指向内存的指针,而偏移量现在是指针的一部分,缓冲区大小则被省略。为了确定数据大小,插件使用subSamples数组。
上述代码展现了最后一部分,希望能帮助读者们理解相关的流程。当数据未加密时,它只是从一个地方复制到另一个地方。
截至目前,我已经提供了挖掘漏洞所需的足够信息,理论上大家可以按照我的上述内容去发现这个漏洞。
漏洞详情
该漏洞的原因在于,没有验证被复制的数据是否超过了目标缓冲区。由于该过程中只对源缓冲区进行了一次检查,对目标缓冲区进行检查默认是Crypto Plugin的第二次检查,确保缓冲区位于堆内并且不超过边界,但这是远远不够的。
我们来看一个例子。假设我们要复制的数据大小为0x1000。由于这个大小是由subsamples数组表示的,因此在该数组中将会有一个条目,其中包含0x1000个字节(以及0个加密字节)。堆的大小也是0x1000字节,源缓冲区将指向整个堆(偏移量 = 0,大小 = 0x1000)。目标缓冲区是出现问题的地方,我们假设偏移量是0x800,大小为0x800,这仍然可以通过默认加密插件的检查。但在这种情况下会出现溢出的情况,0x800字节将被写入在堆的后面。
PoC
请注意,MemoryBase对象是IMemory libbinder接口的实现。这是一个使用Binder将引用传递给其他Binder对象的例子。这也是Binder角色反过来的一个例子。特权流程是“客户端”,因此它会通过Binder请求信息,并对其进行验证。
漏洞产生的影响
该漏洞允许攻击者使用任意数据覆盖目标进程中的内存。由于这是内存页级别的溢出,因此暂时没有任何缓解措施可以阻止这一漏洞的利用。由于默认Crypto Plugin的检查,数据必须从共享内存开始,这一点仍然受到限制。这也就意味着,只有位于共享内存之后的内存才可以被覆盖。此外,内存中的许多部分通常是未分配或不允许写入的,如果尝试在其中进行写入将导致出现段错误。
受影响的进程取决于供应商的设置。如果供应商不将HAL分成不同的进程,那么mediadrmserver会受到影响;如果供应商将它们分开,那么Crypto Plugin的每个HAL服务都会受到影响。由于默认的Crypto Plugin代码仅仅会留下指向目标缓冲区的指针,并且大小仅有子样本决定,供应商的代码并不能发现它接收到了格式错误的数据。这一点说明,供应商编写的这一部分代码仍然是脆弱的。理论上,供应商可能会忽略AOSP的默认加密插件代码,并利用自己的代码来检测格式错误的数据,但实际上,我没有发现任何供应商能这么做。
可能产生的影响
假设攻击者设法利用此漏洞将特权提升为易受攻击服务的特权,那么让我们来看看他可以实现的功能。请注意,这部分大多是推测性的。我没有编写漏洞利用表,但是我对于如何借助该漏洞升到具有完整root权限有一些思路。
现在,就是Android的SELinux规则发挥作用的地方。即使易受攻击的服务拥有更多的权限,SELinux仍会严重限制它们。尽管如此,即使在限制之后,我们仍然留下了一个非常有趣的权限:完全访问TEE设备。
在这种情况下, Treble项目的额外隔离几乎没有任何作用。易受攻击的进程将是可以访问TEE设备的进程,无论是否它存在分离到多个进程。在分离的情况下保护的唯一过程是中介服务器。
那么,我们可以通过完全访问TEE来做些什么? 根据Gal Beniamini的研究表明,许多设备无法正确吊销旧的易受攻击的TEE信任。这意味着,如果我们攻击具有旧的易受攻击的Trustlet的设备,则可以使用TEE设备的访问权限,加载Trustlet并将其用于TEE上的代码执行。更重要的是,Gal Benimaini此前也展示过基于Qualcomm设备的TEE代码执行如何实现root特权。
漏洞成因
前面,我已经多次提到Treble项目中是如何对代码的部分区域进行重大修改。在修改之前,目标缓冲区甚至无法以这样的格式设置。就在这一修改之后,引入了此漏洞。
正如前文所说,重构的这部分代码中,有很多地方都比较混乱,也存在一些冗余的内容。尽管代码混乱或出现冗余并不一定会使代码易受攻击,但它确实提高了这种可能性,因为它使代码更难以审计。因此,虽然有时我们难以发现漏洞,但却更容易发现混乱或冗余的代码。尽管我深知,与实际编写出好的代码相比,对不好的代码进行批评是更加容易的,但我要坚持提出,我认为这部分的代码应该进行改进。
结论
尽管Google声称,Treble项目有助于提升Android的安全性,但在这个例子中却完全相反。Treble项目本身并没有什么问题,但关键问题是其实现的过程处理的不是非常好。
大家可以在GitHub上面找到触发漏洞的PoC的完整源代码,同时还有一些额外的信息。
时间线
2017年12月20日 发现漏洞
2017年12月28日 将漏洞详情和PoC提交给Google
2017年12月29日 收到Google的初步回应
2018年3月5日 Google发布补丁