ESP32 是一款在创客社区中因其低价(约 5 欧元)和实用功能而广受欢迎的微控制器:它具有双核 CPU、内置 Wi-Fi 和蓝牙连接以及 520 KB 的 RAM。它也被商业使用,应用于从智能二氧化碳计到工业自动化控制器的设备。用于编程 ESP32 的大部分软件开发工具包是开源的,但无线部分(Wi-Fi、蓝牙、低级 RF 功能)除外:这些功能以预编译库的形式分发,然后编译到开发者编写的固件中。
闭源的 Wi-Fi 实现与开源实现相比有几个缺点:
- 您依赖于供应商(在这种情况下是 Espressif)来添加功能;如果您有一些非标准的用例,您可能会失去机会。例如,ESP32 不支持符合标准的网状网络(IEEE 802.11s);有一个Espressif 制作的部分闭源网状网络实现,但这相当有限:网状网络具有树形拓扑,并在连接到根网络的节点上使用 NAT,这使得从网状网络外部连接到网状网络中的节点变得困难。该协议也没有文档,因此与其他设备不兼容。
- 很难审计实现的安全性:由于没有可用的源代码,你必须依赖黑箱模糊测试和逆向工程来发现安全漏洞。
- 此外,开源实现将使低功耗 Wi-Fi 网状网络的研究变得更加经济实惠;如果每个节点仅需约 5 欧元,涉及数百个节点的研究在适度预算下是可负担的。
Espressif 在他们的 esp32-wifi-lib 仓库中有一个未解决的问题,要求开源 MAC 层。在那个问题中,他们在 2016 年确认开源上层 MAC 在他们的路线图上,但截至 2023 年,仍然没有发布任何内容。拥有源代码将使我们能够实现符合 802.11s 标准的网状网络。
目标
该项目的主要目标是构建一个最小化的替代品,以取代 Espressif 的专有 Wi-Fi 二进制文件。我们并不打算与使用 Espressif ESP-IDF API 的现有代码保持 API 兼容,而是希望拥有一个完全可用的开源网络栈。
本节其余部分将包含有关网络协议栈和 Wi-Fi(802.11 标准)如何工作的相关信息,因此如果您已经熟悉,可以跳过此部分。
网络协议栈的 OSI 模型(应用层/表示层/会话层之间的区别有点模糊)
上面可以看到一个显示网络协议栈的图示。计算机网络是通过网络协议栈实现的,协议栈中的每一层都有其特定的目的;这种设计使得更换层变得更容易,并允许对各层进行独立开发。协议栈底部的层与物理世界进行交互(例如,通过使用无线电波或电信号);每一层都增加了自己的功能。Wi-Fi(工程师称之为 802.11 标准)在底部的两层中实现:物理层(无线电波形的样子,……)和 MAC 层(我们如何连接到接入点,存在什么数据包,如何将数据包发送到本地设备,……)。
在 ESP32 上,物理层是通过硬件实现的;大部分 MAC 层是通过专有代码实现的。一个显著的例外是发送确认帧:如果设备接收到一个帧,它应该发送一个数据包以确认该数据包已正确接收。这个 ACK 数据包需要在大约 10 微秒内发送;在软件中很难准确把握这个时机。
有三种类型的 MAC 帧:
- 管理帧:主要用于管理接入点与站点(客户端)之间的连接
- 控制帧:帮助传送其他类型的帧(例如确认帧,但也包括请求发送和清除发送)
- 数据帧:包含 MAC 层上方层的数据
先前的工作
由于 Espressif 似乎不会很快发布开源的 MAC 实现,我们只能自己来创建。这相当困难,因为我们在 ESP32 上发送和接收 802.11 数据包的硬件完全没有文档。这意味着我们需要对硬件进行逆向工程;首先,我们需要记录硬件的功能,然后我们需要编写自己的代码以正确与之交互。在 2021 年,Uri Shaked 对 ESP32 Wi-Fi 硬件进行了非常轻微的逆向工程,以便在他的模拟器中模拟它。这样,ESP32 的程序可以在模拟器中运行,而不是在真实硬件上运行。Shaked 对此进行了演讲,但只讨论了关于硬件的非常高层次的细节。Espressif 有他们自己的 QEMU 分支(一个流行的开源模拟器),也可以模拟 ESP32,但这个分支不支持模拟 Wi-Fi 硬件。在 2022 年,Martin Johnson 为他们自己的 Espressif QEMU 分支添加了对 Wi-Fi 硬件的基本支持。模拟的 ESP32 可以连接到虚拟接入点,或者让虚拟客户端连接到它。
esp-idf(ESP32 的 SDK)有一个传输帧的功能( esp_wifi_80211_tx
),但该功能仅接受某些类型的帧;它不允许发送大多数管理帧,严重限制了基于此 API 构建 802.11 MAC 栈的实用性。他们还有一个功能( esp_wifi_set_promiscuous_rx_cb
)用于在接收到帧时接收回调。
工具
在我们开始逆向工程 802.11 PHY 硬件的工作原理以及我们如何与之互动之前,我们首先需要找到或构建能够帮助我们的工具。我们将使用三种主要方法:
- 静态逆向工程:我们拥有实现 Wi-Fi 协议栈的编译库,因此我们可以查看编译后的代码并尝试将其反编译为人类可读的代码。通过这些更易读的代码,我们接着尝试了解硬件期望软件执行的操作。
- 动态代码分析在仿真器中:我们可以在仿真器中运行固件,并检查它如何与虚拟硬件交互。这有很多自由度来检查硬件的优点,但缺点是仿真器可能与真实硬件的行为不同。由于我们需要自己编写仿真外设,这个风险是真实的:Wi-Fi 外设没有公开的数据手册,因此我们必须根据与其交互的代码来猜测硬件的行为。
- 在真实硬件上进行动态代码分析:我们可以在实际的 ESP32 上运行固件,并使用 JTAG 调试器进行调试。这使我们能够设置断点、检查内存和寄存器、暂停和恢复执行……缺点是与在模拟器中运行相比,调试能力更有限:我们只能设置 2 个断点,无法设置观察点(在对特定地址进行内存读/写时触发的断点)……与使用模拟器相比,最大的优势是我们可以确定硬件的行为是正确的。
静态分析
对于静态分析,我们使用 Ghidra,这是一个由 NSA 制作的开源逆向工程工具。开箱即用,Ghidra 尚不支持Xtensa(ESP32 的 CPU 架构),但有一个插件可以添加支持。ESP32 SDK 中使用的构建工具生成 ELF 文件(可以包含元数据的一种二进制文件)和扁平二进制文件:使用 ELF 文件的好处是可以自动设置大多数函数名称。
仿真器中的动态分析
我们从 Martin Johnson 对 Espressif 版本的 QEMU(一个流行的开源模拟器)的分支开始,并将他们的更改移植到 Espressif 最新版本的 QEMU 分支上。ESP32 通过内存映射 IO 与其外设通信:通过读取和写入某些内存地址,外设向 CPU 提供信息并执行操作。为了帮助逆向工程,我们在 QEMU Wi-Fi 外设中添加了日志语句,记录对其内存范围的每次访问。
此外,我们还在 QEMU 中实现了堆栈展开;这对于每个与 Wi-Fi 相关的硬件外设的内存访问都进行此操作。这样,我们可以为每个外设访问获取完整的堆栈跟踪。符号没有被剥离,因此这是一个非常有用的工具。然而,为了使堆栈展开正常工作,我们必须以单步模式运行 QEMU:QEMU 有一个 JIT 编译器,可以将模拟的汇编指令序列编译成优化的基本块。这大大提高了执行速度,但由于 CPU 执行状态仅在基本块的开头保证正确,如果在这样的基本块中间发生外设内存访问,堆栈展开算法将给出错误的结果。
以单步模式运行会抵消 QEMU JIT 编译器的许多好处,导致代码运行得更慢。与执行跟踪提供的大量信息相比,这并不是一个太大的缺点。
以下是 QEMU 记录的单个内存访问的示例:它是对地址 3ff46094
的写入 ( W
),值为 00010005
,由函数 ram_pbus_force_test
完成。调用栈的其余部分也被记录,并在可用时翻译为符号名称。
W 3ff46094 00010005 ram_pbus_force_test 400044f4 set_rx_gain_cal_dc set_rx_gain_testchip_70 set_rx_gain_table bb_init register_chipv7_phy esp_phy_load_cal_and_init esp_phy_enable wifi_hw_start wifi_start_process ieee80211_ioctl_process ppTask vPortTaskWrapper
最后,我们还修正了 MAC 地址的处理(与马丁·约翰逊的版本相比),以便数据包捕获中的数据包具有正确的 MAC 地址,而不是硬编码的地址。
在真实硬件上的动态分析
为了在真实硬件上动态分析固件,我们使用 JTAG 硬件调试接口。通过在 ESP32 和 JTAG 调试器之间连接一些跳线,我们可以调试 ESP32。我们按照这个 GitHub 仓库中描述的步骤使我们的 JTAG 调试器(CJMCU-232H)正常工作。
除了 JTAG 调试器,我们还将 USB Wi-Fi 适配器直接连接到 ESP32:ESP32 的 ESP32-WROOM-32U 变体具有天线连接器。我们将该天线连接器连接到一个 60 dB 的衰减器(这会将信号衰减 60dB),然后将其连接到无线适配器的天线连接器。这样,我们将能够仅接收来自 ESP32 的数据包,而 ESP32 将仅接收无线适配器发送的数据包。
这个想法不幸的是并没有完全奏效:来自外部接入点的足够无线电波泄漏到天线连接器,使得无线接收器也接收到了它们的数据包。我们尝试用一个油漆罐建造一个低成本的法拉第笼来防止这种情况,但这只减少了外部信号 10dB:这去掉了一些接入点,但并不是全部。目前的解决方案显然不是理想的,因此我们已经开始着手建造一个更好、更大的法拉第笼,使用导电面料和光纤数据通信。
接收器连接到 ESP32,中间有两个 30 dB 的衰减器
用油漆罐制作的法拉第笼,使用铜带封闭 USB 连接器的孔,并使用铁氧体磁环减少射频泄漏
建筑
软 MAC 与硬 MAC
SoftMAC(软件 MAC)和 HardMAC(硬件 MAC)指的是实现 Wi-Fi MAC 层的两种不同方法。SoftMAC 依赖软件来管理 MAC 层功能,这提供了灵活性和易于修改的优势,但可能会消耗更多的电力/CPU 周期。另一方面,HardMAC 将 MAC 层处理卸载到专用硬件上,从而减少 CPU 使用和电力消耗,但限制了在没有硬件更改的情况下适应新功能的能力。
ESP32 似乎采用了 SoftMAC 方法:您可以直接发送和接收 802.11 帧(而不是使用 HardMAC,在这种情况下,您告诉硬件您想连接到某个 AP,然后它会自动构建必要的帧并发送)。这对我们的开源实现来说是个好消息,因为已经存在用于 SoftMAC 的开源 802.11 MAC 栈(例如,Linux 内核中的 mac80211)。
外设
Wi-Fi 功能是通过多个硬件外设实现的,每个外设负责功能的不同部分。通过逆向工程,以下外设被识别为“用于 Wi-Fi 功能”(这些是可以访问外设的内存地址):
- MAC 外设,位于 0x3ff73000 到 0x3ff73fff 和 0x3ff74000 到 0x3ff74fff
- RX 控制寄存器,位于 0x3ff5c000 到 0x3ff5cfff
- 基带,从 0x3ff5d000 到 0x3ff5dfff
chipv7_phy
(?) 在 3ff71000 到 3ff71fffchipv7_wdev
(?) 从 3ff75000 到 3ff75fff- RF 前端,位于 3ff45000 到 3ff45fff 和 3ff46000 到 3ff46fff
- 模拟在 3ff4e000 到 3ff4efff(这也被连接到 GPIO 引脚的 DAC 使用)
需要注意的是,这些外设在地址空间中被镜像到另一个位置:
通过 0x3FF40000 ~ 0x3FF7FFFF 地址空间(DPORT 地址)访问的外设也可以通过 0x60000000 ~ 0x6003FFFF(AHB 地址)访问。(0x3FF40000 + n)地址和(0x60000000 + n)地址访问相同的内容,其中 n = 0 ~ 0x3FFFF。
生命周期
通过编写一些最小化的固件,仅在循环中发送数据包,并使用之前描述的三种逆向工程策略,确定了发送数据包的 Wi-Fi 硬件生命周期的高层次概述:
- 调用
esp_wifi_start()
,这间接调用esp_phy_enable()
esp_phy_enable()
负责初始化 wifi 硬件:- 校准 PHY 硬件:这尝试补偿硬件的缺陷。根据数据表,这至少包括:I/Q 相位匹配;天线匹配;补偿载波泄漏、基带非线性、功率放大器非线性和射频非线性(我更偏向于软件而不是电子工程师,所以我不太清楚这些术语的具体含义)。此校准可以存储到非易失性存储器和内存中。这是为了避免每次 ESP32 从调制解调器睡眠中唤醒时都进行完全校准。
- 初始化 MAC 外设:设置接收 MAC 地址过滤器,设置接收数据包的缓冲区,设置自动确认策略,设置芯片自身的 MAC 地址。
- 设置各种物理无线电属性(发射速率、频率、发射功率等)
- 设置电源管理计时器:如果数据包发送不够频繁,调制解调器的省电计时器将启动,并去初始化部分 Wi-Fi 硬件以节省电力。
- 现在,我们准备发送一个数据包:
- 唤醒一些 Wi-Fi 外设从深度睡眠中恢复,并在需要时恢复它们的校准
- 设置一些与数据包相关的元数据(可能是速率和其他物理层设置)
- 创建一个 DMA 条目,包括数据包的长度和包含 MAC 数据的缓冲区地址。MAC 帧校验和由硬件自动计算。DMA 代表直接内存访问:这意味着我们只需告诉硬件数据包的位置和长度,硬件就会自行读取该内存并传输数据包。
- 将 DMA 条目的最低位写入硬件寄存器,然后通过在该寄存器的位掩码中设置一个位来启用传输。
- 一旦数据包发送,中断 0 将触发以通知我们传输的成功程度。我们可以对冲突和超时做出反应(可能也会对收到的确认应答做出反应?)。我们还必须清除指示数据包已发送的中断位。
实现传输数据包
作为一个(非常有限的)概念验证,我们希望通过直接使用内存映射外设发送任意的 802.11 帧,而不使用 SDK 函数。正如上面的生命周期图所示,在传输之前,我们首先需要初始化 wifi 硬件。不幸的是,这个初始化比发送数据包要复杂得多:初始化硬件大约需要 50000 次外设内存访问,而发送一个数据包(包括处理中断)大约只需要 50 次。这些数字并不完全准确,但它们给出了所涉及复杂性的一个大致概念。
对于基本的“传输数据包”概念验证,我们目前仍在使用专有功能来初始化 wifi 硬件。我们遇到的问题是,在初始化后,调制解调器的省电计时器会启动并取消初始化 wifi 外设,导致我们无法发送数据包。为了解决这个问题,我们使用 SDK 发送一个数据包,然后立即调用未记录的 pm_disconnected_stop()
函数,该函数禁用调制解调器的省电模式计时器。之后,我们可以通过直接写入 MAC 外设地址来发送任意数据包。对于这个概念验证,我们不需要替换 wifi 事件的中断处理程序:现有的专有处理程序可以很好地处理“数据包已发送”的中断。
基本概念的基本证明有效,我们可以通过直接读写内存地址来传输任意数据包!
当前路线图
现在我们可以传输数据包,但我们还有很多工作要做:这是待办事项清单,按优先级大致排序
- ☑ 发送数据包
- ☐ 接收数据包:为此,我们需要执行以下操作:
- 设置 RX 策略(根据 MAC 地址过滤数据包)/启用混杂模式以接收所有数据包
- 设置我们希望通过 DMA 接收数据包的内存地址
- 将 wifi 中断替换为我们自己的中断;代码表明可能存在某种 wifi 看门狗,我们需要弄清楚如何照顾它。
- ☐ 如果我们收到一个发往我们的数据包,则发送确认(ACK)数据包回去
- ☐ 实施更改 wifi 频道、速率、发射功率等…
- ☐ 将我们的实现与现有的开源 802.11 MAC 栈结合,以便 ESP32 能够与接入点关联
- ☐ 实现硬件初始化(现在由 esp_phy_enable() 完成)。这将是一项艰巨的任务,因为所有的校准例程都需要实现,但也有很高的回报:我们将拥有一个完全无 blob 的 ESP32 固件。
以及一份可能的未来扩展列表,这些扩展尚未在路线图上,但无论如何都是有用的:
- ☐ 实施调制解调器省电:在不使用时关闭调制解调器
- ☐ AMSDU, AMPDU, HT40, QoS
- ☐ 在硬件中进行 WPA2 等所需的加密,而不是在软件中进行
- ☐ 蓝牙
- ☐ 为所有反向工程的寄存器编写 SVD 文档。SVD 文件是一个描述微控制器硬件特性的 XML 文件,这使得可以从硬件描述自动生成 API。Espressif 已经有一个包含已记录硬件寄存器的 SVD 文件;我们可以记录未记录的寄存器并(自动)合并它们。
代码
所有代码和文档都可以在esp32-open-mac GitHub 组织中找到。我认为,特别是 QEMU 分支由于其内存追踪功能,对其他逆向工程师来说非常有用。
更新
自从开始写这篇博客文章以来,接收数据包的功能也已实现。为了实现这一点,我们需要实现 Wi-Fi MAC 中断处理程序并管理 RX DMA 缓冲区。这意味着我们现在可以仅使用开源代码发送和接收数据包:硬件初始化仍然使用专有代码,但在完成此设置后,仅使用开源代码来发送和接收数据包,不再执行任何专有代码。第二部分在这里。
声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与技术交流之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。