Problema
En entornos macOS donde se usan herramientas de JavaScript (npm, node) es posible observar la aparición de procesos node -e que ejecutan código ofuscado cada vez que se lanza cualquier script de npm. El proceso escribe un archivo temporal bajo /Library/Preferences/Logging/, lo elimina inmediatamente y mantiene el código en memoria mediante un descriptor de archivo colgante. La carga realiza conexiones salientes a direcciones IP sospechosas y persiste después de reinstalar node o limpiar los módulos del proyecto. Dado que el artefacto nunca queda en disco de forma permanente, los escáneres tradicionales basados en firmas (por ejemplo, Malwarebytes) no lo detectan y las herramientas de diagnóstico como dtrace o gcore son bloqueadas por System Integrity Protection (SIP). El síntoma típico es la aparición de un proceso node -e con una cadena de rotación y decodificación atob() cada vez que se ejecuta npm run ….
Causa
Este comportamiento suele derivarse de una cadena de compromiso que aprovecha:
-
Persistencia en memoria mediante archivo borrado – El payload crea un archivo en una ruta de sistema (p.ej.,
/Library/Preferences/Logging/.plist-cache.*), lo abre, lo mapea y lo elimina. El descriptor sigue vivo, permitiendo que el código siga ejecutándose aunque el archivo desaparezca del árbol de archivos. -
Ganchos de npm – npm ejecuta scripts definidos en
package.jsonmediantenode -e. Un atacante que logra inyectar código en el binario denpmo en la variable de entornoNODE_OPTIONSpuede forzar la ejecución de código arbitrario cada vez que cualquier script se lanza, sin necesidad de dependencias del proyecto. -
Evasión de SIP – macOS protege los binarios del sistema y la mayoría de los puntos de inserción con SIP. Sin embargo, la carga se ejecuta como proceso de usuario y no necesita modificar binarios protegidos, por lo que SIP solo bloquea la instrumentación (
dtrace,gcore) pero no la propia ejecución. -
Uso de técnicas de ofuscación – Rotaciones de cadena y decodificación base64 (
atob) dificultan la detección por firmas estáticas. La presencia de marcadores comoglobal['_V']='A9-4584'oglobal['e']='NPM'es una pista de que el código está pensado para ser identificado internamente. -
Redes de comando y control (C2) – Conexiones a puertos 80/443 hacia IPs no pertenecientes a la infraestructura de la organización indican que el payload está enviando datos o recibiendo instrucciones.
Solución
Una estrategia eficaz combina detección en tiempo real, captura de memoria y endurecimiento de la cadena de ejecución de npm. Los pasos recomendados son:
1. Auditar variables de entorno y configuraciones de npm
- Revise
NODE_OPTIONS,NPM_CONFIG_*y cualquier alias de shell que invoquenodeonpm. Un atacante puede inyectar--require <payload>o-e "<code>"mediante estas variables. - Elimine o restablezca valores sospechosos:
unset NODE_OPTIONS
npm config delete prefix
npm config delete script-shell
2. Bloquear la ejecución de código arbitrario en npm
- Use la política de ejecución de npm
--ignore-scriptsen entornos donde no se necesiten scripts post‑install. - En CI/CD, añada
npm ci --ignore-scriptsy habilitenpm auditpara detectar paquetes comprometidos.
3. Captura de la carga en memoria
Dado que SIP impide dtrace y gcore en su modo normal, hay dos rutas:
a) Desactivar temporalmente SIP (solo en máquinas de análisis)
- Reinicie en modo de recuperación (
Cmd+R). - Abra Terminal y ejecute
csrutil disable. - Reinicie y capture la memoria con
gcoreo trace condtrace. - Vuelva a habilitar SIP (
csrutil enable) después del análisis.
b) Utilizar herramientas que operan bajo el contexto de usuario
- osquery: permite consultar procesos y mapeos de archivo sin requerir privilegios de kernel.
osqueryi "SELECT pid, path, fd, type FROM file WHERE path LIKE '/Library/Preferences/Logging/.plist-cache%';"
- lldb con
process attach --pid <pid>para volcar la región de texto del procesonode -e. El volcado se guarda en un archivo que luego puede ser desensamblado conotool -tV.
4. Detectar patrones de red
- Configure el firewall (pf o Little Snitch) para bloquear conexiones salientes a IPs no autorizadas.
- Use
nettopolsof -iTCP -sTCP:ESTABLISHED -nPpara observar conexiones establecidas por procesosnode.
5. Remediar la persistencia
- Elimine cualquier archivo residual bajo
/Library/Preferences/Logging/aunque esté marcado como borrado. - Revise los permisos de esa carpeta; asegúrese de que solo root pueda escribir allí.
- Reinicie la máquina en modo seguro para forzar la eliminación de procesos colgantes y volver a cargar los launch daemons limpios.
6. Refuerzo a largo plazo
- Habilite Gatekeeper y XProtect con la política
sudo spctl --master-enable. - Aplique Endpoint Detection and Response (EDR) que incluya análisis de memoria (por ejemplo, Elastic Endpoint Security o CrowdStrike) para capturar payloads fileless.
- Mantenga node y npm actualizados; las versiones recientes incluyen mitigaciones contra la inyección de
NODE_OPTIONSdesde variables de entorno no confiables.
Cuándo aplicar esta solución
- Síntomas: aparición de procesos
node -econ código ofuscado al ejecutar cualquiernpm run; conexiones salientes a IPs desconocidas; ausencia de detecciones en escáneres on‑disk. - Entornos: máquinas de desarrollo macOS que usan npm/node; servidores CI/CD basados en macOS; estaciones de trabajo que ejecutan scripts de automatización.
- No aplica: si la carga se ejecuta como launch daemon persistente o si el binario de node está firmado y no se observa comportamiento anómalo en la línea de comandos.
Código
# 1. Listar procesos node -e sospechosos
pgrep -fl "node -e"
# 2. Obtener descriptor de archivo mapeado (ejemplo pid=1234)
lsof -p 1234 | grep txt
# 3. Volcar memoria con lldb (requiere permisos de usuario)
lldb -p 1234 -b -o "process save-core /tmp/node_core_1234.core" -o "quit"
# 4. Buscar marcadores en la imagen volcada
strings /tmp/node_core_1234.core | grep -E "A9-4584|global\['e'\]='NPM'"
# 5. Bloquear IPs sospechosas con pf
echo "block drop out quick to 166.88.134.62" | sudo pfctl -f -
echo "block drop out quick to 23.27.13.43" | sudo pfctl -f -
Verificación
-
Confirmar ausencia de procesos sospechosos
pgrep -fl "node -e"No debe devolver resultados después de reiniciar la máquina o de haber eliminado la variable
NODE_OPTIONS. -
Comprobar que no existen archivos bajo la ruta objetivo
ls -l /Library/Preferences/Logging/.plist-cache*La lista debe estar vacía.
-
Validar que el firewall bloquea las IPs
sudo pfctl -s rules | grep -E "166\.88\.134\.62|23\.27\.13\.43"Cada regla debe aparecer como
block drop. -
Re‑ejecutar un script npm y observar que no se crea nuevo proceso
npm run hellopgrep -fl "node -e"debe seguir sin resultados.
Notas adicionales
- Desactivar SIP solo debe hacerse en máquinas de análisis aisladas; nunca en entornos de producción.
- Si la organización ya cuenta con un EDR, configure una regla de detección basada en la cadena
global['_V']='A9-4584'o en la ruta de escritura/Library/Preferences/Logging/. - En CI/CD, añada una etapa de verificación que ejecute
npm config get script-shelly falle si el valor no es el esperado (por defecto,sh). - Mantener una lista blanca de IPs autorizadas para tráfico saliente desde procesos
nodereduce falsos positivos al bloquear C2.