在这篇博客中,我们将完整解释如何攻击上一篇博客中提到的漏洞。
上次说到,我已经将漏洞报告给了高通。他们送给我了一台新的Moto X 2014,这将是以后的很多个博客的主题。(更加深入到TrustZone架构以及其他该设备上的安全部件)
当开发这个攻击时,我手头上只有我的Nexus 5。这意味着所有的内存地址以及下面其他特定的信息都是基于这个设备。
以防有人想要重新实现下面阐述的实验,这里提供我的设备的版本:
好了,现在开始正题吧!
依据上一篇博客,你已经知道了那个漏洞可以允许攻击者在TrustZone内核的虚拟内存空间的任意位置写入一个值为0的DWORD。
0写原语并不好用。他们收到的限制非常大,而且经常会无法基于他们产生攻击。为了能依靠这个漏洞产生可靠地攻击,我们先要利用这个弱漏洞产生一个强漏洞。
由于TrustZone内核被加载在一个已知物理地址,这意味着所有地址预先就已经被知道了,所以不需要通过执行来探索。
然而,TrustZone内核的内部数据结构以及状态大部分都未知并且会因为很多不同的进程与TrustZone内核交互而改变(从外部中断,到安全世界的应用程序等等)。
而且,TrustZone的代码分段被映射为只读权限,这在安全开机的过程中就被验证了的。这意味着一旦TrustZone的代码被加载到了内存中,理论来说它不应该再被改变。
上图是TrustZone内存映射与权限
那么,我们如何才能利用一个0写原语来完成任意代码执行?
我们可以尝试编辑TrustZone内核内任何可更改的数据(不如说堆,栈,全局),这也许会是允许我们创建一个更好原语的跳板。
就像我们上篇博客里说的,当一个SCM命令被调用了,任意一个参数,如果他是一个指向内存的指针,那么他将被TrustZone内核验证。这个验证是用来确保这个指针的物理地址在允许范围内,而不是比如说在TrustZone内核所用的内存范围。
这些验证听上去似乎是一个很好的备选,因为如果我们可以禁止掉这个功能,我们将可以利用其他的SCM调用来创造各种不同的原语。
让我们从给内存验证函数一个名字开始。从现在开始,我们将会叫他
“tzbsp_validate_memory”.
以下是这个函数的反编译:
这个函数实际上调用了两个内部函数来完成验证,我们讲叫他们做“is_disallowed_range”和“is_allowed_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位) 的与验证区域相交的区域,在上面表中都会存在一个比特来代表那个区域的数据是允许还是不允许。如果是不允许,函数将会返回一个代表这个结果的值。不然,这个函数将会把这个区域当做有效。
虽然这个函数长了些,这个函数仍然不难。基本上,它简单的遍历了一个静态数列,这个数列包含以下的结构:
这个函数会开始遍历一个表里的每一个向量,这个表的位置在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版本号。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!