Panorama: la única técnica que se queda quieta#

Los tres capítulos anteriores persiguieron cosas que se mueven. El C2 de 5.7 se caza mientras habla; el implante de 5.8 se analiza mientras se ejecuta; la exfiltración de 5.9 deja una ventana de minutos y después ya ocurrió. En todos, el defensor llega tarde por diseño: la señal es transitoria y su vida útil termina cuando el adversario apaga el proceso.

La persistencia es la excepción, y conviene entender por qué antes de mirar una sola herramienta. La conclusión de 3.14 era que toda persistencia consiste en suscribir código propio a un disparador de ejecución que el sistema operativo ya honra. Esa definición trae consigo una consecuencia defensiva que no se puede evitar sin dejar de persistir: para que el sistema honre el disparador, el disparador tiene que estar registrado en algún lugar que el sistema consulte. Una unidad de systemd que no está en disco no arranca. Una clave SSH que no está en authorized_keys no autentica. Un módulo de kernel que no se carga no engancha nada. El artefacto sobrevive al reinicio porque tiene que sobrevivir al reinicio, y eso lo vuelve un objeto quieto, presente y —la propiedad que importa— enumerable.

De ahí la tesis del capítulo, que tiene dos mitades y una advertencia. La primera: los disparadores que Linux honra son finitos y están documentados, así que la detección de persistencia no es una persecución sino una auditoría —se compara un conjunto acotado de lugares contra una línea base de lo que debería ejecutarse solo—. La segunda: cuando el adversario baja al kernel, esa regla se invierte, porque las herramientas con las que se hace la enumeración corren sobre el sistema que se está auditando y el sistema comprometido les miente; ahí la única salida es un punto de observación externo. Y la advertencia, que es donde se pierden los programas reales: finito no significa pequeño, y el sistema legítimo escribe en esos mismos lugares todos los días. La dificultad de este capítulo no es saber dónde mirar —eso ya lo enumeró 3.14 desde el lado ofensivo—, sino sostener la línea base que convierte «hay una unidad de systemd» en «hay una unidad de systemd que no estaba ayer».

La telemetría nativa y el hueco que deja#

5.2 estableció la capa de datos del lado Windows y una regla que vale igual acá: no se detecta lo que no se registra. El equivalente Linux es más viejo, más simple y considerablemente más incompleto.

La bitácora de autenticación —/var/log/auth.log en familias Debian, /var/log/secure en las derivadas de Red Hat— documenta lo que en un compromiso importa: aperturas de sesión, uso de sudo, cambios de credenciales, altas de cuentas y de grupos. En una intrusión real esta bitácora suele ser la primera evidencia legible: una apertura de sesión de tipo CRON para el usuario root a una hora en la que nadie programó nada, seguida de un sudo apt-get install de un paquete que ningún administrador pidió, es exactamente la firma de un adversario que ya tiene privilegio y está montando su infraestructura en el host. El journal de systemd agrega algo que el texto plano no tiene: campos estructurados (_PID, _COMM, _UID, _SYSTEMD_UNIT) que permiten consultar sin inventar expresiones regulares.

Esa diferencia con Windows es más importante de lo que parece a la hora de operar. El modelo de Event Logs de 5.2 entrega eventos numerados y con esquema: 4688 significa lo mismo en todas las máquinas de la organización, y una regla escrita una vez se aplica en todas. En Linux, buena parte de la telemetría llega como texto semiestructurado producido por cada demonio a su gusto, con formatos que cambian entre distribuciones y entre versiones del mismo paquete. La consecuencia práctica es que en un despliegue Linux la mitad del trabajo de detección es la normalización, y ese trabajo es invisible en cualquier hoja de producto: una regla de caza que funciona perfecto en el laboratorio falla en producción porque la flota tiene tres distribuciones y dos formatos de sshd. Es el mismo argumento que cerraba el capítulo fuente sobre análisis de registros —conocer el formato crudo y no depender del panel—, con una carga operativa mayor.

Pero el hueco grande es otro, y es el que define casi todo lo que sigue. Linux no audita la creación de procesos por defecto. No existe un equivalente nativo y activo de 4688: sin configuración adicional, un host Linux no puede responder «qué se ejecutó ayer a las tres de la mañana, con qué argumentos y quién era el padre». Esa es precisamente la pregunta que resuelve la mayoría de las investigaciones de persistencia, porque el artefacto en disco dice qué se plantó y solo la telemetría de ejecución dice que efectivamente disparó y qué hizo entonces. Un host Linux con la configuración de fábrica es, para los fines de este capítulo, un host mayormente ciego. Todo lo que viene —auditd, FIM, eBPF— existe para tapar ese hueco.

Los registros son archivos de texto y el adversario que persiste ya tiene privilegio. El mismo root que instala la unidad de systemd puede editar /var/log/auth.log con un editor de texto: no hay que evadir nada, solo borrar líneas. Cualquier programa de detección de persistencia en Linux que se apoye únicamente en logs locales está confiando su evidencia a la custodia del atacante. Las dos contramedidas son baratas y complementarias: reenvío inmediato a un colector remoto —donde el adversario tendría que comprometer un segundo sistema para alterar lo ya enviado— y el sellado criptográfico del journal (Forward Secure Sealing), que no impide el borrado pero lo vuelve detectable. Es el mismo razonamiento que el 1102 de 5.2 y el mismo que lleva a exigir audit logs remotos y de solo anexado en el plano de control de contenedores.

La línea base de auto-ejecución#

Si el artefacto es enumerable, el instrumento primario es un inventario. Enumerar el conjunto de auto-ejecución significa recorrer todas las familias de disparador que 3.14 describió y volcarlas a un estado comparable: unidades y temporizadores de systemd, la totalidad de los crontab —los de usuario, /etc/crontab, /etc/cron.d/ y los directorios cron.*—, los authorized_keys de cada cuenta, los fragmentos de /etc/profile.d/ y los rc de shell, update-motd.d, las reglas de udev y los hooks del gestor de paquetes.

systemctl list-unit-files --state=enabled --no-pager systemctl list-timers --all --no-pager for u in $(cut -d: -f1 /etc/passwd); do crontab -l -u "$u" 2>/dev/null | sed "s/^/[$u] /"; done find /etc/cron.d /etc/cron.hourly /etc/cron.daily /etc/profile.d /etc/update-motd.d -type f -printf '%T+ %p\n' find /home /root -name authorized_keys -printf '%T+ %p\n' -exec cat {} \;

Lo que hace útil a ese inventario no es ejecutarlo durante un incidente —cuando ya hay poco con qué comparar— sino haberlo ejecutado antes, sobre un host que se sabe limpio, y haberlo versionado. La detección no es la lista: es el diff contra la lista anterior. Una unidad nueva en /etc/systemd/system/ no es sospechosa por su nombre —el adversario competente la llamará algo como dbus-update.service, justamente porque el nombre es lo único que controla gratis—, sino por no haber estado ahí la semana pasada en un host cuyo rol no cambió.

Esto le da a la immutable infrastructure un valor defensivo que suele venderse como una ventaja de despliegue. Cuando los hosts se reconstruyen desde una imagen en lugar de parchearse en vivo, la línea base no hay que mantenerla: es la imagen, y cualquier divergencia respecto de ella es, por definición, algo que alguien introdujo después del despliegue. En una flota inmutable la pregunta «¿esta unidad de systemd debería existir?» tiene respuesta automática. En una flota de servidores administrados a mano durante ocho años, esa misma pregunta requiere encontrar a la persona que administraba ese servicio en 2019.

Queda una trampa que ya apareció en 5.6 a propósito del baselining de UEBA y que acá reaparece idéntica: una línea base capturada sobre un host ya comprometido hereda el compromiso. La unidad maliciosa queda incorporada como normal y desaparece de todos los diff futuros. De ahí que la línea base de referencia deba venir del artefacto de construcción —la imagen, la receta de configuración, el manifiesto del paquete— y no de la observación del sistema en producción. El sistema en producción solo puede decir en qué estado está, nunca en qué estado debería estar.

Auditoría de escritura y de carga: auditd#

auditd es la respuesta directa al hueco de la sección anterior, y su valor se reparte en tres capacidades distintas que conviene no mezclar.

La primera son las reglas de vigilancia sobre rutas (-w), que registran quién tocó un archivo y con qué proceso. Aplicadas sobre el conjunto de disparo —/etc/systemd/system/, /etc/cron*, /etc/pam.d/, /etc/ld.so.preload, los authorized_keys— convierten cada plantación en un evento con atribución, no en un cambio que alguien notará en el próximo inventario. La diferencia entre el FIM y auditd acá es precisa y vale tenerla clara: el FIM dice que el archivo cambió, auditd dice quién lo cambió, y en una investigación esa segunda mitad es la que se conecta con el resto de la cadena.

La segunda es la auditoría de ejecución (execve), que es lo que finalmente da a Linux algo comparable al 4688 de 5.2: línea de comandos completa y relación padre-hijo. Con eso se recuperan las correlaciones que en Windows sostienen la detección de comportamiento —un cron que engendra un bash que abre un socket saliente, un sshd que engendra un intérprete fuera de todo horario— y que sin ella son simplemente invisibles.

La tercera es específica y de altísima fidelidad: la regla sobre init_module y finit_module, las llamadas que cargan un módulo de kernel. Su valor está en el tiempo: captura el insmod en el instante en que ocurre, que es el único instante en que el rootkit LKM todavía no se ha ocultado. Un módulo que se desenlaza de lsmod un segundo después de cargarse ya no aparecerá en ningún inventario del host, pero la llamada que lo cargó quedó registrada. Es el equivalente Linux de aquella observación de 5.8 sobre la ofuscación: hay un punto del proceso en que el adversario no puede esconderse todavía, y ese punto es donde hay que poner el sensor.

auditctl -w /etc/systemd/system/ -p wa -k persist_systemd auditctl -w /etc/ld.so.preload -p wa -k persist_preload auditctl -w /root/.ssh/authorized_keys -p wa -k persist_sshkey auditctl -a always,exit -F arch=b64 -S init_module -S finit_module -k kmod_load ausearch -k persist_preload -i

Los límites conviene decirlos con la misma franqueza con la que 5.8 midió sus sensores. auditd cuesta: auditar execve sin filtrar en un servidor ocupado genera un volumen que hay que dimensionar en serio y puede degradar el rendimiento, de modo que el ajuste de reglas es un proyecto y no una casilla. Sus reglas son sintácticamente frágiles y silenciosamente específicas de la arquitectura —una regla escrita solo para b64 no ve nada de lo que ocurra en un binario de 32 bits—. Su configuración es a su vez un objetivo: deshabilitar o recortar el subsistema de auditoría es una técnica catalogada, y el cese repentino de eventos de auditd es en sí mismo una alerta que hay que construir explícitamente, porque un sensor apagado no genera el evento que avisaría de su apagado. Y hay un límite estructural que se cobra en el capítulo siguiente: auditd no entiende los espacios de nombres; en un host que corre contenedores, sus eventos son difíciles de atribuir a la carga de trabajo correcta, y esa es una de las razones por las que la detección en contenedores necesita otra clase de sensor —lo desarrolla 5.11—.

Integridad de archivos y la línea base que el sistema ya trae#

El FIM cubre la otra mitad: vigilar por cambios los directorios de disparo, con AIDE o con el módulo equivalente del EDR. Su regla de oro es que la base de datos de referencia no puede vivir en el host que vigila, porque un adversario con root que encuentra la base la regenera y el FIM pasa a certificar el estado comprometido como correcto. Base fuera del host, o firmada, o ambas. Su segundo parámetro es la frecuencia: un FIM que corre una vez por día define una ventana de un día en la que la plantación existe y nadie la ve. El FIM detecta estados, no actos; por eso complementa a auditd en lugar de reemplazarlo.

Dentro del conjunto vigilado hay una entrada que merece nombre propio y que ya se anunció en 3.14: la aparición de /etc/ld.so.preload. Es probablemente el indicador de más alta fidelidad de todo este capítulo, y por una razón puramente estadística: en un sistema legítimo ese archivo casi nunca existe. No hay que discriminar contenido bueno de contenido malo ni mantener listas de excepciones; la existencia misma del archivo es la señal, y su tasa de falsos positivos en una flota normal es cercana a cero. Es el tipo de regla que 5.1 llamaba de alta fidelidad: barata de escribir, imposible de ignorar cuando dispara.

Y hay una fuente de línea base que casi nadie explota y que el sistema regala. En una distribución empaquetada, el gestor de paquetes ya guarda el hash esperado de cada archivo que instaló. Verificar el sistema contra sus propios paquetes —rpm -Va, debsums— produce en minutos la lista de binarios y archivos de configuración que difieren de lo que el proveedor publicó. Contra la familia de persistencia que subvierte la autenticación, esa comprobación es directamente el control adecuado: un pam_unix.so parcheado para registrar credenciales, o un sshd troyanizado, son exactamente binarios del sistema cuyo hash ya no coincide con el del paquete que los instaló. No es un control perfecto —no cubre lo compilado a mano, y un adversario suficientemente cuidadoso puede manipular la base del gestor—, pero es una línea base criptográfica que ya está presente en el host, no cuesta desplegar nada y responde una pregunta que de otro modo obliga a un análisis binario.

Dónde está la señal, disparador por disparador#

Con esos instrumentos, cada familia de persistencia de 3.14 tiene una señal que rinde más que las otras. Lo que sigue no es un catálogo paralelo al ofensivo sino el criterio de priorización.

En systemd, la señal fuerte no es la unidad nueva en abstracto sino el contenido de su ExecStart: un servicio del sistema que ejecuta algo desde /tmp, /dev/shm o un directorio personal, o cuya línea de arranque contiene un intérprete con carga codificada, es anómalo con independencia de cómo se llame la unidad. El Restart=always que el adversario necesita para sobrevivir a que lo maten es a la vez su firma. Los temporizadores merecen atención propia porque son más silenciosos que cron y se revisan menos; systemctl list-timers es una consulta de treinta segundos que casi ningún inventario incluye. Y hay una variante que elude el disco entero: una unidad transitoria creada con systemd-run no deja archivo que el FIM pueda ver —pero systemd la anota igual en el journal, que es dónde hay que buscarla—.

En cron, lo que discrimina es la combinación de dos campos: qué se ejecuta y desde dónde. @reboot es un disparador legítimo y también el favorito para relanzar un implante; una entrada de cron de sistema que invoca un script alojado en el directorio personal de un usuario cruza una frontera de privilegio que casi ninguna tarea legítima necesita cruzar.

Las claves SSH son el caso más difícil de todo el capítulo, y por la misma razón que hacía difícil el pass-the-hash en 5.6 y el replay de token en 5.3: no producen nada anómalo, producen autenticación válida. Una clave añadida a authorized_keys no es malware, es una credencial que funciona; el acceso posterior es indistinguible de administración legítima. La detección tiene que ocurrir en el momento de la escritura —de ahí que authorized_keys esté entre las rutas de auditd— o en la gestión: cuando las claves autorizadas se distribuyen centralmente (AuthorizedKeysCommand) en lugar de vivir en archivos que cada cuenta controla, un archivo local con contenido deja de ser un cambio a evaluar y pasa a ser algo que no debería existir.

En PAM y en el enganche de userland, la señal ya se dijo: verificación contra el paquete para lo primero, existencia de /etc/ld.so.preload para lo segundo, más el rastro en vivo de un proceso que corre con la variable puesta —visible en /proc/PID/environ para quien sepa que hay que mirar ahí—.

Cuando el host miente: verificar desde fuera#

Todo lo anterior comparte un supuesto que hasta ahora no se puso en duda: que las herramientas con las que se audita el host dicen la verdad sobre el host. El rootkit existe precisamente para invalidar ese supuesto, y a partir de ahí el capítulo cambia de método.

Un rootkit de userland que engancha las funciones de la biblioteca estándar hace que ls no liste su archivo y que las utilidades de red no muestren su conexión, porque envuelve las funciones que esas utilidades usan. Su debilidad es que el engaño vive en una capa que se puede saltear: comparar dos vistas del mismo hecho delata la inconsistencia —un archivo que no aparece al listar el directorio pero responde si se lo consulta directamente; un puerto que no figura en la salida de la herramienta habitual pero sí en la tabla que el kernel expone en /proc/net/tcp; procesos ausentes de la lista pero presentes como entradas numéricas de /proc—. Es exactamente la triangulación de 5.5 entre pslist y psscan, aplicada en vivo: dos caminos hacia el mismo dato, uno de los cuales el rootkit olvidó cubrir.

Un rootkit de kernel elimina esa comodidad. Cuando el engaño se implementa en el kernel —enganchando la tabla de llamadas al sistema o las funciones que las sirven—, ambos caminos pasan por el código comprometido y la comparación deja de ser prueba de nada. Un módulo que se desenlaza de la lista de módulos cargados no aparece en lsmod porque lsmod lee esa lista. Sigue habiendo discrepancias explotables —la lista de módulos frente a lo que expone /sys/module, o el comportamiento anómalo ante señales que algunos rootkits didácticos usan como canal de control— pero son fragilidades de implementaciones concretas, no un método general. El método general es aceptar que el host comprometido no es un testigo confiable sobre sí mismo y buscar un punto de observación externo. Hay cuatro, y son complementarios:

  • La memoria. Lo que 5.5 hizo con Windows vale aquí sin cambios de fondo: capturar la RAM y analizarla offline permite comparar la tabla de llamadas al sistema contra sus direcciones esperadas y listar los módulos que el host oculta. El rootkit puede mentirle a las herramientas del host, pero no puede alterar lo que ya quedó en el volcado que se analiza en otra máquina.
  • El disco montado desde fuera. Un sistema arrancado desde medio limpio, o la imagen del disco montada en otro host, ve los archivos tal como están: el rootkit no está ejecutándose y por lo tanto no está mintiendo. Es lento y requiere apagar o clonar, pero es concluyente.
  • eBPF. Falco y Tracee observan las llamadas al sistema desde dentro del kernel, por debajo de la capa donde el rootkit de userland fabrica su engaño: la escritura en /etc/ld.so.preload, la carga de un módulo, un execve que no debería ocurrir. Es el mismo subsistema que arma al atacante —3.14 lo mencionaba como base de una generación de rootkits sin módulo— usado por el defensor. Merece un matiz honesto: eBPF no es inmune por naturaleza. Frente a un rootkit de kernel ya cargado, el sensor eBPF corre en el mismo dominio de confianza que el adversario y puede ser subvertido; el orden de carga importa, y su ventaja decisiva es contra el enganche de userland, contra la ejecución en contenedores y —sobre todo— como sensor que ve el ataque mientras se instala, antes de que exista rootkit alguno.
  • La red. Es el punto externo más barato y el que suele olvidarse: el host puede mentir sobre sus conexiones, pero el conmutador por el que pasan no. Un beacon invisible en el propio host aparece igual en la telemetría de 5.7. Cuando el endpoint deja de ser confiable, el plano de red pasa de complemento a fuente primaria.
Un rootkit de kernel confirmado no se limpia: se reinstala. Erradicar consiste en garantizar que no queda código del adversario, y esa garantía requiere una enumeración confiable de lo que hay en el sistema —que es exactamente la capacidad que el rootkit de kernel destruye—. Descargar el módulo, borrar el archivo y declarar el host recuperado es apostar a que el adversario dejó una sola cosa y no fue cuidadoso. La reconstrucción desde medio limpio, con las credenciales de ese host rotadas y con los datos restaurados desde una copia anterior al compromiso, es la única erradicación con garantía. Y aplica la regla del tempo de 5.4: reconstruir un host mientras el adversario mantiene acceso a los otros solo le indica qué se descubrió.
flowchart TD
  Q["Sospecha de persistencia en un host Linux"]
  Q --> B["Inventario de auto-ejecución\nsystemd · cron · authorized_keys · profile.d · udev"]
  Q --> P["Verificación contra paquetes\nrpm -Va · debsums → PAM/sshd troyanizados"]
  Q --> A["auditd\n-w rutas de disparo · execve · init_module"]
  Q --> F["FIM (AIDE)\n· /etc/ld.so.preload = alta fidelidad"]
  B & P & A & F --> D{"¿Las vistas del host\nson coherentes entre sí?"}
  D -->|Sí| S["Correlación en el SIEM\n= una historia de post-explotación"]
  D -->|No: el host se contradice| X["El host no es testigo confiable\n→ observar DESDE FUERA"]
  X --> M["Memoria: tabla de syscalls · módulos ocultos"]
  X --> O["Disco montado offline"]
  X --> E["eBPF: Falco / Tracee"]
  X --> N["Telemetría de red: el switch no miente"]
  M & O & E & N --> R["Erradicación = reconstruir\n(no limpiar)"]

Postura, ordenada por retorno real#

  1. Habilitar telemetría de ejecución. Sin auditoría de execve un host Linux no puede responder qué corrió; es el hueco que precede a todo lo demás y el de mayor retorno por unidad de esfuerzo.
  2. Reenviar los registros fuera del host de inmediato. El adversario que persiste tiene privilegio para editar los archivos locales. Sin colector remoto la evidencia queda bajo su custodia.
  3. Versionar la línea base de auto-ejecución y derivarla del artefacto de construcción, nunca de la observación de un host en producción, que solo dice en qué estado está y no en cuál debería estar.
  4. Verificar contra los paquetes. Es gratis, ya está en el host y responde por sí sola la familia de persistencia que subvierte la autenticación.
  5. Priorizar las reglas de alta fidelidad: /etc/ld.so.preload, carga de módulos, ExecStart que apunta a rutas temporales. Poca frecuencia, casi ningún falso positivo, y la fatiga de alertas de 5.1 no se activa.
  6. Reducir la superficie de disparadores en lugar de vigilarla toda: Secure Boot con firma de módulos y la deshabilitación de la carga de módulos tras el arranque cierran la familia más difícil de detectar; las claves SSH distribuidas centralmente eliminan una categoría entera de artefacto local.
  7. Tener resuelto de antemano el camino de observación externo —captura de memoria, sensor eBPF desplegado antes del incidente, telemetría de red—, porque el momento de conseguirlo no es cuando ya se sospecha que el host miente.
  8. Infraestructura inmutable donde el rol lo permita. Es la medida que convierte la detección de persistencia de un problema de vigilancia continua en una comparación contra una imagen conocida, y que además erradica en cada despliegue.

Referencias#