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:

  1. 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.

  2. Ganchos de npm – npm ejecuta scripts definidos en package.json mediante node -e. Un atacante que logra inyectar código en el binario de npm o en la variable de entorno NODE_OPTIONS puede forzar la ejecución de código arbitrario cada vez que cualquier script se lanza, sin necesidad de dependencias del proyecto.

  3. 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.

  4. 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 como global['_V']='A9-4584' o global['e']='NPM' es una pista de que el código está pensado para ser identificado internamente.

  5. 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 invoque node o npm. 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-scripts en entornos donde no se necesiten scripts post‑install.
  • En CI/CD, añada npm ci --ignore-scripts y habilite npm audit para 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)

  1. Reinicie en modo de recuperación (Cmd+R).
  2. Abra Terminal y ejecute csrutil disable.
  3. Reinicie y capture la memoria con gcore o trace con dtrace.
  4. 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 proceso node -e. El volcado se guarda en un archivo que luego puede ser desensamblado con otool -tV.

4. Detectar patrones de red

  • Configure el firewall (pf o Little Snitch) para bloquear conexiones salientes a IPs no autorizadas.
  • Use nettop o lsof -iTCP -sTCP:ESTABLISHED -nP para observar conexiones establecidas por procesos node.

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_OPTIONS desde variables de entorno no confiables.

Cuándo aplicar esta solución

  • Síntomas: aparición de procesos node -e con código ofuscado al ejecutar cualquier npm 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

  1. 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.

  2. Comprobar que no existen archivos bajo la ruta objetivo

    ls -l /Library/Preferences/Logging/.plist-cache*
    

    La lista debe estar vacía.

  3. 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.

  4. Re‑ejecutar un script npm y observar que no se crea nuevo proceso

    npm run hello
    

    pgrep -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-shell y falle si el valor no es el esperado (por defecto, sh).
  • Mantener una lista blanca de IPs autorizadas para tráfico saliente desde procesos node reduce falsos positivos al bloquear C2.