Guía definitiva: contener y limpiar una máquina Linux comprometida
La primera vez que me llamaron por un servidor “raro” tardé casi una hora en entender lo que estaba pasando. El panel de control mostraba todo en verde, el servicio respondía, pero el consumo de CPU subía y bajaba sin motivo y había un proceso que nadie reconocía en la lista. La respuesta rápida era reiniciar el servidor y seguir como si nada. La respuesta correcta, que tomé esa misma tarde cuando ya no había evidencia, fue mucho más larga: aislar la máquina, entender qué había pasado, y decidir si se reparaba o se reconstruía desde cero.
Ese segundo camino es el que casi ningún tutorial te enseña bien. Existe una diferencia enorme entre “limpiar” un servidor comprometido y “contenerlo” de verdad. Limpiar es borrar lo que el atacante dejó: cron jobs, claves SSH, cuentas extra. Contener es detener la propagación, preservar lo que todavía sirve como prueba y decidir con criterio si el sistema es recuperable. Esta guía recorre ese recorrido completo en el orden correcto, con los comandos verificados y los límites claros de cada herramienta.
TL;DR — Lo que vas a tener al final
- Tiempo de setup: 45-90 minutos para la contención en vivo (cortar red, congelar procesos, volcar memoria); el peritaje completo del disco con dc3dd son horas según el tamaño del volumen.
- Requisitos: acceso root o sudo, una máquina forense externa con dc3dd y Volatility 3 instalados, y al menos el doble del tamaño del disco en almacenamiento vacío.
- Resultado: un procedimiento que sigue el orden de volatilidad del RFC 3227, con el host aislado pero vivo, evidencia hasheada con SHA-256 y una lista documentada de mecanismos de persistencia para eliminar.
- Troubleshooting: si los escáneres aplicados sobre la imagen forense reportan binarios del sistema alterados, o si un módulo del kernel no se puede descargar, no sigas limpiando: reconstruye el sistema.
Por qué la contención va antes que la limpieza
Cuando un servidor se compromete, el impulso natural es buscar el archivo sospechoso, borrarlo y seguir operando. Ese camino rara vez termina bien, y las razones son técnicas, no de opinión.
La primera es que el atacante rara vez depende de un solo archivo. Un rootkit de Linux suele combinar varias piezas: un binario reemplazado en /bin o /usr/bin, una entrada en /etc/ld.so.preload que inyecta código en cada proceso que arranca, una clave SSH autorizada que le da acceso persistente, un cron job que se reescribe solo, y un módulo del kernel que oculta todo lo anterior. Si borras el binario pero dejas el ld.so.preload, el malware sigue cargándose. Si limpias el cron pero no las claves SSH, el atacante vuelve.
La segunda razón es la evidencia. Cada comando que ejecutas en la máquina comprometida modifica el estado: escribe en el disco, cambia el timestamp de archivos, abre conexiones. En el momento en que identificaste la anomalía, es posible que el servidor ya tuviera mecanismos ocultos activos, y reiniciar puede ser justo lo que los destruye. Si reinicias, el malware puede detectar que no arrancó con su configuración habitual y borrarse. Por eso el manual RFC 3227 (Evidence Collection and Archiving, febrero de 2002) es tan específico: “Don’t shutdown until you’ve completed evidence collection” y “Don’t trust the programs on the system. Run your evidence gathering programs from appropriately protected media”.
El marco moderno que da estructura al proceso es NIST SP 800-61 Revisión 3, publicada en abril de 2025. Es importante decir de entrada que esta revisión cambió el esquema: las cuatro fases clásicas (preparación, detección y análisis, contención y eliminación, y post-incidente) venían de la Revisión 2, de agosto de 2012. La Revisión 3 reorganiza todo el documento sobre las seis funciones del NIST Cybersecurity Framework 2.0: Govern, Identify y Protect preparan; Detect, Respond y Recover ejecutan la respuesta al incidente. Si vas a citar las cuatro fases, di explícitamente que son de la Revisión 2.
| Fase | Qué haces en la práctica | Error típico |
|---|---|---|
| Contener | Cortar la red sin apagar el sistema, terminar procesos sospechosos sin matarlos de golpe | Reiniciar “para limpiar” y perder la evidencia volátil |
| Preservar | Volcar memoria, collecting logs remotos, imagen forense del disco con hash | Copiar archivos sueltos con scp en vez de una imagen completa |
| Analizar | Comparar /proc contra ps, buscar SUID, revisar cron, systemd, preload, claves SSH | Instalar herramientas forenses en la máquina comprometida |
| Erradicar y recuperar | Eliminar persistencia, rotar credenciales, parchear, monitorear | Borrar el archivo sospechoso y declarar el servidor sano |
Una advertencia que aparece en casi todas las guías serias y que vale la pena repetir: si no confías en el kernel, la opción más prudente es reconstruir en lugar de limpiar. Un rootkit a nivel de kernel puede interceptar tus propios comandos de verificación y mostrarte una falsa señal de “todo limpio”. En ese escenario, cualquier limpieza que hagas se apoya en un criterio de auditoría que el atacante controla.
Corta la red sin apagar el servidor
Contener significa que el atacante pierde su canal de mando, pero el sistema sigue vivo para que puedas recolectar la evidencia volátil. Si apagas la máquina en este punto, el malware que dependía de un proceso en memoria se pierde y no hay forma de saber qué hacía.
La herramienta base es ss, que viene en el paquete iproute2 y ya está instalada en cualquier distribución seria. Antes de cortar nada, mira quién está conectado:
# Ver todas las conexiones establecidas con el proceso asociado
ss -tnp state established
# Ver conexiones de un proceso concreto (sustituye PID por el número real)
ss -tnp state established | grep PID_NUMERO
# Conexiones en escucha: cada puerto abierto es un vector de entrada
ss -tulpn
El primer comando te da el mapa de la conexión activa: la IP de destino, el puerto y el PID del dueño. El tercero lista todo lo que está escuchando, que es el inventario de lo que un atacante puede intentar alcanzar desde afuera.
Una vez identificado el proceso de mando remoto, hay dos formas de cortarlo. La primera es suave y reversible: matar el socket con la opción -K de ss, que envía la señal de destrucción directamente al socket sin tocar el proceso:
# Cerrar el socket al atacante por IP de destino (destruye la conexión TCP)
ss -K dst IP_DESTINO
# Verificar que la conexión murió
ss -tnp state established | grep IP_DESTINO
La segunda es bloquear el tráfico a nivel de firewall, que es lo que haces cuando quieres cortar toda la salida de la máquina sin que el atacante lo note de inmediato:
# Bloquear todo el tráfico saliente, se acepta y se bloquea en iptables
iptables -P OUTPUT DROP
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A OUTPUT -d IP_SERVIDOR_GESTION -j ACCEPT
# Verificar las reglas activas
iptables -L -n -v
# Revertir el bloqueo si necesitas reconectar tráfico normal
iptables -P OUTPUT ACCEPT
iptables -F OUTPUT
Un detalle que parece obvio pero cuesta caro: si bloqueas la salida antes de copiar la evidencia a un servidor externo, te quedas sin salida. La regla de aceptación para la IP de gestión está en el bloque porque sin ella te quedas sin salida para copiar la evidencia. Y antes de cortar, lee los logs que sí están fuera: si el servidor envía a un servidor central de logs, esos registros ya no son evidencia volatile y sobreviven al corte de red.
Y aquí hay una advertencia que no es opcional. La propia RFC 3227, en su sección 2.2, advierte que desconectar o filtrar la red puede activar “deadman switches” en el malware: mecanismos que detectan que la máquina se quedó sin conectividad y borran la evidencia como respuesta. Es decir, el corte de red que parece la medida más prudente puede ser exactamente lo que dispara la destrucción de pruebas. Por eso el orden importa: captura primero el volcado de memoria y las conexiones que puedas, y solo después corta la salida. Y si el aislamiento se hace por medio de un firewall externo en vez de reglas locales, el atacante no ve el cambio, que es el escenario ideal.
Para lo que no es tráfico de red sino procesos que ya están corriendo en la máquina, kill con la señal STOP congela el proceso sin matarlo. Eso es más útil de lo que parece: un proceso congelado sigue en memoria y se puede analizar con Volatility 3 después, y deja de ejecutar su lógica mientras tanto. Lo que ya se transmitió no se recupera, y otros procesos del host pueden seguir enviando datos.
# Congelar el proceso sospechoso (SIGSTOP) sin terminarlo
kill -STOP PID_NUMERO
# Ver el estado del proceso: T significa detenido (stopped)
ps -o pid,stat,comm -p PID_NUMERO
# Liberarlo si resultara ser legítimo
kill -CONT PID_NUMERO
# Terminación definitiva, solo cuando ya no se necesita su memoria
kill -9 PID_NUMERO
Verificar con ps -o stat es importante porque el estado T distingue un proceso detenido de uno zombie (Z) o dormido (S). Si ves T, el proceso está congelado y su memoria sigue intacta.
Volca la memoria antes que cualquier otra cosa
La RAM es el primer eslabón de la cadena de volatilidad: contiene las claves de cifrado en claro, los tokens de sesión, las cadenas de comandos que el atacante ejecutó y los archivos mapeados en memoria que todavía no se escribieron al disco. El RFC 3227 pone el orden completo así, de lo más volátil a lo más estable:
- Registros de CPU y caché
- Tabla de enrutamiento, caché ARP, tabla de procesos, estadísticas del kernel, memoria
- Sistemas de archivos temporales
- Disco
- Logs remotos y datos de monitoreo relevantes
- Configuración física y topología de red
- Medios de archivo
La herramienta más práctica para esto en un servidor Linux es AVML (Acquire Volatile Memory for Linux), de Microsoft. Es un binario estático escrito en Rust que no necesita compilar nada en el objetivo ni saber de antemano la distribución o el kernel. Escribe en formato LiME, el formato de referencia que usa la mayoría de las herramientas de análisis de memoria.
# Descargar el binario estático y darle permisos de ejecución
curl -LO https://github.com/microsoft/avml/releases/latest/download/avml
chmod +x avml
# Volcar la memoria al disco (formato LiME)
sudo ./avml acquire memoria.lime
# Verificar el tamaño del volcado y calcular su hash
ls -lh memoria.lime
sha256sum memoria.lime
Aunque el binario es estático y no requiere instalar nada en el servidor, el comando necesita privilegios de root: AVML lee /proc/iomem para mapear las regiones de memoria física y sin CAP_SYS_ADMIN falla de inmediato con “need CAP_SYS_ADMIN to read /proc/iomem”. Por eso el sudo no es opcional, aunque el artefacto en sí no instale nada en el sistema comprometido.
AVML tiene tres fuentes de memoria disponibles: /dev/crash, /proc/kcore y /dev/mem. Si no indicas una en la línea de comandos, las va probando en orden hasta encontrar una funcional. Hay un límite importante: si el kernel tiene kernel_lockdown habilitado, el volcado no va a funcionar. En ese caso documenta el hecho y sigue con la imagen de disco.
Para analizar el volcado ya en tu estación forense, Volatility 3 es la herramienta más usada. Un detalle de instalación que suele hacer tropezar a la gente: el comando no se llama volatility3. Al instalar el paquete se instalan los ejecutables vol y volshell.
# Instalar Volatility 3 en un entorno virtual
python3 -m venv ~/venvs/forense
source ~/venvs/forense/bin/activate
pip install volatility3
# Verificar que el comando vol existe
vol --help
# Listar los procesos en el volcado de memoria
vol -f memoria.lime linux.pslist
# Ver el árbol de procesos (sirve para detectar procesos sin padre legítimo)
vol -f memoria.lime linux.pstree
# Ver los sockets abiertos en memoria
vol -f memoria.lime linux.sockstat
El análisis más útil para confirmar un rootkit es linux.psscan, que lista procesos escaneando las estructuras de memoria en lugar de confiar en la tabla de procesos del kernel. Si linux.pslist y linux.psscan muestran conteos distintos, hay procesos ocultos: el kernel está mintiendo. Volatility 3 trae además linux.lsmod para listar los módulos del kernel cargados, linux.vmaregexscan para buscar código inyectado en las regiones de memoria de cada proceso, y linux.bash para recuperar el historial de comandos de shell que quedó en memoria.
Un detalle sobre el estado de los comandos: en Volatility 3, linux.check_modules y linux.malfind siguen existiendo pero están marcados como deprecated, y el propio vol --help lo indica. Para el mapeo de memoria usa linux.vmaregexscan, que acepta una expresión regular y escanea todas las áreas de memoria virtual; para los módulos usa linux.lsmod contra la lista de /proc/modules que leas del sistema. Esa distinción importa porque un tutorial que te manda a malfind te está mandando a un comando que la propia herramienta marca como obsoleto.
Haz una imagen forense del disco, no una copia
Copiar archivos con scp solo te da lo que pensaste buscar. Una imagen forense te da todo el disco, con los archivos borrados incluidos, y te deja analizarla con herramientas que no existían en el momento del incidente. Es la diferencia entre investigar y adivinar.
La herramienta de referencia es dc3dd, un parche de GNU dd con capacidades forenses que calcula hashes de entrada y salida en la misma pasada. El formato es el de dd con algunas opciones extra:
# Instalar dc3dd (disponible en Debian, Ubuntu y Fedora)
sudo apt install dc3dd
# Crear la imagen del disco con hash SHA-256 de entrada y salida
# hof= es la clave: hashea la salida y verifica que coincida con la entrada
sudo dc3dd if=/dev/sda of=/mnt/forense/disco.img hof=/mnt/forense/disco.img hash=sha256 log=/mnt/forense/hash.txt
# El log registra el hash de entrada, el de la imagen y el veredicto [ok]
cat /mnt/forense/hash.txt
# Verificar la imagen por fuera y comparar con el disco original
sha256sum /mnt/forense/disco.img
El log= es lo que sostiene la cadena de custodia: registra el hash del disco original, el de la imagen resultante y el resultado de la verificación. Ojo con un detalle de sintaxis que hace perder tiempo: of= por sí solo calcula únicamente el hash de la entrada, no el de la imagen que acabas de escribir. Para que dc3dd hashee la salida y la compare con el original necesitas además hof= apuntando al mismo archivo; entonces el log imprime un [ok] cuando la copia es idéntica, que es exactamente la comprobación que un perito va a pedir. El flag hashlog= no existe en dc3dd y aborta la ejecución.
Cuando la imagen no cabe en un solo medio, la utilidad split de coreutils la parte en trozos; dc3dd por su lado genera salidas múltiples con hofs=, que es una opción distinta. Es importante el orden: divide primero, hashea después sobre el conjunto, para que el hash cubra la imagen completa y no solo un trozo.
# Partir la imagen en trozos de 2 GB
split -b 2G /mnt/forense/disco.img /mnt/forense/parte_
# Calcular el hash del conjunto concatenando los trozos en streaming
cat /mnt/forense/parte_* | sha256sum | sed 's/ -$/ disco.img/' > /mnt/forense/disco.sha256
# Verificar el hash recalculado contra el original
sha256sum -c /mnt/forense/disco.sha256
El detalle del sed no es cosmético: cat ... | sha256sum escribe el nombre del archivo como - porque la entrada vino por stdin, y un sha256sum -c posterior intenta abrir un archivo llamado guion y falla con “No such file or directory”. Reemplazar ese - por el nombre real es lo que hace que la verificación sirva.
La estación forense que usas importa menos de lo que parece, pero hay un criterio claro: todo lo que corra contra la imagen debe ejecutarse desde un medio protegido, no desde el sistema comprometido. SIFT Workstation de SANS y Tsurugi Linux son las dos distribuciones forenses de referencia para Linux, y ambas vienen con las herramientas de análisis ya resueltas. Verificar que la imagen se montó en solo lectura antes de analizarla no es opcional:
# Montar la imagen en solo lectura y con nodev, nosuid
sudo mkdir -p /mnt/forense/montaje
sudo mount -o ro,loop,noexec,nodev,nosuid /mnt/forense/disco.img /mnt/forense/montaje
# Confirmar que el montaje es de solo lectura
mount | grep forense
# Desmontar al terminar el análisis
sudo umount /mnt/forense/montaje
Busca los mecanismos de persistencia uno por uno
Con la imagen montada en solo lectura, la búsqueda es metódica. Estos son los lugares donde se esconde la persistencia en un servidor Linux comprometido, ordenados por frecuencia real.
Procesos y puertos
Un rootkit de espacio de usuario oculta sus procesos de ps comparando contra /proc. La diferencia entre ambos es la señal clásica. Si un PID existe en /proc pero no aparece en ps, el kernel está filtrando la tabla de procesos.
# Listar los PIDs que /proc conoce
ls /proc | grep -E '^[0-9]+$' | sort > /tmp/proc_pids.txt
# Listar los PIDs que ps reporta
ps -e -o pid= | tr -d ' ' | sort > /tmp/ps_pids.txt
# Los que están en /proc pero no en ps: candidatos a proceso oculto
comm -23 /tmp/proc_pids.txt /tmp/ps_pids.txt
La misma técnica aplica a las conexiones de red, con una salvedad importante sobre qué compara cada herramienta. ss no lee /proc/net/tcp: según el propio strace, abre un socket NETLINK_SOCK_DIAG y le pide al kernel la lista de sockets por esa vía. /proc/net/tcp es la tabla que exporta el kernel en el sistema de archivos virtual de proc. Son dos caminos distintos hacia la misma información, y eso es justamente lo que hace útil la comparación: un rootkit que intercepta la lectura de /proc/net/tcp no tiene por qué interceptar la consulta por netlink, ni al revés.
# Conexiones ESTABLISHED que ve el kernel directamente en /proc/net/tcp
awk 'NR>1 && $4=="01"' /proc/net/tcp | wc -l
# Conexiones ESTABLISHED que reporta ss (solo IPv4, para comparar como a como)
ss -Hant4 state established | wc -l
# Si los números difieren, hay sockets ocultos
# No olvides IPv6: /proc/net/tcp6 va aparte y ss mezcla ambos protocolos
awk 'NR>1 && $4=="01"' /proc/net/tcp6 | wc -l
ss -Hant6 state established | wc -l
Dos detalles que hacen que esta comparación dé falsos positivos si no los tienes presentes. El primero: ss omite la columna State cuando filtras por state established, así que un grep -c ESTAB siempre devuelve cero y parece que hay discrepancia en todas las máquinas. La forma correcta de contar es ss -Hant state established | wc -l, donde -H suprime la línea de encabezado. El segundo: /proc/net/tcp solo contiene IPv4, mientras que ss mezcla IPv4 e IPv6 en la misma salida. Comparar el total de ss contra /proc/net/tcp da una diferencia constante en cualquier servidor con conexiones IPv6, y eso no significa que haya rootkit. Por eso hay que comparar IPv4 contra IPv4 y IPv6 contra IPv6, por separado.
Para archivos borrados que siguen abiertos por un proceso, lsof con el selector de enlaces es la herramienta:
# Archivos eliminados del disco pero aún abiertos por procesos
lsof +L1
# Archivos abiertos dentro de directorios temporales,typical de droppers
lsof +D /tmp
lsof +D /dev/shm
Binarios y módulos del kernel
Los binarios del sistema con permisos SETUID o SETGID son el mecanismo clásico de escalamiento de privilegios. Un atacante que consigue escribir uno puede tener root sin haber sido root todavía:
# Binarios con bit SETUID o SETGID en rutas habituales
find /bin /sbin /usr/bin /usr/sbin -perm /6000 -type f 2>/dev/null
# Comparar la lista con lo que debería estar: revisar la integridad
sudo dpkg --verify 2>/dev/null | grep -vE "^[[:space:]]*$" | head -20
# Módulos del kernel cargados en memoria
lsmod
# Contenido de /etc/ld.so.preload: si tiene algo, es inyección de código
cat /etc/ld.so.preload
Ese ld.so.preload merece atención especial porque es la técnica que más se le escapa a la gente. El archivo lista bibliotecas compartidas que el linker carga en cada proceso del sistema antes de ejecutar el programa. Un atacante que logra escribir ahí una biblioteca maliciosa obtiene ejecución de código en todo lo que se ejecute, incluyendo tus propias herramientas de verificación. Es la razón por la que borrar el binario sospechoso no sirve de nada mientras esa línea siga ahí.
# Verificar si ld.so.preload tiene contenido y vaciarlo
cat /etc/ld.so.preload
sudo cp /etc/ld.so.preload /mnt/forense/ld.so.preload.bak
sudo tee /etc/ld.so.preload < /dev/null
# Regenerar la caché del linker
sudo ldconfig
Persistencia en tareas, servicios y cuentas
Los cron jobs, las unidades de systemd y las cuentas de usuario son el trío que casi siempre está comprometido cuando hay un rootkit:
# Cron jobs de root y de todos los usuarios
sudo crontab -l -u root
sudo ls -la /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/
# Unidades de systemd habilitadas, incluidas las que nadie recuerda instalar
systemctl list-unit-files --state=enabled
# Buscar servicios que apunten a rutas temporales o inusuales
systemctl list-unit-files --state=enabled | grep -iE "tmp|dev/shm|var/tmp"
# Cuentas con UID 0 que no deberían existir
awk -F: '$3==0 {print $1}' /etc/passwd
# Claves SSH autorizadas de todos los usuarios
sudo find /home /root -name "authorized_keys" -type f 2>/dev/null
Un detalle sobre las claves SSH que vale la pena verificar explícitamente: no basta con revisar authorized_keys. El atacante también puede haber dejado una authorized_keys2, modificado el archivo sshd_config para aceptar un AuthorizedKeysFile alternativo, o añadido un PermitRootLogin yes con su propia clave en /root/.ssh/. Verifica el archivo de configuración, no solo los archivos de claves.
# Configuración efectiva de sshd (no el archivo, lo que sshd realmente usa)
sudo sshd -T | grep -iE "authorizedkeysfile|permitrootlogin|passwordauthentication"
# Ver las claves del usuario root
sudo ls -la /root/.ssh/
sudo cat /root/.ssh/authorized_keys
Verifica el sistema con herramientas externas, no con las del host
Esta es la parte que más se salta la gente y la que más caro sale. Si el rootkit está en el kernel o en una biblioteca inyectada por ld.so.preload, cualquier herramienta que ejecutes en esa máquina puede devolver un resultado falso. Las herramientas forenses se ejecutan desde un medio externo, sobre la imagen, no contra el sistema vivo.
Para Rootkit de espacio de usuario, chkrootkit es la opción de referencia. Y hay un matiz de sintaxis que conviene tener claro: chkrootkit sí soporta escanear un sistema montado en otro directorio con -r, pero la versión de rkhunter que instalas en Debian y Ubuntu no: su opción --rootdir está deprecada, el binario solo imprime un aviso y descarta el argumento. Si buscas algo para escanear una imagen montada, chkrootkit -r es el que funciona.
# Escanear una imagen montada con chkrootkit (soporta -r de verdad)
sudo chkrootkit -r /mnt/forense/montaje
# rkhunter sobre el sistema vivo, con la sintaxis que sí funciona
sudo rkhunter --check --skip-keypress
# En rkhunter, --rootdir solo emite el aviso de deprecación y no escanea nada
Para una revisión de configuración y hardening, lynis es el que más rápido da una foto general, y clamscan sirve para buscar firmas de malware conocidas. Para reglas propias de detección, yara es la herramienta para búsqueda por patrones, y su sintaxis es yara [opciones] ARCHIVO_DE_REGLAS OBJETIVO, con -r para recursión en directorios:
# Escanear con las firmas de ClamAV
sudo clamscan -r /mnt/forense/montaje
# Lynis: auditoría completa del sistema
sudo lynis audit system
# Yara con reglas propias, en recursión sobre la imagen
yara -r /ruta/mis-reglas.yar /mnt/forense/montaje
Si los escáneres reportan binarios del sistema con tamaño o checksum alterado, o si un módulo del kernel que debería ser descargable no lo es, la lectura honesta es que la imagen no es confiable para limpieza. A partir de ahí, el procedimiento correcto cambia a reconstrucción: preservar la imagen como evidencia, crear una máquina nueva desde la última imagen limpia conocida, migrar los datos y rotar todas las credenciales desde el lado seguro.
Decide entre limpiar y reconstruir con criterios claros
No todo incidente justifica una reconstrucción, y no todo incidente se puede limpiar. La decisión depende del alcance del compromiso, no del pánico del momento. Estos son los criterios que uso:
Reconstruye si el compromiso incluyó acceso root (si confirmas escalada a UID 0 y no puedes acotar el alcance con la evidencia forense, lo más prudente es reconstruir), si hay indicio de rootkit a nivel de kernel, si los binarios del sistema están alterados, si hay un mecanismo de persistencia que no puedes explicar y no puedes eliminar de forma verificable, o si el sistema estuvo expuesto sin segmentación y no puedes listar de forma confiable todo lo que el atacante tocó. La regla práctica: si no puedes demostrar que la imagen está limpia, trátala como contaminada.
Limpia si el acceso fue de una cuenta sin privilegios, el atacante no escaló a root, los binarios del sistema verifican correctamente contra los paquetes, y los mecanismos de persistencia identificados son finitos y verificables uno por uno. En ese caso, la limpieza es más rápida y barata que reconstruir, siempre que los mecanismos de persistencia identificados se puedan verificar uno por uno tras aplicar el arreglo.
En cualquiera de los dos caminos, hay tres acciones que no se negocian. La primera es rotar todas las credenciales desde un equipo limpio: claves SSH, contraseñas, tokens de API, claves de base de datos. Si el atacante tuvo acceso root, ninguna credencial que estuviera en esa máquina sigue siendo confiable, aunque la limpies. La segunda es revisar el alcance lateral: los logs de autenticación del resto de la flota van a decir si el atacante saltó a otros hosts, y eso es lo que define el tamaño real del incidente. La tercera es cerrar la vía de entrada, no solo limpiar el síntoma: parchear la vulnerabilidad, rotar la credencial que se filtró y bloquear el vector en el borde.
Y el paso que casi nadie documenta: deja por escrito qué pasó. Un incidente con un informe de una página que dice qué se encontró, qué se hizo y qué queda pendiente vale más que un servidor limpio sin contexto, porque el siguiente incidente va a ser parecido y la documentación es lo que hace que la respuesta tome minutos en lugar de días.
Preguntas frecuentes
¿Debo reiniciar el servidor comprometido para detener al atacante?
No, salvo que el reinicio sea la única forma de detener el daño activo. Al reiniciar pierdes la memoria volátil: claves de cifrado, tokens de sesión, cadenas de comandos y archivos mapeados que nunca se escribieron al disco. El RFC 3227 lo dice de forma explícita: no apagues el sistema hasta haber completado la recolección de evidencia. Congela los procesos con kill -STOP, corta las conexiones con ss -K y bloquea la salida con iptables. El reinicio se justifica solo si el sistema es inestable o si hay un temporalizador de borrado.
¿Cuál es la diferencia entre rkhunter y chkrootkit, y cuál uso?
Los dos buscan rootkits, pero con alcances y sintaxis distintas. chkrootkit tiene la opción -r que sí funciona: escanea una imagen de disco montada en un directorio, por ejemplo chkrootkit -r /mnt/forense/montaje. rkhunter en Debian y Ubuntu tiene --rootdir deprecada: el binario imprime un aviso y descarta el argumento sin escanear nada, así que sobre una imagen montada no sirve. Para el sistema vivo, rkhunter --check --skip-keypress funciona, y lynis audit system aporta la revisión de hardening que ninguno de los dos cubre.
¿Por qué buscar conexiones en /proc/net/tcp si ss ya muestra las conexiones?
Porque no son la misma fuente de datos. ss abre un socket NETLINK_SOCK_DIAG y le pide al kernel la lista de sockets por esa vía, mientras que /proc/net/tcp es la tabla que el kernel exporta en el sistema de archivos proc. Un rootkit que intercepta una de las dos rutas no tiene por qué interceptar la otra, y esa asimetría es la que hace detectable el ocultamiento. La comparación entre el conteo de entradas con estado 01 en /proc/net/tcp y el conteo de ss -Hant4 state established revela la discrepancia. Dos precauciones evitan falsos positivos: usa -H en ss porque al filtrar por state established se omite la columna State (un grep -c ESTAB siempre da cero), y compara IPv4 contra IPv4, porque /proc/net/tcp no incluye las conexiones IPv6 que sí cuenta ss. Lo mismo aplica a los procesos: comparar los PIDs de /proc contra los de ps -e es la forma más barata de detectar ocultamiento.
¿Cómo verifico si un binario del sistema fue reemplazado por el atacante?
Con dpkg --verify, que compara los archivos instalados por un paquete contra los metadatos que guardó la base de datos de dpkg en el momento de la instalación. Cualquier archivo que no coincida aparece listado con su ruta. Ojo con la sintaxis: la opción espera el nombre del paquete, no la ruta del archivo, así que dpkg --verify /usr/bin/less falla con “not a valid package name”. Para un binario concreto primero ubicas su paquete con dpkg -S /usr/bin/less, que devuelve algo como “less: /usr/bin/less”, y luego verificas ese paquete con dpkg --verify less. Complementa con una revisión de permisos SETUID y SETGID con find /bin /usr/bin -perm /6000, y sobre todo revisa /etc/ld.so.preload: si tiene contenido, hay inyección de código en todos los procesos. Un binario que no coincide con su paquete o un preload con entradas es motivo suficiente para reconstruir en lugar de limpiar.
¿Volatility 3 y dc3dd se pueden instalar en el servidor comprometido para analizarlo ahí?
No. La RFC 3227 indica no confiar en los programas del sistema comprometido y ejecutarlos desde medios protegidos. En un sistema con rootkit a nivel de kernel, cualquier binario que corras puede estar interceptado, y el análisis puede devolverte datos sesgados o directamente lo que el atacante quiere que veas. Instala las herramientas en una estación forense separada y aplícalas sobre la imagen. Si necesitas recolectar datos en vivo, usa un binario estático independiente como avml, descargado de una fuente verificada y con su hash comprobado.
