Détecteur de rootkit Linux basé sur eBPF utilisant une analyse multi-canal à vues croisées (sched_switch, NMI, /proc) pour détecter DKOM, la falsification de points de trace et le masquage de processus avec vérification d'intégrité au niveau matériel.
Intégrité des processus système et analyse multi-vues
« Je vais chanter, alors brille, SPiCa... »
SPiCa est un détecteur de rootkits Linux basé sur eBPF, écrit en Rust. Le nom provient de la chanson de Hatsune Miku SPiCa et de l'étoile qu'elle évoque — Spica (Alpha Virginis), le point le plus brillant de la Vierge. Ce qui ressemble à une étoile unique à l'œil nu est en réalité une binaire spectroscopique : deux étoiles en orbite mutuelle, impossibles à distinguer comme objets séparés sans mesurer leur spectre. SPiCa applique le même principe à l'observation du noyau : plusieurs canaux indépendants mesurent le même état du noyau depuis des mécanismes physiquement distincts, et un rootkit qui en supprime un est exposé par les autres.
Avertissement : Une partie significative de cette base de code a été générée ou refactorisée avec l'aide de GLM. Des tests rigoureux et une conception itérative ont été appliqués, mais examinez le code pour la sécurité et les performances avant toute utilisation en production.
SPiCa est conçu pour vaincre l'adversaire contraint par eBPF — un attaquant disposant de privilèges élevés (CAP_BPF ou CAP_SYS_ADMIN) qui charge un programme eBPF privilégié dans le noyau. Cet adversaire est fondamentalement plus faible qu'un rootkit LKM car le vérifieur BPF impose des contraintes strictes :
| Contrainte | Effet sur l'adversaire |
|---|---|
| Boucles bornées (~1M instructions par programme) | Ne peut pas scanner toute la mémoire du noyau |
| Sécurité des pointeurs (le déréférencement direct nécessite une cible prouvablement valide et bornée) | Ne peut pas suivre des pointeurs arbitraires |
| Aides restreintes (liste blanche par type de programme) | Ne peut pas appeler kmalloc, vmalloc ni aucune fonction d'allocation du noyau |
Aucune écriture arbitraire dans le noyau (bpf_probe_write_kernel fortement restreint) | Ne peut pas modifier task_struct, les tableaux de fonctions de tracepoint, ni le bytecode du programme BPF |
| Accès aux cartes limité aux références chargées | Ne peut pas accéder aux cartes appartenant à d'autres programmes (sauf énumération par ID — voir §7) |
Avec les LKM bloqués par la porte LSM après le démarrage, cet adversaire contraint est la menace réaliste restante. Le mécanisme anti-contournement de SPiCa est calibré pour cette menace — chaque défense est honnête sur ce qu'elle couvre et ce qu'elle ne couvre pas.
init_module. SPiCa élève le niveau en bloquant le vecteur LKM le plus simple, mais ne plafonne pas l'adversaire.SPiCa est la couche de dernier recours dans une pile de défense en profondeur, pas un substitut pour les couches supérieures.
SPiCa exécute quatre programmes eBPF attachés à des hooks du noyau, plus un moteur de détection en espace utilisateur qui corrèle leurs sorties avec la vue qu'a le système de lui-même (/proc).
| Canal | Mécanisme | Ce qui le supprime | Coût de la suppression |
|---|---|---|---|
/proc | Énumération de répertoire procfs | DKOM (détacher le processus de la liste des tâches) | Faible — technique de rootkit bien connue |
sched_switch | Tracepoint eBPF sur l'ordonnanceur | Détacher le programme du tableau funcs ; filtrer le contexte | Moyen — nécessite une écriture noyau dans le tableau funcs |
| NMI | Interruption du compteur de performance matériel → événement de performance eBPF | Accrocher le gestionnaire IDT/NMI | Panique du noyau presque certaine — interruption matérielle, non masquable |
La propriété architecturale clé : un rootkit ne peut pas supprimer les trois canaux simultanément sans que la suppression elle-même devienne détectable ou déstabilisatrice. Supprimer le NMI nécessite de patcher l'IDT (table des descripteurs d'interruption), ce qui provoque une panique sur la plupart des noyaux. C'est la « bombe vivante » — la seule voie de l'attaquant vers la cécité totale est celle qui fait probablement planter le système.
sched sees it, /proc doesn't → DKOM NMI finds sched's heartbeat stopped → TAMPER sched goes globally silent, /proc != empty → SILENT
Chaque classe de détection est un verdict différentiel : une divergence entre deux ou plusieurs canaux. Le moteur de détection est une fonction pure sur le registre + instantané /proc + horodatages des canaux — pas d'E/S, pas d'effets secondaires, entièrement testable unitairement.
### La refonte du NMI : de l'observation à l'intégrité
Dans la conception originale, le NMI était un second canal d'observation des processus qui échantillonnait le CPU et rapportait quelle tâche était en cours d'exécution. C'était redondant : sched_switch observe déjà l'ordonnancement, et le NMI échantillonnait les mêmes données via un mécanisme différent. La redondance coûtait environ 1000+ événements de tampon en anneau par seconde par CPU de données de processus qui, 99,999 % du temps, confirmaient « oui, l'ordonnanceur fait ce que l'ordonnanceur fait ».
Dans l'architecture repensée, **le NMI est réutilisé de l'observation des processus vers la vérification d'intégrité des tracepoints.** Il ne rapporte plus quel processus est sur le CPU. Au lieu de cela, il vérifie que `sched_switch` est effectivement en cours d'exécution en lisant un heartbeat partagé `.bss`. Cela :
1. Élimine environ 99 % du trafic du tampon en anneau NMI (événements quasi nuls en régime permanent)
2. Détecte directement le détachement, la suppression et les échecs d'attachement BTF des tracepoints (le bug BTF original — voir [§11](#11-the-btf-bug-incident))
3. S'exécute depuis une interruption matérielle, en dehors du chemin de distribution des tracepoints — immunisé contre `bpf_override_return`, l'interception kprobe et la manipulation du tableau de fonctions
4. Lit les globales `.bss` via un accès mémoire direct, pas via les helpers BPF — immunisé contre `fmod_ret` sur les fonctions helpers