对 macOS NFS 客户端访问缓存竞态(CVE-2026-43687)的独立逆向工程,以及一个可用的 PoC,能够触发该竞态并用 dtrace 实时捕获。
该漏洞是 _nfs_vnop_access 中对 nfsnode 访问缓存指针的未同步读取。恶意 NFSv3 服务器可以驱动访问缓存在另一个线程读取它时重新分配,从而产生内核内存泄露,且恶意服务器可以影响泄露内容。macOS 26.7 通过在 nfsnode+0x158 处插入一个 lck_rw_t 并在缓存读取周围以共享方式持有该锁来修复此问题。
| 字段 | 值 |
|---|---|
| CVE | CVE-2026-43687 |
| 组件 | com.apple.filesystems.nfs (_nfs_vnop_access) |
| 受影响版本 | macOS Tahoe 26.6 及更早版本,iOS 26.x 及更早版本 |
| 修复版本 | macOS Tahoe 26.7、macOS Golden Gate 27、iOS 26.7、iOS 27 |
| 公告影响 | “连接到恶意 NFS 服务器可能会泄露内核内存。” |
| CVSS v3.1 | 6.5(中危)— AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N |
| 报告者 | KRsecurity 的 R4mbb 和 Peter Malone(根据 Apple 的公告) |
Apple 针对 CVE-2026-43687 的公告记录了影响和修复版本。它没有记录技术机制:
nfsnode 中哪个字段存在竞态access(2) 而不是 stat(2)在撰写本文时未找到公开的技术文章。本仓库通过独立的逆向工程分析填补了这一空白,分析了 26.6 与 26.7 之间的 NFS kext,并提供了一个可在实时目标上复现该竞态的可用 PoC。
这不是发现声明。 该 CVE 由 R4mbb 和 Peter Malone 报告,并由 Apple 修复。此处的贡献是技术分析和复现。
NFS 客户端中的 _nfs_vnop_access 在单次调用内读取 nfsnode+0x158 两次——该指针指向按 UID 的访问缓存数组——且不持有任何锁:
; macOS 26.6, com.apple.filesystems.nfs
fffffe000b52def4 ldr w8, [x20, #0x160] ; count
fffffe000b52def8 cmp w23, w8
fffffe000b52defc b.ge ...
fffffe000b52df00 ldr x8, [x20, #0x158] ; cache ptr (read #1)
...
fffffe000b52df98 ldr x8, [x20, #0x158] ; cache ptr (read #2)
fffffe000b52dfb0 ldr w21, [x9] ; dereference
写入方 _nfs_nget 会在客户端看到来自服务器的新 UID 时,用 kalloc_data 重新分配数组并将结果存储到 +0x158:
fffffe000b52100c bl 0xfffffe000b6a1dd8 ; kalloc
fffffe000b521010 str x0, [x22, #0x158] ; cache ptr
fffffe000b521018 str w20, [x22, #0x160] ; cache count
如果另一个线程在读取方的两次加载之间到达同一 nfsnode 上的 _nfs_nget,第二次加载会返回新指针,而第一次读取——已经用于计算偏移——是基于旧指针的。随后的解引用会从已释放的内核堆中读取。
26.7 的修复:
fffffe000b9e3064 str x0, [x22, #0x168] ; cache ptr moved
fffffe000b9e306c str w28, [x22, #0x170] ; cache count moved
fffffe000b9e307c add x0, x22, #0x158 ; lock slot
fffffe000b9e3084 bl _lck_rw_init ; init RW lock
以及在 _nfs_vnop_access 中:
fffffe000b9f0200 add x0, x20, #0x158
fffffe000b9f0204 bl _lck_rw_lock_shared ; take the lock
fffffe000b9f0208 ldr x9, [x20, #0x168] ; cache pointer
...
fffffe000b9f0230 bl _lck_rw_unlock_shared ; release the lock
结构体布局变化:
_nfs_nget 和 _nfs_vnop_access 的反汇编差异——见
docs/PATCH_DIFF.mdnfsnode+0x158+0x158 处插入 lck_rw_t,缓存指针移至 +0x168,在读取周围获取 lck_rw_lock_sharedaccess(2) 进入 _nfs_vnop_access;stat(2) 经过 _nfs_getattr,永远不会到达易受攻击的函数完整文章见 docs/ANALYSIS.md,地址和日志样本见 docs/ARTIFACTS.md。
poc.sh — 单文件、自包含的 PoC:
nfsd 以释放端口 2049127.0.0.1 上启动一个恶意 NFSv3 服务器,在每次回复中轮换所报告的 UIDnoac 将导出挂载到新的挂载点test -r / test -w 发起 access(2)nfs_vnop_access,记录任何 nfsnode+0x158 在调用中途发生变化的调用_nfs_vnop_access 中的未同步读取在单次调用期间观察到数组发生变化该竞态是泄露的前提条件。要将竞态转化为实际泄露,攻击者需要观察到读取方使用了陈旧指针,并将这些字节传播到他们可以读取的地方。在 arm64e 上,nfsnode+0x158 处的值是 PAC 签名的,且每次启动的密钥无法从用户态获得,因此仅靠 dtrace 可以观察到竞态,但无法解码指针。参见 docs/ANALYSIS.md 中的“已测试并排除的路径”部分。
所演示的影响是竞态本身——正是 26.7 的 RW 锁修复所关闭的那个窗口。
dtrace 可用(某些安装可能需要调整 SIP)python3、dscl、mountPoC 完全在目标上运行。恶意 NFS 服务器绑定到 127.0.0.1,挂载通过回环进行。这使 PoC 保持自包含且无需网络配置即可复现。
chmod +x poc.sh
sudo ./poc.sh
可通过环境变量进行可选调整:
sudo HAMMER_COUNT=30 RUN_SECONDS=600 ./poc.sh
HAMMER_COUNT 设置每个用户 hammer 线程的数量(使用现有的 nfsuserNNN 账户,并按需创建)。RUN_SECONDS 设置 dtrace 窗口。
[*] ensuring nfsuser accounts exist (UID 201..240)
nfsuser accounts available: 12
[*] starting evil NFS server on 127.0.0.1:2049
[*] mounting /Users/Shared/nfs_test (with noac)
mount check: hello.txt statable
[*] spawning up to 12 per-user threads + 1 root loop
started 12 user threads + 1 root loop
[*] running dtrace for 180s — looking for RACE lines
[*] stopping dtrace
[*] cleaning up
=================== SUMMARY ===================
RACE events caught: 16
Vulnerable interleaving observed — sample:
RACE nd=fffffe5f53cbb940 in=(0,0) out=(b0967e0023297878,0)
RACE nd=fffffe5f534db940 in=(0,0) out=(84dd7e0023297878,0)
RACE nd=fffffe5f53cbb940 in=(0,0) out=(c9a37e0023297878,0)
RACE nd=fffffe5f538ab940 in=(0,0) out=(1867e0023297878,0)
RACE nd=fffffe5f539db940 in=(0,0) out=(149bfe0023297878,0)
CVE-2026-43687 trigger SUCCESSFUL
===============================================
每一行 RACE 都是一次 nfs_vnop_access 调用,其中访问缓存指针在调用中途发生了变化。
在已修补的系统(26.7 / 27)上,相同的工作负载产生零个 RACE 事件。反汇编对比见 docs/PATCH_DIFF.md。
PoC 服务器中的两个 bug 在开发过程中被修复,并记录在此,以便构建类似工具的人不会遇到它们:
ACCESS3resok 需要 post_op_attr,而不是 fattr3。 RFC 1813 将回复定义为 post_op_attr obj_attributes; uint32 access;。post_op_attr 在 fattr3 之前包含一个 bool 前缀。省略该 bool 会使回复短 4 字节;客户端会静默拒绝它,挂载永远无法使用。
对 AppleDouble 名称(._*)的 LOOKUP 必须返回 NFS3ERR_NOENT (2),而不是 NFS3ERR_STALE (70)。 macOS 在正常路径解析期间会探测 ._<name> 附属文件。返回 STALE 会毒化挂载。
两者都记录在 docs/ANALYSIS.md 中。
RACE 输出中的 out 值在不同 nfsnode 之间具有恒定的低 48 位模式(...7e0023297878),仅在高 16 位变化。这证实该字段是 PAC 签名或混淆的,而不是原始内核指针。你可以从用户态观察到状态转换(NULL → 已填充),但如果没有内核每次启动的 PAC 密钥,就无法解码或解引用该指针。
出于同样的原因,针对 KDK 的符号化对这些值没有用——它们不是文本相对地址。
本仓库仅供防御性安全研究和教育使用。
MIT。见 LICENSE。
| 偏移 | macOS 26.6 | macOS 26.7 |
|---|
+0x158 | 缓存数组指针 | lck_rw_t |
+0x160 | 缓存计数 | (锁的一部分) |
+0x168 | (其他) | 缓存数组指针 |
+0x170 | (其他) | 缓存计数 |