概述
Hexagon是高通研发的数字信号处理器(DSP,Digital Signal Processor) 。目前市面上基于高通平台的手机都会使用该芯片,它主要用来进行音视频处理、AI计算以及信号调制解调。虽然它只是一枚协处理器,但它运行了一套完整的系统,这套系统包括:supervisor、内核(kernel)和应用。在此之前,已经有研究人员对Hexagon安全进行了相关研究(这里只列出了我认为有价值的资料):
- Attacking Hexagon: Security Analysis of Qualcomm’s aDSP简单介绍了Hexagon架构、如何与AP进行通信、攻击面分析以及盲测DSP应用;
- Pwn2Own Qualcomm DSP介绍了通过QEMU模拟的方式Fuzz DSP、内核攻击面、发现的漏洞(降级攻击、数据序列化、应用和内核);
- In-Depth Analyzing and Fuzzing for Qualcomm Hexagon Processor 介绍了一种基于路径覆盖的Fuzz方法
基于以上资料,我进行了独立的Hexagon安全研究。由于Hexagon代码闭源、有独立的指令架构、仅少量的官方资料,研究起来有一定的难度。我希望这篇博客能够帮助研究人员进一步了解Hexagon。具体来说,这篇博客的主要贡献如下:
- 公开我实现的利用方法,据我所知,已有议题只是介绍发现的漏洞,从未公开利用方法;
- 一种新的Fuzz方法,它可以在(某个)真机上动态测试应用、内核和虚拟机;
背景知识介绍
目前常见的是第六代Hexagon芯片(V6x),它有三个PD(Protection Domains):supervisor、Guest OS和User。supervisor拥有最高权限。supervisor有一个开源实现hexagonMVM,我认为它有一定的参考性,但由于代码年久失修,它可能无法反映近几年硬件情况。我其实更愿意称Guest OS为kernel,这样也许更容易理解(不一定对)。高通实现了名为QuRT的内核,但它并未开源。Linux内核同样支持Hexagon架构,你可以在arch/hexagon下找到相关代码。应用通常在User mode下运行,它的权限最低。
高通提供了相关的SDK方便开发者开发应用。这个SDK非常强大,除了提供代码编译环境,它还提供了模拟运行环境,你可以在这个环境中测试应用。需要注意的是:厂商对手机进行了限制,没有厂商签名的应用只能使用有限的API,甚至无法在手机上运行。
V67版本的Hexagon有32个通用寄存器,系统寄存器个数未知。你可以在SDK的模拟环境中(hexagon-sim)打印出所有系统寄存器,但它能否反映硬件真实情况不得而知。我没有找到系统寄存器相关资料,所有这些寄存器的含义同样是一个迷。Hexagon有自己的指令集,它支持 very long instruction word(VLIW)packet。编译器可以将1至4条指令组成VLIW,位于VLIW中的指令可以并行执行(更多信息可以参阅Qualcomm ® Hexagon™ V67 Programmer’s Reference Manual)。以下是一个VLIW例子:
LOAD:C014739C { r2 = ##0x51FFFE
LOAD:C01473A4 r4 = r16
LOAD:C01473A8 memw(r16 + #4) = r2.new }
r2寄存器被赋值为0x51FFFE,r16寄存器的值赋给r4,将r2寄存器的值0x51FFFE写入内存。你可以使用SDK中hexagon-llvm-objdump工具反编译二进制文件,从而获取相关指令,也可以通过IDA插件idp_hexagon反编译二进制文件。目前IDA插件并不能将指令翻译为伪代码,因此只能通过阅读汇编指令来了解程序行为。
应用漏洞挖掘
DSP应用位于手机以下目录:
- /vendor/dsp/adsp:包含在ADSP(audio)中运行的shell及应用;
- /vendor/dsp/cdsp:包含在CDSP(compute)中运行的shell及应用;
- /vendor/lib/rfsa/adsp/:其他应用;
具体的应用以so的形式存在,它们不能单独运行。当应用加载时,首先需要加载shell程序,它是一个通用的程序框架。在ADSP中,它的名字是fastrpc_shell_0,而在CDSP中,有两个不同的shell:fastrpc_shell_3和fastrpc_shell_unsigned_3。前者用于运行有厂商签名的应用,后者用于运行没有签名的应用。正如前文提到的,没有签名的应用可用的API非常有限。这些应用都是潜在的攻击对象。
除此之外,SDK提供了一些开源库,比如用于神经网络的hexagon_nn库。源码分析要比阅读汇编指令轻松的多,因此我优先选择分析这些开源库。
hexagon_nn是高通开发的将神经网络加载到Hexagon的框架。我在这个库中发现了多个漏洞,并实现了shellcode执行。由于厂商使用了相同签名,因此有问题的库可以运行在不同品牌的手机上,同时,由于存在降级攻击,即便是最新版本的手机也可能被攻击。
信息泄露
hexagon_nn库提供了一个非常有趣的API:
src/interface.c
218 int hexagon_nn_get_dsp_offset(uint32_t *libhexagon_addr, uint32_t *fastrpc_shell_addr)
219 {
220
223 *fastrpc_shell_addr = (uint32_t)&qurt_sem_add;
224 *libhexagon_addr = (uint32_t)&hexagon_nn_get_dsp_offset;
230 }
这个API返回shell和hexagon_nn中两个变量的地址。通过静态分析,我可以确认上述两个变量分别在shell和hexagon_nn中的偏移,从而可以计算出shell和hexagon_nn的加载地址。
同时,厂商在编译hexagon_nn库时开启了调试功能,我可以获得动态分配的对象的地址。首先通过hexagon_nn_domains_set_debug_level() 设置graph的debug级别:
src/interface.c
335 int hexagon_nn_set_debug_level(nn_id_t id, int level)
336 {
337 struct nn_graph *graph;
338 if ((graph = nn_id_to_graph(id)) == NULL) return -1;
339 if (level < 0) level = 0;
340 graph->debug_level = level;
341 return 0;
342 }
之后,我可以通过hexagon_nn_domains_snpprint() 来获得graph信息:
src/interface.c
hexagon_nn_domains_snpprint()
|
|-> hexagon_nn_snpprint()
|
|-> do_snpprint()
48 void do_snpprint(struct nn_graph *nn, char *buf, uint32_t n)
49 {
57 PRINTF_APPEND(buf,n,len,"nn @ %p: id=0x%lx debug_level=%d\n",nn,nn->id,nn->debug_level);
58 for (node = nn->head; node != NULL; node = node->next) {
61 PRINTF_APPEND(buf,n,len,"node @ %p: id=0x%x type=0x%x(%s) n_inputs=%d n_outputs=%d padding=%x(%s)\n", // 泄露node地址信息
62 node,
63 (unsigned int)node->node_id,
64 (unsigned int)node->node_type,
65 opname,
66 (unsigned int)node->n_inputs,
67 (unsigned int)node->n_outputs,
68 node->padding,
69 padname);
70 if (nn->debug_level > 0) for (i = 0; i < node->n_inputs; i++) {
71 PRINTF_APPEND(buf,n,len,"... input %d @ %p <src_id %x out_idx %d>\n", // 泄露input地址信息
72 i,
73 node->inputs[i],
74 (unsigned int)node->input_refs[i].src_id,
75 (unsigned int)node->input_refs[i].output_idx);
76 }
77 if (nn->debug_level > 0) for (i = 0; i < node->n_outputs; i++) {
78 PRINTF_APPEND(buf,n,len,"... output %d @ %p\n", i,node->outputs[i]); // 泄露output地址信息
79 }
84 }
任意写
hexagon_nn支持创建不同类型的node,我可以通过hexagon_nn_domains_append_empty_const_node()创建const类型的node:
src/interface.c
hexagon_nn_domains_append_empty_const_node(data_len)
|
|-> hexagon_nn_append_empty_const_node(data_len)
|
|-> do_append_empty_const_node(data_len)
|
|-> hexagon_nn_empty_const_ctor(data_len)
|
|-> const_tensor = tensor_alloc(data_len)
| |
| | if (data_size) {
| | } else {
| | newtensor->data = NULL;
| | }
| | newtensor->max_size = newtensor->data_size = data_size;
| | newtensor->self = newtensor;
|
| self->n_outputs = 1;
| self->outputs = &const_tensor->self
如果传入的data_len是0,那么新创建的tensor->data为NULL。这个tensor最终会赋值到node的outputs。我可以通过上述漏洞泄露outputs内容,因此,我可以知道tensor的地址。之后,我可以通过hexagon_nn_populate_const_node() API进行任意写:
hexagon_nn_populate_const_node(data, data_len, target_offset)
|
|-> do_populate_const_node(data, data_len, target_offset)
|
|-> hexagon_nn_populate_const(data, data_len, target_offset)
|
| start = (uint8_t *) node->outputs[0]->data + target_offset;
| memcpy(start, data, data_len);
hexagon_nn_populate_const_node()并没有检查tensor->data_size,而是直接根据用户传入的offset进行拷贝,从而导致无条件的write-what-where漏洞。
漏洞利用
尽管《Pwn2Own Qualcomm DSP》演示了如何在Pixel手机上攻击DSP,但相关的利用细节并未公开。
要实现任意代码执行,我需要一块具备可写可执行属性的内存区域。从hexagon-readelf的结果来看,没有这样的内存区域。因此,我需要调用mprotect()来改变现有的内存区域属性。QuRT扩展了标准API,它实现了以下函数:
libs/common/qurt/computev65/include/qurt/qurt_mmap.h
int qurt_mem_mprotect(const void *addr, size_t length, int prot);
#define QURT_PROT_NONE 0x00
#define QURT_PROT_READ 0x01
#define QURT_PROT_WRITE 0x02
#define QURT_PROT_EXEC 0x04
为了调用qurt_mem_mprotect(),我需要劫持一个函数指针,这个函数指针需要至少3个参数,且前三个参数可以控制。我选取的是nn_option_descriptor结构体:
include/nn_graph_options.h
148 struct nn_option_descriptor {
149 char const *name;
150 int typecode;
151 option_setter_fp setter_func;
152 int settercode;
153 int defval;
154 };
它的功能是描述一个配置选项,比如选项的名字(name字段),配置选项的处理函数(setter_func)等。其中setter_func的定义如下:
typedef int (*option_setter_fp)( struct nn_graph * nn, int code, int value );
当要设置一个类型为int的选项时,hexagon_nn会调用nn_option_set_int():
src/graph_options.c
hexagon_nn_set_graph_option()
|
|-> nn_option_set_int()
53 int nn_option_set_int( struct nn_graph * nn, char const *name, int value )
54 {
55 struct nn_option_descriptor const * descp = OptionDescTable;
81 while( descp->name != 0 ){
82 if( strcmp(descp->name,name)==0){
83 logmsg(nn,3,"set %s = %d", name, value);
84 return (descp->setter_func)( nn, descp->settercode, value);
85 }
86 ++descp;
87 }
89 }
OptionDescTable是全局变量,我可以修改descp->setter_func和descp->settercode。同时,我也可以控制value参数。现在唯一的问题是如何控制nn。nn表示一个graph,正常情况下,graph是动态分配的,它的地址不可控。而mprotect()第一个参数是内存地址,因此,我需要在目标内存位置构建一个假的graph。
通过hexagon_readelf工具可以读取hexagon_nn_skel.so各个段信息:
src/interface.c
218 int hexagon_nn_get_dsp_offset(uint32_t *libhexagon_addr, uint32_t *fastrpc_shell_addr)
219 {
220
223 *fastrpc_shell_addr = (uint32_t)&qurt_sem_add;
224 *libhexagon_addr = (uint32_t)&hexagon_nn_get_dsp_offset;
230 }0
我选取0x113000的位置存放shellcode,所以需要在这里构造一个假的graph。先使用读原语读取0x113000处的数据,目的是尽可能较少地修改数据来构造假的graph:
src/interface.c
218 int hexagon_nn_get_dsp_offset(uint32_t *libhexagon_addr, uint32_t *fastrpc_shell_addr)
219 {
220
223 *fastrpc_shell_addr = (uint32_t)&qurt_sem_add;
224 *libhexagon_addr = (uint32_t)&hexagon_nn_get_dsp_offset;
230 }1
设置graph的id以便后续可以找到它;使debug_level为0,防止意外读取其他数据,导致崩溃;设置next_graph为NULL来避免读取垃圾数据。最后将graph注册到graph_table中:
src/interface.c
218 int hexagon_nn_get_dsp_offset(uint32_t *libhexagon_addr, uint32_t *fastrpc_shell_addr)
219 {
220
223 *fastrpc_shell_addr = (uint32_t)&qurt_sem_add;
224 *libhexagon_addr = (uint32_t)&hexagon_nn_get_dsp_offset;
230 }2
这样,hexagon_nn可以通过nn_id_to_graph()找到我构造的graph:
src/interface.c
218 int hexagon_nn_get_dsp_offset(uint32_t *libhexagon_addr, uint32_t *fastrpc_shell_addr)
219 {
220
223 *fastrpc_shell_addr = (uint32_t)&qurt_sem_add;
224 *libhexagon_addr = (uint32_t)&hexagon_nn_get_dsp_offset;
230 }3
现在我可以通过调用hexagon_nn_set_graph_option() API来调用qurt_mem_mprotect():
src/interface.c
218 int hexagon_nn_get_dsp_offset(uint32_t *libhexagon_addr, uint32_t *fastrpc_shell_addr)
219 {
220
223 *fastrpc_shell_addr = (uint32_t)&qurt_sem_add;
224 *libhexagon_addr = (uint32_t)&hexagon_nn_get_dsp_offset;
230 }4
当内存属性改变后,我可以向其中写入shellcode:
src/interface.c
218 int hexagon_nn_get_dsp_offset(uint32_t *libhexagon_addr, uint32_t *fastrpc_shell_addr)
219 {
220
223 *fastrpc_shell_addr = (uint32_t)&qurt_sem_add;
224 *libhexagon_addr = (uint32_t)&hexagon_nn_get_dsp_offset;
230 }5
接着修改setter_func指针,使其指向shellcode,触发shellcode。如何确认shellcode是否真的执行了?一种方法是通过logcat查看日志:
src/interface.c
218 int hexagon_nn_get_dsp_offset(uint32_t *libhexagon_addr, uint32_t *fastrpc_shell_addr)
219 {
220
223 *fastrpc_shell_addr = (uint32_t)&qurt_sem_add;
224 *libhexagon_addr = (uint32_t)&hexagon_nn_get_dsp_offset;
230 }6
我故意写入一些非法指令:
src/interface.c
218 int hexagon_nn_get_dsp_offset(uint32_t *libhexagon_addr, uint32_t *fastrpc_shell_addr)
219 {
220
223 *fastrpc_shell_addr = (uint32_t)&qurt_sem_add;
224 *libhexagon_addr = (uint32_t)&hexagon_nn_get_dsp_offset;
230 }7
如果shellcode得到执行,logcat会显示以下日志:
src/interface.c
218 int hexagon_nn_get_dsp_offset(uint32_t *libhexagon_addr, uint32_t *fastrpc_shell_addr)
219 {
220
223 *fastrpc_shell_addr = (uint32_t)&qurt_sem_add;
224 *libhexagon_addr = (uint32_t)&hexagon_nn_get_dsp_offset;
230 }8
从日志中我们可以看到PC指向0x616104,与我们存放shellcode的位置相符(有地址随机化)。从而证明shellcode得到执行。
内核漏洞挖掘
我从《Pwn2Own Qualcomm DSP》得知驱动是一个潜在的攻击面。分析驱动的前提是知道驱动的处理函数入口。一种方法是通过字符串搜索,比如搜索“/dev”查看可能的设备名字:
src/interface.c
218 int hexagon_nn_get_dsp_offset(uint32_t *libhexagon_addr, uint32_t *fastrpc_shell_addr)
219 {
220
223 *fastrpc_shell_addr = (uint32_t)&qurt_sem_add;
224 *libhexagon_addr = (uint32_t)&hexagon_nn_get_dsp_offset;
230 }9
这种方式只能找到部分驱动。还有一种方式是通过qurt_qdi_obj_t结构体来寻找。qurt_qdi_obj_t结构体定义如下:
src/interface.c
335 int hexagon_nn_set_debug_level(nn_id_t id, int level)
336 {
337 struct nn_graph *graph;
338 if ((graph = nn_id_to_graph(id)) == NULL) return -1;
339 if (level < 0) level = 0;
340 graph->debug_level = level;
341 return 0;
342 }0
其中invoke是驱动处理函数,refcnt表示引用计数。refcnt的值非常有意思:
src/interface.c
335 int hexagon_nn_set_debug_level(nn_id_t id, int level)
336 {
337 struct nn_graph *graph;
338 if ((graph = nn_id_to_graph(id)) == NULL) return -1;
339 if (level < 0) level = 0;
340 graph->debug_level = level;
341 return 0;
342 }1
初始情况下,它的值为QDI_REFCNT_INIT,即0x51FFFE。通过搜索这个特定值,我能找到更多的驱动入口函数。在分析这些处理函数之前,需要确认函数参数。从qurt_qdi_pfn_invoke_t的定义发现,这些处理函数的参数如下:
src/interface.c
335 int hexagon_nn_set_debug_level(nn_id_t id, int level)
336 {
337 struct nn_graph *graph;
338 if ((graph = nn_id_to_graph(id)) == NULL) return -1;
339 if (level < 0) level = 0;
340 graph->debug_level = level;
341 return 0;
342 }2
从qurt_qdi_driver.h头文件中的注释可以得知参数布局:
src/interface.c
335 int hexagon_nn_set_debug_level(nn_id_t id, int level)
336 {
337 struct nn_graph *graph;
338 if ((graph = nn_id_to_graph(id)) == NULL) return -1;
339 if (level < 0) level = 0;
340 graph->debug_level = level;
341 return 0;
342 }3
现在可以开始分析驱动了。
i2c驱动任意地址读漏洞
我在i2c驱动中找到多个漏洞,比较好的漏洞包括任意地址写0和任意地址读。这里仅介绍任意地址读漏洞。dev_i2c_invoke()首先查看method,并将相关参数保存到各个寄存器中:
src/interface.c
335 int hexagon_nn_set_debug_level(nn_id_t id, int level)
336 {
337 struct nn_graph *graph;
338 if ((graph = nn_id_to_graph(id)) == NULL) return -1;
339 if (level < 0) level = 0;
340 graph->debug_level = level;
341 return 0;
342 }4
如果方法为0x1XX,跳转到dev_i2c_methods():
src/interface.c
335 int hexagon_nn_set_debug_level(nn_id_t id, int level)
336 {
337 struct nn_graph *graph;
338 if ((graph = nn_id_to_graph(id)) == NULL) return -1;
339 if (level < 0) level = 0;
340 graph->debug_level = level;
341 return 0;
342 }5
dev_i2c_method_0x109()存在任意地址读问题,分析如下:
src/interface.c
335 int hexagon_nn_set_debug_level(nn_id_t id, int level)
336 {
337 struct nn_graph *graph;
338 if ((graph = nn_id_to_graph(id)) == NULL) return -1;
339 if (level < 0) level = 0;
340 graph->debug_level = level;
341 return 0;
342 }6
这个函数直接从r0 + #0x2C偏移处读取数据,然后返回给应用。
gpio驱动任意地址写漏洞
drv_gpio_invoke()函数逻辑如下:
src/interface.c
335 int hexagon_nn_set_debug_level(nn_id_t id, int level)
336 {
337 struct nn_graph *graph;
338 if ((graph = nn_id_to_graph(id)) == NULL) return -1;
339 if (level < 0) level = 0;
340 graph->debug_level = level;
341 return 0;
342 }7
如果sub_F0127090()的返回值不为0(经过测试,sub_F0127090()返回值不为0),那么函数最终会调用sub_F00F2D08():
src/interface.c
335 int hexagon_nn_set_debug_level(nn_id_t id, int level)
336 {
337 struct nn_graph *graph;
338 if ((graph = nn_id_to_graph(id)) == NULL) return -1;
339 if (level < 0) level = 0;
340 graph->debug_level = level;
341 return 0;
342 }8
它根据用户传入的method的idx调用相关函数。需要注意的是:method低8位需要减去0xF4。drv_gpio_method_0()存在任意地址写问题,分析如下:
src/interface.c
335 int hexagon_nn_set_debug_level(nn_id_t id, int level)
336 {
337 struct nn_graph *graph;
338 if ((graph = nn_id_to_graph(id)) == NULL) return -1;
339 if (level < 0) level = 0;
340 graph->debug_level = level;
341 return 0;
342 }9
这个函数最终将参数1的值写入参数2指向的内存中。
内核漏洞利用
现在我已经找到读写原语,我需要搭建起从AP到Hexagon内核攻击的桥梁。具体来说,我需要在hexagon_nn应用中编写shellcode,它可以将AP发来的请求转发给上述读写原语,从而实现在AP侧直接读写Hexagon内核。
首先,我复刻了SDK中已有的hexagon_nn项目,在项目中编写了读写原语相关函数:
src/interface.c
hexagon_nn_domains_snpprint()
|
|-> hexagon_nn_snpprint()
|
|-> do_snpprint()
48 void do_snpprint(struct nn_graph *nn, char *buf, uint32_t n)
49 {
57 PRINTF_APPEND(buf,n,len,"nn @ %p: id=0x%lx debug_level=%d\n",nn,nn->id,nn->debug_level);
58 for (node = nn->head; node != NULL; node = node->next) {
61 PRINTF_APPEND(buf,n,len,"node @ %p: id=0x%x type=0x%x(%s) n_inputs=%d n_outputs=%d padding=%x(%s)\n", // 泄露node地址信息
62 node,
63 (unsigned int)node->node_id,
64 (unsigned int)node->node_type,
65 opname,
66 (unsigned int)node->n_inputs,
67 (unsigned int)node->n_outputs,
68 node->padding,
69 padname);
70 if (nn->debug_level > 0) for (i = 0; i < node->n_inputs; i++) {
71 PRINTF_APPEND(buf,n,len,"... input %d @ %p <src_id %x out_idx %d>\n", // 泄露input地址信息
72 i,
73 node->inputs[i],
74 (unsigned int)node->input_refs[i].src_id,
75 (unsigned int)node->input_refs[i].output_idx);
76 }
77 if (nn->debug_level > 0) for (i = 0; i < node->n_outputs; i++) {
78 PRINTF_APPEND(buf,n,len,"... output %d @ %p\n", i,node->outputs[i]); // 泄露output地址信息
79 }
84 }0
编译上述函数后,我可以获得相关的shellcode指令。需要注意的是这两个函数使用了shell程序中提供的API,我们需要在运行时进行链接,以便它们能够正确地调用API。我手动编写了跳转表,以便能够调用目标函数:
src/interface.c
hexagon_nn_domains_snpprint()
|
|-> hexagon_nn_snpprint()
|
|-> do_snpprint()
48 void do_snpprint(struct nn_graph *nn, char *buf, uint32_t n)
49 {
57 PRINTF_APPEND(buf,n,len,"nn @ %p: id=0x%lx debug_level=%d\n",nn,nn->id,nn->debug_level);
58 for (node = nn->head; node != NULL; node = node->next) {
61 PRINTF_APPEND(buf,n,len,"node @ %p: id=0x%x type=0x%x(%s) n_inputs=%d n_outputs=%d padding=%x(%s)\n", // 泄露node地址信息
62 node,
63 (unsigned int)node->node_id,
64 (unsigned int)node->node_type,
65 opname,
66 (unsigned int)node->n_inputs,
67 (unsigned int)node->n_outputs,
68 node->padding,
69 padname);
70 if (nn->debug_level > 0) for (i = 0; i < node->n_inputs; i++) {
71 PRINTF_APPEND(buf,n,len,"... input %d @ %p <src_id %x out_idx %d>\n", // 泄露input地址信息
72 i,
73 node->inputs[i],
74 (unsigned int)node->input_refs[i].src_id,
75 (unsigned int)node->input_refs[i].output_idx);
76 }
77 if (nn->debug_level > 0) for (i = 0; i < node->n_outputs; i++) {
78 PRINTF_APPEND(buf,n,len,"... output %d @ %p\n", i,node->outputs[i]); // 泄露output地址信息
79 }
84 }1
有了内核读写原语之后,我可以轻易地劫持函数指针。但这并不是我想要的,我的想法是能否借助Hexagon攻击AP内核?例如能否将内存映射到Hexagon中,通过Hexagon来攻击Android内核?之所以有这样的想法,原因在于Hexagon芯片很高级:它有自己的MMU。虽然这种想法最终没有实现,但我还是想分享一下研究过程。
要实现这种攻击,我需要完成两件事情:一是需要了解Hexagon的页表格式;二是识别出页表寄存器。《Hexagon Virtual Machine Specification》中描述了页表相关格式。Hexagon的MMU支持两种不同的页表。第一种是Translation List项:
src/interface.c
hexagon_nn_domains_snpprint()
|
|-> hexagon_nn_snpprint()
|
|-> do_snpprint()
48 void do_snpprint(struct nn_graph *nn, char *buf, uint32_t n)
49 {
57 PRINTF_APPEND(buf,n,len,"nn @ %p: id=0x%lx debug_level=%d\n",nn,nn->id,nn->debug_level);
58 for (node = nn->head; node != NULL; node = node->next) {
61 PRINTF_APPEND(buf,n,len,"node @ %p: id=0x%x type=0x%x(%s) n_inputs=%d n_outputs=%d padding=%x(%s)\n", // 泄露node地址信息
62 node,
63 (unsigned int)node->node_id,
64 (unsigned int)node->node_type,
65 opname,
66 (unsigned int)node->n_inputs,
67 (unsigned int)node->n_outputs,
68 node->padding,
69 padname);
70 if (nn->debug_level > 0) for (i = 0; i < node->n_inputs; i++) {
71 PRINTF_APPEND(buf,n,len,"... input %d @ %p <src_id %x out_idx %d>\n", // 泄露input地址信息
72 i,
73 node->inputs[i],
74 (unsigned int)node->input_refs[i].src_id,
75 (unsigned int)node->input_refs[i].output_idx);
76 }
77 if (nn->debug_level > 0) for (i = 0; i < node->n_outputs; i++) {
78 PRINTF_APPEND(buf,n,len,"... output %d @ %p\n", i,node->outputs[i]); // 泄露output地址信息
79 }
84 }2
各个标志位含义:
src/interface.c
hexagon_nn_domains_snpprint()
|
|-> hexagon_nn_snpprint()
|
|-> do_snpprint()
48 void do_snpprint(struct nn_graph *nn, char *buf, uint32_t n)
49 {
57 PRINTF_APPEND(buf,n,len,"nn @ %p: id=0x%lx debug_level=%d\n",nn,nn->id,nn->debug_level);
58 for (node = nn->head; node != NULL; node = node->next) {
61 PRINTF_APPEND(buf,n,len,"node @ %p: id=0x%x type=0x%x(%s) n_inputs=%d n_outputs=%d padding=%x(%s)\n", // 泄露node地址信息
62 node,
63 (unsigned int)node->node_id,
64 (unsigned int)node->node_type,
65 opname,
66 (unsigned int)node->n_inputs,
67 (unsigned int)node->n_outputs,
68 node->padding,
69 padname);
70 if (nn->debug_level > 0) for (i = 0; i < node->n_inputs; i++) {
71 PRINTF_APPEND(buf,n,len,"... input %d @ %p <src_id %x out_idx %d>\n", // 泄露input地址信息
72 i,
73 node->inputs[i],
74 (unsigned int)node->input_refs[i].src_id,
75 (unsigned int)node->input_refs[i].output_idx);
76 }
77 if (nn->debug_level > 0) for (i = 0; i < node->n_outputs; i++) {
78 PRINTF_APPEND(buf,n,len,"... output %d @ %p\n", i,node->outputs[i]); // 泄露output地址信息
79 }
84 }3
页大小信息:
src/interface.c
hexagon_nn_domains_snpprint()
|
|-> hexagon_nn_snpprint()
|
|-> do_snpprint()
48 void do_snpprint(struct nn_graph *nn, char *buf, uint32_t n)
49 {
57 PRINTF_APPEND(buf,n,len,"nn @ %p: id=0x%lx debug_level=%d\n",nn,nn->id,nn->debug_level);
58 for (node = nn->head; node != NULL; node = node->next) {
61 PRINTF_APPEND(buf,n,len,"node @ %p: id=0x%x type=0x%x(%s) n_inputs=%d n_outputs=%d padding=%x(%s)\n", // 泄露node地址信息
62 node,
63 (unsigned int)node->node_id,
64 (unsigned int)node->node_type,
65 opname,
66 (unsigned int)node->n_inputs,
67 (unsigned int)node->n_outputs,
68 node->padding,
69 padname);
70 if (nn->debug_level > 0) for (i = 0; i < node->n_inputs; i++) {
71 PRINTF_APPEND(buf,n,len,"... input %d @ %p <src_id %x out_idx %d>\n", // 泄露input地址信息
72 i,
73 node->inputs[i],
74 (unsigned int)node->input_refs[i].src_id,
75 (unsigned int)node->input_refs[i].output_idx);
76 }
77 if (nn->debug_level > 0) for (i = 0; i < node->n_outputs; i++) {
78 PRINTF_APPEND(buf,n,len,"... output %d @ %p\n", i,node->outputs[i]); // 泄露output地址信息
79 }
84 }4
实际上adsp.elf有一个段全部是Translation List项。使用hexagon-readelf读取的adsp.elf信息如下:
src/interface.c
hexagon_nn_domains_snpprint()
|
|-> hexagon_nn_snpprint()
|
|-> do_snpprint()
48 void do_snpprint(struct nn_graph *nn, char *buf, uint32_t n)
49 {
57 PRINTF_APPEND(buf,n,len,"nn @ %p: id=0x%lx debug_level=%d\n",nn,nn->id,nn->debug_level);
58 for (node = nn->head; node != NULL; node = node->next) {
61 PRINTF_APPEND(buf,n,len,"node @ %p: id=0x%x type=0x%x(%s) n_inputs=%d n_outputs=%d padding=%x(%s)\n", // 泄露node地址信息
62 node,
63 (unsigned int)node->node_id,
64 (unsigned int)node->node_type,
65 opname,
66 (unsigned int)node->n_inputs,
67 (unsigned int)node->n_outputs,
68 node->padding,
69 padname);
70 if (nn->debug_level > 0) for (i = 0; i < node->n_inputs; i++) {
71 PRINTF_APPEND(buf,n,len,"... input %d @ %p <src_id %x out_idx %d>\n", // 泄露input地址信息
72 i,
73 node->inputs[i],
74 (unsigned int)node->input_refs[i].src_id,
75 (unsigned int)node->input_refs[i].output_idx);
76 }
77 if (nn->debug_level > 0) for (i = 0; i < node->n_outputs; i++) {
78 PRINTF_APPEND(buf,n,len,"... output %d @ %p\n", i,node->outputs[i]); // 泄露output地址信息
79 }
84 }5
顺便提下:adsp中包含了supervisor和QuRT两部分代码,前者位于0xf0000000,后者位于0xb0000000。按照上述格式解析0xe65c5000段中的数据:
src/interface.c
hexagon_nn_domains_snpprint()
|
|-> hexagon_nn_snpprint()
|
|-> do_snpprint()
48 void do_snpprint(struct nn_graph *nn, char *buf, uint32_t n)
49 {
57 PRINTF_APPEND(buf,n,len,"nn @ %p: id=0x%lx debug_level=%d\n",nn,nn->id,nn->debug_level);
58 for (node = nn->head; node != NULL; node = node->next) {
61 PRINTF_APPEND(buf,n,len,"node @ %p: id=0x%x type=0x%x(%s) n_inputs=%d n_outputs=%d padding=%x(%s)\n", // 泄露node地址信息
62 node,
63 (unsigned int)node->node_id,
64 (unsigned int)node->node_type,
65 opname,
66 (unsigned int)node->n_inputs,
67 (unsigned int)node->n_outputs,
68 node->padding,
69 padname);
70 if (nn->debug_level > 0) for (i = 0; i < node->n_inputs; i++) {
71 PRINTF_APPEND(buf,n,len,"... input %d @ %p <src_id %x out_idx %d>\n", // 泄露input地址信息
72 i,
73 node->inputs[i],
74 (unsigned int)node->input_refs[i].src_id,
75 (unsigned int)node->input_refs[i].output_idx);
76 }
77 if (nn->debug_level > 0) for (i = 0; i < node->n_outputs; i++) {
78 PRINTF_APPEND(buf,n,len,"... output %d @ %p\n", i,node->outputs[i]); // 泄露output地址信息
79 }
84 }6
第二种是普通的页表。HVM 定义了两级虚拟页表:第一级将虚拟地址空间分解为 1020 个 4MB 段,每个段由页表条目(PTE)表示。
一级 PTE 始终包含映射的虚拟内存页面的大小: (1) 对于 4MB 或更大的页面,第一级条目包含翻译和页面的权限信息。 (2) 对于小于 4MB 的页面,第一级条目包含指向二级页表的指针。
一级 PTE 的低三位对条目类型和页面大小进行编码,其余位的定义因条目类型而异:
src/interface.c
hexagon_nn_domains_snpprint()
|
|-> hexagon_nn_snpprint()
|
|-> do_snpprint()
48 void do_snpprint(struct nn_graph *nn, char *buf, uint32_t n)
49 {
57 PRINTF_APPEND(buf,n,len,"nn @ %p: id=0x%lx debug_level=%d\n",nn,nn->id,nn->debug_level);
58 for (node = nn->head; node != NULL; node = node->next) {
61 PRINTF_APPEND(buf,n,len,"node @ %p: id=0x%x type=0x%x(%s) n_inputs=%d n_outputs=%d padding=%x(%s)\n", // 泄露node地址信息
62 node,
63 (unsigned int)node->node_id,
64 (unsigned int)node->node_type,
65 opname,
66 (unsigned int)node->n_inputs,
67 (unsigned int)node->n_outputs,
68 node->padding,
69 padname);
70 if (nn->debug_level > 0) for (i = 0; i < node->n_inputs; i++) {
71 PRINTF_APPEND(buf,n,len,"... input %d @ %p <src_id %x out_idx %d>\n", // 泄露input地址信息
72 i,
73 node->inputs[i],
74 (unsigned int)node->input_refs[i].src_id,
75 (unsigned int)node->input_refs[i].output_idx);
76 }
77 if (nn->debug_level > 0) for (i = 0; i < node->n_outputs; i++) {
78 PRINTF_APPEND(buf,n,len,"... output %d @ %p\n", i,node->outputs[i]); // 泄露output地址信息
79 }
84 }7
以下是s位在不同值下对应的条目格式:
src/interface.c
hexagon_nn_domains_snpprint()
|
|-> hexagon_nn_snpprint()
|
|-> do_snpprint()
48 void do_snpprint(struct nn_graph *nn, char *buf, uint32_t n)
49 {
57 PRINTF_APPEND(buf,n,len,"nn @ %p: id=0x%lx debug_level=%d\n",nn,nn->id,nn->debug_level);
58 for (node = nn->head; node != NULL; node = node->next) {
61 PRINTF_APPEND(buf,n,len,"node @ %p: id=0x%x type=0x%x(%s) n_inputs=%d n_outputs=%d padding=%x(%s)\n", // 泄露node地址信息
62 node,
63 (unsigned int)node->node_id,
64 (unsigned int)node->node_type,
65 opname,
66 (unsigned int)node->n_inputs,
67 (unsigned int)node->n_outputs,
68 node->padding,
69 padname);
70 if (nn->debug_level > 0) for (i = 0; i < node->n_inputs; i++) {
71 PRINTF_APPEND(buf,n,len,"... input %d @ %p <src_id %x out_idx %d>\n", // 泄露input地址信息
72 i,
73 node->inputs[i],
74 (unsigned int)node->input_refs[i].src_id,
75 (unsigned int)node->input_refs[i].output_idx);
76 }
77 if (nn->debug_level > 0) for (i = 0; i < node->n_outputs; i++) {
78 PRINTF_APPEND(buf,n,len,"... output %d @ %p\n", i,node->outputs[i]); // 泄露output地址信息
79 }
84 }8
对于页大小是4MB和16MB的页表,只有一级页表。此时,它是page table(而不是page directory)。其中的页表项格式如下:
4M页表项:
src/interface.c
hexagon_nn_domains_snpprint()
|
|-> hexagon_nn_snpprint()
|
|-> do_snpprint()
48 void do_snpprint(struct nn_graph *nn, char *buf, uint32_t n)
49 {
57 PRINTF_APPEND(buf,n,len,"nn @ %p: id=0x%lx debug_level=%d\n",nn,nn->id,nn->debug_level);
58 for (node = nn->head; node != NULL; node = node->next) {
61 PRINTF_APPEND(buf,n,len,"node @ %p: id=0x%x type=0x%x(%s) n_inputs=%d n_outputs=%d padding=%x(%s)\n", // 泄露node地址信息
62 node,
63 (unsigned int)node->node_id,
64 (unsigned int)node->node_type,
65 opname,
66 (unsigned int)node->n_inputs,
67 (unsigned int)node->n_outputs,
68 node->padding,
69 padname);
70 if (nn->debug_level > 0) for (i = 0; i < node->n_inputs; i++) {
71 PRINTF_APPEND(buf,n,len,"... input %d @ %p <src_id %x out_idx %d>\n", // 泄露input地址信息
72 i,
73 node->inputs[i],
74 (unsigned int)node->input_refs[i].src_id,
75 (unsigned int)node->input_refs[i].output_idx);
76 }
77 if (nn->debug_level > 0) for (i = 0; i < node->n_outputs; i++) {
78 PRINTF_APPEND(buf,n,len,"... output %d @ %p\n", i,node->outputs[i]); // 泄露output地址信息
79 }
84 }9
16M页表项:
src/interface.c
hexagon_nn_domains_append_empty_const_node(data_len)
|
|-> hexagon_nn_append_empty_const_node(data_len)
|
|-> do_append_empty_const_node(data_len)
|
|-> hexagon_nn_empty_const_ctor(data_len)
|
|-> const_tensor = tensor_alloc(data_len)
| |
| | if (data_size) {
| | } else {
| | newtensor->data = NULL;
| | }
| | newtensor->max_size = newtensor->data_size = data_size;
| | newtensor->self = newtensor;
|
| self->n_outputs = 1;
| self->outputs = &const_tensor->self0
其他页表项:
src/interface.c
hexagon_nn_domains_append_empty_const_node(data_len)
|
|-> hexagon_nn_append_empty_const_node(data_len)
|
|-> do_append_empty_const_node(data_len)
|
|-> hexagon_nn_empty_const_ctor(data_len)
|
|-> const_tensor = tensor_alloc(data_len)
| |
| | if (data_size) {
| | } else {
| | newtensor->data = NULL;
| | }
| | newtensor->max_size = newtensor->data_size = data_size;
| | newtensor->self = newtensor;
|
| self->n_outputs = 1;
| self->outputs = &const_tensor->self1
我找到了hexagonMVM代码中创建页表的代码,它创建了16MB页表,格式如下:
src/interface.c
hexagon_nn_domains_append_empty_const_node(data_len)
|
|-> hexagon_nn_append_empty_const_node(data_len)
|
|-> do_append_empty_const_node(data_len)
|
|-> hexagon_nn_empty_const_ctor(data_len)
|
|-> const_tensor = tensor_alloc(data_len)
| |
| | if (data_size) {
| | } else {
| | newtensor->data = NULL;
| | }
| | newtensor->max_size = newtensor->data_size = data_size;
| | newtensor->self = newtensor;
|
| self->n_outputs = 1;
| self->outputs = &const_tensor->self2
可以看到每个页表项重复出现4次,与文档中描述一致。
在尝试寻找页表寄存器时,我发现SDK的文档有以下描述:
src/interface.c
hexagon_nn_domains_append_empty_const_node(data_len)
|
|-> hexagon_nn_append_empty_const_node(data_len)
|
|-> do_append_empty_const_node(data_len)
|
|-> hexagon_nn_empty_const_ctor(data_len)
|
|-> const_tensor = tensor_alloc(data_len)
| |
| | if (data_size) {
| | } else {
| | newtensor->data = NULL;
| | }
| | newtensor->max_size = newtensor->data_size = data_size;
| | newtensor->self = newtensor;
|
| self->n_outputs = 1;
| self->outputs = &const_tensor->self3
这段话的意思是AP侧总要分配连续的物理内存给DSP,有时这是一种难以满足的要求。有的设备会在DSP芯片前面加上SMMU,SMMU可以将零散的(不连续的)物理地址映射到连续的虚拟地址上,此时DSP看到的是连续的“物理地址”(实际是SMMU给它营造的虚拟地址)。通过引入SMMU,AP可以分配零散的内存给DSP,从而提高了内存的使用效率。
SMMU的加入显然会限制DSP的访存能力:DSP看到的不再是物理地址,而是SMMU导出的虚拟地址。这意味着DSP每次访存都要经过SMMU的翻译。此时,即便我可以篡改DSP的页表,也无法逃离SMMU的牢笼(它们之间的关系类似hypervisor和kernel)。因此,上述想法仅在已有漏洞的情况下无法实现。
Fuzz方法
过去某段时间,国内二手机市场出现过谷歌Pixel 4工程机。这些工程机的Secure boot是关闭的,有意思的是它们可以安装谷歌发布的最新release版本系统。我过去研究过这种工程机,它存在着一种隐秘的攻击:钉枪攻击。简单来说这种攻击方式利用了ARM架构的调试特点:它支持核间调试。我可以在一台Pixel 4手机上完成自我调试,例如使用CPU1调试CPU2。有意思的是:我(位于EL1)可以使被调试的CPU进入EL3,以侵占的方式篡改EL3内存。实现这种攻击只需要能够加载AP侧内核module即可。
既然能够修改EL3内存,篡改Hexagon的内存应该不是难事。因为EL3是AP的最高权限级别,CPU在这个级别下理应能够看到所有内存。现在我需要做的就是找到Hexagon系统在内存中的位置。根据adsp.elf信息,我发现它的加载地址是0x8be00000。除了这种方法之外,也可以通过系统日志确定:
src/interface.c
hexagon_nn_domains_append_empty_const_node(data_len)
|
|-> hexagon_nn_append_empty_const_node(data_len)
|
|-> do_append_empty_const_node(data_len)
|
|-> hexagon_nn_empty_const_ctor(data_len)
|
|-> const_tensor = tensor_alloc(data_len)
| |
| | if (data_size) {
| | } else {
| | newtensor->data = NULL;
| | }
| | newtensor->max_size = newtensor->data_size = data_size;
| | newtensor->self = newtensor;
|
| self->n_outputs = 1;
| self->outputs = &const_tensor->self4
从日志中可以发现ADSP固件加载地址同样是0x000000008be00000。还有一种方法是从device tree中查找相关信息,device tree其实指示了DSP系统加载位置,内核根据它的指示完成加载(lpass:Low Power Audio Subsystem):
src/interface.c
hexagon_nn_domains_append_empty_const_node(data_len)
|
|-> hexagon_nn_append_empty_const_node(data_len)
|
|-> do_append_empty_const_node(data_len)
|
|-> hexagon_nn_empty_const_ctor(data_len)
|
|-> const_tensor = tensor_alloc(data_len)
| |
| | if (data_size) {
| | } else {
| | newtensor->data = NULL;
| | }
| | newtensor->max_size = newtensor->data_size = data_size;
| | newtensor->self = newtensor;
|
| self->n_outputs = 1;
| self->outputs = &const_tensor->self5
其中pil_adsp_region用来存放ADSP代码,”removed-dma-pool”表示这部分内存完全被ADSP占据。no-map表示内核不能映射该地址。而adsp_region是内核用来与DSP进行通信的共享内存,它指定内核保留16M(0x1000000)内存,”shared-dma-pool”表示这段内存不是ADSP独享的,内核也可以使用空余内存。内核日志中有如下分配与之对应(?):
src/interface.c
hexagon_nn_domains_append_empty_const_node(data_len)
|
|-> hexagon_nn_append_empty_const_node(data_len)
|
|-> do_append_empty_const_node(data_len)
|
|-> hexagon_nn_empty_const_ctor(data_len)
|
|-> const_tensor = tensor_alloc(data_len)
| |
| | if (data_size) {
| | } else {
| | newtensor->data = NULL;
| | }
| | newtensor->max_size = newtensor->data_size = data_size;
| | newtensor->self = newtensor;
|
| self->n_outputs = 1;
| self->outputs = &const_tensor->self6
qcom,lpass@17300000指示了固件adsp(qcom,firmware-name=”adsp”)加载到0x7b表示的内存处(memory-region = <0x7b>),而pil_adsp_region的phandle是0x7b。 从以上信息可以确认adsp加载地址是0x8be00000。
映射Hexagon内存到EL3
我编写了相关工具来解析EL3的页表。我发现EL3并没有映射Hexagon内存。因此,我需要手动映射Hexagon到EL3。EL3的t0sz是36,根据ARMv8手册,ttbr寄存器指向level1页表,该页表支持Block类型的映射,格式如下:
src/interface.c
hexagon_nn_domains_append_empty_const_node(data_len)
|
|-> hexagon_nn_append_empty_const_node(data_len)
|
|-> do_append_empty_const_node(data_len)
|
|-> hexagon_nn_empty_const_ctor(data_len)
|
|-> const_tensor = tensor_alloc(data_len)
| |
| | if (data_size) {
| | } else {
| | newtensor->data = NULL;
| | }
| | newtensor->max_size = newtensor->data_size = data_size;
| | newtensor->self = newtensor;
|
| self->n_outputs = 1;
| self->outputs = &const_tensor->self7
Lower block attributes格式如下:
src/interface.c
hexagon_nn_domains_append_empty_const_node(data_len)
|
|-> hexagon_nn_append_empty_const_node(data_len)
|
|-> do_append_empty_const_node(data_len)
|
|-> hexagon_nn_empty_const_ctor(data_len)
|
|-> const_tensor = tensor_alloc(data_len)
| |
| | if (data_size) {
| | } else {
| | newtensor->data = NULL;
| | }
| | newtensor->max_size = newtensor->data_size = data_size;
| | newtensor->self = newtensor;
|
| self->n_outputs = 1;
| self->outputs = &const_tensor->self8
level1页表映射粒度是1GB,因此0x8be00000的起始地址是0x80000000,页表项是0x80000705。建立好映射后,我可以通过EL3来读写Hexagon所有内存,包括supervisor、QuRT和应用。
借助钉枪攻击,我可以修改Hexagon的所有代码。这种方法为Fuzz提供了强大的基础支持。同时,它和其他已知方法一样,存在着显而易见的限制。这里无意于比较孰优孰劣,只是想分享下我找到的新方法。
总结
Hexagon是一个非常复杂的系统。由于缺少相关资料,相关研究进展缓慢。这篇博客公开了我的一些发现,希望能够为后来者提供些许帮助。
还没有评论,来说两句吧...