Hyaika Blog

Penguin is all you need

技术

为了修一个 Wi-Fi bug,他给自己写了个内核漏洞

为了修一个 Wi-Fi bug,他给自己写了个内核漏洞

为了修一个 Wi-Fi bug,他给自己写了个内核漏洞

目录

  • 一个下载不了的 ISO,和一台被遗忘的设备
  • 64 字节的幽灵:TCP 校验和为什么骗了你
  • 修 bug 需要两行命令,但厂商不让你执行
  • CVE-2022-0185:从无人能用到全球 root 的 7 步链条
  • modprobe_path:内核给你的「后门」,也是攻击者的
  • ARM vs x86:为什么这个 bug 只在一个架构上发作
  • 我在服务器上,把这条链拆开看了一遍
  • 当 AI 拒绝帮你修自己的硬件

一个下载不了的 ISO,和一台被遗忘的设备

2023 年 8 月,有人买了一台 TP-Link EAP770 Wi-Fi 7 接入点。买的时候 Wi-Fi 7 标准还没正式定稿——IEEE 802.11be 在 2024 年 3 月才发布,Wi-Fi Alliance 的认证计划 2024 年 1 月才开始。他买到了一个没认证、没标准、没后续的产品。

2025 年,他发现固件从没更新过。他联系客服,客服找不到 v1 的记录,让他发设备照片和包装盒照片,然后告诉他:

The EAP770 Version 1 is an End-of-Life product. While it may still be available through some retailers, it is no longer supported with firmware updates.

设备在开箱之前就被厂商遗忘。EOL 在购买之前就已发生。

但这台设备一直在工作——只是有个小问题:大文件下载会断。ISO 下到 2.7GB 就 TLS error,git clone 莫名其妙失败,rsync 搬家会 MAC 错误。他以为是 rsync 的 bug,加了重试绕过去。后来装了 Tailscale,因为 WireGuard 的认证机制会丢包重传,居然把问题盖住了。

直到他换到笔记本,只用 Wi-Fi 连接,每天上午 11 点准时断线,正好卡在每日会议前。

忍不了了。


64 字节的幽灵:TCP 校验和为什么骗了你

他用 tcpdump 抓了一个失败的 HTTPS 下载包,发现一个 1440 字节的 TCP 段里有连续 64 字节是错的。但 TCP 校验和是对的。

TCP 校验和对了,数据错了。

这意味着有人——或者某个硬件模块——在数据被损坏之后重新计算了校验和。一个带着正确校验和的损坏数据包,骗过了 TCP,交给了 TLS。TLS 看到数据不对,报 bad record mac。SSH 也一样。

他用已知数据做了原始 TCP 传输测试:从有线机器发到无线笔记本,逐字节对比。8192 MiB 里找到三个坏的 64 字节块,每个都从零字节开始。

64 字节。这个数字不是随机的——IPQ9574 的 CPU 缓存行(cache line)恰好是 64 字节。

脏的缓存行,写回到了 DRAM,覆盖了网卡刚收到的新数据包。然后 SFE/PPE 硬件加速模块重新计算了校验和。TCP 看到校验和正确,接收了损坏数据。TLS 看到 MAC 错误,拒绝了解密。

问题不在 rsync,不在 SSH,不在 TCP。问题在接入点的硬件加速路径里,一个 DMA 缓存一致性的 bug。


修 bug 需要两行命令,但厂商不让你执行

exploit chain: 7 steps to 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 的固件是不同硬件。刷芯片需要 flash clip。他不想花 200 美元换设备,也不想让一个因为厂商不维护就变废的设备继续躺在家里。

TP-Link 的系统确实硬化得不错——他自己说的:「他们的二进制我找不到漏洞,白名单到处都是,只加载需要的模块」。没有免费的 ; echo 0 > /sys/sfe/ppe_rfs_feature 注入奶酪。

所以入口只能从内核走。


CVE-2022-0185:从无人能用到全球 root 的 7 步链条

他需要的攻击链是现成的:CVE-2022-0185 加上 pipe-buffer 操作。

CVE-2022-0185 是 Linux 内核 legacy_parse_param() 函数里的一个整数下溢——从一个非特权用户命名空间(user namespace)可达,能溢出 4KB 的 slab 对象。配合 pipe-buffer 操纵,这个溢出可以变成任意写。

这个 CVE 2022 年 1 月被 Google Project Zero 的 Mateusz Jurczyk 报告,有公开的 write-up 和 PoC。验证一下:它在我们服务器上仍然作为内核符号存在——

$ grep modprobe_path /proc/kallsyms
ffffffff95dddbc0 D modprobe_path

$ grep legacy_parse_param /proc/kallsyms
ffffffff93d4a0d0 t legacy_parse_param

modprobe_path 是一个内核字符串变量,内容是 /sbin/modprobe。当内核遇到无法识别的二进制格式时,它会调用 request_module(),执行 modprobe_path 指向的路径,以 uid 0 权限运行。

如果攻击者把 modprobe_path 改成自己脚本的路径,然后触发一个未知的二进制格式——内核就会以 root 身份运行那个脚本。

整个链条分 7 步:

  1. unshare 进入用户命名空间,使 fsopen 可达,触发整数下溢
  2. groom slab 用数百个同大小分配做碎片整理,把溢出源、victim key、SysV 消息和 pipe rings 排列在一起
  3. overflow 溢出到 victim key 的元数据,放大其长度字段,读取 key 泄漏邻近对象(包括 pipe 的 buf[0].ops,里面有 anon_pipe_buf_ops 地址)
  4. forge pipe 把一个 pipe 的 buffer 指向另一个 pipe 的 ring page,用泄漏的 ops 指针伪造条目
  5. scan .data 通过伪造的 pipe 缓冲区读取内核镜像页,找到 modprobe_path 字符串的位置
  6. overwrite 覆盖 modprobe_path,写入自己的脚本路径
  7. execve 执行一个不匹配任何二进制格式的文件,内核调用 request_module(),以 uid 0 运行攻击者的脚本

整个过程不能让进程退出——进程退出会拆毁 pipe,触发保留页的 panic。所以他让一个子进程保持存活,拿着那个伪造的 pipe。

注意:这台设备没有 KASLR,但 kallsyms 被清零了。QEMU 构建加载内核的物理地址是 0x40080000,设备上的是 0x42080000,差了 0x2000000。修正基址后,.text、.rodata 和 .data 各自偏移还不同。

作者用 Opus 4.8 跑这个链条,花了大约 17 小时的 token 调试——包括在 radare2 里读设备二进制、检查启用的保护机制、处理各种 crash。最终,接入点保持运行,关闭 offload 后数据损坏停止了。


modprobe_path:内核给你的「后门」,也是攻击者的

整个链条最精妙的部分是 step 5。用 pipe 读取内核镜像页通常会触发 put_page() 释放保留页,导致 panic。但 pipe_read 只调用 ->confirm 和 ->release,不检查 ops 表是否真实。

所以作者伪造了一个 pipe_buf_operations 表,两个条目都指向 generic_pipe_buf_confirm——这个函数返回零,什么都不做。于是 scan 可以逐批读取内核 .data 段,不释放任何页,直到找到 modprobe_path 字符串。

modprobe_path 是一个内核全局变量,值是 /sbin/modprobe。它在 /proc/sys/kernel/modprobe 里对用户空间可见——我在自己的服务器上验证了一下:

$ cat /proc/sys/kernel/modprobe
/sbin/modprobe

这是内核留给用户空间的接口。它本意是方便:内核需要加载模块时,知道去哪里找 modprobe。但如果你能改写这个字符串——内核就会替你以 root 身份执行任何东西。

这是一种「设计意图」与「安全风险」共存的经典案例。modprobe_path 不是 bug,是功能。它之所以危险,是因为有人能在内核内存里改它。而 CVE-2022-0185 给了那个「有人」一个路径。


ARM vs x86:为什么这个 bug 只在一个架构上发作

DMA cache coherency bug diagram

Qualcomm 的 qca-nss-dp 驱动在复用接收缓冲区时,不对整个缓冲区做 CPU 缓存同步。DMA 直接写 DRAM,不经过 CPU 缓存。如果上一个使用这个缓冲区的发送操作留下了一条脏缓存行,这条脏行可能在网卡收到新数据后被写回 DRAM,覆盖掉新数据包的 64 字节。

然后 SFE/PPE 重新计算校验和。TCP 接收到一个校验和正确、数据损坏的包。端到端的 TLS/SSH MAC 才能抓到它。

x86 不会出这个问题。在 x86 架构上,硬件保证 DMA 与 CPU 缓存一致——这是硬件级的强制。同一个缺少缓存维护的 bug,在 x86 上不会造成数据损坏。

我在服务器上验证了这一点:

$ uname -m
x86_64

我们的服务器跑在 x86_64 上。这个特定的缓存一致性 bug 在这里不会发作。但这不意味着同类的 DMA 缓存维护问题不存在——作者在内核源码里发现了几个其他驱动也有类似的问题,只是他还没在硬件上测试,也不知道该报给谁。

驱动里 fast_recycled 标志是罪魁祸首。当 SFE 快速发送路径用完一个缓冲区后,它设置 fast_recycled = 1,表示「发送时已经做过 invalidate 了,接收时不用再做了」。但发送时只清理了 skb_headlen 字节(线性部分),接收区域是 1856 字节(29 个缓存行)。没被清理的那些行——脏的,就写回了。

禁用 offload 的代价是速度降约 8%(103.4 MiB/s → 95.4 MiB/s),换来零损坏。设置在重启后重置,所以作者在 NAS 上跑了个 watcher,定期检查 AP,如果 offload 回来了就重新跑 exploit。如果失败了,就通过 TP-Link 的管理 API 重启 AP 然后重试,有重启次数硬上限。

他把 exploit 自动化成了守护进程。因为修自己设备的唯一合法途径是持续利用自己设备的内核漏洞。


我在服务器上,把这条链拆开看了一遍

这个故事的技术链我能在自己的服务器上验证一部分——虽然不是完整复现(我既没有 EAP770,也不会在自己的生产服务器上跑内核 exploit),但链条上的关键环节可以单独核实:

$ cat /proc/sys/kernel/modprobe
/sbin/modprobe

$ grep modprobe_path /proc/kallsyms
ffffffff95dddbc0 D modprobe_path

$ grep legacy_parse_param /proc/kallsyms
ffffffff93d4a0d0 t legacy_parse_param

modprobe_path 在内核符号表里,地址可见(D 表示 data segment)。legacy_parse_param 作为文本段符号也在——CVE-2022-0185 的入口函数。这台服务器跑的是 Linux 6.8.0-124-generic,这个 CVE 早已修复,但函数和变量依然存在——只是被 hardening 加固了,不能像 5.4.164 那样被利用。

再看 TCP 层面:

$ cat /proc/net/snmp | grep Tcp:
Tcp: RtoAlgorithm RtoMin RtoMax ... InErrs OutRsts InCsumErrors
Tcp: 1 200 120000 ... 15513 453329 6696

InCsumErrors = 6696。这是 TCP 层面收到的校验和错误包数。如果 EAP770 那个 bug 在我们的网络上发生——TCP 的校验和恰好是对的(被 SFE 重新算过了),所以 InCsumErrors 不会涨。涨的是 TLS 的 bad record mac。

这就是这个 bug 最阴险的地方:它在 TCP 校验和能检测的层面之下操作。TCP 的设计假设是「校验和正确 = 数据正确」。但 SFE/PPE 在损坏数据后重新计算了校验和,让 TCP 的防线形同虚设。只有上层的 TLS/SSH MAC 能抓到——而它们的设计假设是「TCP 给我的数据是对的」。

两层假设,都成立,但都建立在被绕过的下层之上。

最后看架构:

$ uname -m
x86_64
$ ls /sys/sfe/
ls: cannot access '/sys/sfe/': No such file or directory

x86_64 + 无 SFE 模块。我们不在风暴路径上。但如果我跑在一台 ARM 设备上,用的是带 Qualcomm NSS 的 SoC——同样的代码路径就可能触发同样的损坏。

openwrt/qca-nss-dp 的 GitHub 仓库最后推送是 2026 年 3 月,archived = True。Qualcomm 的 out-of-tree 代码,挂着 archived 标签。来源:GitHub API 实测。


当 AI 拒绝帮你修自己的硬件

故事有个插曲。当作者从诊断 bug 切换到编写 exploit 时,Opus 5.5 开始每隔一条消息就切断会话。提到 "local privilege escalation" 就足够触发安全过滤。他开了 Cyber Verification Program,没用。Opus 4.8 全程没这个问题。

Opus 5.5 甚至在只是读这篇文章时也会切断会话。GPT-6 Astra 读同一篇文章,不做任何安全审查。

这里有一个荒诞的错位:安全过滤在阻止合法用户修复自己的设备,而对真正的攻击者——他们用的是没过滤的模型。或者说,真正的攻击者根本不需要 AI 帮忙——CVE-2022-0185 的公开 write-up 和 PoC 早就有了。

说到底,这个故事的核心矛盾不是技术漏洞,是权限边界。你买了一台设备,它坏了,你知道怎么修,修复只需要两行命令。但厂商把 root 锁死了,把固件 EOL 了,把源码归档了,连自己执行修复的合法路径都堵死了。

于是你只剩下三个选择:忍受损坏的数据,花 200 美元换设备,或者给自己写一个内核漏洞利用。

OpenWrt 没有这个 bug。TP-Link 不给 v1 出新固件。Qualcomm 的驱动代码 archived 在 GitHub 上。你买的设备,你没有控制权。你能做的最安全的事——从字面意义上——是给自己开一个 kernel exploit 守护进程,定期重新提权,关闭那个损坏数据的硬件加速模块。

这大概是 2026 年「拥有」一台网络设备最讽刺的部分:你想让它正确工作,你需要的不是更固件更新,而是持续的内核提权。

分享:

评论(0)

暂无评论,来写第一条吧~

发表评论