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.
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, unexecveque 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.
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#
- Habilitar telemetría de ejecución. Sin auditoría de
execveun 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. - 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.
- 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.
- 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.
- Priorizar las reglas de alta fidelidad:
/etc/ld.so.preload, carga de módulos,ExecStartque apunta a rutas temporales. Poca frecuencia, casi ningún falso positivo, y la fatiga de alertas de 5.1 no se activa. - 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.
- 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.
- 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#
blue-dfir/attack-defense/cap-16— Log Analysis: la bitácora de autenticación de Linux (/var/log/auth.log, sesionesCRONderoot,sudo apt-get install) como evidencia de compromiso del anfitrión, y la correlación transversal entre fuentes como método —incluida la advertencia de conocer el formato crudo y no depender del panel—.- 3.14 · Post-explotación y persistencia en Linux — el catálogo ofensivo completo de
disparadores que este capítulo audita, del
rcde shell al rootkit de LKM. - 5.5 · Forense de memoria — el análisis de la tabla de llamadas al sistema y la triangulación entre vistas, aplicables aquí sin cambios de fondo.
- 5.2, 5.1 y 5.6 — la capa de datos, el criterio de fidelidad de las reglas y la trampa de la línea base heredada.
- MITRE ATT&CK — TA0003 Persistence, con T1543.002 Systemd Service, T1053.006 Systemd Timers, T1098.004 SSH Authorized Keys, T1556.003 Pluggable Authentication Modules, T1574.006 Dynamic Linker Hijacking, T1547.006 Kernel Modules and Extensions, T1014 Rootkit, T1070.002 Clear Linux or Mac System Logs y T1562.012 Disable or Modify Linux Audit System; del lado de las mitigaciones, M1047 Audit, M1045 Code Signing y M1029 Remote Data Storage.
- AIDE, the Linux Audit project, Falco y Tracee como implementaciones de referencia de FIM, auditoría y detección en runtime por eBPF.