关于ZAKER 免费视频剪辑 合作 加入

深入考察 Qualcomm DSP 的安全漏洞(下)

在本文中,我们将为读者详细介绍在对 Qualcomm DSP 进行安全测试过程中发现的安全漏洞,以及相应的利用方法,这里是本文的下篇。

(接上文)

自动生成的代码

下面,让我们看一下开源的 hexagon_nn 库,它是 Hexagon SDK 3.5.1 的一部分。这个库导出了很多用于神经网络计算的函数。

对库进行编译时,Hexagon SDK 会自动生成存根文件 hexagon_nn_stub.c 以及 skel 模型 hexagon_nn_skel.c。通过对这些模块进行手动审查,我们轻松找到了多处安全问题,具体如下所示。

编组字符串(char *)参数

int hexagon_nn_op_name_to_id ( const char* name, unsigned int* node_id ) 函数需要一个输入参数(name)和一个输出参数(node_id)。下面的存根代码是由 SDK 生成的,专门用于处理这两个参数:

我们可以看到,除了现有的两个参数外,在 _pra 数组的开头处还创建了第三个 remote_arg 元素。这个特殊的 _pra [ 0 ] 参数用于保存字符串 name 的长度。

实际上,字符串 name 本身被保存在第二个 remote_arg 元素(_praIn [ 0 ] )中,其长度将再次存储在该数组元素中,但这次是存放到 _praIn [ 0 ] .buf.nLen 字段中。

之后,skel 代码会提取这两个长度,并将它们作为有符号的 int 值进行比较——问题就出在这里。攻击者可以忽略存根代码,将一个负值(大于或等于 0x80000000)写入第一个 remote_arg 元素中,从而绕过这个验证。然后,这个伪造的长度被用作内存偏移,最终导致崩溃(越过堆的边界进行读取操作)。

对于所有需要字符串参数的对象函数来说,都会生成相同的代码。

编组输入输出缓冲区

下面,我们考察 int hexagon_nn_snpprint ( hexagon_nn_id, unsigned char* buf, int bufLen ) 函数。该函数需要一个缓冲区及其长度作为参数。需要注意的是,该缓冲区同时用于输入和输出数据。因此,在存根代码中,它被分成两个独立的缓冲区(输入缓冲区和输出缓冲区)。同样的,两个缓冲区的长度(_in1Len 和 _rout1Len)都存储在另外的 remote_arg 元素(_pra [ 0 ] )中。

skel 函数在调用对象函数之前,会将输入缓冲区复制(使用 _MEMMOVEIF 宏)到输出缓冲区。在这里,需要复制的数据的长度,就是保存在特定的 remote_arg 元素(_pra [ 0 ] )中的输入缓冲区的长度。

因此,只要攻击者控制了这个值,所有的验证检查都可以轻松绕过——只需将输入缓冲区的长度变为负值即可。

在检查缓冲区边界时,将长度的类型转换为有符号的 int 类型,就是导致溢出漏洞的罪魁祸首。

总而言之,自动生成的代码将漏洞引入到了 Qualcomm、OEM 和所有其他使用 Hexagon SDK 的第三方开发者的库中。由于 SDK 中存在严重的漏洞,导致预装在安卓智能手机上的几十个 DSP 骨架库都面临严重的安全威胁。

如何利用 DSP 的漏洞

让我们来看看在专有 DSP 骨架库中发现的众多漏洞中的一个,并着手创建 "read-what-where" 原语和 "write-what-where" 原语。

在大多数 Android 设备上都可以找到 libfastcvadsp_skel.so 库。在下面的例子中,这个库的版本为 1.7.1,提权自 Sony Xperia XZ Premium 设备。恶意的安卓应用程序可以通过向 remote_handle_invoke 函数提供特制的参数,导致 libfastcvadsp_skel.so 库发生崩溃。图 5 中的数据文件展示了这种精心制作的参数示例。

图 5:导致 libfastcvadsp_skel.so 崩溃的数据文件

正如你所看到的,这里调用了 0x3F 方法,并为其提供了一个输入参数和三个输出参数。其中,输入参数的内容以字节 0x14 开始,并包含以下主要字段:

· 红色 0x02 表示要读多少个半字(长度)。

·黄色 0x44332211 表示从哪里开始该读取操作(源地址)。这个值是相对于 DSP 堆中第一个输出参数起始地址的偏移量。使用这个偏移量,我们就可以控制读取的起始地址。我们可以让这个偏移量变成自己想要的长度,甚至可以是负数。

·青色的 0x04 显示该读取操作的结束位置(目的地址)。这个值也是偏移量。

实际上,崩溃是由于源地址不正确造成的。

图 6:转储的崩溃信息

下面是读取原语的缩减版 POC 代码。

其中,输入参数总是紧跟在位于 DSP 堆中输出参数之后。因此,在写入原语中,我们需要根据第一个输出参数的长度来移动源地址(所有其他参数都是空的)。

攻击者可以在 DSP 进程(User PD)的地址空间中操纵源和目的偏移量进行读写操作。第一个输出参数和内存中的 libfastcvadsp_skel.so 库之间的偏移量是一个常值。同时,在 skel 或对象库的数据段中找到一个指针来触发调用也是很容易的事情。出于安全考虑,我们就不公布利用 DSP 进程实现代码执行的完整 POC 代码了。

关于 DSP 用户域的研究总结

对 Qualcomm DSP 用户域的部分骨架库和对象库的安全研究过程中,我们发现了两个具有全局性的安全问题:

·DSP 库缺乏相应的版本控制。这使得恶意的 Android 应用程序可以进行降级攻击,从而在 DSP 上运行含有已知漏洞的库。

·Hexagon SDK 中的漏洞导致隶属于 Qualcomm 和手机供应商的代码中出现了数百个隐蔽的漏洞。由于 Hexagon SDK 存在安全漏洞,使得几乎所有嵌入基于 Snapdragon 的智能手机中的 DSP 骨架库都容易受到攻击。

我们向 Qualcomm 报告了在几十个 DSP 库中发现的近 400 个独特的崩溃问题,其中包括下列库:

·libfastcvadsp_skel.so

·libdepthmap_skel.so

·libscveT2T_skel.so

·libscveBlobDescriptor_skel.so

·libVC1DecDsp_skel.so

·libcamera_nn_skel.so

·libscveCleverCapture_skel.so

·libscveTextReco_skel.so

·libhexagon_nn_skel.so

·libadsp_fd_skel.so

·libqvr_adsp_driver_skel.so

·libscveFaceRecognition_skel.so

·libthread_blur_skel.so

为了证明这一点,我们利用其中一个已发现的漏洞,获得了在基于 Snapdragon 处理器的设备上执行无签名代码的能力,受影响的设备包括 Samsung、Pixel、LG、小米、OnePlus、HTC 和 Sony 手机。

利用这些漏洞,一个可以访问 DSP 的用户域的安卓应用就能:

·触发 DSP 内核崩溃并重启移动设备。

·隐藏恶意代码,因为反病毒软件不会扫描 Hexagon 指令集。

·由于 cDSP 负责对来自摄像头传感器的流媒体视频进行预处理,因此,攻击者可以接管这些数据。

·访问 DSP 内核驱动程序。驱动程序中的漏洞可以将应用程序的权限提升为虚拟机操作系统或 DSP 内核的权限。

DSP 驱动程序

QuRT 操作系统实现了自己的设备驱动程序模型,称为 QuRT 驱动调用(QDI)。并且,QDI 是无法通过 Android API 进行访问的。像 POSIX 一样,QDI 设备驱动程序的操作权限高于请求驱动服务的用户代码。此外,QDI 还提供了一个简单的驱动程序调用 API,用以屏蔽所有与特权模式相关的实现细节。

实际上,作为 Hexagon SDK 的一部分的 libqurt.a 库中就包含了 QDI 基础设施。同时,FastRPC shell 就是利用这个库静态链接的。

在 QuRT 的可执行二进制文件中,我们可以找到几十个 QDI 驱动程序。它们通常被命名为 /dev/...、/qdi/...、/power/...、/drv/...、/adsp/... 或 /qos/...。同时,我们可以使用 int qurt_qdi_open ( const char* drv ) 函数来访问 QDI 驱动程序,该函数将返回设备句柄,其实就是一个小整数。设备句柄的作用类似于 POSIX 文件描述符。

实际上,QDI 只提供了一个宏,即必要的、用户可见的 API。这个 qurt_qdi_handle_invoke 宏负责所有通用驱动程序操作。事实上,qurt_qdi_open 只是这个宏的一个特例,下面是宏的参数:

QDI 句柄或预定义的常量值之一。

定义所请求的操作的方法编号。在 SDK 的头文件中,我们可以看到下列编号:

·编号 1 和 2 是为名称注册和名称查询保留的。

·编号 3-31 保留用于对已经打开的句柄的 POSIX 类型的操作。

·编号 32-127 保留给 QDI 基础设施。

·编号 128-255 保留给自动生成的方法使用,比如由 IDL 生成的方法。

·256 和更高的编号用于私有方法。驱动程序可以根据需要使用这些方法。

·零到九个可选的 32 位参数。

qurt_qdi_handle_invoke 宏用于调用相关的设备驱动调用函数,该函数实现了主要的驱动逻辑,并提供了指定的方法编号和可选参数。

这面是一个从用户 PD 代码中调用 QDI 驱动程序的例子:

QDI 驱动程序需要使用 int qurt_qdi_devname_register ( const char *name, qurt_qdi_obj_t *opener ) 函数在 QuRT 中注册自己。为此,驱动程序需要提供自己的名字和一个指向 opener 对象的指针,来作为该函数的参数。

opener 对象的第一个字段是驱动程序的调用函数。QuRT 将调用这个函数来处理来自用户 PD 或另一个驱动程序的驱动请求,并提供以下参数:

·QDI 句柄,代表发送 QDI 请求的客户端。

·发出该 QDI 请求的 opener 对象。

·由调用者提供的 QDI 方法。

·由调用者提供的九个可选参数。

一般来说,驱动程序的调用函数相当于一个通过 QDI 方法 ID 进行切换的 " 运算符 "。并且,每个方法都可以使用与所提供的参数数量不同的参数;参数类型是 qurt_qdi_arg_t。

请注意,驱动程序调用函数是基于模糊测试的漏洞研究的一个很好的测试对象,因为其方法是通过 ID 识别的,而不是通过名字识别的,因此,调用者不需要知道参数的确切数量和它们的实际类型,就能调用驱动程序的各种方法。

利用基于反馈的方式对 QDI 驱动程序进行模糊测试

为了在 Ubuntu 电脑上对 QDI 驱动进行模糊测试,我们依然是组合使用了 QEMU Hexagon 和 AFL,这与前面对 DSP 库进行模糊测试一样。但是,这里没有实现 skel_loader 程序,而是实现了另一个 Hexagon ELF 二进制文件 qdi_exec,该程序用于:

·将作为第一个命令行参数收到的数据文件解析为 QDI 方法 ID 和一个由 9 个参数组成的数组,供驱动程序调用函数使用。

·通过第二个命令行参数中指定的地址调用驱动程序调用函数,并提供 QDI 方法 ID 和从数据文件中解码的参数。

对于 qdi_exec 程序,我们使用的输入文件的格式为:

头部(4 字节)。它包含三个非常有用的字段:

·QDI 方法 ID(10 位,位于低地址处)。在图 7 的示例中,其值为 0x01。

·参数的数量(4 位)。在这个示例中,只用了一个参数;其余八个参数被认为是零。

·参数类型的掩码(9 位)。正如我们前面提到的,每个参数都是一个数字或一个指向缓冲区的指针。在掩码中,每个参数用一个比特表示。值为 0 意味着该参数是一个数字,正值意味着该参数是一个缓冲区。

缓冲区参数的长度(每个参数占 4 字节)。在这个例子中,长度为 0x0A 的 /dev/diag 字符串被用作参数。

缓冲区参数的内容。

图 7:用于模糊测试 QDI 驱动程序的输入数据文件

实际上,QDI 驱动程序是作为 QuRT ELF 的一部分实现的。但是,它们没有被 Qualcomm 包含在 QuRT 的 runelf.pbn 版本中,而我们在模拟器中运行的正是这个版本,因此,我们必须对 runelf.pbn ELF 文件做如下修补:

·在 runelf.pbn 中加入用于物理设备的 QuRT ELF 的程序段。我们使用了从 Pixel 4 设备上提取的 aDSP 二进制文件。

·将 QDI 驱动程序使用的 malloc 和 memcpy 内核函数重定向到其用户模式实现。因为这两个内核级内存函数限制了用户和内核空间之间的数据传输。

图 8:QDI 驱动程序的模糊测试方案

之后,我们使用 AFL 对数据文件的内容进行处理,并在模拟器上触发执行经过修补的 runelf.pbn。这样,runelf.pbn 就会加载我们的 qdi_exec 程序,而后者将直接调用 QDI 驱动程序的调用函数。

我们通过对 QuRT 二进制文件进行逆向分析,通过手动方式找到了 QDI 驱动调用函数的起始地址。同时,我们发现 opener 对象恰好位于驱动程序名称旁边的代码中。

借助于 AFL,我在 Snapdragon 855 aDSP 内置的十几个 QDI 驱动程序中发现了许多崩溃现象;其中大部分也适用于 cDSP。

如何利用 QDI 驱动程序中的安全漏洞

QDI 驱动程序中的任何故障,都可以被用来导致 DSP 内核崩溃,并重新启动移动设备。例如,下面的每一行代码都会引起 DSP 崩溃,也就是说,可以用来对设备发动 DoS 攻击。

出于研究目的,我们成功地利用了 /dev/i2c QDI 驱动程序中的几个任意内核读写漏洞,以及 /dev/glink QDI 驱动程序中的两个代码执行漏洞。出于安全考虑,我们就不公布 POC 代码了,但我们注意到,这些漏洞的利用方法都相当简单。例如,下面是实现读取原语的例子:

实际上,恶意的安卓应用可以利用 QDI 驱动程序中发现的漏洞,以及前面在用户 PD 的 DSP 库中发现的安全漏洞,这样的话,它们就能够在 DSP 虚拟机操作系统的上下文中执行自定义的代码。

从虚拟机操作系统 PD 请求安卓服务

如果我们试图从 DSP 虚拟机操作系统代码中打开一个与 Android 系统有关的文件,会发生什么情况呢?答案是,QuRT 会将我们的请求重定向到一个特殊的 Android 守护进程。如图 9 所示,在 Snapdragon 855 设备上,存在两个 aDSP 守护进程和一个 cDSP 守护进程,并且,它们是以不同的权限运行的:

图 9:DSP Android 守护进程

在 Pixel 4 设备上,这些守护进程的启动命令可以在 init.sm8150.rc 文件中找到。

图 10:Pixel 4 上的 init.sm8150.rc 启动文件

这些高权限的 vendor.adsprpcd 和 vendor.cdsprpcd 守护进程用于处理 DSP 虚拟机操作系统请求。其中,在 u:r:adsprpcd:s0 和 u:r:cdsprpcd:s0 上下文中,只能访问与 DSP 相关的目录和对象。

小结

我们认为,aDSP 和 cDSP 子系统是非常有前途的安全研究领域。首先,DSP 可以被第三方的 Android 应用程序调用。其次,DSP 可以处理个人信息,如流经设备传感器的视频和语音数据。第三,正如我们在本文中介绍的那样,DSP 组件中存在许多安全问题。

Qualcomm 公司为我们披露的 DSP 漏洞分配了相应的漏洞编号,分别为:CVE-2020-11201、CVE-2020-11202、CVE-2020-11206、CVE-2020-11207、CVE-2020-11208 和 CVE-2020-11209。对于 QDI 驱动程序中发现的漏洞,Qualcomm 公司决定不为其分配 CVE 编号。本文中介绍的所有漏洞,都已通过 2020 年 11 月的高通公司安全补丁成功修复。

另外,出于研究目的,我们利用了一些已发现的漏洞,并获得了在所有基于 Snapdragon 的移动终端的 aDSP 和 cDSP 上执行特权代码的能力。

以上内容由"嘶吼RoarTalk"上传发布 查看原文
一起剪

一起剪

ZAKER旗下免费视频剪辑工具

一起剪

觉得文章不错,微信扫描分享好友

扫码分享