查看原文
其他

攻击TrustZone系列(Pt.3) -- 对MSM8974的完整TrustZone攻击

2017-09-20 wx_rd.cheung 看雪学院


在这篇博客中,我们将完整解释如何攻击上一篇博客中提到的漏洞。

 

上次说到,我已经将漏洞报告给了高通。他们送给我了一台新的Moto X 2014,这将是以后的很多个博客的主题。(更加深入到TrustZone架构以及其他该设备上的安全部件)



0号患者





当开发这个攻击时,我手头上只有我的Nexus 5。这意味着所有的内存地址以及下面其他特定的信息都是基于这个设备。

 

以防有人想要重新实现下面阐述的实验,这里提供我的设备的版本:

google/hammerhead/hammerhead:4.4.4/KTU84P/1227136:user/release-keys

 

好了,现在开始正题吧!



漏洞原语(原语即为最小单位操作)




 

依据上一篇博客,你已经知道了那个漏洞可以允许攻击者在TrustZone内核的虚拟内存空间的任意位置写入一个值为0的DWORD。

 

0写原语并不好用。他们收到的限制非常大,而且经常会无法基于他们产生攻击。为了能依靠这个漏洞产生可靠地攻击,我们先要利用这个弱漏洞产生一个强漏洞。

 


制造一个任意写原语





由于TrustZone内核被加载在一个已知物理地址,这意味着所有地址预先就已经被知道了,所以不需要通过执行来探索。

 

然而,TrustZone内核的内部数据结构以及状态大部分都未知并且会因为很多不同的进程与TrustZone内核交互而改变(从外部中断,到安全世界的应用程序等等)。

 

而且,TrustZone的代码分段被映射为只读权限,这在安全开机的过程中就被验证了的。这意味着一旦TrustZone的代码被加载到了内存中,理论来说它不应该再被改变。




上图是TrustZone内存映射与权限

 

那么,我们如何才能利用一个0写原语来完成任意代码执行?

 

我们可以尝试编辑TrustZone内核内任何可更改的数据(不如说堆,栈,全局),这也许会是允许我们创建一个更好原语的跳板。

 

就像我们上篇博客里说的,当一个SCM命令被调用了,任意一个参数,如果他是一个指向内存的指针,那么他将被TrustZone内核验证。这个验证是用来确保这个指针的物理地址在允许范围内,而不是比如说在TrustZone内核所用的内存范围。

 

这些验证听上去似乎是一个很好的备选,因为如果我们可以禁止掉这个功能,我们将可以利用其他的SCM调用来创造各种不同的原语。

 


TrustZone内存验证




 

让我们从给内存验证函数一个名字开始。从现在开始,我们将会叫他 “tzbsp_validate_memory”.

 

以下是这个函数的反编译:




这个函数实际上调用了两个内部函数来完成验证,我们讲叫他们做“is_disallowed_range”和“is_allowed_range”。



is_disallowed_range







如图,这个函数实际上用下面的方法用了给定地址的前12个比特:

——前7个比特位用作表的索引,包含了128个值,每个值32比特宽。(从地址0xfe8304ec开始,存在一个表,这个表里面有128个32位宽的用于比较参量)

——后5个比特位用来与那些个32位宽的参量作比较(top_12_bits & 0x1f 使得top_12_bits里只剩后5位,2的5次方为32,所以1最多可以向左移31位,所以这是要用32位宽的原因)




换句话说,对每个1MB(所以不用管地址的后20位) 的与验证区域相交的区域,在上面表中都会存在一个比特来代表那个区域的数据是允许还是不允许。如果是不允许,函数将会返回一个代表这个结果的值。不然,这个函数将会把这个区域当做有效。

 


is_allowed_range







虽然这个函数长了些,这个函数仍然不难。基本上,它简单的遍历了一个静态数列,这个数列包含以下的结构:




这个函数会开始遍历一个表里的每一个向量,这个表的位置在tzbsp_validate_memory函数调用这个函数时给定,当遇到当前向量里end_maker偏移处的值为0xffffffff会停下来。


每个由这样一个向量确定的范围将会被验证以确定这个内存范围是被允许的,虽然如此,就像上面反编译所示,只要那个向量的flags偏移的第二个比特位被置位,那么这个验证就会被省去!





攻击验证函数




现在我们知道了验证函数是怎么工作的,是时候看看我们怎样才能使用0写原语来使验证函数不工作。


首先,就像之前讲的,“is_disallowed_range”函数使用一个满是32-bit向量的表,其中每一比特代表1MB的内存。若比特位设置为1,则代表该1MB内存不被允许,而设置为0代表被允许。


这意味着我们通过0写原语把该表所有比特置位为0很简单就可以使这个函数没有用处。这样做的话,所有1MB的内存块都会被标志为允许。


接下来看看下一个函数“is_allowed_range”。这个函数会复杂点,像之前所说,flags偏移的第二个比特位置位的内存块会与给定地址比较。然而,对于那些该比特位没有置位的内存块,将不会有验证,那个内存块会被略过。


由于在设备上的内存块表里,只有第一个区域是与TrustZone内核的内存区域有关,所以我们只要0写掉这个区域的flags。这样我们就可以绕过验证函数,使得该内存地址被认为是有效的。



回到制造一个写原语





所以现在既然我们已经搞定了边界验证函数,我们可以自由的放任何内存地址当做参数传到一个SCM调用里,并且它将会没有任何障碍的被执行。


但我们有离制造写原语更近一步吗?理想的情况是,我们能控制一个SCM调用,通过这个SCM调用来把一些数据写到指定地址。


不幸的是,在看过了所有的SCM调用后,我发现没有任何一条符合这个条件。


然而,不需要担心!一条SCM调用不能解决的问题,我们就把几条连起来。逻辑上,我们可以把一个任意写原语分成以下几步:


—— 创造 一个能在指定地址的不能指定的数据

—— 控制 不能指定的数据使其包含需要的内容

—— 复制 创造的数据到目标地址



创造





虽然没有SCM调用看上去是一个用来创造可控制的数据的备选,但是存在一个可以用来创造不可控制的数据到指定地点的调用 - “tzbsp_prng_getdata_syscall”


这个函数,就像他名字所说,可以用来在制定地点产生一段随机字节的缓存。这个函数一般被安卓用来加强Snapdragon SoCs上的硬件随机数生成。


在任何情况下,这个SCM调用需要接受两个参数: 输出地址和输出长度(字节数)。


一方面来说,这很棒 - 如果我们相信硬件随机数生成,我们可以确认我们用这个函数生成的每个字节都可以当做输出。另一方面,这意味着我们不能控制产生的数据。



控制





即使当使用硬件随机数时任意输出的有可能,也许我们有可能确认生成的数据就是我们想要写的数据。


为了这么做,来想想下面这个游戏 - 假如你有个老虎机,老虎机有四个列,每个列有256种可能性。每次你玩,所有列都会转,然后停在任意一个可能性。那么你需要玩多少次才能中大奖?我们算一算,这总共有 4294967296 (2^32) 个可能结果,所以每次中大奖的可能是 10^(-10)。你得玩上一阵子了。。


但如果你作弊的话呢?比如说,你可以一列一列得玩?是不是就简单很多啦?


概率上,这叫伯努利分布。即独立重复事件。其实这就是个学术点的说法。


所以你想到了这和我们的写原语相关没?我们接着来:


首先,我们需要找到一个SCM调用,这个调用要可以从TrustZone内核里一个写允许的内存地址返回它的值到调用它的函数。


有很多函数都有这个功能。一个备选是“ tzbsp_fver_get_version ”调用。这个函数被正常世界用于得到不同TrustZone部件的内部版本号。它通过得到一个整数来知晓需要确认版本号的部件,以及一个版本号会被写在的地址。然后,这个函数简单的遍历一个静态数列,这个静态数列包涵部件ID和版本号。当找到与得到的部件ID相同的ID时,所对应的版本号会被写到输出地址。


现在,使用“ tzbsp_prng_getdata_syscall ”函数,我们可以开始操纵任何版本号的值,一次一个字节。为了知道每次我们产生的字节的值,我们可以简单的调用之前提到的SCM,我们只需提供我们在修改的部件的部件ID以及一个指向可读数据的返回地址(在linux内核)。


我们可以重复这两步,直到我们满意我们生成的数据。之后再去到下一个字节。这意味着在一系列重复后,我们可以确认我们可以得到我们想要的DWORD版本号。



复制





最终,我们想要把我们生成的数据值传到一个我们想要的地址位置。幸运的是,这一步非常直截了当。我们只需要调用“ tzbsp_fver_get_version”SCM函数。但这次,我们简单的把目标地址放到之前放返回的地址的参数里。这将会使这个函数将我们生成的DWORD数据放到我们制定的位置去,从而完成我们的任意写原语。



呼...接下来?





现在开始,事情变得简单起来。首先,虽然我们有了一个写原语,它用起来挺笨重的。也许我们可以创建一个用起来更便利的写原语。


我们可以通过创建我们自己的SCM调用来完成这一步,这个新SCM将是一个简单的写-什么-到哪里零件。这听起来挺难的,但其实很一目了然。


在之前的博客中,我们提到所有的SCM调用都是间接通过一个大数列来达成的。这个大数列里包含有指向各个SCM调用的指针,传递的参数个数,名字等等。


这意味着我们可以用写原语来改写一个我们觉得不重要的SCM调用在大数列里的指针地址,让它指向我们自己的 写-什么-到哪里零件。快速的在TrustZone内核代码中搜索会发现有许多这样的零件。比如说:



这个代码会简单的把R0里的值写到R1这个地址里,并且返回。棒。


最终,我们再创建一个读零件。也是用上面的方法,替代掉另一个不重要的SCM调用。这个零件会少见点,但也可以在TrustZone内核找到,如下:



这个零件把从R0加偏移量R1后的地址里读到的值放入R0。酷。



写新代码





到这一步,我们有了完整的对TrustZone内核内存的读写方法。我们还没实现的,是在TrustZone内核里执行任意代码。当然,有人会认为我们可以用通过将零件串起来的方式来执行任意代码(感兴趣的可以去看看ROP),但这手动做挺麻烦,自动做挺难。


还有一些解决这个问题的方法。


一个可能的方式是在正常世界里写一段代码,然后让安全世界跳过去,这听上去简单,其实做起来难。


如第一篇博客里讲的,当处理器在安全模式下,这意味着SCR(安全配置寄存器)里的NS(非安全)比特位是关闭着的,它只能够执行在MMU的翻译表里被标志为安全的内存页(即是,NS比特位是关闭的)。




这意味着为了执行我们正常世界的代码,我们首先需要改写TrustZone内核的翻译表,从而把我们的代码所在的内存页标志为安全。


虽然这是有可能的,但很麻烦。


另一种方式则是在TrustZone内核的代码分段加一段新代码或是覆盖现有代码,这也将让我们能够改变现有内核的行为,这之后也会变得方便。


然而,这看上去不见得比第一个方法简单。毕竟,TrustZone内核的代码分段被映射为只读,当然不能是可写。


虽然如此,这只是个小问题!这实际上可以通过使用ARM MMU的一个叫领域的功能来完成,而不需要修改翻译表。


在ARM翻译表里,每一个向量都有一个位置列出它的权限,还有一个位置代表这个翻译所属的领域。总共有16个领域,每个翻译属于一个领域。


在ARM MMU里,有个叫做DACR(领域访问控制寄存器)。这个32位寄存器有16对比特,每对对应一个领域,它们被用于决定该领域的翻译是否要读访问,写访问,均或均不用错误。




每当处理器尝试访问一个给定地址,MMU通过那个翻译的访问权限首先检查该访问是否可行。如果访问被允许,则不会产生错误。


不然的话,MMU会检查DACR中对应该领域的那对比特是否被置位。若是被置位,则错误被抑制住并且访问被允许。


这意味着我们只要把DACR的值全设为0xffffffff,那么MMU就会允许所有对任意被映射地址的访问,无论是读还是写,并且不会产生错误。(更重要的是,这不用修改翻译表)。


但如何我们才能设置DARC呢?很明显的,在TrustZone内核初始化使时,它也把DACR的值设置为了一个预定义的值(0x55555555),如下图:



虽然如此,我们可以简单的设置R0为我们想要的值,然后控制我们的代码去往初始函数的第一个操作码。


这样DACR就被设置成我们要的值咯,我们的前路一片光明 - 我们可以简单的写或者覆盖TrustZone内核里的代码咯。


为了让事情简单点,也许在TrustZone内核没有用的地址处写下我们的代码会是个好选择。其中一个备选就是代码空洞。


代码空洞指的是那些被映射了但里面没有内容的地址(这往往出现在被分配的地址区域的末端)。他们通常是由于内存映射都有一个粒度(就是单位,比如1kb,1mb),因此往往最后的地方都会有些空余的空间。


在TrustZone内核中有很多个这种的代码空洞,这使得我们可以写小段的代码并执行他们。这种方式是最为方便的。



小结一下





所以这个攻击有些些复杂。我们看看我们所需要完成的所有步骤吧:


——通过0写原语绕过内存验证函数

——通过TrustZone硬件随机数生成器在指定地址创建一个想要的DWORD

——通过读取对应的部件版本号来验证创建的DWORD

——将创建的版本号替代一个指向现有SCM调用的指针,这个创建的版本号为一个写零件的地址

——用写零件构建一个读零件

——用写零件替代一个指向现有SCM调用的指针,这个写零件里存着初始函数的位置,这样我们可以改写DACR

——改写DACR成(0xffffffff)来允许我们改写TrustZone内核的代码分段

——在TrustZone内核的代码空洞里写代码

——执行!:)



代码




我为这个漏洞写了一个攻击,包括了Nexus 5 所需的全部符号(包括之前说的指纹)。


首先,为了使得这个攻击能发送所有需要的SCM调用给TrustZone内核,我创建了一个打了补丁的 msm-hammerhead内核。这里面加入的这些功能并且开放给了安卓的用户空间。


我选择通过添加新的IOCTLs到已有的驱动里,QSEECOM(第一篇博客中提到的)。这些IOCTLs使调用它的函数可以发送原始的SCM调用(正常,或者原子)到TrustZone内核,包含任何数据。


你可以在这里找到所需的内核修改。


对于那些使用Nexus 5设备的人们,我个人建议使用 Marcin Jabrzyk 的教程(教你如何不需要放到设备里就能编译和启动一个自定义的内核)


在用修改过的内核启动设备后,你需要一个用户空间的应用程序来调用新添加的IOCTLs来发送SCM给内核。


我写了个这样的应用。


最后,这个攻击是用python写的。它使用一个用户空间的应用程序来通过修改过的Linux内核发送SCM调用到TrustZone内核,并且允许在TrustZone内核中执行任意代码。


你可以在这里找到完整的攻击代码。



使用这个攻击





使用这个攻击很直接,下面是你要做的:


——用修改过的内核打开设备(Marcin的教程)

——编译FuzzZone二进制码并把它放到 /data/local/tmp

——在shellcode.S里写任意ARM代码

——执行 build_shellcode.sh脚本来生成一个shellcode二进制码

——执行 exploit.py来在TrustZone内核里执行你的代码




受影响的设备






在发现的时候,这个漏洞影响所有使用MSM8974 SoC的设备。我在汇报漏洞之前创建了一个脚本来静态的检查这类设备的ROMs,并且发现以下设备有漏洞:


请注意:这个漏洞已经被高通修复。这个列表并不是全部,因为这只是我做的一次静态分析。



时间线:

——19.09.14 - 汇报漏洞

—— 19.09.14 - 高通回复

—— 22.09.14 - 高通确认漏洞

—— 01.10.14 - 高通通知客户

—— 16.10.14 - 高通通知运营商,申请14天的贸易禁止

—— 30.10.14 - 贸易禁止解除


并且在我通知高通的时候,我被告知了他们内部已经查到了这个漏洞。然而,这类问题通常需要很长一段时间来发行修复,所以,在我研究的时候,并未修复。



终语





我很乐意听到来自于你的任何回复,请在下方留言!欢迎提问。


译者:这周估计没空了。。周末有空再接着下一篇。





本文由看雪论坛 wx_rd.cheung 编译,来源bits-pleaseblogspot@laginimaineb

转载请注明来自看雪社区

热门阅读


点击阅读原文/read,

更多干货等着你~



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存