Realtek rtl8821ae 芯片
wifi 芯片位于 m.2 模块上,可以笔记本电脑中被更换。它具有 2.4GHz 和 5GHz wifi 支持等功能,与 wifi 芯片的通信通过 PCIe 进行。
要找到这些固件很容易,它在 Linux 上启动时从 /lib/firmware/rtlwifi 加载。实际上有两套固件:一套用于正常使用(rtl8821aefw_29.bin),另一套稍小一些用于远程唤醒 (rtl8821aefw_wowlan.bin),这是一种通过 wifi 唤醒设备的技术,英文名为Wake-on-WLAN)。 ..._29.bin 在 ip link set dev wlan0 up 上加载,..._wowlan.bin 在 ip link set dev wlan0 down 上加载。它们不是永久固件,而只是加载到芯片的 RAM 中。
该芯片还有一个由realtek编写的Linux内核中的上行wifi驱动程序,这并不能说明它的质量,因为它仍然是由realtek编写的驱动程序。
在同一芯片上还有一个蓝牙芯片,可以通过 USB 2.0 进行通话。内核中似乎实现了一些 BT-Coexistence 协议,以确保 wifi 和 BT 部分不会干扰。
与rtl8821ae的所有通信都是内存映射的:有一个4K大小的配置空间,可以写入和读取。此外,还有一个64K的tx缓冲区和一个64K的rx缓冲区用于发送和接收数据包。
基本固件结构
查看固件,它显然是基于 8051 的。但是,它不会在 0x0000 处加载。当直接在固件映像上运行时,at51 base 返回 0x3fe0。结果发现有一个 0x20 字节的标头,因此固件本身在 0x4000 处加载。
从 0x4000 开始的代码路径也通向 main 函数:在典型的 Keil 编译器方式中,它跳转到 ?C_START 函数,该函数用于初始化由 Keil C(X)51 编译器发出的用于初始化静态变量的静态变量,之后它跳转到 MAIN 函数。
MAIN 函数有点奇怪:它设置一些内存,然后将一个地址压入堆栈,设置一个计时器并返回到它刚刚压入堆栈的地址。事实证明,realtek 正在使用 RTX51 tiny,这是一个非常小的实时内核,用于管理任务和信号。内核基本上会跟踪任务状态并在切换任务堆栈时重新定位它们。 at51 libfind 会找出各种 Keil 库的位置,作为其中的一部分,它还会找到 rtx51 内核函数,如 OS_SWITH_TASK 或 _ISR_SEND_SIGNAL。
普通固件有两个任务(另外一个只用于初始化):一个只在特定信号时被激活,另一个在无限循环中运行。
虽然没有公开数据表记录 8051 端的 I/O 寄存器,但大部分内容很容易找到:Linux 驱动程序从主机端定义配置空间中的寄存器。通过查看 8051 未访问的寄存器,可以看到当映射到 XDATA 偏移量 0x0 时,这些寄存器与驱动程序源中未定义的寄存器类似。然后很容易对定义寄存器的 reg.h 文件进行一些处理,以将所有名称导入 ghidra。
转储Mask ROM
MASK ROM,是制造商为了要大量生产,事先制作一颗有原始数据的ROM或EPROM当作样本,然后再大量生产与样本一样的 ROM。固件在 0x4000 处加载,但它仍然会调用 0x4000 以下的函数,这意味着芯片上有一个永久Mask ROM,它也负责将固件下载到芯片上。不知道这些功能会使逆向工程有点困难,因此获得该固件会很好。
最简单的方法是修改固件并将字节从 CODE 空间复制到配置空间寄存器中,因为我们已经知道这些是如何映射的。但是有一个障碍:固件的最后两个字节似乎是一个校验和,这意味着我们需要知道校验和是如何计算的。然而,校验和将在Mask ROM 内计算,所以我们不能只看固件来了解它是如何完成的。幸运的是,delsum 很快就表明它只是一个 16 位 XOR 校验和,相当于 poly=0x0001 的 CRC。
从用户空间读取和写入配置空间通常是不可能的,所以我需要重新编译内核,以添加对/dev/ mems进行此操作的支持。我选择通过在VM中执行内核并使用VFIO将wifi卡映射到其中来完成此操作,因为重新编译传播服务器内核对我来说时间太长,并且标准配置几乎只适用于 VM。另外,我知道 modconfig 可用,但是 VM 方法在标准配置下仍然可以正常工作,并且不会花费太多时间。
内核与芯片上的固件通信主要使用的是所谓的H2C,它基本上把一些简短的(比如8字节)命令放入配置空间,并让固件读取。通过在ghidra中查看从固件到该点的引用,很容易找到读取该点的固件代码。不幸的是,当我自己处理这些事务时,我无法得到固件的响应。
于是我决定硬编码读取到固件的地址,修改 0x4000 处的跳转,将字节从硬编码地址复制到未使用的配置空间寄存器 (REG_ROM_VERSION),然后继续执行主程序。对于每个读取地址,固件都会被修改,内核模块会重新加载使用新地址修改的固件。这种方法的读取速度大约为每秒 3 个字节。虽然花了几个小时,但可能仍然比找出固件不响应 H2C 命令的原因要快。
蓝牙固件
有一件事我一开始没有意识到,但回顾起来似乎很明显,那就是芯片还有一个单独的蓝牙固件(在/lib/firmware/rtlbt中)。这一次它不是基于8051的,但是我不能轻易地找出它的架构是什么。为了解决这个问题,我制作了一个工具,比较不同的版本的固件通过比较他们使用成对对齐,以便在不同的固件版本之间的相同但在不同地址的部分仍然显示并排。
需要注意的是,在许多地方,都插入了字节00 65,在此之后,两个地址都以4个字节对齐,这意味着它被用作一种填充。当然,用于填充的完美指令应该是NOP。所以我进行了搜索,但“0065 NOP”并没有真正产生任何结果,但是搜索“6500 NOP”(相反的字节序)确实产生了一些有趣的结果,表明 mips16e(mips 的拇指扩展)使用该操作码进行 NOP,事实上,这确实正确地分解了固件,使之变得有意义。
然而,我对蓝牙固件本身不是那么感兴趣,因为它不是基于8051,所以我继续我的wifi固件的分析。首先,我并没有真正拥有任何蓝牙设备。
驱动程序如何发送数据包
wifi的工作方式是在802.11规范中规定的。打开它,你会看到3500页密密麻麻的缩写词。
802.11 帧具有以太网帧的某种作用,但除了承载数据之外,还有各种各样的控制帧来执行某些操作,例如与 AP 关联、与 AP 进行身份验证、电源管理等。数据帧(以及一些控制帧)也可以使用各种密码和协议进行加密。
在加密帧的情况下,还有一些额外的数据,如序列号,其目的是重放保护,这也意味着 802.11 数据帧可以根据是否加密而具有不同的大小。加密通常由 rtl8821ae 本身完成,驱动程序只是将未加密的内容放入加密通常所在的帧中,然后芯片使用硬件对其进行加密。
在 802.11 数据帧内部有一个 LLC 标头,在大多数情况下,它仅用于告知内容的 EtherType。
发送数据包时,驱动程序将整个帧放入 tx 缓冲区,在其前面有一个 40 字节的标头。标头告诉 wifi 硬件帧的大小、是否使用加密、发送的速率和类似的东西。 802.11 帧内还有一个持续时间字段,指示数据包发送所需的时间,这也是由硬件计算的。
硬件有 8 个用于不同目的的队列,可以在标头中指定,一个用于信标帧,一个 MGQ(管理队列?)无论队列如何,数据包都以循环方式在 256 字节边界上放入 tx 缓冲区。
欢迎使用 Realtek RealWoW Tech
在 Linux 驱动程序中,有一个名为 rtl8821ae_set_fw_rsvdpagepkt 的函数,它会在加载新固件时调用。它将信标帧等帧和包含 ARP 响应的数据帧加载到 tx 缓冲区的高端,驱动程序将其视为保留区域,然后这些帧的偏移量通过一些H2C命令发送到固件。
这显然也是出于wowlan的目的,这样固件就可以在主机休眠时响应ARP请求,这样数据包就可以通过IP发送到wifi芯片。wowlan固件负责的另一件事是GTK握手,当设备离开或加入网络时,AP 向所有设备发送一个新的组加密密钥,用于广播/多播目的。
要了解固件如何处理这些,可以查看保存这些偏移量的 H2C 命令。它将它们写入一些 XDATA 位置,因此通过查看引用这些位置的内容,可以找出固件的哪个部分发送了帧。
结果是在XDATA 0xfc00-0xfcff的tx缓冲区中有一个256字节的窗口,地址的更高的8位可以用0xfd10的XDATA寄存器设置。类似地,可以从XDATA 0xfb00-0xfbff访问rx缓冲区,0xfd11有更高的8位。
发送数据包时,很难弄清楚具体是什么发送了数据包,因为有很多事情要做。固件在 tx 标头中设置一些字段,还在 802.11 标头中设置一些位,计算是否加密以找出内容的偏移量,对于 ARP 数据包,它还检查诸如 EtherType 之类的内容并进行响应。当然,解析数据包时会出现更高的复杂性。发送数据包的关键部分似乎是将REG_TXPKTBUF_MGQ_BDNY设置为txbuffer地址的高8位。注意,数据包是256字节对齐的,然后向REG_CPU_MGQ_INFORMATION + 3写入0x20。
查看wowlan固件,可以发现还有一个H2C命令,它似乎为额外的数据包设置了更多配置。在解析代码中,还有一个地方是在帧数据内容的开始处检查字节0x45,这就是IPv4数据包的开始。解析代码然后检查由上述 H2C 命令给出的目标地址和 UDP 端口,并根据应该放入 tx 缓冲区的模式检查数据。如果一切都匹配,则将字节 0x30 作为 wowlan 唤醒原因并唤醒主机。
在Linux驱动程序源代码中,这个原因被命名为FW_WOW_V2_REALWOW_V2_WAKEUPPKT(并没有真正处理)。那么这个所谓的 RealWoW 是什么呢?
不幸的是,Linux 驱动程序没有实现 Realtek RealWoW Tech,并且不清楚如何从 realtek 获得新 ID,所以我放弃了对它的尝试。
但是对固件的分析表明它可能通过定期向服务器发送 UDP 数据包(由驱动程序提供)来工作,服务器以某种 ACK 响应。当在网站中输入正确的 ID 后,服务器会向设备发送一个唤醒 UDP 数据包。可以进行此操作,由于定期发送UDP数据包,所以连接已经打开,wifi固件然后通过PCIe唤醒设备。
一个纯粹基于 8051 的键盘记录器
既然固件已经解决了一些问题,那么我们自己做些改变怎么样?计划是让 EC 获取笔记本电脑键盘的按键,并将它们发送到 wifi 芯片,然后由 wifi 芯片将它们发送到网络。
DMA 不能从 EC 工作,因为它位于 LPC 总线后面,这意味着它将依赖 ISA DMA,这非常糟糕,并且只能访问最低 的16MB 的内存。因此,无法通过 DMA 进行通信。
在上一篇文章中,我分析了EC固件,让我们看一下上一篇文章中的图表:
现在我写到 wifi m.2 芯片上没有连接 EC_TX 和 EC_RX 跟踪(EC 和 rtl8821ae 之间的右下方)。但请注意,EC_RX 跟踪也通过另一个引脚连接到 rfkill(bt) 线,并且在通往 CPU 的路上有一个电阻器,以便 EC 可以有效地覆盖 CPU 而不会造成短路,可以试着用它来传输数据。
经过一些测试,似乎该方法也阻止了wifi的射频能力。但事实证明,如果在使用后再次将其拉低,那就不是什么大的问题,因为主机和固件似乎只看到一些数据包被下载。
因为只有一条迹线,所以控制器必须以某种方式同步,而不是单独的时钟线。我决定采用 UART-ish 方法,这意味着位只是一个接一个地传输,它们之间有一个设定的时间间隔。
EC 已经使用了 1ms 计时器,因此我只是修补了计时器中断以检查 16 字节密钥缓冲区中的新密钥,并以 1 位/毫秒的速度通过 EC_RX 引脚发送它们。
在wifi端,执行固件时出现了问题。事实证明,wifi固件只是在短时间内停止执行以节省电量。与其尝试读取 realtek 驱动程序以了解电源管理的工作方式,还不如定期写入似乎使其保持清醒的配置空间。
现在wifi固件也需要知道一些时间概念,其中有标准的 8051 外设,但它们似乎不会产生中断,因此必须在主循环中进行轮询。一个快速的实验表明,它的频率大约为6.67 MHz,这意味着8051使用了一个80MHz时钟源,这似乎是由驱动程序源中提到的80MHz所备份的。
另一个实验表明,更改 rfkill(bt) 行会更改 REG_GPIO_PIN_CTRL_2 的第 3 位,这样就可以实现UART的另一端。我只是将定时器设置为每 1/3 ms 产生一次溢出,以便有一个更高的采样率使UART正常工作。一旦传输了一个字节,EC会在下一个字节之前暂停一段时间。
在 EC 等待期间,wifi芯片将发送一个包含一个字节的UDP数据包到配置的IP地址和端口,这是通过未加密的wifi实现的。
虽然加密可能是可行的,但它也需要付出一些努力。序列号必须更新,如果主机也在传输数据包,则必须通过tx缓冲区来找到当前的数据包。由于重复的序列号,它也无法从主机发送一些数据包。固件也有可能扫描周围未加密的wifi网络,与它们关联,然后通过DNS发送数据,这通常会绕过潜在的强制门户。
在树莓派上,我设置了一个小程序,它接受 UDP 数据包并将 PS/2 代码转换为 uinput 事件。 这样,键盘就可以有效地充当树莓派的键盘了。
有趣的是,这实际上是一个键盘记录器,它在运行时不会在 CPU 上运行任何代码。 只需要刷新一次EC的固件,更换wifi固件即可加载修改后的版本。
本文翻译自:https://8051enthusiast.github.io/2021/07/05/002-wifi_fun.html如若转载,请注明原文地址