在本文中,我们讨论一下如何发现并利用一个漏洞,获取高通的安全可执行环境 (QSEE) 的代码执行权限。
QSEE攻击面
在上一篇文章中提到,高通的 TrustZone 实现启用“普通世界”操作系统向“安全环境”中的用户空间环境加载可信应用程序(名为 trustlets ),称之为 QSEE 。
QSEE 通过发送特制的 SMC 调用(由“安全世界”内核处理)为“普通世界”提供服务。然而,由于 SMC 不能从用户模式调用,“普通世界”和 trustlet 的通信必须通过“普通世界”的操作系统内核。
但是,“普通世界”的常规用户进程有时需要与提供特定服务的 trustlet 通信。例如,当播放 DRM 保护的多媒体文件时, Android 平台中处理多媒体文件的进程“ mediaserver ” ,不行与适当的 DRMtrustlet 通信,来解密和渲染该多媒体文件。同样的,处理加密密钥的“ keystore ” 进程,也需要与提供安全存储和操作的 trustlet ( keymaster )。那么如何实现这些进程与 trustlet 通信呢?
答案是,使用名为“ qseecom ” 的 Linux kernel 设备,使用户空间进程可以执行多个 TrustZone 相关的操作,例如,向安全环境加载 trustlet ,与加载的 trustlet 进行通信等。
虽然这些都是必须的,但是也存在潜在的隐患。与 TrustZone 通信会暴露一个很大的攻击面——如果 trustlet 在一个存在漏洞的设备上加载,我们就可以获取可信执行环境中的代码执行权限。另外,由于可信执行环境可以映射和写入“普通世界”的所有物理内存,即使没有漏洞,它也可以影响“普通世界”的操作系统内核(通过直接修改“安全世界”的内核代码)。
由于上述隐患,该设备的访问权限限制为进程需要的最小集。只有以下四个进程可以访问“ qseecom ” :
surfaceflinger (running with”system” user-ID)
drmserver (running with “drm”user-ID)
mediaserver (running with”media” user-ID)
keystore (running with”keystore” user-ID)
这意味着,如果我们能够成功控制以上四个进程中的任意一个,我们就可以直接攻击 trustlet ,绕过 Linux 内核。其实,这也是我们接下来要做的事情——但是会在该系列文章的最后。
让我们假设已经获取了 mediaserver 进程中的代码执行权限,我们就可以聚焦 trustlets 提供的攻击面。用下图可以更加形象地展示该系列文章中的利用链和本文的重点:
漏洞范围
目前还不能确定该漏洞确切的影响范围,但是静态检查了一些设备 (Droid Turbo,Nexus 6,Moto X 2nd Gen) 后,发现都存在该漏洞。我相信,该漏洞的影响应该是十分广泛的,可能已经影响了大多数基于 Qualcomm 的设备。
那么,为什么该问题如此普遍?该漏洞存在于 trustlet 中,因此不会依赖 TrustZone 内核(不同的 SoC 会有所改变),而是依赖于代码,该代码在不同的设备中以相同的方式执行。因此,所有使用 trustlet 的设备应该都会受到影响。
另外,在有些设备上存在漏洞的代码可能略有不同(可能是相同代码的旧版本)。尽管这样,这些设备仍然受影响。即使你搜索不到本文中提到的特定字符串也不要疑惑,可以通过使用上一篇文章中提到的工具对 trustlet 逆向工程来检测你的设备是否真实受到影响。
进入Widevine
之前,我们决定将研究重点放在“ widevine ” trustlet 上,用于播放 DRM 加密的多媒体文件。 widevine 具有中等复杂度( ~125KB )且使用广泛(根据其官网,在 20 亿设备上可用),似乎是很好的选择。
安装好原始的 trustlet 后,我们会发现一个待分析的 ELF 文件。首先来看一下 trustlet 注册的用于处理传入命令的函数:
命令中的前 32 位值用于指定命令代码,高字节用于将命令分为 4 类。
通过查看分类处理函数发现,其分类十分丰富——总共有大约 70 种不同的支持命令。但是检查 70 中不同的命令十分耗时——或许我们可以尝试比较快捷的方式?例如,在生产设备时可能会遗留命令的分类?
由于与 trustlet 交互的库是受保护的,因此我们不能从源码中找答案。但是我编写了一段 IDAPython 脚本来搜索 QSEECom_send_cmd 命令的所有引用,用于向 trustlet 发送命令并检查“ command-code ” 值的函数会与引用相对应。然后将结果按上述分类进行分组,得到了一下结果:
因此,没有应用使用 5X 命令,可疑!
通过在 5X 分类中删选函数,我们得到下列命令:
继续分析:将数据从请求缓冲区拷贝到“ malloc ” -ed 缓冲区(注意,这里的长度字段我们不能控制,而是依赖于传递到 QSEOS 的真实缓冲区长度)。然后该函数的数据流根据请求缓冲区中的一个标志进行分发。来跟踪一组发送到 PRDiagVerifyProvisioning 的数据流:
最终,我们成功找到一个漏洞。
经过简单的验证(例如检查命令缓冲区中的第一个 DWORD 是否为 0 ),该函数会检查我们特制的命令缓冲区中的第四个 DWORD 。将上图中的值设为零,我们就获取了将代码从请求缓冲区复制到全局缓冲区中的权限,并使用第三个 DWORD 作为长度参数。由于该代码路径只执行存在漏洞的 memcpy 函数,因此就比较方便处理,那么我们就选择使用该代码路径。
另外,你可能不太清楚上述函数中引用的“全局缓冲区”,毕竟它并没有传递到该函数中,仅仅只是被引用,通过使用寄存器R9。
还记得上篇分析的trustlet的超大的读写数据段吗?该数据段中存储了所有的trustlet修改数据——栈、堆和全局变量。为了从代码中的任意位置快速访问该数据段,Qualcomm决定使用特定平台的R9寄存器作为“全局寄存器”,该寄存器的值不能修改,并且默认只想该数据段的起始位置。根据 ARM AAPCS ,这是比较实际有效的方法:
接下来?
我们已经得到了一个原语,现在需要分析哪段数据是我们可控的。再次使用较短的IDAPython代码,我们可以搜索所有位于溢出缓冲区开始地址后的“全局缓冲区(R9)”的引用,结果如下:
然而,几乎所有的函数都对我们控制数据没有任何帮助,尤其是大多数函数只是在这些内存位置存储文件的系统路径,这就意味着我们没有机会控制数据流。
原语方式
由于我们并没有找到函数指针或者直接的方式在溢出的缓冲区后操纵控制流,因此在获取代码执行权限使我们需要升级缓冲区溢出的原语。
通览上述函数,我们发现有几个函数引用了一个比较有趣的数据块:
上图可见,0×32 DWORD数据块,开始于0x169C偏移,用于存储“会话”。一旦客户端向Widevine trustlet发送命令,trustlet会首先创建一个会话,随后的所有操作都会使用该会话的标识符完成。这是十分必要的,例如,可以允许多个应用程序在完全不同的内部状态下,同时解密DRM内容。
该会话是复杂的结构体,这就意味着它可能会为我们所用。但是,上述提到的0x32DWORD数据块只存储指向该会话结构体的指针,而不是结构体本身。这就意味着,如果我们想要覆盖这些值,它们需要指向QSEE中可用的地址(否则,只会导致trustlet崩溃)。
为了特制合法的会话指针,我们需要找出trustlet加载的位置。查询相关代码表明,QSEOS会限制trustlet与“普通世界”的联系,它通过创建一个特殊的名为“secapp-region”的内存区域实现该功能,trustlet的内存段会在该区域被处理。MPU也会通过阻止“普通世界”以任何方式访问trustlet来对其进行保护(试图从“普通世界”访问这些物理位置会导致设备重启)。
另一方面,trustlet存储在安全区域并且可以访问它自己的内存段。但事实上trustlets可以访问secapp区域中所有已分配的内存,即使这些内存属于其他的trustlet!然而,若请求其中未分配的内存则会导致trustlet崩溃。
这样看来貌似有了一个成型的计划。我们可以使用溢出原语覆盖指向“secapp”区域中的位置的指针。现在,我们拥有了一个命令,命令可以使用特制的会话指针导致读取请求。但是如果trustlet发送出这些命令后崩溃,我们就失败了(我们只需要重启trustlet即可)。否则我们就可以获得“secapp”区域中的分配页。
但是,我们怎么知道该页属于哪个trustlet呢?
我们已经可以区分未分配页和分配页了,现在我们需要一些可以根据内容区分页的方法。
这里有一个方法——我们可以找到一个根据会话指针的读取值不同而执行不同操作的函数:
该函数尝试访问位于session_pointer + 0xDA的数据,如果该值等于1,那么就会返回24,否则,会返回35。
这就像“挑西瓜”,通过“轻敲”不同的内存位置和听声音,我们可以推断其内容。那么我们需要做的就是为trustlet赋予独特的“声音”。
由于我们只能区分1和非1值,我们可以通过创建包含1和0的模式来标记trustlet。例如,以下是一个不会出现在任何其他trustlet的模式:
现在,我们可以通过使用溢出原语将该模式写入数据段中来标记trustlet。
最后,我们通过重复一下步骤找出trustlet:
如果“听”起来是“空的”(例如,trustlet崩溃)——我们需要重启trustlet;
否则,“轻击”包含我们的标记模式的位置序列,如果“听”起来与上述模式相同,就是我们的trustlet。
当然,QSEOS使用的检查分配方式通过检查相关的内存位置可以加快我们的步伐。例如,QSEOS貌似是连续分配trustlet的,这意味着使用trustlet的一半大小增量从“secapp”区域的末尾扫描到开始就可以完全匹配。
写原语
既然我们已经得到了在安全区域中找到trustlet的方法,可以特制“有效”的会话指针,指向trustlet中的位置,接下来我们就需要找到创建写原语的方法。那么……有没有可以向会话指针中写入可控数据的函数呢?
几乎所有向会话指针中写入数据的函数都不支持写入数据的控制,但是,有一个函数貌似可以帮得上忙:
该函数生成一个随机的DWORD用作“随机数”,然后检查第一次调用后是否有足够的时间。如果有,就通过调用addNonceToCache将该随机数添加到会话指针。
首先,由于“time”字段保存在溢出缓冲区后的全局缓冲区中,我们可以使用溢出原语清除它,因此可以删除该限制并任意调用该函数。注意,这里生成的随机值会写入返回给用户的缓冲区——这就意味着,随机数生成后,调用者也会知道该值。
让我们来看看随机数是如何存储在会话指针中的:
因此,在该会话指针中存在一个16个随机数的数组——开始于0×88。当添加一个随机数时,所有的旧数就会全部向右移动一位,并且该新数会位于数组的第一位。这是一个非常强大的写原语。
当我们想要向特定的位置写入数据时,可以将该会话指针指向该位置(减去该随机数数组的偏移)。然后我们开始生成数组,直到数组中的最低字节(这是一个小端机器)与我们要写入的数据匹配。如果匹配,我们可以依次递增会话指针,生成下一个字节,等等。
我们已经有足够的原语来特制一个完整的漏洞利用了,我们需要做的只是找到一个可写入的值,劫持应用程序的控制流。
接下来看一下负责处理6X命令分类的函数:
上述代码中,函数通过使用存储在全局缓冲区中的作为数组索引的命令 ID 来调用请求命令。每个支持的命令都由数组中一个 12 字节的条目表示,分为如下四部分:
命令代码( 32 位)
处理函数自身的指针( 32 位)
最小的输入长度( 16 位)
最小的输出长度( 16 位)
如果信息有效,指针执行,传入作为第一个参数的用户缓冲区和第二参数的输出缓冲区。
如果我们选择了正确的 6X 命令,就可以覆盖上述数组中相应的条目,使函数指针指向我们想要执行的任意代码段。然后,简单调用该命令就可以使 trustlet 执行我们控制的内存位置的代码。
需要注意的是,不能选择后面我们要用到的命令后的函数,因为我们使用的写原语会注销后面的 15 个双字(或者说, 5 个数组项)。以下为向命令数组中添加条目的函数:
其中有 6 个与未使用函数相对应的连续条目,如果我们直接覆盖他们前面的条目,后续就会出现问题。
尽管我们可以劫持控制流,但仍不具备在 QSEE 中执行任意代码的完整权限。由于 trustlets 的代码段是不可写入的,并且 TrustZone 内核不允许系统调用为新的可执行页分配内存,那我们就没有办法创建可执行的 shellcode 了。
如果将逻辑全部写成 ROP 链十分麻烦。经过慎重考虑,我们其实不需要写太长的 ROP 链。我们可以借助“图灵机”来实现一系列操作。
对于给定的一段代码,我们可以通过完全在“普通世界”执行代码来轻易地模拟控制流和逻辑。其实只有以下操作是必须在 QSEE 中执行的:
读取和写入内存
通过 TrustZone 内核执行系统调用
我们只需要写一个很短的 ROP 链,该链需要实现以下功能:
1 、将控制流劫持到一个独立的栈中
2 、准备用于函数调用的参数
3 、调用既定的 QSEE 函数
4 、将结果返回给用户,并在 QSEE 中存储可执行文件
该 ROP 链需要做的就是使用提供的参数执行给定的 QSEE 函数,我们可以使用它执行任意系统调用。至于内存访问—— 中有大量的函数用于读取或写入 gadgets 。模型如下:
综上所述,我们已经得到了创建一个完整的漏洞利用的各个部分,以下为每个阶段的简单描述:
1 、通过重复“敲击” secapp 安全区域并“听”,找到 Widevine 应用程序;
2 、使用随机数生成命令创建写原语
3 、使用写原语覆盖未使用的 6X 命令
4 、使用较短的 ROP 链执行任意代码
流程图如下:
完整的利用代码获取: https://github.com/laginimaineb/cve-2015-6639
其中还包括上面提到的 shellcode 样本,该代码读取 TrustZone 的安全文件系统 SFS 中读取文件,该文件系统使用特殊的硬件密钥(设备上运行的软件不可访问该密钥)加密, 尽管如此,我们可以通过“安全世界”访问 SFS ,甚至提取关键的加密密钥,例如用于解密 DRM 内容的密钥。
尽管我们用于了 QSEE 中的代码执行权限,但是我们不能访问由 TrustZone 内核的系统调用提供的 API 。例如,如果我们想要解锁引导程序,就需要将目标转向设备的 QFuses ,也可以理解为,通过 QSEE 是不可能的。因此,下一篇文章我们可能要尝试提升 TrustZone 内核中的权限。
*原文地址: bits-please ,vul_wish编译,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)