对Broadcom无线芯片组的逆向分析研究
2022-2-20 11:50:0 Author: www.4hou.com(查看原文) 阅读量:39 收藏

导语:在此文章中我记录了学习Linux内核驱动程序,分析Broadcom固件,复现漏洞,在仿真器上运行固件,Fuzzing以及发现5个漏洞(CVE-2019-8564,CVE-2019-9500,CVE-2019-9501,CVE-2019-9502,CVE-2019-9503)。

Broadcom是全球无线设备的主要供应商之一。由于这些芯片用途广泛,因此构成了攻击者的高价值目标,因此,在其中发现的任何漏洞都应视为带来了很高的风险。在此博客文章中,我记录了我在Quarkslab实习的情况,其中包括获取,反转和Fuzzing固件,以及发现一些新漏洞。

0x00 介绍

Broadcom是全球无线设备的主要供应商之一,他们生产带有43系列标签的无线芯片,从智能手机到笔记本电脑,智能电视和物联网设备,你几乎可以在任何地方找到这些芯片。你可能会在不知道的情况下就使用了它,例如,如果你有戴尔笔记本电脑,则可能正在使用bcm43224或bcm4352卡;如果你拥有iPhone,Mac笔记本,Samsumg手机或Huawei手机等,也可能使用Broadcom WiFi芯片。

由于这些芯片用途广泛,因此变成了攻击者的高价值目标,因此,在其中发现的任何漏洞都应视为会带来很高的风险。

我研究了很长一段时间的Broadcom芯片,将已知漏洞复制并移植到其他易受攻击的设备,以学习和改进几种常见的信息安全惯例,在此文章中,我记录了我的研究过程,包括获取,逆向和Fuzzing固件,以及分析发现的一些新漏洞。

0x01 关于WLAN和Linux

在开始之前,让我们看一下802.11无线标准。

1997年创建的第一个IEEE 802.11标准[1]标准化了PHY和MAC层,这是最低的两个OSI层。

对于PHY层,选择了两个频带:红外(IR)频带和微波频带(2.4GHz);之后,其他标准(例如802.11a [2])带来了另一个频率范围(5GHz)。

MAC层使用三种类型的帧:管理,数据和控制;802.11标头帧的帧控制字段标识任何给定帧上的类型。1584961556518.png

管理帧由MLME(MAC子层管理实体)实体进行管理。根据处理MLME的内核的位置,我们可以得到两种主要类型的无线芯片实现:SoftMAC(其中MLME在内核驱动程序中运行)和HardMAC(也称为FullMAC),其中MLME在固件中嵌入在嵌入式固件中。芯片并不是像生活中看到的那么简单,存在一些混合实现,例如,探测响应和请求由驱动程序管理,而关联请求和身份验证则由芯片的固件处理。

FullMAC设备在功耗和速度方面提供了更好的性能,这就是为什么它们在智能手机中得到大量使用,并且往往成为市场上使用最多的芯片的原因。它们的主要缺点是限制了用户发送特定帧或将其设置为监视模式的能力,为此,将需要直接编辑芯片上运行的固件。

从Linux操作系统的角度来看,以上内容为我们提供了无线堆栈中组件的两种主要布局:当无线设备是SoftMAC设备时,内核将使用称为'mac80211'的特定Linux内核模块(LKM)。该驱动程序公开MLME API以便管理管理帧,否则内核将直接使用硬件驱动程序并将MLME处理卸载到芯片的固件中。

1584961578973.png

0x02 Broadcom 的bcm43xxx芯片

Broadcom bcm43xxx系列同时具有HardMAC和SoftMAC卡。不幸的是,我们找不到所分析所有芯片的所有数据表,赛普拉斯收购Broadcom的“ IoT业务”分支后,已经发布了一些可用的数据表;但是,有些芯片同时集成了WLAN和蓝牙函数,例如bcm4339或bcm4330。

分析的所有芯片都将ARM Cortex-M3或ARM Cortex-R4用作非关键时间操作的主要MCU,因此我们需要处理两个类似的指令集:armv7m和armv7r。这些MCU具有一个ROM和一个RAM,其大小取决于芯片组的版本。

所有时间紧迫的操作都由D11内核的Broadcom专有处理器实现,该处理器主要负责PHY层。

这些芯片使用的固件分为两部分:一部分写入ROM,不能修改,另一部分由驱动程序上传到芯片的RAM。这样,供应商仅通过更改固件的RAM部分就可以为其芯片添加新函数或编写更新。

1584961611277.png

FullMAC芯片非常有趣,首先如在固件代码中实现MLME层之前所述,但是它们还提供卸载函数,例如ARP缓存,mDNS,EAPOL等。这些芯片还具有一些硬件加密模块,可以加密和解密密码,流量,管理密钥等。

所有卸载函数都增加了攻击面,为我们提供了一个不错的研究空间。

为了与主机(应用处理器)进行通信,b43系列使用了几种总线接口:USB,SDIO和PCIe。

1584961638947.png

在驱动程序方面,我们可以将bcm43xxx驱动程序集分为两类:开源和专有。

开源:

· b43 (reversed from proprietary wl / old SoftMAC / Linux)

· brcmsmac(SoftMAC / Linux)

· brcmfmac(FullMAC / Linux)

· bcmdhd(FullMAC / Android)

专有:

· broadcom-sta aka'wl'(SoftMAC && FullMAC / Linux)1584961665277.png

“ wl”驱动程序在诸如路由器之类的嵌入式系统上最常用。它通常也用在笔记本电脑上,而笔记本电脑的驱动程序不支持brcmfmac / brcmsmac,例如Dell XPS上的bcm4352芯片。另外,wl驱动程序使用其自己的MLME,不需要LKM'mac80211'处理管理帧,从而为攻击者扩大了攻击面。

Broadcom发行的版本通常称为“混合”驱动程序,因为代码的主要部分来自两个已编译的ELF(在编译时使用的对象)。为什么两个?因为一个用于x86_64体系结构,另一个用于i386。这些对象保存了驱动程序的主要代码,因此公开了许多Broadcom API的函数。

芯片的固件和wl驱动程序共享许多代码,因此在一个中发现的漏洞也可能在另一个中存在。

0x03 获取固件

1)第一部分:RAM固件

如前所述,固件分为两部分。最容易抓住的部分是RAM部分,该部分由驱动程序加载到RAM中,这部分包含主MCU使用的代码和数据,以及D11内核使用的微代码。

固件的此部分未签名,并且使用CRC32校验和“验证”完整性。这导致了一些固件修改,以便添加诸如监控器模式之类的函数;例如,SEEMO Lab发布了NEXMON项目[3],这是一个了不起的框架,用于通过用C编写补丁来修改这些固件。

在我们的研究中,我们遇到了两种可能的RAM固件映像格式:第一个也是最常遇到的是没有特定结构的简单二进制blob;第二种是TRX格式,在bcm43236芯片上工作时很容易解析。

使用.bin RAM固件时,通常在文件末尾有一个字符串,用于显示:

· 芯片版本

· 芯片用于主机进行加密狗通信的总线

· 固件提供的函数;p2p,TDLS等

· 固件版本

· CRC校验和

· 创建日期。

当使用的驱动程序是brmfmac或bcmdhd时,我们可以直接从主机文件系统获取RAM固件。在Linux上,我们可以在/ lib / firmware / brcm中找到它,在Android上可以在/ system / vendor / firmware中找到它 。

在其他情况下,它会根据我们使用的系统而有所不同:

1584961691355.png

如果使用的驱动程序是专有wl,我们可以在LKM的.data部分中找到固件的RAM部分,可以使用LIEF轻松提取[8]

 >>> 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]
 >>> with open("/tmp/bcm4352_ramfw.bin", 'wb') as f:
 ...     f.write(bytes(bcm4352_fw))
 ...
 442233
 >>>
 $ strings /tmp/bcm4352_ramfw.bin | tail -n 1
 4352pci-bmac/debug-ag-nodis-aoe-ndoe Version: 6.30.223.0 CRC: ff98ca92 Date: Sun 2013-12-15 19:30:36 PST FWID 01-9413fb21

发布的bcm4352固件最新采用WL上的Linux驱动程序的日期2013年

2)第二部分:ROM简介

固件的ROM部分是了解这些芯片内部的最重要的部分。

为了拿到ROM部分,我们需要知道它的映射位置。查找基址的最佳方法是读取驱动程序的头文件,例如在bcmdhd的头文件/include/hndsoc.h 中;另一种替代方法是读取Nexmon项目README,该项目根据我们的MCU型号为我们提供了其他基址,精明的读者可能会发现这些地址不同。Nexmon项目指定具有Cortex-M3的芯片的ROM加载为0x800000,bcmdhd的标头显示为0x1e000000,两者都是正确的,似乎ROM和RAM映射了两次。此外,知道基址可以为我们提供有关所使用的MCU的线索,例如,如果将ROM转储到0x000f0000,则表明该芯片正在使用ARM Cortex-R4。

1584961734791.png

3)在Android系统上获取ROM

在Android上,我们可以使用dhdutil工具,该工具是旧wlctl实用程序的Android开源改进分支,通过使用此工具的“内存字节”函数,我们可以转储芯片组的RAM,在某些情况下还可以转储ROM。

 adb shell /data/local/tmp/dhdutil -i wlan0 membytes -r 0x0 0xa0000 > rom.bin

例如,在依赖Cortex-R4的Nexus 5中使用的bcm4339芯片上,ROM被直接转储。不幸的是,在较旧的bcm4330(Cortex-M3)上,此函数无效;但是,只要你可以与RAM交互,就可以Hook一个函数,该存根将把ROM逐片复制到RAM中的空区域,之后,我们可以转储所有ROM的分片。

1584961798349.png

4)恢复Linux系统上的ROM

在具有brcmfmac驱动程序的Linux上,我们无法直接访问ROM。因此,我们需要找到一种直接在ROM或RAM中与芯片内存交互的方法。幸运的是,当芯片使用SDIO总线与主机进行通信时,开源brcmfmac驱动程序将公开brcmf_sdiod_ramrw函数,此函数使我们可以从主机读取和写入芯片组的RAM。

如果我们修改驱动程序以便在此函数周围添加一个ioctl包装器,则可以从一个很小的userspace实用程序读取和写入芯片组的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:

· Hook位于RAM中并由动作X调用的函数

· 将ROM逐片复制到RAM中的空白区域

· 转储所有新复制的ROM片并将其串联。

此协议与我们在芯片的MCU是Android上的Cortex-M3时使用的协议相同;但是,这次我们不得不修改驱动程序并构建自己的工具以使用新驱动程序的ioctl。

在RPI3芯片(bcm43430)上工作时,我们选择了这种方法。

5)在特定情况下获取ROM部分

还有许多其他可能的方案:

如果你的芯片将brcmfmac驱动程序与PCIe总线一起使用怎么办?如果你的芯片使用专有驱动程序“ wl”在嵌入式系统中怎么办?如果主机操作系统上没有shell,该怎么办?或者,如果你没有权限?等等...

在所有其他情况下,你都有几种可能:如果可以访问硬件,则可以寻找UART访问,或者可以挂接wl驱动程序,在“ SFR微型解码器”(bcm43236)上工作时,我们选择了UART访问。

 RTE (usbrdl) v5.90 (TOB) running on BCM43235 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 BCM43235 r3 @ 20.0/96.0/96.0MHz
 000000.007 ei 1, ebi 2, ebo 1
 000000.054 wl0: Broadcom BCM43235 802.11 Wireless Controller 6.37.14.105 (r)
 000000.060 no disconnect
 000000.064 reclaim section 1: Returned 91828 bytes to the heap
 000001.048 bcm_rpc_buf_recv_mgn_low: Host Version: 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()
 with open("/tmp/bcm43236_rom.bin", 'wb') as f:
    f.write(dump)

0x04 逆向固件

在最后一部分中,我们使用了很多术语: RAM固件,一定不要把它与“ RAM snaphot”相混淆,后者是运行时整个RAM的转储。

如Gal.Beniamini,在固件初始化之后,RAM中的某些代码将被回收并用于芯片组的内部堆,如果要分析那些固件,则需要使用正版RAM固件和RAM快照来分析它们。

1)逆向笔记

将所有内容加载到IDA中后,你会注意到没有内容被识别或定义,我们将需要选择所有内容并强制使用IDA对其进行分析。即使IDA识别并正确定义了许多代码和数据,仍然会有很多字符串和无法识别的代码,或者将数据解释为代码。这就使得IDApython派上用场的地方;使用一个小的脚本,我们就能正确定义代码和数据。

当IDA可以正确识别所有内容时,有趣的部分开始了。通常,如果正确设置了基址,则应该弹出很多外部参照,并应检测到数千个函数,我们没有任何符号,代码本身看起来很难理解。

首先要做的事情之一就是识别所使用的类似libc的函数,例如memcpy,memove等,这可以手动完成,也可以使用Sybil(一种函数分析工具)来完成。

固件依靠其自己的内部“控制台”来打印信息。该控制台是RAM中2048个字节的简单缓冲区,因此,固件获得了自己的printf,可以通过存在的众多格式字符串轻松识别。还有其他字符串格式设置函数,例如sprintf / snprintf,当找到内部格式设置函数并对其进行交叉引用时,可以轻松识别它们。

与堆内存管理相关的函数(malloc和free)可以通过不同的方式来标识:我们可以通过调试字符串或寻找经典模式来找到malloc:x = malloc(y); memset(x,0,y); 当找到malloc时,我们看到分配器使用一个空闲块的单个链接列表。

交叉引用链接列表的指针可为我们提供free函数:

1584961886812.png

分配器很容易理解:它是结合在一起的最合适的分配器。分配器通常位于RAM中,因此可以在设备之间更新或从一个版本到另一个版本进行更改。

固件使用许多结构,特别是一种wlc_info的结构,其中包含控制芯片所需的一切。NEXMON项目背后的Matthias SCHULTZ(SEEMO实验室)几个月前发表了他的论文[6]。在他的论文中,他提供了有关这些不同结构的大量信息,并将API的符号名称链接到它们在参数中采用的结构。

固件初始化例程可以通过以下方式轻松找到:

· 跟随复位地址调用(通常在0x0找到)。

· 搜索负责CRC检查的函数。通过搜索表值之一(例如:77073096),可以轻松找到CRC32函数,然后交叉引用此函数将导致固件身份检查。

· 搜索“ WFI”指令并向后交叉引用,初始化后,芯片仅等待中断。1584961906403.png

2)数据包流

现在看看如何在FullMAC设备中管理帧,接收到帧后,将触发中断,并在FIQ中断处理程序中开始帧处理。

看一下如何在bcm4339固件中处理帧,从分析快速中断处理程序(FIQ)开始,我们注意到该处理程序将获取位于0x181100上并指向0x181e48处的函数的函数指针。

1584962077983.png

此函数包含两个分支:一个用于捕获错误(例如错误的内存访问),其他用于实际的帧处理。

如果发生内存冲突,第一个分支将在内部控制台上打印寄存器dump和堆栈跟踪(这对开发漏洞利用代码非常有用:)

1584962606632.png

如果跟随第二个分支,则最终会到一个位于0x0181A88的函数,该函数将循环访问位于0x00180E5C并包含指向函数的指针的链表:

1584961941672.png

如果遵循所有嵌套的调用,我们将以wlc_dpc函数结束。

该函数从wlc_hw结构中检索一个名为macintstatus的变量(在brcmsmac的旧版本中),并执行一些检查。我们感兴趣的值依赖于宏MI_DMAINT(值0x8000)中定义的二进制掩码,如果设置了这些位,将跳转到函数wlc_bmac_recv中。

1584961991089.png

此函数将从位于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)。

1584962021267.png

如果我们想知道帧数据是如何处理的,只需要查看wlc_recv_mgmt_ctl函数的内部,该函数将从帧的FC字段中提取子类型,然后将其分派给相应的处理程序。

1584964565539.png

0x05 仿真和Fuzzing

只有一篇文章提到了固件仿真,它由COMSECURIS [13]发布,该工具是可在Lua [14]中编写脚本修改后的Qemu版本。

由于我们不想模拟所有固件,因此决定采用自己的方法。

首先,我们尝试使用Unicorn框架模拟代码的某些部分(在函数中对printf的简单调用)。

我们围绕Unicorn仿真引擎设计了一个很小的类包装器,使我们可以轻松定义所有仿真参数,并使用jscon配置文件加载它们。这些参数包括:

· ROM文件及其基地址

· RAM快照文件及其基地址

· 起始仿真地址

· 停止仿真地址

· 开始时的CPU上下文

使用我们的RAM快照和以前收集的ROM,RAM快照包含所需的所有内容,代码和初始化的结构。

然后决定在wlc_recv函数开始Fuzzing。为此,我们需要将wlc struct指针放在r0中,并使用帧数据制作一个skb_buff结构,然后将其指针放在r1中。

为了获得示例语料库,我们已经嗅探了在各种情况下发送到设备的流量,然后直接使用pcap文件,现在的Fuzzing策略比较笨拙,因为我们只使用随机的bitflip,并使用静态种子来复现Crash。

在这种情况下,重要的是要创建RAM快照的上下文会影响Fuzzing和所采用的代码路径。例如,如果我们要Fuzzing与AP关联期间使用的帧,则需要在芯片未连接到任何AP时转储RAM。

因此程序如下:对于我们的pcap文件中的每个帧,随机翻转一些位,将带有Fuzzing处理的帧数据的d11标头写入我们的RAM snaphot中,然后为我们的数据创建一个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"
      }
 }

必须确保:

· 我们的帧已正确解析和处理。

· 在Fuzzing期间,我们不会一次又一次地陷入相同的代码路径中。

为了确保正确模拟帧处理,我们通过打印每个pc地址生成了一条trace,然后验证我们是否正确访问了相应的帧处理程序。

为了确定是否通过Fuzzing发现了新的代码路径,我们实现了一个新路径的度量。首先,我们进行空白仿真,而不会Fuzzing pcap文件中的帧。在此运行期间,我们记录所有PC地址并将它们作为键存储在字典中;当开始Fuzzing时,我们会继续记录所有PC地址;如果来自Fuzzing运行的地址不在我们的字典中,那么我们得出结论,我们发现了一条新路径。

1584962138388.png

我们还需要正确检测错误。如果我们尝试在有效的映射之外进行读取或写入,则Unicorn会发现内存访问冲突,但是如何检测堆溢出?COMSECURIS提供了解决方案:Hook分配器函数。

为了遵循所实现的不同动作,我们实现了一种跟踪格式,例如drcov,这使我们可以复现并仔细分析IDA Pro中的Fuzzing会话。

0x06  漏洞挖掘

过去已发现并公开披露了多个漏洞,例如Nitay Artenstein在[12]中发现的CVE-2017-9417 ,Gal Beniamini还发现了芯片固件和Linux内核驱动程序中的多个漏洞,利用这些漏洞可以远程破坏主机,如iPhone 7所示。1584962156469.png

到目前为止,芯片固件中发现的大多数漏洞是由于对信息元素的长度值没有做好限制造成的。信息元素,简称IE,是在IEEE 802.11b管理/数据帧中使用的标签长度值(TLV)数据结构,这些IE用于携带请求方或访问点所需的任何信息,IE有两种:普通的和特定于供应商的。供应商特定的IE具有值为221(0xdd)的标记,并且数据字段以四个字节开头:3个字节包含供应商OUI,一个字节指示IE类型。

1584962173730.png

在固件中,我们分析了专用于解析这些IE的函数,名为bcm_parse_tlvs。该函数返回以下结构:

 typedef  struct  bcm_tlv  { 
         uint8_t  id ; 
         uint8_t  len ; 
         uint8_t  data [ 1 ]; 
 }  bcm_tlv_t ;

通过交叉引用,我们找到了操纵IE的所有调用站点,其中一些函数只是一个包装,用于查找具有特定供应商OUI的供应商IE,交叉引用该包装器可以使我们获得更多函数。

通过反复查看所有这些外部参照,我们发现了先前发现的漏洞,例如CVE-2017-0561,这是由于在memcpy调用期间直接使用快速转换IE的长度值作为size参数而导致堆缓冲区溢出[5]。值得注意的是,在不同的固件中,我们分析了CVE-2017-0561容易受到攻击的函数位于ROM中,因此其代码不可修复,为了修复漏洞,供应商必须停用TDLS函数。

CVE-2019-9501和CVE-2019-9502:两个堆溢出漏洞

继续遍历bcm4339固件上的bcm_parse_tlvs调用,并在0x14310处找到了一个包装函数,该包装函数搜索OUI为00:0F:AC的IE供应商,该IE 在802.11i(增强安全机制)协议规范中用于选择密码套件,身份验证和密钥管理(AKM)套件以及EAPOL密钥数据封装以使用[15]

1584962204797.png

交叉引用此函数将我们引到0x14304处的另一个包装,将其命名为wlc_find_gtk_encap,该包装仅可从位于0x7B45C处的一个函数调用,该函数在引用内部的格式化字符串后命名为wlc_wpa_sup_eapol。

1584962250421.png

看一下此函数对返回的bcm_tlv结构的作用:

1584962280517.png

该函数调用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);
         ...
 }

在这里有一个明显的堆缓冲区溢出,即使用不受信任的源(可能是恶意AP)控制的长度将IE数据复制到固定大小的缓冲区中。Gal Beniamini已在同一wlc_wpa_plumb_gtk函数中发现了其他问题:CVE-2017-11121和CVE-2017-7065。

有一个堆缓冲区溢出,可能还有另一个。我们需要了解如何到达此代码路径,并且需要在IE提取后立即检查在memcpy调用中使用的缓冲区的大小,进一步检查 wl驱动程序后,发现缓冲区大小固定为32个字节。

我们发现了两个缓冲区溢出:第一个允许我们最多溢出219个字节,第二个87个字节,那么如何触发这些漏洞呢?

WPA2协议使用EAPOL(LAN上的EAP)和一个临时密钥(GTK)来加密WLAN中的多播流量,此密钥在EAPOL四次握手期间发送到工作站,该握手封装在EAPOL-Key消息3中的IE供应商中。

1584962318095.png

该wlc_wpa_sup_eapol函数是负责EAPOL的交换期间解析接入点的消息,如果我们在EAPOL-M3中提供大小为255的GTK,则会触发这些溢出。

为了轻松实现此目的,我们只需修补两行hostapd:

1584962333260.png

由于固件代码和wl专有驱动程序共享许多代码,因此我们在驱动程序中发现了相同的问题,这意味着在使用FullMAC设备的系统上,控制恶意访问点的攻击者可能会破坏芯片,而在具有SoftMAC设备的系统上,攻击可能会导致内核内存直接受损。

为了验证我们的发现,尝试使用驱动程序wl将易受攻击的SoftMAC bcm43263芯片连接到在EAPOL交换期间提供PoC的恶意接入点:

1584962355837.png

这些问题出现在所有分析的固件以及所有版本的wl驱动程序中;但是,尽管代码在所有固件中都存在,但似乎并非所有版本都使用该代码;例如,它在我们分析过的bcm4339的固件版本中没有使用,但是在bcm43430设备的所有固件版本中使用。

为了成功利用这些漏洞,必须远程操作堆布局以获得重叠的块。Gal Beniamini已经的文章涵盖了芯片固件堆开发的所有方面[5] [9] [10] [11]。另一位研究者Nitay Artenstein也谈到了这一问题[12],在他的案例中,溢出更容易被利用,因为他能够直接粉碎相邻块中的指针,从而使任何内存都可写。

如上所述,在这些芯片上进行堆利用的一个主要问题是堆布局操纵。几乎没有一种原始方法可以允许在受控的范围内进行受控的大小分配。我们可能会在几个“管理操作”帧处理程序中找到几个受控的大小分配原语,但是每次使用该原语时都会释放分配的块;另一方面,这些芯片上的所有RAM均设置有RWX权限,并且没有漏洞利用缓解机制。

Linux brcmfmac  驱动程序中的漏洞

在研究Broadcom固件的过程中,我们还发现了brcmfmac中的两个错误,该错误是Linux内核的FullMAC卡的开源无线驱动程序。

这些芯片使用以下三种BUS接口之一:USB,SDIO和PCIe。加密狗建立在总线的顶部,有两种机制用于加密狗主机和加密狗通信主机。

第一种通信方法主要用于主机加密狗通信,并且基于自定义的ioctl。我们可能会在固件代码中发现ioctl处理程序是一个难看的大开关盒。

1584962374665.png

第二种通信机制称为固件事件。芯片使用这些固件事件通知主机不同事件:扫描结果,关联/解除关联,身份验证等。这些事件封装在以太类型为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);

最后一个参数是一个布尔值,用于指示是否处理了包含固件事件的帧。因此,我们检查了驱动程序的代码,以查看是否使用具有true值的handle_event参数调用了此函数。

使用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 event packets */
                 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 void brcmf_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的代码位置。

1584962469698.png

如果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等固件事件。

此漏洞可用于绕过is_wlc_event_frame中进行的固件内部检查,从而通过USB或PCIe总线将固件事件远程发送到芯片上的主机。

CVE-2019-9500:brcmf_wowl_nd_results堆缓冲区溢出

现在,我们已经能够远程发送固件事件,再研究一下它们是如何处理和调度的。

固件事件处理从函数brcmf_fweh_event_worker开始,它将调用函数brcmf_fweh_call_event_handler。

 static int brcmf_fweh_call_event_handler(struct brcmf_if *ifp,
                                         enum brcmf_fweh_event_code code,
                                         struct brcmf_event_msg *emsg,
                                         void *data)
 {
         struct brcmf_fweh_info *fweh;
         int err = -EINVAL;
 
         if (ifp) {
                 fweh = &ifp->drvr->fweh;
 
                 /* handle the event if valid interface 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 interface object\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(在无线LAN上唤醒)函数时,将取消注册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, "Event data 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 NET LOST event. Ignore\n");
                 return 0;
         }
 
         if (le32_to_cpu(pfn_result->count) < 1) {
                 brcmf_err("Invalid result count, 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 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;
 }

当调用memcpy来复制SSID时,使用的长度是事件帧数据中提供的长度,并且不会进行检查。802.11标准指定eSSID不得超过32个字节,但是攻击者可以远程发送ssid大小大于32个字节的固件事件,从而触发堆缓冲区溢出。该问题已被静默修补。

在停用WOWL时,同一个事件的处理程序(BRCMF_E_PFN_NET_FOUND)brcmf_notify_sched_scan_results 中发现了类似的问题。该问题已在2017年4月由Broadcom静默修补[17],但是忘记了启用WoWL时使用的处理程序。当我们发现这些问题时正在使用过时的brcmfmac版本时,通过修改aircrack -ng套件中的工具airbase -ng [18]来完成触发brcmf_notify_sched_scan_results中的溢出并恐慌内核的PoC 。也可以使用scapy [19]或修改wpa_supplicant或hostapd进行利用或仅进行PoC。

0x07 研究结论

在此文章中我记录了学习Linux内核驱动程序,分析Broadcom固件,复现漏洞,在仿真器上运行固件,Fuzzing以及发现5个漏洞(CVE-2019-8564,CVE-2019-9500,CVE-2019-9501,CVE-2019-9502,CVE-2019-9503)。这些漏洞中的两个同时存在于受影响的Broadcom芯片的Linux内核和固件中。最常见的利用情况导致远程拒绝服务,尽管实现技术上具有挑战性,但不应将对远程代码执行的利用作为最坏的情况而丢弃。

在发布此博客文章时,我们还没有详尽列出受影响的设备。

0x08 参考文献

 [1] http://ant.comm.ccu.edu.tw/course/92_WLAN/1_Papers/IEEE%20Std%20802.11-1997.pdf
 [2] IEEE Std 802.11a-1999 (Supplement to IEEE Std 802.11-1999), Part 11: Wireless LAN Medium Access Control (Mac) and Physical Layer (PHY) Specifications: High-speed Physical Layer in the 5 GHZ Band, LAN/MAN Standards Committee, IEEE Computer Society, approved 16 September 1999.
 [3] (1, 2) https://github.com/seemoo-lab/nexmon
 [4] https://android.googlesource.com/kernel/common/+/bcmdhd-3.10/drivers/net/wireless/bcmdhd/include/hndsoc.h
 [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
 [15] https://mentor.ieee.org/802.11/dcn/04/11-04-0588-01-000i-tutorial-using-ouis-to-identify-cipher-and-akm-suites.doc
 [16] https://googleprojectzero.blogspot.com/2017/04/over-air-exploiting-broadcoms-wi-fi_11.html
 [17] https://github.com/torvalds/linux/commit/4835f37e3bafc138f8bfa3cbed2920dd56fed283#diff-66ea469ce534d8c3ba7147099b87fe78
 [18] https://www.aircrack-ng.org/
 [19] https://scapy.net/

本文翻译自:https://blog.quarkslab.com/reverse-engineering-broadcom-wireless-chipsets.html如若转载,请注明原文地址:


文章来源: https://www.4hou.com/posts/gQBY
如有侵权请联系:admin#unsafe.sh