想下载 ISO,结果写了个内核漏洞
2023 年 8 月,我买了一台光鲜亮丽的 Wi-Fi 7 接入点,型号是 TP-Link EAP770。 连接有点不太稳定,视频偶尔会卡顿,但这没太困扰我。
我主要用通过以太网连接的台式机工作。
需要移动时,我会把 ~ 目录通过 Wi-Fi rsync 到笔记本电脑上然后带走。
那些几 GiB 以上的大文件传输开始失败。
SSH 报告出现 MAC 错误,我花了一段时间调试 rsync 和 SSH。
其他一切都正常,调整 rsync 选项有时好有时坏,Wireshark 显示零校验和错误和零重传,单条随机字节 UDP 流传输完美无缺。
我相信 TCP 能交付正确的字节,因为这就是该死的 TCP 存在的意义,所以我确信问题出在 rsync 或 SSH 上。
我写了个带重试功能的 rsync 包装脚本,然后就继续往前走了。
后来我碰巧开始使用 Tailscale 来处理家庭网络内的流量,传输就不再需要重试了。 WireGuard 在认证失败时会丢弃数据包,所以隧道内的 TCP 连接会看到数据包丢失并重新传输。 既然传输正常了,我就不再深究了。
零固件更新#
这台设备是 EAP770(EU) v1.0,搭载高通 IPQ9574,运行 Linux 5.4.164,固件版本为 1.0.0 Build 20230605 Rel. 71641。
这是出厂时自带的固件,显然也是 TP-Link 最后一次记得这款设备存在的时刻 :)
我于 2023 年 8 月购买了它,而 Wi-Fi 7 标准直到 2024 年 9 月才最终确定。 它 没有 Wi-Fi Alliance 认证,买它真是我蠢了。 请在标准最终确定且设备获得认证之前不要购买任何产品。
2025 年,我注意到其他所有设备都在更新,而这台还停留在原始固件。 由于网站只提供 v2 版本的固件,我在六月联系了客服。 他们在数据库里找不到 v1 版本,要求我提供设备和包装盒的照片,然后给我发了这个:
EAP770 版本 1 是一款已停产的产品。尽管部分零售商可能仍有库存,但它已不再获得固件更新支持。
挺好。
追踪损坏源头#
2026 年,我决定迁移到 NixOS,发现从 Fastly 的 Nix 二进制缓存通过 HTTP/2 下载一个 128 MiB 文件会失败,但在 HTTP/1.1 下却能正常下载。
我至今不明白为什么 HTTP/1.1 能起作用,我猜只是因为 HTTP/2 传输速度更快。
git 克隆操作也会失败,HTTPS 连接还会因 bad record mac 错误中断。
三月份,使用 wget 下载 NixOS ISO 时也因 TLS 错误报错:
2026-03-05 12:37:18 (31.6 MB/s) - Read error at byte 2722477636/3683778560 (error:0A000119:SSL routines::decryption failed or bad record mac; error:0A000139:SSL routines::record layer failure). Retrying.
我强行指定使用 HTTP/1.1,随后又将其搁置。
九月份,我将工作站改为构建农场,转而使用通过 Wi-Fi 连接的笔记本电脑。
Wi-Fi 每天也在 11:00 左右掉线,恰好在我每日通话之前。
连续几天在通话前掉线后,我决定再次打开潘多拉魔盒,与 Opus 5.5 一同排查问题根源。
剧透一下:这些掉线其实是另一个独立的 bug。
我用 BPF 探针监控了笔记本的 Qualcomm 驱动,并以 root 权限在 AP 上运行了 apstats。
问题出在笔记本 Wi-Fi 网卡中的闭源固件上;详见文末的更新。
我曾尝试启用和禁用 Wi-Fi 7,在客户端关闭 GRO,并将 Framework 16 中的 MediaTek 网卡更换为 Qualcomm 网卡。 更换新网卡和关闭 GRO 似乎有所缓解,但都无法阻止数据损坏。
我用 tcpdump 抓取了一次失败的 HTTPS 下载数据包,发现一个 1440 字节的 TCP 分段中有 64 个连续字节错误,但 TCP 校验和却是有效的。
TcpInCsumErrors 保持为零。
TCP 层接受了该分段并将其传递给 TLS,随后 TLS 因 bad record mac 拒绝该记录。
坏掉的那段记录解不开密,没法对照预期数据检查坏字节。 我改用一台有线机器和 Wi-Fi 笔记本之间跑原始 TCP,发送已知数据逐字节比对。 只有同时跑多条 TCP 流、并且是从有线到 Wi-Fi 方向时,才会出现损坏。 测试在 8192 MiB 里发现了三个坏的 64 字节块,每个块开头都是零。
损坏的报文校验和却是对的,说明损坏发生后有东西重新算过校验和,问题就变成了:链路上是什么在改写 TCP 头。 把 MediaTek 网卡换成 Qualcomm 的之后损坏依旧,所以不是笔记本的问题。 有线客户端完全正常,坏帧上的 CCMP MIC 也完好无损,所以既不是网线也不是无线链路。 那就只剩 AP 内部以太网和 Wi-Fi 之间的桥接了。 64 字节正好是 IPQ9574 核心的 cache line 大小,这指向的是 cache 或 DMA 的 bug,而不是单纯一位翻转。 这条链路上唯一既会加速桥接流量又会重算校验和的,就是 Qualcomm 的 SFE/PPE offload。
两个需要 root 的写入#
读了驱动源码之后,发现关闭 SFE/PPE 只需要这两条设置:
echo 0 > /sys/sfe/l2_feature
echo 0 > /sys/sfe/ppe_rfs_feature
这两个 sysfs 文件只有 root 才能写,但 TP-Link 的 SSH shell 是以 uid 1 运行的,且不带任何 capabilities。
GUI 里也没有对应的设置项。
另外也没有 v1 固件镜像可以打补丁刷入,v2 固件又是给不同硬件用的。
我试了同门兄弟 EAP773 的 EAP773(EU)_V1_1.0.14,设备直接拒收。
更新版的 v2 固件都是加密的。
直接烧写芯片则需要夹具。
我不想费劲去找夹具,更不想把这该死的 AP 刷成砖。
我可不想再花 200 美元换个 AP,就因为 TP-Link 懒得维护他们已经卖给我的这台。 设备就摆在我家里,我知道要改哪个设置,可他们的软件不让我改。
不过还是得夸一下 TP-Link:这套系统加固得相当到位。
在他们自己的二进制程序里我一个攻击向量都没找到,到处都是白名单,而且只加载需要的模块。
所以别想捡 ; echo 0 > /sys/sfe/ppe_rfs_feature 这种便宜。
那就只能从内核下手了。
借助 Opus 4.8,我构建了一条利用链,组合了 CVE-2022-0185 漏洞和 pipe 缓冲区操纵技术。CVE-2022-0185 是 legacy_parse_param() 中的整数下溢漏洞,非特权用户命名空间即可触发,可导致对 4 KiB 堆对象产生越界写入。通过操纵 pipe 缓冲区,这种越界写入可转化为任意地址写入。当时网络上已有针对这些漏洞的公开分析和利用代码(PoC)。
利用程序必须保证 AP 继续运行,因为断开的卸载(offload)功能在重启后仍会失效。由于堆溢出会破坏 4 KiB 堆对象的状态,导致第二次尝试时内核 panic,因此每次启动只有一次触发机会。我首先向 Opus 提供了原语(primitives)方面的指导,随后让它反复迭代,直至利用程序稳定。
Exploit details由于没有 v1 GPL 源码,我使用了最接近的已发布 EAP783 源码,其内核配置与 AP 的 /proc/config.gz 仅有三行外观性差异。我基于该配置搭建了一个 QEMU 测试环境,在其中跑通利用链后,再迁移到真实设备上进行验证。
该内核未启用 KASLR,因此各次启动间的地址保持一致,但 kallsyms 已被置零,仍需在内核运行时动态计算地址。QEMU 编译版本将内核加载到物理地址 0x40080000,而真实设备将其加载至 0x42080000,导致计算偏差 0x2000000。正确的基准地址可在 EAP783 源码中的构建配置变量中找到。即使修正了基准地址,设备上的 .text、.rodata 和 .data 段相对于 QEMU 编译版本的偏移量各不相同,因此无法通过添加统一偏移值来对齐所有地址。
利用过程如下:
- 切换到用户命名空间,以便访问
fsopen并触发下溢漏洞。 - 通过数百次相同大小的分配对 4 KiB 堆进行碎片整理,精心布置内存布局,使越界写入源、受害密钥、SysV 消息队列和 pipe 环缓冲区紧密相邻。
- 将一条管道的缓冲区重指向另一条管道的环形页,然后用泄漏的 ops 指针在第二个环形页中伪造一个条目。 `pipe_buf_can_merge()` 会校验 ops 指针是否匹配,因此通过伪造条目执行合并写入可以访问任意物理页,包括内核映像页。
- 扫描 `.data` 段以查找 `modprobe_path`。 通常通过管道读取内核映像页时,在缓冲区被消费后会对保留页调用 `put_page()`,从而导致 `Bad page map` 错误并引发 panic。 但 `pipe_read` 仅调用 `->confirm` 和 `->release`,且不检查 ops 表是否真实。 将一个伪造的 `pipe_buf_operations` 条目都指向 `generic_pipe_buf_confirm`(该函数仅返回零,不执行其他操作),就能在不释放页面的情况下让扫描器读取这些页。 然后可以分批遍历 `.data` 段直到找到目标字符串。
- 覆盖 `modprobe_path` 并 `execve` 一个魔术字不匹配任何二进制格式的文件。 内核会调用 `request_module()`,以全局 `uid 0` 身份执行被篡改的路径。
- 保持一个持有伪造管道的子进程存活。 如果进程退出,管道会被拆除,并在保留页上引发 panic。
让这一切跑通大约花了 17 个小时,期间用 Opus 4.8 进行了大量 token 消耗,包括在 radare2 中阅读设备二进制文件、检查已启用的防护机制以及调试崩溃问题。 AP 保持在线,禁用 offload 后内存损坏停止发生。
DMA 缓存一致性漏洞#
高通的 `qca-nss-dp` 驱动在将接收缓冲区交还给网卡前,未对整个接收缓冲区进行 CPU 缓存同步,导致数据包缓冲区被复用。 在此硬件上,DMA 直接写入 DRAM,不会保持 CPU 缓存同步。 上一次使用缓冲区时遗留的脏缓存行可能在网卡收到新数据包后被写回,从而在内存中覆盖新数据包的一部分。
SFE/PPE 随后对损坏的 payload 重新计算校验和,因此客户端收到的 TCP 校验和对于乱码数据而言是“有效”的。 TCP 层接受了它,但 TLS 和 SSH 的端到端 MAC 检测到了损坏,报告 `bad record mac` 和 `corrupted MAC` 错误。
在 x86 架构上,硬件会保证 DMA 与 CPU 缓存的一致性,所以即便缺少同样的缓存维护操作也不会导致数据损坏。 在翻阅内核源码时,我发现其他几个驱动里也存在类似的 DMA 缓存维护问题。 目前我还不知道如何在硬件上验证这些问题,也不清楚该向谁报告。
驱动细节这是 Qualcomm 的 out-of-tree NSS 代码,可以在 openwrt/qca-nss-dp 镜像仓库中找到。
SFE 的快速发送路径会从当前的 skb->data 开始清理 skb_headlen(skb) 字节,覆盖了发送包的线性部分。
483 buff_addr = (dma_addr_t)virt_to_phys(skb->data);
484 EDMA_TXDESC_BUFFER_ADDR_SET(txd, buff_addr);
485
486#ifdef EDMA_40BIT_SUPPORT
487 EDMA_TXDESC_BUFFER_ADDR_HI_SET(txd, buff_addr);
488#endif
489
490 dmac_clean_range_no_dsb((void *)skb->data, (void *)(skb->data + buf_len));
随后它将 skb->fast_recycled 设为 1,并把这个缓冲区返还给 per-CPU 回收链表,期间不清空缓冲区内容。
735 /*
736 * We set fast_recycled flag if packet has taken the
737 * SFE fast transmit path, so that any Rx DMA driver
738 * that allocates this packet later can avoid
739 * an invalidate operation
740 */
741 if (likely(skb->fast_xmit) && likely(skb->is_from_recycler)) {
742 skb->fast_recycled = 1;
743 }
回收器会把 sk_buff 结构体中 tail 字段之前的部分清零,恢复 fast_recycled,并把 skb->data 重置为 skb->head + NET_SKB_PAD。
当这个缓冲区被重新用于接收时,补充路径会看到该标志并跳过缓存失效操作:
edma_rx.c#L477-L491477 /*
478 * Invalidate skb->data
479 * A73 flush operation does an invalidate operation as well.
480 * If the packet is fast transmitted and hence fast recycled,
481 * we can be assured that invalidate was already done at the
482 * time of previous transmit
483 */
484 if (unlikely(!skb->fast_recycled)) {
485 dmac_inv_range_no_dsb((void *)skb->data,
486 (void *)(skb->data + rx_alloc_size -
487 EDMA_RX_SKB_HEADROOM -
488 NET_IP_ALIGN));
489
490 }
491 skb->fast_recycled = 0;
接收区长度为 1856 字节,即 29 个 cache line,起始位置在 head + 192。而之前的发送操作仅清理了不同偏移位置的两到三个 cache line。
那些未被清理范围内的脏 cache line 可能在网卡执行 DMA 后被写回,每次会用 64 字节覆盖接收到的报文。普通的 cache 淘汰就可能引发这种写回。在 arm64 上,非对齐范围的失效操作也会触发:__dma_inv_area 在边缘的未完成行上执行 dc civac(clean and invalidate,清理并失效)。一旦脏数据在 DRAM 中覆盖了报文,再执行 CPU cache 的失效操作也无法找回这些被修改的字节。
网卡在那次覆盖之前校验了原始报文的 checksum,AP 的接收路径随后根据硬件状态位将其标记为 CHECKSUM_UNNECESSARY。PPE 内部的具体序列隐藏在专有硬件和驱动代码中,但从抓包结果来看,checksum 是在报文被损坏之后才计算的。
缓冲复用由 CONFIG_SKB_RECYCLER 开启,这是一个编译期选项,没有运行时开关。关闭 offload 功能会让缓冲区不再走快速发送路径,驱动便不再设置 fast_recycled,接收阶段就会在把缓冲区交给网卡前对其做完整的失效操作。
只有下载到 Wi-Fi 客户端的流量才走 AP 的有线接收路径;上传流量不走,线对线的流量直接由交换机处理。
Opus 5.5 找到了这个 bug,然后就不再提了#
当我从诊断数据损坏阶段转入应用修复时,Opus 5.5 开始在大约每隔一条消息就切断会话。 只要提到类似“本地权限提升”的内容就足以触发防护。 我的账户已启用“网络验证程序”,但这并未起到作用。 Opus 4.8 在利用漏洞过程中没有遇到这个问题。
5.5 的防护机制还会在我仅要求它阅读这篇文章时终止会话。 GPT-6 Astra 则能毫无障碍地阅读和编辑同一篇文章,无需进行任何网络验证。
修复前后对比#
我在两台相同的机器之间,分别在启用和禁用卸载(offload)功能的情况下测试了原始 TCP 传输。
| 卸载状态 | 方向 | 数据量 | 速度 | 损坏的 64 字节块 |
|---|---|---|---|---|
| 开启 | 局域网 -> Wi-Fi | 8192 MiB | 103.4 MiB/s | 3 |
| 关闭 | 局域网 -> Wi-Fi | 整夜运行 | 95.4 MiB/s | 0 |
在该测试中,禁用卸载功能导致下载速度大约降低了 8%。 为了获取正确数据,我不得不牺牲性能,因为 TP-Link 不让我妥善修复自己的设备,真是糟透了。
启用卸载功能时,通过 Wi-Fi 从未能在单一流中成功下载大文件;只有分块下载能勉强通过。 禁用卸载功能后,整夜运行测试显示零损坏块,此后大文件下载每次都成功。 此前,从 Wi-Fi 到局域网的上传一直正常。
禁用卸载功能后,/sys/sfe/exceptions 中的 NOT_SUPPORT_BRIDGE 计数器会递增,因为桥接流量不再走加速通道,而是转回常规路径。
重启后设置会重置,因此我 NAS 上的监控程序会轮询接入点(AP),一旦发现卸载功能重新开启,就再次运行利用程序。 如果尝试失败,它会通过 TP-Link 的管理 API 重启 AP,并在下次启动时重试,同时对重启次数设有硬性上限。
Qualcomm 若有意愿,可以修复其驱动程序,或将其发布在更开放的平台上。 OpenWrt 没有此缺陷,且无论如何 TP-Link 都不会为我的 AP 发布新固件。 不过,我在 Linux 中发现的类似 DMA 漏洞确实值得进一步深究。 我会在周末回头研究那些。
更新:追查数据包丢失#
我装在 Framework 16 里的 Qualcomm 无线网卡是 WCN785x,每隔几个小时就会停止发送数据,持续 1 到 4 分钟。
由于我本来就有 AP 的 root 权限,我在 EAP770 上跑了 apstats,同时在笔记本上开着 BPF 探针,抓到了三次掉线,双方日志都齐了。
AP 发给笔记本的所有数据都到达并被 ACK 了,说明笔记本的无线发射正常、AP 的接收也正常。 只有笔记本发出的数据帧停住了。
在笔记本这边,数据包源源不断进入 ath12k,队列涨到了数千帧。
固件每次只确认几帧,最后通过 CMD_REMOVE_AGED_FRAMES 把剩余的帧退回驱动,一次掉线中退回了 26388 帧里的 10630 帧。
除此之外驱动没有任何动静。
在 AP 这边,rx_data 大部分时间停留在 +0,只是偶尔出现一小波突发,RX 错误为零、重传也为零,说明帧根本没离开网卡。
然后网卡自己恢复了,在两秒内把积压的数据全部发了出去。
AP 看到 rx_data+3273,随后是 +12498,笔记本则看到 7751 帧被确认。
信号强度在 -31 到 -42 dBm 之间,掉线时没有扫描在进行,笔记本的 IP 层在掉线期间也一切正常。
我已经在用最新的上游固件 WLAN.HMT.1.1.c7-00108 UPSTREAM-3。
ath12k 只能收到完成状态,至于帧什么时候发射出去,全由 Qualcomm 闭源的 amss.bin 内部决定。
所以嫌疑只剩固件了。
我至今不明白它为什么停止发射。 掉线期间那几波能正常发出的突发,看起来像是 802.11ax/be 的上行调度、trigger-based 上行或 MU EDCA,但我在 AP 上没找到相关设置来验证。
我把 AP 的 5 GHz 无线从 Wi-Fi 7 切到了 Wi-Fi 6(802.11ax),挂了个监控跑 24 小时。
如果掉线消失了,那就是 Wi-Fi 7 的 bug,我就继续用 Wi-Fi 6,并向 ath12k 提交 bug 报告。
如果没消失,说明固件不管什么模式都有问题,那就要么提 bug 报告,要么换一块 Intel 网卡。