
Detector de rootkits en Linux basado en eBPF que utiliza análisis de vista cruzada multicanal (sched_switch, NMI, /proc) para detectar DKOM, manipulación de tracepoints y ocultación de procesos con verificación de integridad a nivel de hardware.
Integridad de Procesos del Sistema y Análisis entre Vistas
"Voy a cantar, así que brilla fuerte, SPiCa..."
SPiCa es un detector de rootkits de Linux basado en eBPF escrito en Rust. El nombre proviene de la canción de Hatsune Miku SPiCa y la estrella a la que hace referencia — Spica (Alfa Virginis), el punto más brillante de Virgo. Lo que a simple vista parece una sola estrella es en realidad una binaria espectroscópica: dos estrellas en órbita mutua, indistinguibles como objetos separados sin medir sus espectros. SPiCa aplica el mismo principio a la observación del núcleo: múltiples canales independientes miden el mismo estado del núcleo desde mecanismos físicamente distintos, y un rootkit que suprime uno es expuesto por los demás.
Descargo de responsabilidad: Partes significativas de este código fueron generadas o refactorizadas con asistencia de GLM. Se aplicaron pruebas rigurosas y diseño iterativo, pero revise el código para seguridad y rendimiento antes del uso en producción.
SPiCa está diseñado para derrotar al adversario restringido por eBPF — un atacante con privilegios elevados (CAP_BPF o CAP_SYS_ADMIN) que carga un programa eBPF privilegiado en el núcleo. Este adversario es fundamentalmente más débil que un rootkit LKM porque el verificador BPF impone restricciones estrictas:
| Restricción | Efecto sobre el adversario |
|---|---|
| Bucles acotados (~1M instrucciones por programa) | No puede escanear toda la memoria del núcleo |
| Seguridad de punteros (desreferenciación directa requiere objetivo comprobablemente válido y acotado) | No puede seguir punteros arbitrarios |
| Ayudantes restringidos (lista blanca por tipo de programa) | No puede llamar a kmalloc, vmalloc ni a ninguna función de asignación del núcleo |
Sin escritura arbitraria en el núcleo (bpf_probe_write_kernel muy restringido) | No puede modificar task_struct, arreglos de funciones de tracepoints ni bytecode de programas BPF |
| Acceso a mapas limitado a referencias cargadas | No puede acceder a mapas pertenecientes a otros programas (a menos que se enumeren por ID — ver §7) |
Con los LKMs bloqueados por la puerta LSM después del arranque, este adversario restringido es la amenaza realista restante. La maquinaria anti-evasiva de SPiCa está calibrada para esta amenaza — cada defensa es honesta sobre lo que cubre y lo que no.
init_module. SPiCa eleva el piso al bloquear el vector LKM más fácil, pero no limita al adversario superior.SPiCa es la capa de último recurso en una pila de defensa en profundidad, no un sustituto de las capas superiores.
SPiCa ejecuta cuatro programas eBPF adjuntos a ganchos del núcleo, más un motor de detección en espacio de usuario que correlaciona su salida con la vista del sistema sobre sí mismo (/proc).
| Canal | Mecanismo | Qué lo suprime | Costo de supresión |
|---|---|---|---|
/proc | Enumeración de directorio procfs | DKOM (desenlace del proceso de la lista de tareas) | Bajo — técnica de rootkit bien conocida |
sched_switch | Tracepoint eBPF en el planificador | Desadjuntar programa del arreglo funcs; filtrar contexto | Medio — requiere escritura en el núcleo al arreglo funcs |
| NMI | Interrupción de contador de rendimiento hardware → evento eBPF perf | Enganchar el manejador IDT/NMI | Pánico del núcleo casi seguro — interrupción hardware, no enmascarable |
La propiedad arquitectónica clave: un rootkit no puede suprimir los tres canales simultáneamente sin que la supresión misma se vuelva detectable o desestabilizadora. Suprimir NMI requiere parchear la IDT (Tabla de Descriptores de Interrupción), lo que provoca pánico en la mayoría de los núcleos. Esta es la "bomba viva" — el único camino del atacante hacia la ceguera total es uno que probablemente bloquee el sistema.
sched sees it, /proc doesn't → DKOM NMI finds sched's heartbeat stopped → TAMPER sched goes globally silent, /proc != empty → SILENT
Cada clase de detección es un veredicto diferencial: una discrepancia entre dos o más canales. El motor de detección es una función pura sobre el registro + la instantánea de /proc + las marcas de tiempo de los canales — sin E/S, sin efectos secundarios, totalmente comprobable mediante pruebas unitarias.
### El rediseño del NMI: de la observación a la integridad
En el diseño original, NMI era un segundo canal de observación de procesos que muestreaba la CPU e informaba qué tarea se estaba ejecutando. Esto era redundante: sched\_switch ya observa la planificación, y NMI muestreaba los mismos datos mediante un mecanismo diferente. La redundancia costaba ~1000+ eventos ring-buffer/segundo/CPU de datos de proceso que el 99.999% del tiempo confirmaban "sí, el planificador está haciendo lo que hace el planificador".
En la arquitectura rediseñada, **NMI se reutiliza de la observación de procesos a la verificación de integridad de los tracepoints.** Ya no informa qué proceso está en la CPU. En su lugar, verifica que `sched_switch` se esté ejecutando realmente leyendo un heartbeat compartido en `.bss`. Esto:
1. Elimina ~99% del tráfico del ring-buffer del NMI (eventos de estado estacionario casi nulos)
2. Detecta directamente desconexión de tracepoints, supresión y fallos de BTF/attach (el error original de BTF — ver [§11](#11-the-btf-bug-incident))
3. Se ejecuta desde una interrupción de hardware, fuera de la ruta de despacho de tracepoints — inmune a `bpf_override_return`, interceptación de kprobes y manipulación de arrays de funciones
4. Lee variables globales de `.bss` mediante acceso directo a memoria, no helpers de BPF — inmune a `fmod_ret` en funciones helper
---
## 3. El canal de observación de sched\_switch