简介
Snapdragon是由美国Qualcomm公司设计和销售的一系列用于移动终端的片上系统(SoC)半导体产品。通常情况下,单个SoC系统内会含有多个CPU内核、Adreno图形处理单元(GPU)、Snapdragon无线调制解调器、Hexagon数字信号处理器(DSP)、Qualcomm Spectra图像信号处理器(ISP)以及其他硬件。
Snapdragon的产品层级一般是根据CPU、GPU和DSP处理器的可扩展计算资源进行划分的。其中,最低级别的产品可能只包含一个Hexagon DSP,而高级别的产品则包含多达四个具有特定用途的Hexagon DSP处理器。例如,在Pixel 4、Samsung S10、Xiaomi Mi 9、LG G8和OnePlus 7等手机中使用的Snapdragon 855(SM8150)SoC,通常包含一个Kryo CPU、一个Adreno 640和四个独立的DSP,每个DSP具有特定的用途,分别用于传感器(sDSP)、调制解调器(mDSP)、音频(aDSP)和计算(cDSP)。在这篇文章中,我们研究了其中的两种DSP:
· cDSP,用于计算密集型任务,如图像处理、计算机视觉、神经网络相关计算和摄像机流数据的处理。
· aDSP,用于音频和语音数据的低功耗处理。
就当前的研究而言,我们将cDSP和aDSP视为一个处理单元(DSP)。另外,这里发现的安全漏洞实际上对两者都适用。
CPU与DSP之间的通信
FastRPC是Qualcomm专有的远程过程调用(RPC)机制,用于支持CPU和DSP之间的远程过程调用。需要说明的是,FastRPC框架使用的是一种典型的代理模式。
图1:FastRPC示意图
在图1中,您可以看到FastRPC组件之间的交互情况:
· 用户模式进程(客户端)启动远程调用。例如,一个Android应用程序在其本机代码中调用其中一个存根函数。
· 存根函数是自动生成的代码,用于将函数调用转换为RPC消息。通常情况下,存根代码将编译为单独的本机库,然后将其与客户端链接。存根代码使用libadsprpc.so和libcdsprpc.so库通过相关的ioctl来调用应用程序处理器(AP)上的DSP RPC驱动程序(/dev/adsprpc-smd或/dev/cdsprpc-smd)。
· DSP RPC内核驱动程序接收远程消息调用,通过共享内存驱动程序(SMD)通道,将队列中的消息发送到DSP上的DSP RPC框架,然后等待响应。
· DSP RPC框架从队列中删除消息,然后将其分派给骨架动态库(skeleton dynamic library)进行处理。
· skel是一个自动生成的库,用于对参数进行解组(unmarshal)并调用目标方法的实现。
· 目标方法(对象)是由Qualcomm或OEM提供的、在DSP上运行的逻辑。
谁可以在DSP上运行自己的代码?
出于安全考虑,DSP仅为OEM和数量有限的第三方软件供应商授予了编程权限。在DSP上运行的代码是由Qualcomm公司签名的。普通的安卓应用没有权限在DSP上执行自己的代码。不过,Snapdragon 855和865 SoC是个例外,Qualcomm公司允许在cDSP上执行低权限的无签名动态共享对象。
应该注意的是,谷歌通过SELinux政策防止第三方应用程序和adb shell访问DSP RPC驱动程序,以保护Pixel设备。
同时,公开提供的Hexagon SDK负责将DSP对象的C/C++源代码编译成适用于DSP上执行的Hexagon(QDSP6)字节码。而存根代码和skel代码是根据为开发人员准备的接口定义语言(IDL)模块自动生成的。Qualcomm IDL用于定义跨内存保护和处理器边界的接口。IDL只公开了该对象的功能,但没有公开它所在的位置或实现它的编程语言。
Android应用程序开发人员能够为DSP实现自定义库,但不能完全执行。Android应用程序只能调用预构建的DSP库。
谁在掌管着DSP?
QuRT是Qualcomm专有的多线程实时操作系统(RTOS),用来管理Hexagon DSP。QuRT的完整性得到了Qualcomm安全可执行环境(QSEE)的信任。QuRT的可执行二进制代码(独立于aDSP和cDSP)具有相应的签名,并分割成多个个文件,这一点与Qualcomm设备上任何其他受信任的应用程序完全相同;其默认位置是/vendor/firmware目录。
对于每个启动远程调用的Android进程,QuRT系统都会在DSP上创建一个单独的进程。当用户进程被生成时,系统会将相应的shell进程(/vendor/dsp/fastrpc_shell_0用于aDSP,/vendor/dsp/fastrpc_shell_3用于cDSP)加载到DSP上。shell负责调用骨架库和对象库。此外,它还实现了DSP的RPC框架,用于为骨架库和对象库提供必要的API。
DSP软件架构提供了不同的保护域(PD)以确保内核软件的稳定性。在DSP中,共有三个保护域:
· 内核域:拥有对所有PD的所有内存的访问权。
· Guest OS域:可以访问自己PD的内存、用户PD的内存,以及某些系统寄存器。
· 用户域:只能访问自己的PD的内存。
无签名的动态共享对象只能在无签名PD内运行;实际上,无签名PD属于用户PD,其对底层DSP驱动程序和线程优先级的访问是受到限制的。无签名PD被设计为只支持一般的计算应用。
对象库以及FastRPC shell都可以在用户PD中运行。
跳过FastRPC流程中的存根代码。
libadsprpc.so和libcdsprpc.so库负责与DSP RPC驱动程序进行通信。这些库导出了两个函数,它们对本研究来说是很有意义的:
· int remote_handle_open(const char* name, remote_handle *ph)函数。这个函数用于在AP上的调用者进程和DSP上一个新的FastRPC shell进程之间打开一个远程会话。这个会话用于与作为第一个参数传递的骨架库进行通信。
· int remote_handle_invoke(remote_handle h, uint32_t scalars, remote_arg *pra)函数。这个函数能够调用骨架库的导出方法。需要注意的是,必须将会话处理程序作为第一个参数进行传递。
使用这两个函数,客户端就可以执行骨架库中实现的所有DSP方法了。此外,由Qualcomm或OEM提供的存根代码可以从该链中跳过。
图2:直接调用DSP
让我们来看看remote_handle_invoke函数的第二个和第三个参数,它们用于对目标方法及其参数进行编码。
参数scalars是一个字,包含以下元数据信息:
· 方法索引和属性(最高字节,以0xFF000000为掩码)。
· 输入参数的数量(以0x00FF0000为掩码)。
· 输出参数的数量(以0x0000FF00为掩码)。
· 输入和输出句柄的数量(以0x000000FF为掩码,其中有四位用于输入,另外四位用于输出)。在现代手机上,如果这个字节不等于0,则意味着DSP调用失败。
参数pra是一个指向目标方法的参数数组的指针(对应于remote_arg元素)。其中,这些参数的顺序如下:输入参数、输出参数、输入句柄、输出句柄。
如您所见,每个输入和输出参数都被转换为一个通用的remote_buf元素。
需要注意的是,如果我们提供的remote_arg数组元素比目标方法所需的多,那么多余的参数就会被骨架库忽略。
对于参数scalars和pra,它们将通过DSP RPC驱动程序和DSP RPC框架进行“原封不动的”传输,并用于每个骨架库提供的特殊invoke函数的第一个和第二个参数。例如,libfastcvadsp_skel.so库提供了一个名为fastcvadsp_skel_invoke的invoke函数,该函数负责按索引调用适当的skel方法。对于每个skel方法来说,它首先对收到的远程参数进行相应的检查,然后将remote_bufs解组为常规类型,并调用相应的对象方法。
正如你所看到的,要从一个skel库中调用一个方法,只需要知道它的索引,然后用remote_buf结构体封装其各个参数即可。我们不需要提供调用函数的名称、参数的类型和数量就能执行调用,这使得骨架库成为非常不错的模糊测试目标。
降级漏洞
Qualcomm在安卓手机上预装了大量的骨架库,并且绝大部分都是专有的;不过,也有一些开源的库,如libdspCV_skel.so和libhexagon_nn_skel.so。
许多骨架库,如libfastcvadsp_skel.so和libscveBlobDescriptor_skel.so,几乎在所有安卓设备上都能找到。然而,像libVC1DecDsp_skel.so和libsysmon_cdsp_skel.so这样的库,则只出现在现代Snapdragon SoC上。
实际上,还有些库是由OEM实现的,它们只能在特定供应商的设备上使用。例如,libedge_smooth_skel.so库可以在Samsung S7 Edge手机上找到,而libdepthmap_skel.so库则只能在OnePlus 6T手机上找到。
一般来说,所有的skel库都位于/dsp或/vendor/dsp或/vendor/lib/rfsa/adsp目录下。在默认情况下,remote_handle_open函数就是扫描的这些路径。此外,还有一个存放路径的环境变量,即ADSP_LIBRARY_PATH,我们可以将新的搜索路径加入其中。
如前所述,所有的DSP库都具有相应的签名,所以,我们无法给它们补丁。然而,任何Android应用程序都可以完成下列任务:在其资产中引入一个具有Qualcomm签名的骨架库,将其提取到应用程序的数据目录中,将该路径添加到ADSP_LIBRARY_PATH的开头,然后打开一个远程会话。这样的话,该库就能成功加载到DSP上,因为它的签名是正确的。
由于加载骨架库时不会进行版本检查,因此,在DSP上运行的可能是一个古董级的骨架库,并且其中很可能存在1-day漏洞。即使较新的骨架库已经存在于设备上,只要在ADSP_LIBRARY_PATH中,把旧版本的的路径放到新版本的路径之前,系统就仍然加载这个库的旧版本。通过这种方式,任何DSP补丁都可以被攻击者轻松绕过。此外,通过分析DSP软件补丁,攻击者就能找到库内已修复的漏洞,然后,只要加载未打补丁的版本,就能利用其中的安全漏洞了。
读者可能会问:对于特定的设备,是否存在该设备已经批准/拒绝的骨架库清单呢?答案是:没有!这就是说,我们可以在Sony Xperia手机上运行为Samsung手机实现的库。换句话说,在某个OEM库中发现的漏洞会危及所有基于Qualcomm芯片的安卓设备。
对Hexagon库继续基于反馈的模糊测试
由于DSP库使用了专有的Hexagon ELF格式,因此,检测Hexagon可执行文件时,最简单的方法就是使用QEMU。实际上,直到2019年底,QEMU才开始支持Hexagon指令集。同时,我们修复了其中的很多bug之后,现在终于能够在模拟器的用户模式下运行真正的DSP库了。
通过将QEMU和AFL结合起来,我们就能够在Ubuntu PC对相应的骨架库和DSP库进行模糊测试了。
为了在模拟器上执行一个库的代码,我们准备了一个简单的程序(Hexagon ELF二进制文件),它负责以下工作:
1.将收到的作为第一个命令行参数的数据文件解析为scalars字和remote_arg数组。
2.利用dlopen函数打开由第二个命令行参数指定的骨架库。该库可能依赖于其他骨架库和对象库。例如,libfastcvadsp_skel.so依赖于libapps_mem_heap.so、libdspCV_skel.so和libfastcvadsp.so lib。所有这些库不仅可以从固件中提取,也可以从真实设备中提取。
3.以scalars和指向remote_arg数组的指针作为函数参数,通过地址来调用invoke函数。例如,fastcvadsp_skel_invoke函数就是对libfastcvadsp_skel.so库进行模糊测试的起始点。
对于我们的程序来说,其输入文件的格式如下所示:
1.scalars值(4字节)。在图3中,scalars等于0x08020200,这意味着通过两个输入和两个输出参数来调用8号方法。
2.输入参数的大小(每个参数占4字节):0x10和0x20。
3.输出参数的大小(每个参数占4字节):0x80200和0x1000。
4.输入参数的值。在这个例子中,第一个参数的值为0x11,占用0x10字节;第二个参数的值是0x22,占用0x20字节。
图3:输入数据文件,用于对DSP库进行模糊测试
对于每个输出参数,我们都为其分配乐指定大小的内存,并将其内容填充为0x1F。
对于大多数骨架库来说,通常都会使用DSP框架和系统调用。不过,我们的简单程序还不能处理这种请求。因此,在执行其余代码之前,我们必须先在模拟器上加载QuRT。为了完成这个任务,最简单的方法不是使用真正的QuRT操作系统,而是使用其“精简”版本runelf.pbn——该版本可以在Hexagon模拟器上运行,并且可以在Hexagon SDK中找到这个版本。
AFL fuzzer不仅可以修改数据文件的内容,并且还能在模拟器上触发runelf.pbn的执行。QuRT首先会加载准备好的ELF二进制文件,然后调用一个目标骨架库。QEMU在执行测试用例后,将向AFL返回代码覆盖矩阵。
图4:DSP库的模糊测试方案
在进行模糊测试后,测试结果让我们大吃一惊:在我们进行测试的所有DSP库中都发现了崩溃现象。仅在libfastcvadsp_skel.so库中就发现了数百个不同的崩溃报告。
有趣的是,大多数崩溃问题都是在骨架库中发现的,而不是在对象库中发现的。这就意味着,Hexagon SDK生成的代码容易产生安全问题。
小结
在本文中,我们将为读者详细介绍在对Qualcomm DSP进行安全测试过程中发现的安全漏洞,以及相应的利用方法,由于篇幅过长,我们将分为两篇发布。更多精彩内容,敬请期待!
本文翻译自:https://research.checkpoint.com/2021/pwn2own-qualcomm-dsp/如若转载,请注明原文地址