翻译:华为未然实验室
预估稿费:300RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
传送门
【技术分享】漏洞挖掘之利用Broadcom的Wi-Fi栈(二)
众所周知,平台安全性是复杂系统安全性的一个不可或缺的组成部分,移动设备更是如此。现代移动平台包括多个处理单元,全都精巧地彼此通信。在应用处理器(AP)上运行的代码已得到广泛研究,但对其他组件的审查却很少。
图1
多年来,由于安全人士的持续关注,在应用处理器上运行的代码的防御力得到了加强。但是,攻击者往往会另辟蹊径。提高一个组件的安全性将不可避免地导致一些攻击者开始在别处寻找更容易的进入点。
该博客系列分两部分,我们将探讨由在移动设备上使用的Broadcom Wi-Fi SoC(系统级芯片)引入的暴露的攻击面。我们将专注于运行安卓的设备(本研究基本上也适用于包括相同的Wi-Fi SoC的其他系统)。第一篇博文将专注于利用Wi-Fi SoC本身,我们将发现和利用能让我们在芯片上远程执行代码的漏洞。在第二篇博文中,我们将进一步将我们的权限从SoC提升到操作系统的内核。通过这两篇文章,我们将展示如何在无需用户交互的情况下仅通过Wi-Fi邻近就完全接管设备。
我们将专注于Broadcom(博通)的Wi-Fi SoC,因为其是移动设备上最常见的Wi-Fi芯片组。使用该平台的设备很多,出于本文目的,我们将展示运行安卓7.1.1版NUF26K的完全更新(当时,现已修复)的Nexus 6P的一个远程代码执行漏洞。
为什么是Wi-Fi?
在过去十年中,Wi-Fi在移动设备上的使用已变得很普遍。Wi-Fi已渐渐演变为一套强大的规范——一些注重物理层,另一些则侧重于MAC层。为了应对日益增加的复杂性,供应商已经开始生产“FullMAC”Wi-Fi SoC。
本质上,这些是独立执行所有的物理层、MAC层及MAC子层管理实体(MLME)处理,从而使操作系统可以从与Wi-Fi有关的复杂(有时是芯片特定的)功能抽离的小型SoC。Wi-Fi FullMAC芯片的推出也改善了移动设备的功耗,因为大部分处理是在低功耗SoC而不是耗电量较大的应用处理器上完成的。也许最重要的是,FullMAC芯片更容易集成,原因是其在固件中实施MLME,从而降低了主机端的复杂性。
但Wi-Fi FullMAC芯片的推出也有代价。引入这些新的硬件、运行专有和复杂的代码库可能会削弱设备的整体安全性,并引入可能危及整个系统的漏洞。
图2
探索平台
为了开始研究,我们需要找到一些方法来探索Wi-Fi芯片。 幸运的是,赛普拉斯最近收购了Broadcom的无线物联网业务,并发布了许多与Broadcom Wi-Fi芯片组相关的数据手册。 通过阅读数据手册,我们深入了解了Wi-Fi芯片组背后的硬件架构。
图3
具体而言,我们可以看到使用的是ARM Cortex R4内核,其运行处理帧的所有逻辑。此外,该数据手册显示,ARM内核具有用于保存固件代码的640KB ROM,以及用于数据处理(例如堆)和存储固件代码补丁的768KB RAM。
要开始分析在ARM内核上运行的代码,我们需要提取ROM的内容,并定位加载到RAM中的数据。
我们先来解决第二个问题——加载到ARM内核的RAM中的数据位于何处?由于该数据不存在于ROM中,因此必须在芯片首次上电时从外部加载。因此,通过读取主机驱动程序中的初始化代码,我们应该可以找到包含RAM内容的文件。实际上,通过驱动程序的代码,我们找到了BCMDHD_FW_PATH配置,其用于表示驱动程序将内容上传到RAM的文件的位置。
那么ROM的内容呢?提取ROM的一种方法是使用主机驱动程序的芯片存储器访问功能(通过SDIO或PCIe上的PIO)直接读取ROM的内容。但是,这样做将需要修改驱动程序,以使我们能够发出转储ROM所需的命令。检索ROM的另一种方法是将我们自己修改的固件文件加载到RAM中,我们将插入一个可用于转储ROM内存范围的小型存根。幸运的是,本文的情况实际上并不需要这些方法,Broadcom提供了一个非常强大的命令行实用程序dhdutil,可用于通过bcmdhd驱动程序与芯片进行交互。
在该实用程序支持的各种功能中,其还允许我们通过发出特殊命令“membytes”直接读取和写入适配器上的内存。由于我们已经知道了ROM的大小(从数据手册中),我们可以直接使用membytes命令来读取ROM的内容。但是,我们还需要先回答最后一个问题——ROM位于哪里?根据有关人员的研究,ROM被加载到地址0x0,RAM被加载到地址0x180000。
最后,把所有这一切放在一起,我们可以从固件文件获取RAM的内容,使用dhdutil转储ROM,并将这两个文件合并成一个文件,然后便可在IDA中开始分析。
图4
分析固件
由于可用内存(ROM和RAM)相对较小,Broadcom为了节省内存而进行了极大的努力。首先,他们从二进制文件中删除了符号和大部分字符串。这样做的额外好处是使固件代码的逆向工程稍微更麻烦。他们还专门选择了Thumb-2指令集,这样可以实现更好的代码密度。因此,BCM4358上的ROM镜像的封装非常紧凑,仅包含不到300个未使用的字节。
但是,这还不够。别忘了,RAM必须容纳堆、栈、全局数据结构及ROM功能的所有补丁或修改。这对少得可怜的768KB而言是一个相当高的要求。为了解决这个问题,Broadcom决定将固件初始化期间使用的所有功能放在两个特殊区域。初始化完成后,这些区域被“回收”,随后转换为堆块。此外,堆块散布在RAM中的代码和数据结构之间,因为后者有时有对齐要求(或直接从ROM引用,因此无法移动)。最终的结果是RAM是一堆混乱的堆块、代码及数据结构。
图5
在花了一些时间分析固件后,我们至少可以开始识别一些包含函数名和其他提示的字符串,以帮助我们了解代码库。此外,NexMon研究人员发布了对应于BCM4339固件的收集的符号。我们可以将相同的符号应用于BCM4339的固件,然后使用bindiff关联更新芯片的更新版本的固件的符号名称。
最后还有一个诀窍——除了我们正在分析的FullMAC SoC外,Broadcom还生产SoftMAC芯片。由于这些SoftMAC芯片不处理MLME层,所以其相应的驱动程序必须执行该处理。因此,许多Broadcom的MLME处理代码都包含在开源SoftMAC驱动程序-brcmsmac中。虽然这并不能帮助我们了解任何芯片特定的功能或更多的内部处理代码,但它似乎与固件的代码有许多相同的实用功能。
寻找bug
现在我们已掌握了固件的结构,并且有了分析的手段,我们终于可以开始寻找bug了。 但我们应该从哪里开始呢?
虽然有上文所述的所有技巧,这仍是一个相对较大和不透明的二进制文件,字符串或符号很少。一种可能的方法是测试固件,以便跟踪在接收和处理数据包时所采用的代码路径。Cortex R4确实有调试寄存器,可用于设置断点和检查各个位置的代码流。或者,我们可以手动定位一组用于从接收到的帧解析和检索信息的函数,并从那里逆向。
这正是熟悉Wi-Fi派上用场之处。Wi-Fi管理帧以小的“标记”数据块(称为信息元素(IE))对大多数信息进行编码。这些标记的数据块为TLV结构,其中标签和长度字段是单字节长。
图6
由于在Wi-Fi帧(数据本身除外)中传输的大部分信息是使用IE进行编码的,所以其是我们逆向工作的良好候选者。此外,由于“标签”值是唯一且标准化的,所以我们可以使用其值来帮助我们熟悉当前处理的代码流。
从brcmsmac驱动程序可以看到,Broadcom使用一个函数从bcm_parse_tlvs帧提取IE。经过简短搜索(通过关联附近的字符串提示),我们在固件的ROM中找到了相同的函数。太好了。
现在我们可以开始交叉引用调用该函数的位置,并逆向每一个调用站点。虽然比逆向固件的每一部分要容易得多,但这仍然需要相当长的时间。
完成所有调用站点的逆向工程后,我发现了一些与处理嵌入在管理帧中的信息元素有关的漏洞。
当连接到支持无线漫游功能(FT或思科的CCKM漫游)的网络时,可以触发其中的两个漏洞。一方面,这些漏洞相对易于利用——是简单的栈溢出。此外,在固件(HNDRTE)上运行的操作系统不使用栈cookie,因此不需要额外的信息泄漏或绕过。
但是,虽然这些漏洞可能很容易利用,但需要一些设置来实现。首先,我们需要广播支持这些功能的Wi-Fi网络。802.11r FT是一种由hostapd实现的开放标准。相比之下,CCKM是一种专有标准。了解如何模拟CCKM网络很不容易。
另外,我们需要弄清楚哪些设备实际上支持上述功能。Broadcom提供许多可由客户授权的功能——并非所有设备都具有所有功能。
幸运的是,Broadcom使区分每个固件镜像中实际存在哪些功能变得容易。下载到芯片的RAM内容中的最后几个字节包含固件的“版本字符串”。该字符串包含固件编译的日期、芯片的修订版本、固件的版本及一列破折号分隔的“标签”。每个标签代表固件镜像支持的一个功能。例如,以下是Nexus 6P的版本字符串:
图7
802.11r FT功能的存在由“fbt”标签指示。类似地,CCKM的支持由“ccx”标签指示。不幸的是,Nexus 6P似乎并不支持这些功能。事实上,在我自己的Android固件镜像库中快速搜索“ccx”功能(支持CCKM)后发现Nexus设备不支持此功能,但众多三星旗舰设备支持该功能。
那么其他两个漏洞呢?两者均与 隧道直接链路建立(TDLS)的实现有关。TDLS连接允许Wi-Fi网络上的对等体在彼此之间交换数据——不通过接入点(AP),这样可防止AP拥塞。
固件中对TDLS的支持由“betdls”和“tdls”标签指示。通过搜索我的固件库可以看到,绝大多数设备确实支持TDLS。
此外,TDLS被指定为802.11z标准的一部分。因为可以获得有关TDLS的所有信息,所以我们可以阅读该标准,以便熟悉Broadcom实现中的相关代码路径。作为开放标准,其还受到开源请求者的支持,比如wpa_supplicant。因此,我们可以检查wpa_supplicant中TDLS功能的实现,以进一步提高对固件中相关代码的了解。
最后,正如我们稍后将看到的,触发这两个漏洞可以由Wi-Fi网络上的任何对等体完成,而无需在被攻击设备上进行任何动作。这使探索这些漏洞更有意思。
无论如何,我们都决定利用TDLS漏洞。但是,在我们这样做之前,让我们先花点时间了解一下TDLS和发现的漏洞(如果您已熟悉TDLS,可跳过这一部分)。
802.11z TDLS 101
有许多同一Wi-Fi网络上的两个对等体希望在彼此之间传输大量的数据的用例。例如,将视频从您的移动设备投射到Chromecast将需要传输大量数据。在大多数情况下,Chromecast应该是相对靠近投射体。因此,将整个数据流从设备传递到AP(只为随后将其传递到Chromecast)是浪费的。
增加一个额外的跳跃(AP)会增加延迟并降低连接的质量。向AP传递这样大量的数据也会对AP本身造成压力,导致拥塞,并且会降低网络上所有对等体的Wi-Fi连接的质量。
这正是TDLS的用武之地。TDLS旨在提供一种不依赖AP的Wi-Fi网络上的对等通信方式。
在空中
我们先熟悉一下TDLS帧的结构。您可能知道,802.11帧使用“标志”字段来指示帧正在传播的“方向”(从客户端到AP、AP到客户端,等等)。TDLS流量选择使用指示Ad-Hoc (IBSS)网络中流量的标志值。
图8
接下来,TDLS帧由特殊的以太类型值0x890D来标识。通过Wi-Fi传输的TDLS帧在“有效载荷类型”字段中使用常数值,表明有效载荷具有以下结构:
图9
TDLS帧的类别也被设置为一个常数值。这使我们只有一个字段来区分不同的TDLS帧类型——“动作代码”。该1字节字段指示我们正在传输的TDLS帧的种类。这反过来控制着接收端解释“有效载荷”的方式。
高级流
在两个对等体可以建立连接之前,双方必须先知晓彼此的存在。这称为“发现”阶段。希望在网络上发现支持TDLS的对等体的Wi-Fi客户端可以通过向对等体发送“TDLS发现请求”帧来实现。接收到此帧的支持TDLS的对等体通过发送“TDLS发现响应”帧进行响应。请求和响应使用1字节的“对话令牌”彼此相关。
图10
接下来,对等体可能希望建立连接。为此,其必须执行3次握手。这种握手具有双重目的,首先表示两个对等体之间成功建立了连接,其次是用于导出TDLS对等体密钥(TPK,用于保护对等体之间的TDLS流量)。
图11
最后,创建连接后,两个对等体就可以在彼此之间交换对等流量。当其中一个对等体希望断开连接时,可以通过发送“TDLS断开”帧来实现。在接收到这样的帧后,TDLS对等体将断开连接并释放所有相关资源。
现在我们已对TDLS有了很好的了解,接下来我们来仔细看看手头的漏洞!
原语
为了确保在建立和断开阶段传送的消息的完整性,相应的TDLS帧包括消息完整性码(MIC)。对于建立阶段,接收到第二个握手消息(M2)后,双方便可导出TPK。使用TPK,TDLS发起者可以计算第三个握手帧内容的MIC,然后可由TDLS响应者验证。
MIC通过编码在握手帧中的IE的内容计算,如下所示:
图12
同样,断开帧也包括一个MIC,通过一组略微不同的IE计算:
图13
那么我们如何在固件的代码中找到这些计算呢?凑巧,一些指向TDLS的字符串遗留在了固件的ROM中,使我们可以快速定位相关的函数。
在对大部分指向处理TDLS动作帧的流程进行逆向工程后,我们最终到达了负责处理TDLS建立确认(PMK M3)帧的函数。该函数首先执行一些验证,以确保请求是合法的。其查询内部数据结构,以确保确实正在与请求对等体建立TDLS连接。然后,其验证Link-ID IE(通过检查其编码的BSSID与当前网络的匹配),并且还验证32字节的发起者随机数(“Snonce”)值(通过将其与存储的初始随机数进行比较)。
建立对请求可能确实是合法的一定程度的置信度后,该函数开始调用一个内部帮助函数,任务是计算MIC并确保其与编码在帧中的一致。固件还包括该函数的名称(“wlc_tdls_cal_mic_chk”)。
对该函数进行逆向工程后,我们得出以下近似高级逻辑:
图14
从上面可以看出,虽然该函数验证RSN IE的长度不超过分配的缓冲区长度(第13行),但其未能验证后续的IE也不会溢出缓冲区。因此,将RSN IE的长度设置为较大的值将导致Timeout Interval和Fast Transition IE越界复制,从而溢出缓冲区。
图15
例如,假设我们将RSN IE(x)的长度设置为最大可能值224,我们会获得如下元素位置:
图16
在该图示中,橙色字段与溢出“无关”。因为其位于缓存区边界内。红色字段表示我们无法完全控制的值,绿色字段表示完全可控的值。
比如,Timeout Interval IE在MIC计算之前验证,且仅具有容许值约束集,这使其不可控制。同样,FTIE的标签和长度字段是恒定的,因此是不可控的。最后,32位“Anonce”值由TDLS响应者随机选择,因此其位于我们的影响范围之外。
但情况并非如此严峻。FTIE本身中的几个字段可以任意选择——比如,在握手中的第一个消息期间,“Snonce”值由TLDS发起者选择。此外,FTIE中的“MIC Control”字段可以自由选择,因为其不是在执行此函数之前验证。
无论如何,现在我们已经对建立阶段的MIC验证进行了审核,让我们将目光转向断开阶段的MIC验证。也许代码也是在那里中断?查看断开阶段的MIC计算(“wlc_tdls_cal_mic_chk”)后,我们得到以下高级逻辑:
图17
再一次直接溢出,没有为确保不超过分配的缓冲区的长度对FT-IE的长度字段进行验证。这意味着通过提供专门设计的FT-IE就可以触发溢出。然而,在触发有漏洞的代码路径之前还是有若干验证,这限制了我们对溢出元素的控制。我们来尝试绘制溢出期间元素的位置:
图18
这似乎更简单——我们不需要担心在溢出之前验证的存储在FTIE中的值,因为其全部放置在缓冲区的范围内。相反,攻击者控制的部分只是不需要进行任何验证的备用数据,因此可以由我们自由选择。也就是说,溢出的程度是非常有限的,我们最多只能多写超过缓冲区范围的25个字节。
编写利用代码
研究堆状态
现在我们已了解了手头的原语,是时候来测试我们的假设是否与现实符合了。为此,我们需要一个测试台,使我们能发送专门设计的帧,从而触发溢出。回想一下,wpa_supplicant是一个完全支持TDLS的开源可移植请求者。这使它成为我们研究平台的首选。我们可以使用wpa_supplicant作为基础来设计我们的帧。这样我们就无需重新实现建立和维护TDLS连接所需的所有逻辑。
为了测试这些漏洞,我们将修改wpa_supplicant,以使我们能发送包含过大FTIE的TDLS断开帧。查看wpa_supplicant的代码可快速识别负责生成和发送断开帧的函数wpa_tdls_send_teardown。通过对该函数添加一些小的更改(绿色),我们应该能够在收到断开帧时触发溢出,导致超写25个字节的0xAB:
图19
现在,我们只需要与wpa_supplicant进行交互,以建立和断开与目标设备的TDLS连接。wpa_supplicant支持很多命令接口,包括一个名为wpa_cli的命令行实用程序,这非常方便。此命令行接口还支持若干暴露TDLS功能的命令:
TDLS_DISCOVER – 发送“TDLS发现请求”帧并列出响应
TDLS_SETUP – 建立与具有给定MAC地址的对等体的TDLS连接
TDLS_TEARDOWN – 断开与具有给定MAC地址的对等体的TDLS连接
实际上,在编译支持TDLS (CONFIG_TDLS)的wpa_supplicant、建立网络、将我们的目标设备和我们的研究平台连接到网络后,我们可以看到发出TDLS_DISCOVER命令是有效的,我们确实可以识别我们的对等体。
图20
我们现在可以发送一个TDLS_SETUP命令,然后发送我们专门设计的TDLS_TEARDOWN。如果一切正常,这应该会触发溢出。然而,这提出了一个略微更微妙的问题——我们如何得知溢出的发生?可能的情况是我们溢出的数据未被使用,或者,当固件崩溃时,其默默重新启动,让我们一无所知。
为了充分回答这个问题,我们需要了解Broadcom堆实现背后的逻辑。深入分析分配算符的逻辑,我们发现其非常简单,其是一个简单的“最适合”分配算符,其执行向前和向后合并,并保持一个空闲块单链表。当分配块时,从最适合空闲块(足够大的最小块)的末端(最高地址)对其进行切取。堆块具有以下结构:
图21
(回想一下,Cortex R4是一款32位ARM处理器,所以所有字节都以低字节序存储)
通过对分配算符的实现进行逆向工程,我们还可以找到指向RAM中第一个空闲块头的指针的位置。将这两个事实结合在一起,我们可以创建一个在给出固件的RAM的转储的情况下可绘制堆的空闲列表的当前状态的实用程序。通过使用dhdutil的“upload”命令可以轻松获取固件的RAM快照。
在写了一个遍历堆的空闲列表并将其内容导出为dot的小型可视化脚本后,我们可以使用graphviz绘制空闲列表的状态,如下所示:
图22
现在我们可以发出专门设计的TDLS_TEARDOWN帧了,立即生成固件RAM的快照,并检查空闲列表是否有任何损坏迹象:
图23
事实上,在断开连接后,空闲列表中的其中一个块的大小突然异常大。回想一下,由于分配算符使用“最适合”,这意味着只要存在其他足够大的空闲块,后续分配将不会被放置在此块中。这也意味着固件不会崩溃,实际上会继续正常运行。如果我们不可视化堆的状态,我们就根本无法确定发生了什么事。
无论如何,现在我们已经确认了溢出事实上已经发生了,现在是转到开发的下一阶段了。我们需要巧妙的工具,以便能够在建立和断开期间监测堆的状态。为此,将固件中的malloc和free函数结合在一起,并追踪其参数和返回值是不错的选择。
首先,我们需要编写一个“补丁程序”,这将使我们可以在给定的RAM驻留函数上插入挂钩。要注意,malloc和free函数都存在于RAM中(它们是RAM的代码块中的第一个函数)。这使我们可以自由地重写其序言,以便为我们自己的代码引入一个分支。我写了一个执行此类挂钩插入的补丁程序,从而可以在调用挂钩函数之前和之后执行小的程序集存根。
简而言之,修补程序是相当标准的 – 它将补丁的代码写入RAM中的一个未使用的区域(堆中最大的空闲块的头),然后从挂钩函数的序言将Thumb-2宽分支插入挂钩本身。
图24
使用我们的新修补程序,我们现在可以调用malloc和free函数,以添加踪迹,使我们能够跟踪堆上发生的每个操作。然后可以通过发出dhdutil的“consoledump”命令,从固件的控制台缓冲区中读取这些跟踪。请注意,在一些较新的芯片上,此命令无效。这是因为Broadcom忘记给指向控制台的数据结构的固件中的magic指针添加偏移量。您可以通过向驱动程序添加正确的偏移量或将magic值和指针写入列表中的探测内存地址之一来解决此问题。
无论如何,您可以在此处找到malloc和free 挂钩以及从固件中解析踪迹所需的相关脚本。
使用新获取的踪迹,我们可以编写一个更好的可视化程序,使我们能够在整个建立和断开阶段跟踪堆的状态。该可视化程序可以看到堆上发生的每个操作,从而可提供更细粒度的数据。我写了这样一个可视化程序(请见:https://bugs.chromium.org/p/project-zero/issues/detail?id=1046#c6 )。
我们来看看建立TDLS连接时的堆活动:
图25
纵轴表示时间——每行都是malloc或free操作后新的堆状态。横轴表示空间——较低地址在左,较高地址在右。红色块表示正在使用的块,灰色块表示空闲块。
从上面可以清楚看到,建立TDLS连接是一个凌乱的过程。对于大小区域均有很多的分配和释放。这么多的噪音对我们来说并不利。回想一下,在建立阶段的溢出是高度受限制的,无论是在写入数据方面,还是在溢出数据的范围方面。此外,溢出发生在建立阶段许多分配之一过程中。这不允许我们在触发溢出之前对堆的状态进行很多的控制。
然而,退一步,我们可以观察到一个相当令人惊讶的事实。除了在TDLS连接建立期间的堆活动,似乎在堆上几乎没有任何活动。事实上,结果表明发送和接收的帧是从共享池而不是堆中提取的。不仅如此,其处理不会导致堆操作——一切都是“就地”完成。即使尝试通过发送包含异常位组合的随机帧来有意地导致分配,固件的堆仍然在很大程度上不受影响。
这有利也有弊。一方面,这意味着堆的结构是高度一致的。在鲜有的数据结构分配事件中,其随后被立即释放——使堆回到了原始状态。另一方面,这意味着我们对堆结构的控制程度相当有限。在大多数情况下,在固件初始化之后,无论堆有什么样的结构,我们都要应付。
也许我们应该看看断开阶段?实际上,在TDLS断开阶段激活踪迹表明,在触发溢出之前非常少,所以其看起来像是一个更方便的探索环境。
图26
虽然这些深入的踪迹对于获取堆状态的高级视图很有用,但是它们很难被破译。事实上,在大多数情况下,只需要对堆的单个快照进行可视化即可,就像我们之前使用graphviz可视化程序所作的一样。在这种情况下,让我们通过允许堆可视化程序根据堆的单个快照生成详细的图形输出来改进我们以前的堆可视化程序。
正如我们之前看到的,我们可以“遍历”空闲列表来提取每个空闲块的位置和大小。此外,我们可以通过遍历空闲块之间的间隙及从每个使用中的块读取“大小”字段来推断使用中的块的位置。我写了另一个完成此工作的可视化程序(请见https://bugs.chromium.org/p/project-zero/issues/detail?id=1046#c6)——从一系列“快照”镜像生成堆状态的可视化。
现在可以使用该可视化程序查看建立TDLS连接后堆的状态。这将是我们在断开阶段触发溢出时需要处理的堆的状态。
(上层:初始堆状态,下层:建立TDLS连接后的堆状态)
图27
我们可以看到,在建立TDLS连接之后,大多数堆的使用的块是连续的,但是也形成了两个孔,其中一个大小为0x11C,另一个大小为0x124。激活断开阶段的踪迹后可以看到发生了以下分配:
图28
突出显示的行表示为断开帧的MIC计算分配的256字节缓冲区,我们可以使用我们的漏洞造成同等大小的溢出。此外,在发送溢出帧之前,似乎堆活动相当低。将上面的堆快照与踪迹文件相结合,我们可以推断出256字节缓冲区最适合的块位于0x11C字节的孔中。这意味着使用我们的25字节溢出,我们可以覆写:
1. 下一个使用中的块的header
2. 下一个使用中的块的内容的几个字节
我们来仔细看看下一个使用中的块,看看是否有什么有趣的信息可以覆写:
图29
下一个块几乎为空(除靠近其head的几个指针外)。这些指针对我们是否有用?也许其是写入对象?或是后期释放?我们可以通过手动破坏这些指针(将它们指向无效的存储器地址,例如0xCDCDCDCD)以及检测固件的异常向量(以查看其是否崩溃)来找到答案。不幸的是,经过许多这样的尝试,发现似乎这些指针实际上均未被使用。
这使我们只剩下一个可能性——破坏使用中的块的“大小”字段。回想一下,一旦TDLS连接断开,与之相关的数据结构将被释放。释放大小我们已经损坏的使用中的块会产生许多有趣的后果。如果我们减小块的大小,我们可以有意地“泄漏”缓冲区的尾端,使其永远保持不可分配。不过,更有趣的是,我们可以将块的大小设置为更大的值,从而导致下一个释放操作创建一个尾端与另一个堆块重叠的空闲块。
图30
一旦一个空闲块与另一个堆块重叠,则随后的分配(重叠的自由块是最适合的块)将从空闲块的末端切取,从而可破坏其尾部的任何字段。但是,在开始构思之前,我们需要确认在断开操作完成后我们可以创建这样的状态(即重叠的块)。
创建一个重叠的块
回想一下,MIC检查只是TDLS连接断开时发生的许多操作之一。可能通过覆写下一个块的大小就这样发生了,一旦在收集TDLS会话的数据结构期间被释放,其可能成为断开过程中后续分配的最适合的块。这些分配可能会导致额外的无意损坏,这会使堆处于不一致的状态,甚至会使固件崩溃。
然而,可能大小的搜索空间没那么大——假设我们只对不大于RAM本身的块大小感兴趣(原因显而易见),我们就可以枚举通过用给定值覆写下一个块的“大小”字段并断开连接而产生的每一个堆状态。这可以通过在发送(执行枚举)上使用脚本来自动执行,并同时获取设备上RAM的“快照”,及观察其状态(无论其是否一致,以及断开后固件是否能恢复操作)。
具体来说,如果我们能创建一个两个空闲块彼此重叠的堆状态,这将是非常有利的。在这种情况下,从一个块获取的分配可以用于损坏另一个空闲块的“下一个”指针。这也许可以用来控制后续分配的位置。
无论如何,在查看几个块大小、断开TDLS连接并观察堆状态后,我们遇到了相当有趣的结果状态!通过用值72覆写“大小”字段并断开连接,我们实现了以下堆状态:
图31
太棒了!所以在断开连接之后,会留下一个零大小的空闲块,重叠一个不同的(较大的)空闲块!这意味着一旦从大块切取了一个分配,其将损坏较小块的“大小”和“下一个”字段。这可能是非常有用的——我们可以尝试将下一个空闲块指向我们希望修改其内容的内存地址。只要该地址中的数据符合一个空闲块的格式,我们就可以说服堆通过随后的分配覆盖该地址的内存。
寻找受控分配
为了开始探索这些可能性,我们首先需要创建一个受控的分配原语,这意味着我们要么控制分配的大小,要么是内容,要么是(理想的)两者。回想一下,正如我们之前看到的,在固件的正常处理过程中实际上很难触发分配——几乎所有的处理都是就地完成。此外,即使是分配数据的情况,其寿命也很短,内存不再使用是会立即被收回。
即使如此,就这样,我们已经看到至少有一组数据结构——其生命周期是可控的,并且包含多个不同的信息片段 – TDLS连接本身。固件必须保留与TDLS连接(只要其活跃)相关的所有信息。也许我们可以找到一些与TDLS相关的、可以作为受控分配的良好候选者的数据结构?
要搜索一个,我们先看看处理每个TDLS动作帧的函数——wlc_tdls_rcv_action_frame。该函数从读取TDLS类别和动作代码开始。然后,其根据接收到的动作代码将帧路由到适当的处理函数。
图32
我们可以看到,除了常规的规范定义的动作代码之外,固件还支持超出规范的动作代码为127的帧。任何超出规范的东西都是可疑的,所以这可能是寻找我们的原语的好地方。
事实上,深入研究该函数后发现,其执行一个相当有意思的任务。它验证帧内容中的前3个字节是否与Wi-Fi联盟OUI (50:6F:9A)匹配。然后,其检索帧的第四个字节,并将其用作“命令代码”。目前,仅实现了两个供应商特定的命令,命令#4和#5。命令#4用于通过TDLS连接发送隧道式探测请求,命令#5用于向主机发送“事件”通知(指示“特殊”帧已到达)。
然而,更有趣的是,我们看到#4命令的实现与我们目前的追求相似。首先,它不需要存在TDLS连接就可被处理。这样我们在断开连接后也可以发送帧。其次,通过在此函数执行期间激活堆踪迹并对其逻辑进行逆向工程,我们发现该函数触发了下列高级事件序列:
图33
太棒了,我们获得了一个受控生命周期、大小及内容的分配(A)。
但是,有一个小小的障碍。修改wpa_supplicant发送此专门设计的TDLS帧会导致完全的失败。虽然wpa_supplicant允许我们完全控制TDLS帧中的许多字段,但它只是一个请求者,而不是MLME实现。这意味着相应的MLME层负责编写和发送实际的TDLS帧。
在我为攻击平台使用的设置上,我有一台运行Ubuntu 16.04的笔记本电脑,和一个TP-Link TL-WN722N适配器。适配器是SoftMAC配置,所以起作用的MLME层是Linux内核中存在的层,即“cfg80211”配置层。
当wpa_supplicant希望创建和发送TDLS帧时,其通过Netlink发送特殊请求,然后由cfg80211框架处理,然后传递给SoftMAC层“mac80211”。然而,令人遗憾的是,mac80211无法处理特殊的供应商框架,因此予以拒绝。尽管如此,这只是一个小小的不便——我给mac80211编写了一些补丁(见https://bugs.chromium.org/p/project-zero/issues/detail?id=1046#c2),增加了对这些特殊供应商框架的支持。应用这些补丁后,重新编译和引导内核,我们现在可以发送我们专门设计的帧了。
为了更容易地控制供应商框架,我还在wpa_supplicant的CLI – “TDLS_VNDR”中添加了对新命令的支持。该命令可以让我们将带任意数据的TDLS供应商帧发送到任何MAC地址。
合二为一
在创建两个重叠块之后,我们现在可以使用我们的受控分配原语从较大块的尾部分配内存,从而将较小的空闲块指向我们选择的位置。但是,无论我们选择哪个位置,“大小”和“下一个”字段都必须有有效的值,否则稍后对malloc和free的调用可能失败,这可能会导致固件崩溃。事实上,我们已经看到了完美的候选者来代替空闲块——使用中的块。
回想一下,使用中的块在与空闲块相同的位置指定其大小字段。对于“下一个”指针,其在空闲块中未使用,但在块分配期间被设置为零。这意味着通过破坏空闲列表来指向使用中的块,我们可以诱使堆认为其只是另一个空闲块,其碰巧也是空闲列表中的最后一个块。
图35
现在我们需要做的是找到一个包含我们要覆写的信息的使用中的块。如果我们使该块称为随后的受控分配的空闲列表中的最适合块,我们将使自己的数据分配到该处,而不是使用中的块的数据,这便有效地替代了块的内容。这意味着我们可以任意替换任何使用中的块的内容。
由于我们希望实现完全的代码执行,所以定位和覆写堆中的函数指针是有利的。但是,我们可以在堆上何处找到这样的值?Wi-Fi标准中有一些必须定期处理的事件。假设固件支持使用通用API来处理这样的定期定时器是有把握的。
由于定时器可能在固件操作期间创建,因此其数据结构必须存储在堆上。为了定位这些定时器,我们可以对IRQ向量表项进行逆向工程,并搜索与处理定时器中断相对应的逻辑。在这样做之后,我们找到一个内容似乎与brcmsmac (SoftMAC) 驱动程序中使用brcms_timer结构相符的条目的链接列表。编写一个简短的脚本(见https://bugs.chromium.org/p/project-zero/issues/detail?id=1046#c7)后,我们可以在给出RAM快照的情况下转储定时器列表:
图36
可以看到,定时器列表是按超时值排序的,大多数定时器的超时时间相对较短。此外,所有定时器在固件的初始化期间被分配,因此存储在恒定地址处。这很重要,因为如果我们想以定时器定位我们的空闲块,我们就需要知道其在内存中的确切位置。
所以剩下的就是使用我们的两个原语用我们自己的数据来替换上面的其中一个定时器的内容,从而将定时器的函数指向我们选择的地址。
我们的计划是:首先,我们将使用上述技术创建两个重叠的空闲块。现在,我们可以使用受控分配原语将较小的空闲块指向上面列表中的其中一个定时器。接下来,我们创建另一个受控分配(释放旧的分配)。这个大小为0x3C,这对定时器块是最适合的。因此,在这一点上,我们将覆写定时器的内容。
图37
但是我们将定时器指向了哪个函数?那么,我们可以使用同样的技巧来征用堆上的另一个使用中的块,并用我们自己的shellcode覆写其内容。在简单搜索堆之后,我们遇到了一个在芯片引导序列期间只包含控制台数据,然后被分配但未被使用的大块。不仅分配相当大(0x400字节),而且其也被放置在一个恒定的地址——对我们的利用代码是绝佳的。
最后,我们如何确定堆的内容可执行?毕竟,ARM Cortex R4有一个内存保护单元(MPU)。与MMU不同,其不允许虚拟地址空间的便利化,但其允许对RAM中不同内存范围的访问权限的控制。使用MPU,堆可以(应该)被标记为RW和不可执行。
通过逆向二进制文件中固件的初始化例程,我们可以看到MPU实际上是在引导过程中被激活。但其使用什么内容配置?我们可以通过编写一个小程序存根来转储出MPU的内容:
图38
当MPU被初始化时,其被有效地设置为将所有内存标记为RWX,这使其无用。这省去了我们的一些麻烦,我们可以方便地从堆中直接执行我们的代码。
最终,我们准备好了利用代码。把它们放在一起后,我们现在可以劫持一个代码块来存储我们的shellcode,然后劫持一个定时器来指向我们存储的shellcode。一旦定时器到期,我们的代码将在固件上执行!
图39
最终,我们经历了研究平台的整个过程,发现了一个漏洞并编写了一个完整的利用代码。虽然本文相对较长,但我还是省略了很多较小的细节。完整的利用代码(包括说明)请见:https://bugs.chromium.org/p/project-zero/issues/detail?id=1046#c2 。
总结
我们已经看到,虽然Wi-Fi SoC上的固件实现非常复杂,但在安全性方面仍然滞后。具体来说,其缺乏所有基本的漏洞利用缓解措施——包括栈cookie、安全断开链接及访问权限保护(通过MPU)。
传送门
【技术分享】漏洞挖掘之利用Broadcom的Wi-Fi栈(二)