
Notas de ingeniería inversa y un PoC autocontenido para la carrera de caché de acceso del cliente NFS de macOS (CVE-2026-43687), con diff de desensamblado del kext y captura de la carrera con dtrace.
Ingeniería inversa independiente de la condición de carrera de la caché de acceso del cliente NFS de macOS (CVE-2026-43687), además de un PoC funcional que desencadena la carrera y la captura en vivo con dtrace.
El fallo es una lectura no sincronizada del puntero de la caché de acceso de nfsnode en _nfs_vnop_access. Un servidor NFSv3 malicioso puede provocar que la caché de acceso se reasigne mientras otro hilo la está leyendo, produciendo una divulgación de memoria del kernel que un servidor hostil puede influir. macOS 26.7 corrige esto insertando un lck_rw_t en nfsnode+0x158 y tomándolo en modo compartido alrededor de la lectura de la caché.
| Campo | Valor |
|---|---|
| CVE | CVE-2026-43687 |
| Componente | com.apple.filesystems.nfs (_nfs_vnop_access) |
| Afecta a | macOS Tahoe 26.6 y anteriores, iOS 26.x y anteriores |
| Corregido en | macOS Tahoe 26.7, macOS Golden Gate 27, iOS 26.7, iOS 27 |
| Impacto según el aviso | "Conectarse a un servidor NFS malicioso puede divulgar memoria del kernel." |
| CVSS v3.1 | 6.5 (Medio) — AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N |
| Reportado por | R4mbb de KRsecurity y Peter Malone (según el aviso de Apple) |
El aviso de Apple para CVE-2026-43687 documenta el impacto y la versión del parche. No documenta el mecanismo técnico:
nfsnode sufre la carreraaccess(2) en lugar de stat(2)No se encontró ningún análisis técnico público en el momento de escribir esto. Este repositorio llena ese vacío con un análisis de ingeniería inversa independiente del kext de NFS entre 26.6 y 26.7, y un PoC funcional que reproduce la carrera en un objetivo en vivo.
Esto no es una reclamación de descubrimiento. El CVE fue reportado por R4mbb y Peter Malone y parcheado por Apple. La contribución aquí es el análisis técnico y la reproducción.
_nfs_vnop_access en el cliente NFS lee nfsnode+0x158 — el puntero al array de caché de acceso por UID — dos veces dentro de una sola llamada, sin mantener ningún lock:
; 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
El escritor, _nfs_nget, reasigna el array con kalloc_data y almacena el resultado en +0x158 cada vez que el cliente ve un nuevo UID del servidor:
fffffe000b52100c bl 0xfffffe000b6a1dd8 ; kalloc
fffffe000b521010 str x0, [x22, #0x158] ; cache ptr
fffffe000b521018 str w20, [x22, #0x160] ; cache count
Si otro hilo alcanza _nfs_nget en el mismo nfsnode entre las dos cargas del lector, la segunda carga devuelve el nuevo puntero mientras que la primera lectura — ya usada para calcular un desplazamiento — se basó en el antiguo. La desreferencia posterior lee del heap del kernel liberado.
La corrección de 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
y en _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
Cambio en el diseño de la estructura:
_nfs_nget y _nfs_vnop_access entre los kexts de NFS 26.6 y 26.7 — ver docs/PATCH_DIFF.mdnfsnode+0x158 en 26.6lck_rw_t insertado en +0x158, puntero de caché movido a +0x168, lck_rw_lock_shared tomado alrededor de la lecturaaccess(2) entra en _nfs_vnop_access; stat(2) pasa por _nfs_getattr y nunca alcanza la función vulnerableVer docs/ANALYSIS.md para el análisis completo y docs/ARTIFACTS.md para direcciones y muestras de logs.
poc.sh — un PoC de un solo archivo y autocontenido:
nfsd de Apple para liberar el puerto 2049127.0.0.1 que rota el UID reportado en cada respuestanoacaccess(2) mediante test -r / test -wnfs_vnop_access, registrando cualquier llamada donde nfsnode+0x158 cambia a mitad de llamada_nfs_vnop_access observando el cambio del array durante una sola llamadaLa carrera es la precondición para la divulgación. Para convertir la carrera en una fuga real, un atacante necesitaría observar al lector usando el puntero obsoleto y propagar esos bytes a algún lugar donde pueda leerlos. En arm64e, el valor en nfsnode+0x158 está firmado con PAC y la clave por arranque no está disponible desde el espacio de usuario, por lo que dtrace por sí solo puede observar la carrera pero no puede decodificar el puntero. Ver la sección "Paths tested and ruled out" en docs/ANALYSIS.md.
El impacto demostrado es la carrera en sí misma — la ventana exacta que cierra la corrección del RW-lock de 26.7.
dtrace disponible (puede requerir ajuste de SIP en algunas instalaciones)python3, dscl, mountEl PoC se ejecuta completamente en el objetivo. El servidor NFS malicioso se enlaza a 127.0.0.1 y el montaje es sobre loopback. Esto mantiene el PoC autocontenido y reproducible sin configuración de red.
chmod +x poc.sh
sudo ./poc.sh
Ajuste opcional mediante variables de entorno:
sudo HAMMER_COUNT=30 RUN_SECONDS=600 ./poc.sh
HAMMER_COUNT establece el número de hilos de martilleo por usuario (usa las cuentas nfsuserNNN que existan, creándolas según sea necesario). RUN_SECONDS establece la ventana de 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
===============================================
Cada línea RACE es una llamada de nfs_vnop_access donde el puntero de la caché de acceso cambió a mitad de llamada.
En un sistema parcheado (26.7 / 27), la misma carga de trabajo produce cero eventos RACE. Ver docs/PATCH_DIFF.md para la comparación del desensamblado.
Dos errores en el servidor del PoC se corrigieron durante el desarrollo y se documentan aquí para que otros que construyan herramientas similares no se topen con ellos:
ACCESS3resok requiere post_op_attr, no fattr3. La RFC 1813 define la respuesta como post_op_attr obj_attributes; uint32 access;. post_op_attr incluye un prefijo bool antes del fattr3. Omitir ese bool hace que la respuesta sea 4 bytes más corta; el cliente la rechaza silenciosamente y el montaje nunca llega a ser utilizable.
LOOKUP para nombres AppleDouble (._*) debe devolver NFS3ERR_NOENT (2), no NFS3ERR_STALE (70). macOS sondea los archivos auxiliares ._<name> durante la resolución normal de rutas. Devolver STALE envenena el montaje.
Ambos están documentados en docs/ANALYSIS.md.
Los valores out en la salida de RACE tienen un patrón constante en los 48 bits bajos (...7e0023297878) entre diferentes nfsnodes, variando solo en los 16 bits altos. Eso confirma que el campo está firmado con PAC u ofuscado, no es un puntero crudo del kernel. Puedes observar la transición de estado (NULL → poblado) desde el espacio de usuario, pero no puedes decodificar ni desreferenciar el puntero sin la clave PAC por arranque del kernel.
Por la misma razón, la simbolización contra el KDK no es útil para estos valores — no son direcciones relativas al texto.
Este repositorio se proporciona únicamente para investigación y educación en seguridad defensiva.
MIT. Ver LICENSE.
| Desplazamiento | macOS 26.6 | macOS 26.7 |
|---|
+0x158 | puntero al array de caché | lck_rw_t |
+0x160 | contador de caché | (parte del lock) |
+0x168 | (otro) | puntero al array de caché |
+0x170 | (otro) | contador de caché |