一个 TP-Link EAP770 无线 AP,从买回来那天起就在悄悄损坏数据:大文件下载会失败,SSH 报 MAC 错误,TLS 报 bad record mac。两年多以后,一位工程师顺着这条线索挖到了高通 qca-nss-dp 驱动里一个 DMA 缓存一致性问题,最后为了改两个只读的 sysfs 开关,写了一条完整的 Linux 内核提权链。
这篇文章把这条链路拆开:从"数据被谁改了"的排障,到硬件级的根因,再到拿 root 的过程。它值得看的地方在于:一个真实世界的缓存一致性 bug 是怎么伪装成网络故障的,以及当厂商不给你修的时候,一个开发者能走多远。
症状:损坏是"有效"的
设备是 EAP770(EU) v1.0,高通 IPQ9574 平台,Linux 5.4.164,固件停留在出厂版本 1.0.0 Build 20230605。作者 2023 年 8 月买的,那时候 Wi-Fi 7 标准都还没定稿。
最早的表现很分散:rsync 同步大于几 GiB 的文件会失败,SSH 报 MAC 错误。看起来像 rsync 或 SSH 的问题,毕竟 Wireshark 抓包里零坏校验和、零重传,单个 UDP 随机字节流也干干净净。作者一度以为是 TCP 该干的活没干好。
关键转折出现在 tcpdump 抓一次失败的 HTTPS 下载:一个 1440 字节的 TCP 段,中间连续 64 个字节是错的,但 TCP 校验和有效。而 TcpInCsumErrors 始终是 0。
这句话是整个事件的题眼:校验和对一段已经损坏的数据是成立的。也就是说,有人在校验和算完之后改动了载荷。TCP 把段交给 TLS,TLS 用端到端的 MAC 才发现问题,于是报 bad record mac。TLS 和 SSH 的完整性校验,是唯一在"TCP 层已经接受"之后还能拦住这类损坏的防线。
进一步的定位很讲究方法论。作者放弃加密流量,改成有线机到 Wi-Fi 机之间跑原始 TCP,收发自己构造的已知数据,逐字节比对:
- 损坏只在多个 TCP 流并发时出现,方向固定为 LAN → Wi-Fi;
- 测试里 8192 MiB 数据出现 3 个坏块,每块正好 64 字节,且都以零开头;
- 换掉笔记本网卡(MediaTek 换高通)后损坏依旧,排除了客户端;
- 有线客户端干净、坏帧的 CCMP MIC 完整,排除了网线和空口。
64 字节正好是 IPQ9574 核心的一条 cache line,这个数字直接把怀疑指向缓存或 DMA,而不是翻转的比特。这条路径上同时"加速桥接流量"又"重算校验和"的,只有高通的 SFE/PPE offload。
根因:一次写回,覆盖了已经收到的包
qca-nss-dp 驱动在把包缓冲还给网卡时,没有同步整块接收缓冲的 CPU 缓存。在这块硬件上,DMA 直接写 DRAM,不跟 CPU 缓存保持一致。于是上一次使用留下的脏 cache line,会在网卡收到新包之后被写回内存,把新包的一部分覆盖掉。
代码层面看得很清楚。发送快速路径只清理当前 skb->data 开始的线性部分:
buff_addr = (dma_addr_t)virt_to_phys(skb->data);
EDMA_TXDESC_BUFFER_ADDR_SET(txd, buff_addr);
dmac_clean_range_no_dsb((void *)skb->data, (void *)(skb->data + buf_len));
然后设置 skb->fast_recycled = 1 把缓冲放回 per-CPU 回收链表,不清内容。等这个缓冲被接收路径复用时,refill 代码看到这个标志就跳过了 invalidate:
if (unlikely(!skb->fast_recycled)) {
dmac_inv_range_no_dsb((void *)skb->data,
(void *)(skb->data + rx_alloc_size -
EDMA_RX_SKB_HEADROOM - NET_IP_ALIGN));
}
skb->fast_recycled = 0;
问题就出在范围对不上:接收区域是 1856 字节(29 条 cache line),从 head + 192 开始;而之前发送只从另一个偏移清理了两三条 line。这个被漏掉的区间里如果留着脏数据,就会在网卡 DMA 之后被写回,一次替换 64 字节。
正常缓存淘汰会触发这种写回;在 arm64 上,对非对齐区间做 invalidate 同样会触发,__dma_inv_area 对边缘 line 用的是 dc civac(clean and invalidate)。数据一旦被覆盖到 DRAM,再 invalidate CPU 缓存也救不回来。
最讽刺的一环在 SFE/PPE:它在损坏之后重算校验和,于是客户端拿到的是一个对垃圾数据成立的全新 TCP 校验和。TCP 照单全收,只有 TLS/SSH 的端到端 MAC 能发现。
x86 上不会有这个问题,因为硬件保证 DMA 与 CPU 缓存一致。这也是为什么这种 bug 只在特定 SoC 上露头:它不是通用内核 bug,是特定平台 + 特定加速路径的组合。作者顺带在内核源码里发现了其他几个驱动有类似的 DMA 缓存维护问题,但暂时没法在硬件上验证,也不知道该报给谁。
缓冲回收由 CONFIG_SKB_RECYCLER 控制,是编译期选项,没有运行时开关。
拿 root:关掉 offload 要先提权
修复方式其实就两条命令:
echo 0 > /sys/sfe/l2_feature
echo 0 > /sys/sfe/ppe_rfs_feature
但这两个文件只有 root 能写,而 TP-Link 的 SSH shell 以 uid 1 运行、没有任何 capability,Web 界面也没有对应开关。更绝的是:设备已 End-of-Life,没有 v1 固件可打补丁;v2 固件是别的硬件,设备直接拒收;新版固件还加密了。直接刷 flash 需要编程夹,作者不想为了一台已经很烦的 AP 去折腾。
设备本身加固得还不错:作者在 TP-Link 自己的二进制里没找到任何可利用点,白名单到处都在,只加载需要的模块。所以入口只能是内核。
于是有了这条链,底子是 CVE-2022-0185:legacy_parse_param() 里的整数下溢,可从非特权 user namespace 触发,能让一次写越过 4 KiB slab 对象的边界。NVD 给它的评分是 CVSS 3.1 = 8.4 HIGH,攻击向量 AV:L/AC:L/PR:N/UI:N,本地、无需权限、无需交互。
利用的关键步骤:
- unshare 进 user namespace,让
fsopen可达,从而能触发下溢; - 整理 4 KiB slab:先做几百次同尺寸分配做碎片整理,再把溢出源、受害者 key、SysV message、pipe ring 摆到相邻位置;
- 溢出进受害者 key 的元数据,放大它的长度字段;
- 读回 key 造成信息泄露,拿到相邻对象,包括某个 pipe 的
buf[0].ops,里面有anon_pipe_buf_ops的地址; - 把一条 pipe 的 buffer 指向另一条 pipe 的 ring page,再用泄露的 ops 指针在第二个 ring 里伪造一个表项;
pipe_buf_can_merge()会检查 ops 指针是否匹配,所以一个伪造表项上的 merge 写就能触达任意物理页,包括内核镜像的页;- 扫 .data 找到
modprobe_path; - 覆写
modprobe_path,然后 execve 一个魔数不匹配任何二进制格式的文件,内核调用request_module(),把这个替换路径以全局 uid 0 执行。
这里有个特别优雅的细节:通过 pipe 读内核镜像页,正常会在缓冲消费时对保留页调 put_page(),触发 Bad page map 然后 panic。但 pipe_read 只调 ->confirm 和 ->release,不检查 ops 表是否是真的。作者伪造一个 pipe_buf_operations,两个入口都指向 generic_pipe_buf_confirm(返回 0、什么也不做),就能在扫描时只读不释放,一路走 .data 找字符串。
因为损坏的 offload 重启后会回来,exploit 必须让 AP 保持运行,不能靠崩溃重启。而溢出会把 4 KiB slab 留在一个状态,第二次尝试就 panic,所以每次启动只有一次机会。
工程量:两个不明显的坑
作者没有 v1 的 GPL 源码(TP-Link 没放),只能拿最接近的 EAP783 源码凑,其内核配置跟设备 /proc/config.gz 差三行装饰性内容。他在 QEMU 里用这份配置把链跑通,再上真机,中间踩了两个坑:
- 加载基址不同:QEMU 把内核装在物理
0x40080000,设备是0x42080000,差0x2000000,所有地址计算全偏。正确基址藏在 EAP783 源码的一个构建配置变量里。 - 段偏移各自不同:即使修好基址,设备的
.text、.rodata、.data相对 QEMU 构建移动的量还不一样,所以"给所有地址加同一个偏移"是行不通的。
内核没有 KASLR,地址在重启之间不变,但 kallsyms 被清零了,地址仍然得运行时找。
整个过程花了大约 17 小时的"token 赌博",包括用 radare2 读设备二进制、核对启用的保护、反复处理崩溃。
效果是可量的:关掉 offload 在这个测试里损失约 8% 的下载速度。但换来的是数据正确:开着 offload 时,他一次都没法单流下载大文件,只有分块下载能过;关掉之后跑一整夜零坏块,之后大文件下载一直正常。上传方向本来就是干净的。
作者在 NAS 上放了个 watcher 轮询 AP,offload 一回来就重跑 exploit;失败就通过 TP-Link 的管理 API 重启 AP 再试,带重启次数上限。
一个开发者的视角
这件事有三层值得记下来的东西。
第一,缓存一致性 bug 会伪装成上层故障。 它表现为 rsync 失败、SSH MAC 错误、TLS bad record mac,全都在错误的地方。诊断的突破口是"校验和有效的损坏数据"这个矛盾:一旦你意识到校验和对垃圾成立,就说明损坏发生在校验和计算之后,嫌疑范围立刻从"传输层"缩到"改包的那一层"。这套推理在排查任何"数据下游对不上上游"的问题时都适用。顺带一个直接的防御启示:端到端完整性校验(TLS/SSH MAC)是最后一层能拦住这类链路层损坏的防线,别为了性能把它省掉。
第二,offload 换性能是有代价的。 SFE/PPE 这类硬件加速让包绕过正常协议栈,8% 的速度换来的是"驱动一有小 bug,正确性就没了"。在关键链路上,遇到数据完整性问题时,先把 offload 关掉做对照实验,是很值得放进口袋的第一步。
第三,当厂商不给你修的时候。 这台 AP 固件停在 2023 年,官方回复是 End-of-Life 不再更新。作者的选择是:设备在我家里、我知道该改哪个开关、而你的软件不让我改,那就自己想办法。这种"自己的硬件凭什么不让修"的态度,是这整篇文章的动机。顺带说一句:买未定稿标准的设备要谨慎,这台 AP 买在 Wi-Fi 7 定稿前,没有 Wi-Fi Alliance 认证,代价是两年多的数据损坏。
对做固件和驱动的同学,这里还有个直接的提醒:CONFIG_SKB_RECYCLER 这类"跳过 invalidate"的优化,前提是发/收两侧的清理范围严格对齐。只要有一次清理范围对不上,"脏 line 写回覆盖新收到的包"就会变成一个只在多流并发下才出现的幽灵 bug,而且校验和会被后来重算,让所有基于校验和的排查手段全部失效。
⚠️ 网络安全免责声明
本文内容仅供技术研究与安全学习之用。文中描述的漏洞分析、攻击链拆解和代码示例均基于公开披露的安全研究,旨在帮助开发者理解攻击原理、提升安全意识和防御能力。
请勿将本文所述技术用于任何未经授权的安全测试、渗透攻击或非法用途。任何因滥用本文信息而造成的法律后果,由使用者自行承担,与本文作者无关。
如发现系统安全漏洞,请通过合法渠道向相关厂商或平台报告,共同维护网络安全生态。
浙公网安备 33010602011771号