这是关于逆向工程 ESP32 Wi-Fi 网络栈系列中的第二篇文章,目标是构建我们自己的开源 MAC 层。在本系列的上一篇文章中,我们构建了用于逆向工程的静态和动态分析工具。我们还开始了发送数据包的传输路径的逆向工程,并以粗略的路线图和对贡献者的呼吁结束。
在这一部分,我们将继续进行逆向工程,从“接收数据包”功能开始:上次我们成功地传输了数据包。本部分的目标是使发送和接收都能正常工作。为了证明我们的设置有效,我们将尝试连接到一个接入点,并向同样连接到网络的计算机发送一些 UDP 数据包。
接收功能
作为简短的回顾,传输功能的工作原理是:
- 将您要传输的数据包放入内存中
- 创建一个 DMA(直接内存访问)结构体。该结构体包含:
- 您要传输的数据包的地址
- 数据包的长度和大小(我还没有完全弄清楚它们之间的区别,但一个似乎总是比另一个大 32)
- 下一个数据包的地址(我们将其设置为 NULL 以传输单个数据包)
- 写一些其他内存外设以配置您即将传输的数据包的设置
- 将 DMA 结构的地址写入内存映射 IO 地址
- 硬件随后自动读取 DMA 结构,并传输数据包
- 完成后,中断 0 将触发,告诉我们传输的成功程度
接收功能似乎使用相同的 DMA 结构,但方式略有不同:
- 设置一个 DMA 结构的链表,其中结构的
next
字段指向链表中的下一个 DMA 结构。最后一个 DMA 结构指向 NULL。每个address
字段指向一个缓冲区,长度和大小字段设置为缓冲区的大小。 - 将第一个 DMA 结构的地址写入内存映射 IO 地址 (
WIFI_BASE_RX_DSCR
)。现在设置完成,我们可以接收数据包。 - 当硬件接收到一个数据包时,它会将数据包放入第一个可用的 DMA 结构的地址中。
length
字段将指示数据包的长度;size
字段将不会被更新。has_data
字段将被设置为 1。 - 中断 0 将触发以通知处理器已接收到数据包。此中断将通知一个非中断任务已接收到数据包。我们应该避免在中断中进行过多处理,因为我们希望尽快返回。
- 在中断之外,我们可以查看 DMA 结构的链表,以查看哪些结构的
has_data
位被设置。然后可以将地址缓冲区进一步传递到 Wi-Fi MAC 栈中。我们希望避免耗尽用于接收数据包的 DMA 结构,因此我们必须扩展链表。我们可以通过分配一个新的 DMA 结构和数据包空间,并将其放在 DMA 链表的末尾来实现,但这种不断的分配和释放会非常低效。相反,我们通过重置现有 DMA 结构的字段并将其插入链表的末尾来回收它们。
实用性
现在我们有了一种基本的接收数据包的方法,但在实现这一点时,没有接收到任何数据包:这可能是由于硬件 MAC 地址过滤器的原因:如果你是一个 Wi-Fi 设备,空中有很多你不感兴趣的数据包。例如,如果你是一个站点(例如,一部手机)并且连接到一个接入点,你实际上并不关心其他接入点发送给它们的站点的数据包。为了避免处理“无趣”数据包的开销,大多数 Wi-Fi 设备都有一个硬件过滤器,你可以设置想要接收的数据包的 MAC 地址。然后,硬件将过滤掉具有不同 MAC 地址的数据包,只将匹配 MAC 地址的数据包转发给软件。
ESP32 似乎也实现了这一功能,但幸运的是,ESP32 还实现了一种监视模式(也称为混杂模式),在该模式下,硬件接收到的每个数据包都会传递给软件。ESP32 SDK 中有一个调用 esp_wifi_set_promiscuous(bool)
,您可以在其中启用或禁用此功能。当我们启用此功能时,确实开始接收数据包。我们最终会进行逆向工程并实现硬件 MAC 地址过滤,但现在我们只会在软件中进行过滤。
连接到接入点
现在我们已经实现了发送和接收,你可能会认为我们可以连接到接入点并开始发送数据包,对吧?其实并不是这样:由于这是一个大项目,我们在每个阶段只实现了最低限度的功能。这与 Ladybird 构建新浏览器的方法是一样的:
如果你试图一次构建一个浏览器的一个规范,甚至是一个功能,你很可能会失去动力,完全失去兴趣。因此,我们倾向于专注于构建“垂直切片”的功能。这意味着设定实际的、跨领域的目标,比如“让 twitter.com/awesomekling 加载”, “让 discord.com 的登录功能正常工作”,以及其他类似的目标。
这种方法非常激励人,但有时会让你感到困惑,当你不得不弄清楚为什么某些东西不起作用时。
步骤 1:使用 Scapy
在我们开始将 ESP32 连接到接入点之前,我们首先通过构建和发送数据包来实现将常规 USB Wi-Fi 适配器连接到接入点,以确保我们理解所需的一切;这样我们就会有一个已知的工作参考实现。我们发现了这篇博客文章,关于使用 Scapy,一个 Python 数据包操作库,连接到开放接入点。我们需要 4 个数据包来建立连接:
- 客户端到接入点的认证
- 认证,从接入点到客户端
- 关联请求,从客户端到接入点
- 协会回应,来自 AP 对客户的回复
之后,如果一切顺利,我们可以从客户端向接入点发送数据帧,并且它们会被接受。我们稍微扩展了博客文章中的代码,以便在连接设置结束时也发送数据帧,并验证了一切正常。对于数据帧,我们使用了 UDP 数据包,因为我们只需构造一次数据包,然后可以不断发送;UDP 是无状态的,不像 TCP。
步骤 2:使用 ESP32
我们在 ESP32 上实现了这一点,通过从 Scapy 复制数据包并在 C 源代码中硬编码数据包内容。为了确保我们能够区分 ESP32 和 Scapy 实现,我们将用于测试的适配器的 MAC 地址替换为一个任意的 MAC 地址( 01:23:45:67:89:ab
)。当我们发送数据包时,我们看到收到了一个 ACK 帧作为我们认证的响应,但我们没有从 AP 收到认证应答。更奇怪的是,ACK 是针对一个不同的 MAC 地址: 00:23:45:67:89:ab
。
显然,MAC 地址不仅仅是 6 个任意字节,其中前 3 个字节是供应商特定的:第一个字节的最后一位指示数据包是单播还是多播。通过使用 01:...
MAC 地址,我们发送了多播数据包而不是单播数据包。
在通过使用不同的 MAC 地址修复此问题后,我们开始从接入点接收数据帧。由于我们没有实现发送 ACK,因此我们从接入点接收到了每个数据帧 4 次:因为接入点没有收到任何 ACK,它会假设数据包没有正确接收。那时,这并不是问题:接入点会愉快地继续进行关联请求和响应。
然而,当我们开始发送数据包时,我们立即开始收到来自接入点的解除关联帧作为对我们数据包的回复。唯一的区别在于(有效的)Scapy 实现和当前的 ESP32 实现之间,就是没有发送确认(ACK);所以我想实现这一点毕竟是必要的。
在软件中发送 ACK 帧并不像看起来那么简单:ACK 帧需要在接收到的帧最后一个符号之后恰好一个 SIFS(短帧间隔)时间段内发送。对于 802.11b,SIFS 仅为 10 微秒;通过硬件和软件的往返时间已经超过 10 微秒,因此我们无法在软件中实现这一点。专有网络栈确实会发送 ACK 帧,因此必须以某种方式实现这一点。实际上,发送 ACK 是在硬件中实现的:通过写入一个内存映射的 IO 地址,您可以配置一个 MAC 地址,硬件将自动发送 ACK。
在实施这一点后,我们在监听 UDP 数据包的计算机上收到了第一个数据包 🎉
成功接收由 ESP32 发送的第一个数据包
由于我们现在自己实现中断,我们可以发送和接收帧,而无需任何专有代码 运行(专有代码仍然用于初始化硬件,但在此之后不再需要)。
当前将数据包内容硬编码的方式适用于证明我们可以连接到 AP 并发送数据包,但不适合我们的最终目标。我们正在寻找一个开源实现,处理 802.11 MAC 层的更高层功能(构建和解析数据包,知道何时发送哪些数据包等)。对于更高层,我们可以在 ESP32 上使用现有的 lwIP TCP/IP 栈。
所有代码都可以在esp32-open-mac GitHub 组织上找到。
路线图
- ☑ 发送数据包
- ☑ 接收数据包
- ☑ 如果我们收到一个发往我们的数据包,则发送确认(ACK)数据包返回
- ☑ 基于 MAC 地址实施硬件过滤,以减少接收到的数据包数量
- ☐ 找到或构建一个开源的 802.11 MAC 实现,以构造我们想要发送的数据包。Linux 内核有 mac80211,但包含完整的 Linux 内核似乎不可行。这并不是 ESP32 特定的;我们理想的情况是找到一个可以传递自定义 TX 和 RX 函数的实现,它们会处理其余的工作。
- ☐ 实施更改 wifi 频道、速率、发射功率等…
- ☐ 实现硬件初始化(现在由
esp_phy_enable()
完成)。这将是一项艰巨的任务,因为所有的校准例程都需要实现,但也有很高的回报:我们将拥有一个完全无 blob 的 ESP32 固件。 - ☐ 为所有反向工程的寄存器编写 SVD 文档。SVD 文件是一个描述微控制器硬件特性的 XML 文件,这使得可以从硬件描述自动生成 API。Espressif 已经有一个包含已记录硬件寄存器的 SVD 文件;我们可以记录未记录的寄存器并(自动)合并它们。
实现硬件初始化和将我们的发送和接收原语连接到开源 802.11 MAC 栈是两个最困难(但最重要)的任务。
奖金:夏洛特打破了一切
夏洛特播放音乐完全破坏了设置:我们黑客空间的音乐设置通过 RTP(实时传输协议)工作。在底层,RTP 发送包含音频数据的 UDP 数据包到一个多播地址;因此,这些数据包也通过 Wi-Fi 传输。由于每秒有大量数据包,接收缓冲区总是满的,其他数据包很少能够被接收/确认。这使得很明显,硬件过滤需要尽快实施;逆向工程的工作量并没有预期的那么大。
硬件过滤似乎有两个“插槽”,每个插槽可以根据目标 MAC 地址和 BSSID 进行过滤(不确定是否可以在每个插槽中同时进行两者过滤,或者必须选择其一)。默认情况下,硬件不会允许任何数据包通过。只有当数据包通过其中一个过滤器并被复制到 RX DMA 缓冲区时,硬件才会发送 ACK 帧:由于混杂模式而被复制到 RX DMA 缓冲区的数据包不会导致发送 ACK 帧。
声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与技术交流之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。