Broadcom无线芯片组的逆向分析之旅
简介
Broadcom是全球无线设备的主要供应商之一,它们旗下在售的无线芯片多达43个不同的系列。从智能手机到笔记本电脑,智能电视和物联网设备,Broadcom的产品几乎无所不在,以至于在不知不觉中,就用到了它们:如果您拥有一台戴尔笔记本电脑,那么,很可能在使用bcm43224或bcm4352网卡;如果您在使用 iPhone、Mac笔记本、三星手机或华为手机等,那么您可能正在使用Broadcom的WiFi芯片。
由于这些芯片分布广泛,对攻击者来说,自然是一个极具价值的目标,站在防御方的角度来看,在这些芯片中发现的任何漏洞都能带来极大的威胁。
2018年,我在Quarkslab进行了为期6个月的实习,目的是将公开已知的漏洞复制并移植到其他易受攻击的设备上,学习几种常见的信息安全实践,以增进对这些设备的了解。在这篇文章中,我们将为读者详细介绍如何获取固件,进行逆向分析和模糊测试,以便挖掘其中的新漏洞。
为了帮助读者理解后面的内容,先让我们简要介绍一下802.11标准及其在Linux系统中的实现。
I.关于WLAN和Linux
在深入探讨之前,让我们先来了解一下802.11无线标准。
第一个IEEE 802.11标准[1]创建于1997年,它标准化了PHY(物理)和MAC层,即OSI模型中最下面的两个层。
对于PHY层,定义了两个频段,即红外(IR)频段和微波频段(2.4GHz)。此后,其他标准,如802.11a [2],又引入了另一个频段(5GHz)。
MAC层使用三种类型的帧:管理帧、数据帧和控制帧。其中,802.11标头中的帧控制字段用以标识特定帧的类型。
管理帧是由被称为MLME(MAC子层管理实体)的实体进行管理的。根据处理MLME的实体的运行位置的不同,我们可以将无线芯片的实现大体分为两种:SoftMAC,其中MLME在内核驱动程序中运行;HardMAC(也称为FullMAC),其中MLME是以固件的形式嵌入在芯片中的。但实际情况要比这里说的要复杂得多,并且还存在一些混合实现形式,例如,探测响应和请求由驱动程序管理,而关联请求和身份验证则归芯片的固件处理。
FullMAC设备在功耗和速度方面提供了更好的性能,因此,它们被大量应用于智能手机,同时,它们也是市场上使用最多的芯片。它们的主要缺点是,限制了用户发送特定帧或将其设置为监视模式的能力。要想克服这些限制,需要直接编辑在芯片上运行的固件。
从Linux操作系统的角度来看,上面给出了无线堆栈中组件的两种主要布局:当无线设备是SoftMAC设备时,内核将使用名为“mac80211”的特定Linux内核模块(LKM)。这个驱动程序公开MLME API以管理管理帧,否则内核将直接使用硬件驱动程序并将MLME处理工作交由芯片的固件进行处理。
II. 关于Broadcom公司的bcm43xxx芯片
Broadcom公司的bcm43xxx系列同时提供了HardMAC和SoftMAC网卡。遗憾的是,我们无法找到待分析的芯片的完整数据表。Cypress公司收购Broadcom的“IoT业务”部门后,公布的数据表屈指可数。值得一提的是,许多芯片都集成了WLAN和蓝牙功能,如bcm4339或bcm4330等芯片。
这里分析的所有芯片都使用ARMCortex-M3或ARM Cortex-R4作为非时间关键操作的主MCU,因此,我们只需处理两个类似的指令集:armv7m和armv7r指令集。这些MCU都提供了一个ROM和一个RAM,其容量随芯片组的版本而异。
所有时间关键型操作均由Broadcom公司专有的D11内核处理器实现,该处理器主要负责PHY层相关操作。
这些芯片使用的固件分为两部分:一部分写入ROM,这部分不能进行修改;另一部分由驱动程序加载到芯片的RAM中。这样的话,供应商只需更改固件的RAM部分即可为其芯片添加新功能或进行更新。
FullMAC芯片非常有趣,首先,如之前所述,MLME层是通过固件代码实现的,同时,它们也提供了负载分流功能,如ARP缓存、mDNS、EAPOL等。这些芯片还提供了许多硬件加密模块,以实现流量的加密和解密、密钥管理等。凡事有利即有弊,负载分流功能提供便利的同时,也增加了攻击面,从而我们提供了一个很好的“游乐场”。
为了与主机(应用处理器)通信,在b43系列中使用了多种总线接口:USB、SDIO和PCIe。
在驱动程序方面,我们可以将bcm43xxx驱动程序集分为两类,即开源驱动程序和专有驱动程序。
开源驱动程序:
- b43 (逆向自wl/老版本的SoftMAC/linux)
- brcmsmac (SoftMAC / Linux)
- brcmfmac (FullMAC / Linux)
- bcmdhd ( FullMAC / Android)
私有驱动程序:
- broadcom-sta 又名 “wl”( SoftMAC && FullMAC / Linux)
“wl”驱动程序是路由器等嵌入式系统中使用最多的驱动程序。同时,它也常用于其芯片(比如戴尔XPS上的bcm4352芯片)不支持brcmfmac/brcmsmac驱动程序的笔记本电脑上。此外,由于wl 驱动程序使用自己的MLME,而不是通过LKM "mac80211"来处理管理帧,因此为攻击者拓展了攻击面。
Broadcom发布的版本通常被称为"混合"驱动程序,因为代码的主要部分来自两个已编译好的ELF对象——在编译时使用。为什么是两个呢?一个对象用于x86_64体系结构,另一个对象用于i386体系结构。这些对象包含了驱动程序的主代码,因此,会暴露许多Broadcom API。
需要指出的是,芯片的固件和 wl 驱动程序的许多代码都是相同的,因此,如果在固件中发现了一个漏洞,那么wl 驱动程序中基本上也会存在相同的漏洞,反之亦然。
III. 获取固件
1)获得第一部分固件:RAM固件部分
如上所述,固件分为两部分。最容易获得的部分是RAM部分,它由驱动程序加载到RAM中。该部分固件包含主MCU使用的代码和数据,以及D11内核使用的微代码。
这部分固件并未使用签名,而是使用CRC32校验和来“验证”代码的完整性。这导致后来不得不对其进行多次的修改,以便添加监视器模式等功能。例如,SEEMO Lab发布的NEXMON项目[3]就是一个非常棒的框架——可以使用C语言来编写补丁程序,从而方便地对这些固件进行修改。
在我们的研究中,我们遇到了两种可能的RAM固件映像格式;第一种也是最常遇到的格式,实际上就是一个简单的、没有特定结构的BLOB(binary large object,二进制对象)。第二种格式为TRX格式,在使用bcm43236芯片时,该格式可以很容易地进行解析。
在使用.bin RAM固件时,我们通常在文件末尾加入一个字符串,用以指出:
- 芯片的版本
- 供芯片用于与主机网卡通信的总线
- 固件提供的功能;p2p、TDLS等
- 固件的版本
- CRC校验和
- 创建日期
当使用的驱动程序为brmfmac或bcmdhd时,我们可以直接从主机文件系统获取RAM固件。在linux系统上,我们可以在/lib/firmware/ brcm中找到RAM固件,或者在/system/vendor/firmware中找到它。在其他情况下,RAM固件所在位置会随着使用的系统的不同而有差异。
如果使用的驱动程序是专有的,我们可能会在LKM的.data部分找到固件的RAM部分。我们可以通过LIEF [8]来轻松提取这部分固件。
>>> import lief
>>> wl =lief.parse("wl.ko")
>>> data =wl.get_section(".data")
>>> for symbol in wl.symbols:
... if "dlarray_" in symbol.name:
... print(symbol.name)
...
dlarray_4352pci
dlarray_4350pci
>>> b4352 =wl.get_symbol("dlarray_4352pci")
>>> bcm4352_fw =data.content[b4352.value : b4352.value + b4352.size]
>>> withopen("/tmp/bcm4352_ramfw.bin", 'wb') as f:
... f.write(bytes(bcm4352_fw))
...
442233
>>>
$ strings /tmp/bcm4352_ramfw.bin | tail -n1
4352pci-bmac/debug-ag-nodis-aoe-ndoeVersion: 6.30.223.0 CRC: ff98ca92 Date: Sun 2013-12-15 19:30:36 PST FWID01-9413fb21
值得注意的是,在Linux上最新的wl 驱动程序中使用的、为bcm4352发布的固件可以追溯到2013年……
2)恢复第二部分固件:ROM部分
固件的ROM部分是最重要的部分,通过它可以了解这些芯片的内部机制。
为了获取ROM部分,我们需要知道它的映射位置。寻找基址的最佳方法,是阅读驱动程序的头文件,例如,bcmdhd的头文件,具体路径为/include/hndsoc.h [4]。另一种方法是阅读Nexmon项目README文件 [3],它根据MCU型号为我们提供了相应的基址。精明的读者可能会发现这些地址是不同的。Nexmon项目指出,使用Cortex-M3内核的芯片的ROM被加载到0x800000地址,而bcmdhd的标头位于0x1e000000地址。实际上,两者都是正确的。在这里,ROM和RAM貌似被映射了两次。此外,基址还可以为了解所使用的MCU提供相应的线索,例如,如果ROM转储到了0x000f0000地址,由此可知该芯片正在使用ARMCortex-R4。
3)在Android系统上获取ROM部分
在Android系统上,我们可以使用dhdutil工具,它是原来的wlctl 程序的Android开源版本。利用这个软件的“membytes”功能,我们可以转储芯片组的RAM,在某些情况下,甚至还可以转储ROM。
adb shell /data/local/tmp/dhdutil -i wlan0membytes -r 0x0 0xa0000 > rom.bin
例如,在Nexus 5使用的bcm4339芯片(基于Cortex-R4)上,就可以直接转储ROM。不幸的是,在较旧的bcm4330(Cortex-M3)芯片上,则无法转储ROM。不过,只要能够与RAM进行交互,就可以设法通过一个函数将ROM的内容逐步复制到RAM的空闲区中,然后,再将完整的ROM内容提取出来即可。
4)在Linux系统上恢复ROM部分
在使用brcmfmac驱动程序的Linux系统上,我们是无法直接访问ROM的。因此,我们需要找到一种直接在ROM或RAM中与芯片内存进行交互的方法。幸运的是,当芯片使用SDIO总线与主机通信时,开源brcmfmac驱动程序会公开函数brcmf_sdiod_ramrw。利用这个函数,我们就可以从主机读写芯片组的RAM了。
如果我们修改驱动程序,给这个函数添加一个ioctl包装器,我们就可以从一个小的、位于用户空间的实用程序来读写芯片组的RAM。
在调用brcmf_sdiod_ramrw之前,我们必须调用sdio_claim_host来霸占SDIO总线。请注意,如果设备未连接到任何接入点,那么它可能处于低功耗模式并且总线可能处于空闲状态,因此,我们需要通过调用bcmf_sdio_bus_sleep和brcmf_sdio_clkctl来确保设备的总线已启动。
int brcmf_ioctl_entry(struct net_device*ndev, struct ifreq *ifr, int cmd)
{
...
sdiobk->alp_only = true;
sdio_claim_host(sdiobk->sdiodev->func[1]);
brcmf_sdio_bus_sleep(sdiobk, false, false);
brcmf_sdio_clkctl(sdiobk, CLK_AVAIL, false);
res = brcmf_sdiod_ramrw(sdiobk->sdiodev, margs->op,margs->addr, buff, margs->len);
if (res)
{
printk(KERN_DEFAULT"[!] Dumpmem failed for addr %08x.\n", margs->addr);
sdio_release_host(sdiobk->sdiodev->func[1]);
kfree(buff);
return (-1);
}
if (copy_to_user(margs->buffer, buff, margs->len) != 0)
printk(KERN_DEFAULT"[!] Can't copy buffer to userland.\n");
...
}
我们需要编写一个小程序来与用户空间的ioctl进行交互。这样,我们就能利用它来读写设备的RAM了:
...
memset(&margs, 0, sizeof(t_broadmem));
margs.addr = strtol(ar[1], NULL, 16);
margs.op = 1;
if (errno == ERANGE)
prt_badarg(ar[1]);
len = strtol(ar[2], NULL, 10);
if (errno == ERANGE)
prt_badarg(ar[2]);
margs.buffer = hex2byte((unsigned char*)ar[3], len);
if ((s = socket(AF_INET, SOCK_DGRAM, 0))< 0)
return (-1);
strncpy(ifr.ifr_name, ar[0], IFNAMSIZ);
margs.len = len;
ifr.ifr_data = (char *)&margs;
if (!(ret = ioctl(s, SIOCDEVPRIVATE,&ifr)))
printf("[+] Write succesfull!\n");
else
printf("[!] Failed to write.\n");
close(s);
free(buf);
return (ret);
...
既然我们能够读写芯片的RAM,接下来,我们将通过以下方式来转储ROM:
- 钩取一个位于RAM中且由操作X调用的函数
- 将ROM逐段复制到RAM的空闲区域(用我们stub函数为其分配的内存)
- 转储新复制的所有ROM片段,并将它们连接起来。
该协议与Android系统上芯片的MCU为Cortex-M3时使用的协议是完全相同的。但是,这次必须修改驱动程序并构建我们自己的工具,才能使用新驱动程序的ioctl。
在处理RPI3芯片(bcm43430)时,我们采取的就是这种方法。
5)在特定情况下获取ROM部分
除了上面介绍的情况之外,还有很多其他可能的情况:
- 如果芯片使用了带有PCIe总线的brcmfmac驱动程序,那该怎么办?
- 如果嵌入式系统中的芯片使用了专有驱动程序“wl”,那该怎么办?
- 如果主机操作系统上没有shell,该怎么办?
- 如果你缺乏相应的权限,那该怎么办?
- 等等……
在所有这些其他情况下,可以通过多种可能的方式来获取固件的ROM部分:如果您可以访问硬件,则可以通过UART访问ROM,此外,也可以通过钩取wl驱动程序来访问ROM。在处理“SFR minidecoder TV”(bcm43236)时,我们就是通过UART访问ROM的。
RTE (usbrdl) v5.90 (TOB) running onBCM43235 r3 @ 20/96/96 MHz.
rdl0: Broadcom USB Remote Download Adapter
ei 1, ebi 2, ebo 1
RTE (USB-CDC) 6.37.14.105 (r) on BCM43235r3 @ 20.0/96.0/96.0MHz
000000.007 ei 1, ebi 2, ebo 1
000000.054 wl0: Broadcom BCM43235 802.11Wireless Controller 6.37.14.105 (r)
000000.060 no disconnect
000000.064 reclaim section 1: Returned91828 bytes to the heap
000001.048 bcm_rpc_buf_recv_mgn_low: HostVersion: 0x6250e69
000001.054 Connected Session:69!
000001.057 revinfo
000063.051 rpc uptime 1 minutes
> ?
000072.558 reboot
000072.559 rmwk
000072.561 dpcdump
000072.563 wlhist
000072.564 rpcdump
000072.566 md
000072.567 mw
000072.569 mu
000072.570 ?
>
这里为115200 b/s。我们可以通过命令md将内存转储到特定地址。不过,我们需要指定地址以及要转储的DWORD数。利用PySerial脚本,我们不仅可以转储ROM,并能获取RAM的实时快照。
#!/usr/bin/env python3
import serial
import binascii
nb = 65535
baseaddr = 0
uart = serial.Serial('/dev/ttyUSB0',115200)
uart.write(b'md 0x%08x 4 %d\n' % (baseaddr,nb))
i = 0
dump = b""
while i != nb:
read = uart.readline().split(b' ')
if b">" in read[0]:
continue
if b"rpc" in read[2]:
continue
print("Dump %s %s\r" % (read[1][:-1], read[2]),end="")
dump += binascii.unhexlify(read[2][:-2])[::-1]
i+= 1
uart.close()
withopen("/tmp/bcm43236_rom.bin", 'wb') as f:
f.write(dump)
IV.对固件进行逆向分析:信标帧之旅
需要注意的是,这里使用的术语“RAM固件”,一定不要与“RAM快照”相混淆,后者是运行时整个RAM的转储。
正如Gal.Beniamini [5]所述,固件初始化之后,RAM中的一些代码将被回收,相应空间将用于芯片组的内部堆。如果想要分析这些固件,则需要使用真正的RAM固件和RAM快照来分析它们。
1)逆向分析碎碎念
当所有内容都加载到 IDA中时,您会注意到,没有任何内容被识别或定义。我们需要选择所有内容并强制 IDA进行分析。即使 IDA识别并正确定义了大部分代码和数据,仍然会有很多字符串和无法识别的代码,或者有些数据被错误地解释成了代码。这时,就该IDApython上场了:只需借助于一个小脚本,我们就能够正确定义代码和数据。
当我们感觉 IDA能够正确识别所有内容时,有趣的工作就真正开始了。通常,如果您已正确设置基址,则会弹出大量交叉引用,并检测到数千个函数。由于我们没有任何符号,并且所有代码都是thumb模式,所以,代码本身看起来非常晦涩难懂。
现在,我们首先要做的事情就是识别所使用的类似库函数的那些函数,比如memcpy、memove等。这项工作既可以手动完成,也可以通过函数占卜工具Sybil [7]完成。
我们知道,固件会利用自己的内部“控制台”来打印信息。该控制台是一个位于RAM中的2048字节的简单缓冲区。因此,固件都有自己定制的printf函数,该函数可通过各种格式字符串轻松加以识别。此外,固件还具有其他字符串格式化函数,如sprintf/snprintf函数,对于这些函数,只要找出内部格式化函数并进行交叉引用,就很容易识别出这些函数。
与堆内存管理相关的函数(malloc和free)可以通过不同的方式进行识别:通过调试字符串来查找malloc,或者通过经典模式,例如“ x=malloc(y); memset(x, 0, y);”模式,来查找malloc函数。找到malloc后,就会看到分配器使用一个单向链表来处理空闲块。通过交叉引用链表的指针,我们就能找到相应的free函数。
对于分配器来说,通常带有合并功能。分配器通常位于RAM中,因此可以对其进行更新,并且,对于不同的设备或不同的版本,可以使用不同的分配器。
固件通常会用到很多结构体,特别是名为WLC_INFO的结构体,其中包含控制芯片所需的所有内容。几个月前,Nexmon项目的发起者Matthias Schultz(Seemo Lab)发表了一篇文章[6]。在这篇文章中,给出了许多结构体方面的信息,并将API 的符号名称与其在参数中采用的结构体进行了关联。
固件初始化例程可以通过以下方式进行识别:
- 找到重置地址调用(通常位于0x0处),在其后面:
- 搜索负责CRC校验的函数。为此,只需搜索某个表值(例如:77073096)就能轻松找到该函数。然后,通过该函数的交叉引用,找到检测固件完整性的代码。
- 搜索“WFI”指令并向后交叉引用。完成初始化后,芯片就会等待中断。
2)数据包的处理流程
现在,让我们看看FullMAC设备是如何管理帧的。当接收到帧时,触发中断,帧首先交由FIQ中断处理程序进行处理。
我们来看看bcm4339固件是如何处理帧的。我们首先考察快速中断处理程序(FIQ),我们发现,这个处理程序将读取位于0x181100处的函数指针,该指针指向0x181e48处的一个函数。
该函数包含两个分支:一个用于捕获错误,例如违规的内存访问,另一个用于实际的帧处理。
如果发生内存非法访问,第一个分支将在内部控制台上打印寄存器转储和堆栈跟踪信息——这一点非常方便,尤其是在开发漏洞利用代码时非常有用!
如果我们跟踪第二个分支,我们最终会进入位于0x0181A88处的函数,该函数将遍历位于0x00180E5C处的链表,其中存放的是一些指向函数的指针:
如果我们跟踪所有嵌套调用,就会发现,最终调用的是wlc_dpc函数。
该函数从wlc_hw结构体中检索名为macintstatus的变量(在旧版本的brcmsmac中调用),并执行一些检查。我们感兴趣的位的值,依赖于宏MI_DMAINT(值0x8000)中定义的二进制掩码,如果这些位被置1,我们将跳转到函数wlc_bmac_recv。
该函数将从位于MCU和D11core的共享内存中的链接列表(rx_fifo)中删除一个帧,并用它构建一个自定义的sk_buff结构体。然后,调用函数wlc_recv,调用时将提供两个参数:一个参数是指向wlc结构的指针,另一个参数是指向刚初始化的skb_buff的指针。
我们可以把这个函数视为帧处理代码的入口点。
结构体skb_buff可能随设备和版本而异,但我们可以利用wlc_recv和wlc_bmac_recv来轻松地重新定义该结构体。
wlc_recv函数将删除由d11core为帧添加的自定义标头,并检索帧的MAC标头。为此,需要检查FC字段的类型子字段,从而将帧正确地分派给相应的处理程序:一个处理程序(wlc_recv_mgmt_ctl)用于处理管理帧和控制帧,另一个处理程序(wlc_recvdata)用于处理数据帧。
如果我们想知道信标帧是如何处理的,只需查看wlc_recv_mgmt_ctl函数即可,该函数从帧的FC字段中提取子类型,然后将其分派给相应的处理程序。
V.仿真与模糊测试
到目前为止,我只找到了一篇提及固件仿真的文章。它是由COMSECURIS [13]发表的一篇介绍其LuaQEMU工具[14]的文章,该工具是一个Qemu的改进版本,支持Lua编写的脚本。
由于我们不想仿真所有固件,因此,我们决定按照自己的套路来。
首先,我们尝试使用Unicorn框架仿真代码的某些部分(在某个函数中调用printf函数的代码)。
我们为Unicorn仿真引擎设计了一个小巧的类包装器,以便能够轻松定义所有仿真参数并使用jscon配置文件加载它们。这些参数包括:
- ROM文件及其基址
- RAM快照文件及其基址
- 启动仿真地址
- 停止仿真地址
- CPU初始上下文
为此,我们将使用RAM快照和以前收集的ROM。RAM快照包含所需的一切,包括代码和经过初始化的结构体。
然后,我们决定开始对wlc_recv函数进行模糊测试。为此,我们需要将wlc结构体指针放到 r0 寄存器中,并使用我们的帧数据创建一个skb_buff结构体,然后将指向该结构体的指针放到 r1 寄存器中。
为了获得样本库,我们将在各种情况下嗅探发送到我们设备的流量,这里会直接使用pcap文件。这里的模糊测试策略非常简单,因为我们只使用随机的比特翻转,为了能够轻松复现结果,这里使用了静态的种子(seed)值。
在这种情况下,需要注意的是,RAM快照的上下文会影响模糊测试和代码的执行路径。例如,如果我们想要对与AP连接期间的帧进行模糊测试的话,我们需要在芯片未连接到任何AP的情况下转储RAM。
下面,我们给出了相应的代码。对于我们的pcap文件中的每一帧,我们会随机翻转一些位,将测试帧的数据连同为其精心设计的d11标头一起写入RAM快照,然后,为我们的数据创建相应的skb_buff并将其写入快照。
{
"rom":
{
"addr": "0x0",
"file": "../../bcm4339/bcm4339_ROM.bin"
},
"ram":
{
"addr": "0x180000",
"file": "../../tmp/unassoc_ram.bin"
},
"cpu_context":
{
"sp": "0x23d194",
"r0": "0x001e8d8c",
"r1": "0x23e6cf"
},
"start_at" : "0x1aafdc",
"stop_at" : "0x1aafe0",
"console_ptr": "0x1eb5d8",
"zone0" :
{
"addr": "0x18000000",
"file": "old/conf/mem1"
}
}
我们必须确保:
- 我们的帧会被正确地解析和处理。
- 在模糊测试期间,不会重复陷入同一条代码执行路径。
为了确保正确仿真了帧的处理过程,我们会打印每个pc地址,生成相应的跟踪数据,以验证是否正确访问了相应的帧处理程序。通过这种方式,我们可以回答下面的问题:对信标帧进行模糊测试时,我们是否正确地到达了wlc_recv_bcn函数?以及,我们的信标帧是如何解析的?
为了确定是否通过模糊测试发现了新的代码执行路径,我们实现了一个新路径度量方法。首先,我们对空转进行仿真,这时不使用pcap文件的帧进行模糊测试。在空转期间,我们记录所有PC地址,并将其作为键存储到字典中。当我们开始模糊测试时,我们会继续记录所有的pc地址。如果在模糊测试期间采集的地址不在我们的字典中,我们就可以得出结论,我们发现了一条新路径。
此外,我们还需要正确检测bug。如果我们尝试在有效映射空间之外进行读写操作,那么Unicorn就发现内存访问违例,但我们该如何检测堆溢出呢?别急,COMSECURIS已经为我们提供了解决方案:钩取分配器函数。
为了跟踪发现的不同动作,我们实现了一种类似drcov的跟踪格式。利用这种格式,我们就可以利用IDA Pro来重放和深入分析模糊测试会话了。
VI. 挖掘漏洞
过去,安全研究人员已经在这些芯片组中发现并公开披露了一些漏洞,如Nitay Artenstein在[12]中公布的CVE-2017-9417漏洞。此外,Gal Beniamini还在芯片固件和Linux内核驱动程序中发现了多个漏洞。通过组合利用这些漏洞,攻击者可以远程入侵机器,如iPhone 7等。
到目前为止,芯片固件中发现的大多数漏洞都是由于信息元素的长度值被滥用所致。信息元素,简称IE,是IEEE 802.11b管理帧/数据帧中使用的标签长度值(TLV)数据结构。这些IE用于提供请求者或接入点所需的各种信息。IE有两种:普通IE和特定于供应商的IE。特定于供应商的IE具有一个值为221(0xdd)的标签,并且数据字段前4个字节的内容为:3个字节存放供应商OUI,1个字节表示IE类型。
在我们分析的固件中,专门用于解析这些IE的函数名为bcm_parse_tlvs。这个函数会返回一个结构体,具体如下所示:
typedef struct bcm_tlv {
uint8_t id;
uint8_t len;
uint8_t data[1];
} bcm_tlv_t;
通过交叉引用,我们可以找出对IE进行处理的所有调用点。其中,某些函数只是一个包装器,用于查找具有特定供应商OUI的供应商IE。之后,交叉引用这个包装器就能找到更多的调用点。最后,我们发现有一百多个调用点对这些TLV进行了处理。
通过遍历所有交叉引用,我们找到了多个之前已被研究人员发现的漏洞,例如CVE-2017-0561,这是一个堆缓冲区溢出[5]漏洞,是由于在memcpy调用期间直接使用 Fast Transition IE的长度值作为长度参数所致。值得注意的是,在我们分析的各种固件中,由于含有CVE-2017-0561漏洞的函数位于ROM中,因此,无法通过为这些代码打补丁的方式来修复漏洞。要想“修复”该漏洞,供应商必须“阉割”TDLS功能。
CVE-2019-9501和CVE-2019-9502:两个堆溢出漏洞
我们继续遍历bcm4339固件上的bcm_parse_tlvs调用点,并在0x14310处找到一个包装器函数,该函数可搜索OUI为00:0F:AC的供应商IE,它用于802.11i(增强安全机制)协议规范,以选择要使用的密码套件、身份验证和密钥管理(AKM)套件以及EAPOL-Key密钥数据封装[15]。
通过交叉引用该函数,可以在0x14304处找到另一个包装器,我们将其命名为wlc_find_gtk_encap,该函数只被0x7B45C处的一个函数所调用,我们将后者命名为wlc_wpa_sup_eapol,这是根据内部引用的格式化字符串命名的。
让我们看一下这个函数对返回的bcm_tlv结构体做了些什么:
该函数首先会调用wlc_find_gtk_encap函数,检查是否返回了一个指向bcm_tlv结构体的指针,如果是的话,则将IE的长度值放入寄存器 r2,将IE数据的地址放入寄存器 r1,将指向缓冲区结构的指针放入寄存器 r0,然后,调用memcpy()函数,将IE的数据复制到 r0 指向的缓冲区中。请注意,这里并没有检查目标缓冲区的大小是否足以容纳 r2 寄存器所指示的字节数。
到目前为止,我们知道结构体有可能会溢出,但不知道目标缓冲区的空间是否足以容纳复制的数据,因此,我们还得继续跟踪执行流程。接下来,使用IE的长度和新复制的缓冲区调用函数wlc_wpa_plumb_gtk。该函数的伪代码如下所示:
int wlc_wpa_plumb_gtk(..., uint8_t *ie_data, uint32_t len_ie, ...)
{
...
uint8_t *buffer;
...
buffer = malloc(164);
if (!buffer)
{
...
}
memset(buffer, 0, 164);
memcpy(buffer, ie_data, len_ie);
...
}
这里我们发现了一个明显的堆缓冲区溢出漏洞:IE数据被复制到固定大小的缓冲区的过程中,使用了由不可信源(一个潜在的恶意AP)控制的长度。Gal Beniamini已经在同一个wlc_wpa_plumb_gtk函数中找到了另外两个漏洞:CVE-2017-11121和CVE-2017-7065。
到目前为止,我们已经找到了一个堆缓冲区溢出漏洞,当然,可能还还存在其他的漏洞。接下来,我们需要了解如何到达这个代码执行路径,我们需要在IE提取后立即检查memcpy调用中使用的缓冲区的大小。通过深入考察wl驱动程序,我们发现该缓冲区大小是固定的,即32个字节。
总之,我们发现了两个缓冲区溢出漏洞:第一个漏洞允许我们溢出最多219个字节,第二个最多溢出87个字节。接下来,我们要解决的问题是“我们该如何触发这些漏洞?”
WPA2协议使用EAPOL(即EAP On LAN)和临时密钥(GTK,即Group Transient Key)来加密WLAN中的多播流量。该密钥会在EAPOL 4次握手期间发送给工作站,封装在EAPOL-Key Message 3中的供应商IE中。
wlc_wpa_sup_eapol函数负责在EAPOL交换期间对接入点消息进行解析。如果我们在EAPOL-M3中提供了长度为255个字节的GTK,就能触发这些溢出漏洞。
为此,我们只需修改两行hostapd内容即可:
由于固件代码和wl专有驱动程序的大部分代码都是一样的,所以不出所料,我们也在驱动程序中发现了相同的溢出漏洞。这意味着在使用FullMAC设备的系统上,控制恶意接入点的攻击者能够破坏芯片,而在使用SoftMAC设备的系统上,攻击会直接危及内核内存。
为了验证我们的发现,我们尝试使用驱动程序wl将易受攻击的SoftMAC bcm43263芯片连接到在EAPOL交换期间提供PoC的恶意接入点:
这些漏洞不仅存在于所分析的所有固件中,同时,也存在于所分析的所有版本的wl驱动程序中。然而,尽管这些代码存在于所有固件中,但并不意味着所有的版本都会使用这些代码。例如,我们分析的BCM4339固件版本就没有使用它们,而是BCM43430设备的所有固件版本都使用了这些含有漏洞的代码。
为了成功利用这些漏洞,需要远程操控堆布局以获得重叠的内存块。Gal Beniamini已经为我们提供了利用芯片固件堆溢出漏洞所需的方方面面[5][9][10][11]。此外,另一位研究人员Nitay Artenstein也在一篇文章中谈到了这一点[12],他讨论的溢出漏洞更容易被利用,因为他能够直接覆盖相邻内存块中的指针,从而实现Write-Anywhere-Anywhere。
如上所述,在利用这些芯片上的堆溢出漏洞时,所面临的一个主要问题是堆布局的操纵问题。几乎没有一种原语允许使用受控的生命周期来控制大小分配。我们可以在几个管理操作帧处理程序中找到几个控制大小分配的原语,但每次使用原语时都会释放分配的块。另一方面,这些芯片上的所有RAM都设置了RWX权限,并且没有提供漏洞利用缓解机制。
Linux brcmfmac驱动程序中的安全漏洞
在研究Broadcom固件的过程中,我们还在brcmfmac即Linux内核的FullMAC卡开源无线驱动程序中发现了两个安全漏洞。
正如我们之前所说,这些芯片可以使用三种总线接口:USB、SDIO和PCIe。在总线之上,通过两种方法来实现网卡与主机之间的通信。
第一种通信方法主要用于主机到网卡的通信,并且是基于自定义ioctl的。我们可以在固件代码中找到ioctl处理程序,它实际上就是一个硕大的switch语句。
第二种通信方法称为固件事件。芯片使用这些固件事件来通知主机出现了哪些事件:扫描结果、关联/解除关联、身份验证等。这些事件都封装在ethertype为0x886c的常规TCP数据包中。
来自Google Project Zero的安全研究人员Gal Beniamini已经在Android Broadcom驱动程序bcmdhd中发现了与这些固件事件相关的多个安全漏洞,攻击者可以利用这些漏洞远程攻击主机,或通过从受感染的网卡来攻击内核的主机。
CVE-2019-9503:绕过is_wlc_event_frame远程发送固件事件
通过仔细阅读Gal.Beniamini发表的文章[16],我们了解到,在2017年4月之前,攻击者可以通过远程发送精心构造的固件事件,让芯片充当外部世界和内核之间的代理。Broadcom实现了一种新机制,可防止芯片将来自外部的帧解释为固件事件。为了做到这一点,他们在固件中引入了一个名为is_wlc_events_frame的新函数,用于检查帧是否为固件事件。在Android上使用的bcmdhd驱动程序中,也存在相同的功能——为了让该解决方案切实有效,必须在固件和驱动程序中进行相同的检查。
具体的逻辑如下所示:
- 在固件端,如果接收到的数据帧看起来像固件事件,直接将其丢弃。
- 在驱动程序中,如果帧是一个事件,则会对其进行相应的处理。
让我们看看开源linux驱动程序brcmfmac是如何对帧进行管理的,以及如何处理固件事件的。当使用SDIO总线时,会设置两个不同的通道:一个用于事件帧,另一个用于所有其他帧。
下面是取自文件sdio.c中的brcmf_sdio_readframes函数的代码:
...
if(brcmf_sdio_fromevntchan(&dptr[SDPCM_HWHDR_LEN]))
brcmf_rx_event(bus->sdiodev->dev, pfirst);
else
brcmf_rx_frame(bus->sdiodev->dev, pfirst, false);
...
我们可以清楚地看到,如果帧来自事件通道,则使用专用函数brcmf_rx_event进行处理,否则将调用函数brcmf_rx_frame进行处理。
函数brcmf_rx_frame的原型位于bus.h头文件,具体如下所示:
void brcmf_rx_frame(struct device *dev,struct sk_buff *rxp, bool handle_event);
其中,该函数的最后一个参数是一个布尔值,用于指示是否处理包含固件事件的帧。为此,我们可以检查驱动程序的代码,看看这个函数被调用时,handle_event参数的值是否为true。
使用USB总线时,没有提供接收事件的专用通道,所以,会对所有帧全部加以处理,即使固件事件亦是如此。
文件usb.c中的brcmf_usb_rx_complete函数的代码如下所示:
...
if (devinfo->bus_pub.state ==BRCMFMAC_USB_STATE_UP) {
skb_put(skb, urb->actual_length);
brcmf_rx_frame(devinfo->dev, skb, true);
brcmf_usb_rx_refill(devinfo, req);
} else {
brcmu_pkt_buf_free_skb(skb);
brcmf_usb_enq(devinfo, &devinfo->rx_freeq, req, NULL);
}
...
因此,如果使用的总线为USB,并且能够设法让帧绕过固件函数is_wlc_event帧的话,我们就可以向驱动程序远程发送固件事件。
我们来看看函数brcmf_rx_frame如何处理固件事件:
void brcmf_rx_frame(struct device *dev,struct sk_buff *skb, bool handle_event)
{
struct brcmf_if *ifp;
struct brcmf_bus *bus_if = dev_get_drvdata(dev);
struct brcmf_pub *drvr = bus_if->drvr;
brcmf_dbg(DATA, "Enter: %s: rxp=%p\n", dev_name(dev), skb);
if (brcmf_rx_hdrpull(drvr, skb, &ifp))
return;
if (brcmf_proto_is_reorder_skb(skb)) {
brcmf_proto_rxreorder(ifp, skb);
} else {
/* Process special eventpackets */
if (handle_event)
brcmf_fweh_process_skb(ifp->drvr, skb);
brcmf_netif_rx(ifp, skb);
}
}
如果handle_event被设置为true,则skb(套接字缓冲区)将被传递给函数brcmf_fweh_process_skb。该函数在fweh.h中的定义如下所示:
static inline voidbrcmf_fweh_process_skb(struct brcmf_pub *drvr, struct sk_buff *skb)
{
struct brcmf_event *event_packet;
u16 usr_stype;
/* only process events when protocol matches */
if (skb->protocol != cpu_to_be16(ETH_P_LINK_CTL))
return;
if ((skb->len + ETH_HLEN) < sizeof(*event_packet))
return;
/* check for BRCM oui match */
event_packet = (struct brcmf_event *)skb_mac_header(skb);
if (memcmp(BRCM_OUI, &event_packet->hdr.oui[0],
sizeof(event_packet->hdr.oui)))
return;
/* final match on usr_subtype */
usr_stype = get_unaligned_be16(&event_packet->hdr.usr_subtype);
if (usr_stype != BCMILCP_BCM_SUBTYPE_EVENT)
return;
brcmf_fweh_process_event(drvr, event_packet, skb->len + ETH_HLEN);
}
这个函数负责验证事件帧。该函数会检查协议是否为0x886c,然后,检查缓冲区的长度是否足以容纳结构体brcmf_event。该结构体的定义如下所示:
/**
*struct brcm_ethhdr - broadcom specific ether header.
*
*@subtype: subtype for this packet.
*@length: TODO: length of appended data.
*@version: version indication.
*@oui: OUI of this packet.
*@usr_subtype: subtype for this OUI.
*/
struct brcm_ethhdr {
__be16 subtype;
__be16 length;
u8 version;
u8 oui[3];
__be16 usr_subtype;
} __packed;
struct brcmf_event_msg_be {
__be16 version;
__be16 flags;
__be32 event_type;
__be32 status;
__be32 reason;
__be32 auth_type;
__be32 datalen;
u8 addr[ETH_ALEN];
char ifname[IFNAMSIZ];
u8 ifidx;
u8 bsscfgidx;
} __packed;
/**
*struct brcmf_event - contents of broadcom event packet.
*
*@eth: standard ether header.
*@hdr: broadcom specific ether header.
*@msg: common part of the actual event message.
*/
struct brcmf_event {
struct ethhdr eth;
struct brcm_ethhdr hdr;
struct brcmf_event_msg_be msg;
} __packed;
最后,它会检查OUI和usr_subtype。如果我们的帧是格式正确的固件事件,它将被传递给函数brcmf_fweh_process_event,之后,该函数会对事件进行排队,以进行相应的处理。
现在,让我们看看函数is_wlc_event_frame是如何在芯片的固件中工作的。我们也可以在bcmdhd源代码中查看它的定义,因为驱动程序和芯片组使用的函数通常必须是相同的,否则帧事件的验证就有可能被绕过。要查找is_wlc_event_frame在芯片固件中的位置及其调用位置,可以跟踪帧数据处理的执行流程,或者直接搜索使用值0x886c的代码的位置。
如果is_wlc_event_frame返回不同于-30的结果,则丢弃该帧。
int is_wlc_event_frame(bcm_event *pktdata,unsigned int pktlen, int exp_usr_subtype, signed int a4)
{
...
if( (bcmeth_hdr_t *)((char *)pktdata + pktlen) > &pktdata->bcm_hdr&& SLOBYTE(pktdata->bcm_hdr.subtype) >= 0 )
return -30;
...
如果字段bcm_hdr.subtype的低位字节大于或等于0,则该函数将返回-30。在brcmf_fweh_processed_skb中并未检查字段子类型,因此,通过提供>=0的子类型,我们就能顺利通过固件检查,这样的话,帧就会被传递给驱动程序,然后,在固件处理程序中作为有效的帧进行处理。当使用的总线为PCIe时,Broadcom实现了他们自己的协议,即MSGBUF,它没有使用特定通道来接收固件事件,这一点与SDIO总线相同。
利用这个漏洞,可将固件事件远程发送到其芯片采用了USB或PCIe总线的主机,从而绕过在is_wlc_event_frame中执行的固件内部检查。
CVE-2019-9500:brcmf_wowl_nd_results中的堆缓冲区溢出漏洞
现在,我们已经能够远程发送固件事件了,下面,让我们来看看它们具体是如何进行处理和发送的。
固件事件的处理始于函数brcmf_fweh_event_worker,该函数将调用函数brcmf_fweh_call_event_handler。
static intbrcmf_fweh_call_event_handler(struct brcmf_if *ifp,
enumbrcmf_fweh_event_code code,
structbrcmf_event_msg *emsg,
void*data)
{
struct brcmf_fweh_info *fweh;
int err = -EINVAL;
if (ifp) {
fweh =&ifp->drvr->fweh;
/* handle the event if validinterface and handler */
if (fweh->evt_handler[code])
err =fweh->evt_handler[code](ifp, emsg, data);
else
brcmf_err("unhandled event %d ignored\n", code);
} else {
brcmf_err("no interfaceobject\n");
}
return err;
}
evt_handler是一个函数指针数组,它是通过调用函数brcmf_fweh_register来为自己赋值的:
/**
*brcmf_fweh_register() - register handler for given event code.
*
*@drvr: driver information object.
*@code: event code.
*@handler: handler for the given event code.
*/
int brcmf_fweh_register(struct brcmf_pub*drvr, enum brcmf_fweh_event_code code,
brcmf_fweh_handler_t handler)
通过搜索该函数的调用位置,我们可以找到所有的事件处理函数。当激活WOWL(Wake Up OnWirelessLAN)功能时,类型为BRCMF_E_PFN_NET_FOUND的事件的处理程序将被注销,并注册另一个处理程序。
这个处理程序就是函数brcmf_wowl_nd_results,其代码如下所示:
brcmf_wowl_nd_results(struct brcmf_if *ifp,const struct brcmf_event_msg *e, void *data)
{
struct brcmf_cfg80211_info *cfg = ifp->drvr->config;
struct brcmf_pno_scanresults_le *pfn_result;
struct brcmf_pno_net_info_le *netinfo;
brcmf_dbg(SCAN, "Enter\n");
if (e->datalen < (sizeof(*pfn_result) + sizeof(*netinfo))) {
brcmf_dbg(SCAN, "Eventdata to small. Ignore\n");
return 0;
}
pfn_result = (struct brcmf_pno_scanresults_le *)data;
if (e->event_code == BRCMF_E_PFN_NET_LOST) {
brcmf_dbg(SCAN, "PFN NETLOST event. Ignore\n");
return 0;
}
if (le32_to_cpu(pfn_result->count) < 1) {
brcmf_err("Invalid resultcount, expected 1 (%d)\n",
le32_to_cpu(pfn_result->count));
return -EINVAL;
}
data += sizeof(struct brcmf_pno_scanresults_le);
netinfo = (struct brcmf_pno_net_info_le *)data;
memcpy(cfg->wowl.nd->ssid.ssid, netinfo->SSID,netinfo->SSID_len); //OVERFLOW YAY!
cfg->wowl.nd->ssid.ssid_len = netinfo->SSID_len;
cfg->wowl.nd->n_channels = 1;
cfg->wowl.nd->channels[0] =
ieee80211_channel_to_frequency(netinfo->channel,
netinfo->channel <=CH_MAX_2G_CHANNEL ?
NL80211_BAND_2GHZ :NL80211_BAND_5GHZ);
cfg->wowl.nd_info->n_matches = 1;
cfg->wowl.nd_info->matches[0] = cfg->wowl.nd;
/* Inform (the resume task) that the net detect information was recvd */
cfg->wowl.nd_data_completed = true;
wake_up(&cfg->wowl.nd_data_wait);
return 0;
}
为复制SSID而调用memcpy函数时,使用的长度值是事件帧数据中提供的长度值,并且没有对其进行相应的检查。802.11标准规定eSSID永远不会超过32个字节,但攻击者能够远程发送ssid长度大于32个字节的固件事件,从而触发堆缓冲区溢出漏洞。当前,这个安全漏洞已经被“悄悄地”修复了。
停用WOWL时,在brcmf_notify_sched_scan_results中也发现了类似的漏洞,实际上,brcmf_notify_sched_scan_results即BRCMF_E_PFN_NET_FOUND的处理程序。这个问题在2017年4月已经被Broadcom"悄悄地"修复[17],但是,启用WoWL时使用的处理程序却被遗忘了。发现这些安全漏洞时,我们使用的是一个比较老的brcmfmac版本,当时通过修改airbase-ng [18](aircrack-ng套件中的工具)实现了一个POC,它可以通过触发brcmf_notify_sched_scan_results溢出漏洞来导致内核崩溃。此外,我们也可以使用scapy[19]或通过修改wpa_supplicant或hostapd来创建漏洞利用代码或PoC代码。
结束语
在这篇文章中,为读者详细介绍了我在Quarkslab为期半年的实习过程中从事的各种实践活动,包括了解Linux内核驱动程序、分析Broadcom固件、重现已知的漏洞、在模拟器上运行固件的各个部分、通过模糊测试发现了5个安全漏洞(CVE-2019-8564、CVE-2019-9500、CVE-2019-9501、CVE-2019-9502和CVE-2019-9503)。其中,有两个漏洞存在于Linux内核和受影响的Broadcom芯片的固件中。对于这些漏洞,最常见的利用场景是发动远程拒绝服务攻击。尽管实现远程代码执行在技术上具有挑战性,但也不得不防。
在这篇文章发表时,还没有关于受影响设备的详尽清单,我们也不知道可以从哪里找到它们。
参考资料
[1] http://ant.comm.ccu.edu.tw/course/92_WLAN/1_Papers/IEEE%20Std%20802.11-1997.pdf
[2] http://www.ahltek.com/WhitePaperspdf/802.11-20%20specs/802.11a-1999.pdf
[3] (1,2) https://github.com/seemoo-lab/nexmon
[5] (1,2,3)https://googleprojectzero.blogspot.com/2017/04/over-air-exploiting-broadcoms-wi-fi_4.html
[6] http://tuprints.ulb.tu-darmstadt.de/7243/
[7] https://github.com/cea-sec/Sibyl
[8] https://lief.quarkslab.com
[9] https://googleprojectzero.blogspot.com/2017/09/over-air-vol-2-pt-1-exploiting-wi-fi.html
[10] https://googleprojectzero.blogspot.com/2017/10/over-air-vol-2-pt-2-exploiting-wi-fi.html
[11] https://googleprojectzero.blogspot.com/2017/10/over-air-vol-2-pt-3-exploiting-wi-fi.html
[12] (1,2) https://blog.exodusintel.com/2017/07/26/broadpwn/
[13] https://comsecuris.com/blog/posts/luaqemu_bcm_wifi/
[14] https://github.com/Comsecuris/luaqemu
[16] https://googleprojectzero.blogspot.com/2017/04/over-air-exploiting-broadcoms-wi-fi_11.html
[18] https://www.aircrack-ng.org/
[19] https://scapy.net/
原文地址:https://blog.quarkslab.com/reverse-engineering-broadcom-wireless-chipsets.html
最新评论