Accroché à Linux : Taxonomie des rootkits, techniques d'accrochage et savoir-faire

Dans cette première partie d'une série en deux parties, nous explorons la taxonomie des rootkits Linux et retraçons leur évolution, depuis le détournement d'objets partagés dans le domaine utilisateur et le crochetage de modules de noyau chargeables dans l'espace noyau jusqu'aux techniques modernes basées sur l'eBPF et l'io_uring.

25 minutes de lectureAnalyse des malwares
Hooked on Linux: Rootkit Taxonomy, Hooking Techniques and Tradecraft

Introduction

Voici la première partie d'une série de deux articles sur les rootkits Linux. Dans ce premier épisode, nous nous concentrons sur la théorie qui sous-tend le fonctionnement des rootkits : leur taxonomie, leur évolution et les techniques d'accroche qu'ils utilisent pour subvertir le noyau. Dans la deuxième partie, nous passons à l'aspect défensif et nous plongeons dans l'ingénierie de détection, couvrant les approches pratiques pour identifier et répondre à ces menaces dans les environnements de production.

Que sont les Rootkits ?

Les rootkits sont des logiciels malveillants furtifs conçus pour dissimuler des activités malveillantes, telles que des fichiers, des processus, des connexions réseau, des modules de noyau ou des comptes. Leur objectif principal est la persistance et l'évasion, ce qui permet aux attaquants de conserver un accès à long terme à des cibles de grande valeur telles que les serveurs, les infrastructures et les systèmes d'entreprise. Contrairement à d'autres formes de logiciels malveillants, les rootkits s'efforcent de rester indétectés plutôt que de poursuivre immédiatement des objectifs.

Comment fonctionnent les Rootkits ?

Les rootkits manipulent le système d'exploitation pour modifier la manière dont il présente les informations aux utilisateurs et aux outils de sécurité. Ils opèrent dans l'espace utilisateur ou dans le noyau. Les rootkits de l'espace utilisateur modifient les processus au niveau de l'utilisateur en utilisant des techniques telles que LD_PRELOAD ou le détournement de bibliothèque. Les rootkits de l'espace noyau s'exécutent avec les privilèges les plus élevés, modifiant les structures du noyau, interceptant les appels de système ou chargeant des modules malveillants. Cette intégration profonde leur confère de puissantes capacités d'évasion, mais augmente le risque opérationnel.

Pourquoi les rootkits sont-ils difficiles à détecter ?

Les rootkits de l'espace noyau peuvent manipuler les fonctions essentielles du système d'exploitation, en détournant les outils de sécurité et en masquant les artefacts de la visibilité de l'espace utilisateur. Ils laissent souvent des traces minimes de leur présence dans le système, évitant les indicateurs évidents tels que les nouveaux processus ou fichiers, ce qui rend la détection traditionnelle difficile. L'identification des rootkits nécessite souvent des analyses de la mémoire, des vérifications de l'intégrité du noyau ou de la télémétrie au-dessous du niveau du système d'exploitation.

Pourquoi les Rootkits sont une épée à double tranchant pour les attaquants

Si les rootkits offrent furtivité et contrôle, ils comportent des risques opérationnels. Les rootkits du noyau doivent être précisément adaptés aux versions et aux environnements du noyau. Des erreurs, telles qu'une mauvaise gestion de la mémoire ou un mauvais accrochage des appels syscall, peuvent provoquer des pannes du système (kernel panics), exposant immédiatement l'attaquant. À tout le moins, ces défaillances attirent l'attention sur le système - un scénario que l'attaquant tente activement d'éviter pour maintenir son emprise.

Les mises à jour du noyau posent également des problèmes : les modifications apportées aux API, aux structures de mémoire ou aux appels de service peuvent interrompre la fonctionnalité du rootkit, ce qui rend la persistance vulnérable. La détection de modules ou de crochets suspects déclenche généralement une enquête criminalistique approfondie, car les rootkits indiquent fortement des attaques ciblées et hautement qualifiées. Pour les attaquants, les rootkits sont des outils à haut risque et à haut rendement ; pour les défenseurs, cette fragilité offre des possibilités de détection par le biais d'une surveillance de bas niveau.

Rootkits Windows vs Linux

L'écosystème Windows Rootkit

Windows est la cible principale du développement de rootkits. Les attaquants exploitent les crochets du noyau, les pilotes et les appels système non documentés pour dissimuler des logiciels malveillants, voler des informations d'identification et persister. Une communauté de recherche mature et une utilisation répandue dans les environnements d'entreprise favorisent l'innovation continue, y compris des techniques telles que DKOM, les contournements de PatchGuard et les bootkits.

Des outils de sécurité robustes et les efforts de renforcement de Microsoft poussent les attaquants à utiliser des méthodes de plus en plus sophistiquées. Windows reste attractif en raison de sa prédominance sur les terminaux d'entreprise et les appareils grand public.

L'écosystème Rootkit Linux

Les rootkits Linux ont toujours fait l'objet de moins d'attention. La fragmentation des distributions et des versions du noyau complique la détection et le développement. Bien qu'il existe des recherches universitaires, la plupart des outils sont obsolètes et les environnements Linux de production manquent souvent de surveillance spécialisée.

Cependant, le rôle de Linux dans le cloud, les conteneurs, l'IoT et le calcul à haute performance en a fait une cible de plus en plus importante. Des rootkits Linux réels ont été observés dans des attaques contre des fournisseurs de services en nuage, des entreprises de télécommunications et des gouvernements. Les principaux défis pour les attaquants sont les suivants :

  • La diversité des noyaux entrave la compatibilité entre les distributions.
  • Les longues durées d'utilisation prolongent l'inadéquation des noyaux.
  • Les dispositifs de sécurité tels que SELinux, AppArmor et la signature des modules augmentent la difficulté.

Les menaces propres à Linux sont les suivantes :

  • Conteneurs & Kubernetes: nouveaux vecteurs de persistance via l'échappement des conteneurs.
  • Appareils IoT: noyaux obsolètes avec une surveillance minimale.
  • Serveurs de production: systèmes sans tête, sans interaction avec l'utilisateur, réduisant la visibilité.

Linux dominant l'infrastructure moderne, les rootkits représentent une menace insuffisamment surveillée et pourtant de plus en plus importante. Il est de plus en plus urgent d'améliorer la détection, l'outillage et la recherche de techniques spécifiques à Linux.

Évolution des modèles de mise en œuvre des rootkits Linux

Au cours des deux dernières décennies, les rootkits Linux ont évolué, passant des techniques de base du "userland" à des implants avancés résidant dans le noyau et exploitant des interfaces modernes du noyau telles que eBPF et io_uring. Chaque étape de cette évolution reflète à la fois l'innovation des attaquants et la réponse des défenseurs, poussant les conceptions de rootkits vers plus de furtivité, de flexibilité et de résilience.

Cette section décrit cette progression, y compris les caractéristiques clés, le contexte historique et les exemples du monde réel.

Début des années 2000 : Rootkits de la zone utilisateur des objets partagés (SO)

Les premiers rootkits Linux fonctionnaient entièrement dans l'espace utilisateur sans nécessiter de modification du noyau, en s'appuyant sur des techniques telles que LD_PRELOAD ou la manipulation des profils shell pour injecter des objets partagés malveillants dans des binaires légitimes. En interceptant des fonctions libc standard telles que opendir, readdir, et fopen, ces rootkits ont pu manipuler la sortie d'outils de diagnostic tels que ps, ls, et netstat. Si cette approche a facilité leur déploiement, leur dépendance à l'égard des crochets du domaine utilisateur a limité leur furtivité et leur portée par rapport aux implants au niveau du noyau ; ils ont été facilement perturbés par de simples redémarrages ou réinitialisations de la configuration. Parmi les exemples les plus marquants, citons le rootkit Jynx (2009), qui utilise les fonctions de libc pour masquer les fichiers et les connexions, et Azazel (2013), qui combine l'injection d'objets partagés avec des fonctions optionnelles en mode noyau. Les techniques de base de cet abus de lien dynamique ont été décrites en détail dans le numéro 61 de Phrack Magazine en 2003.

Milieu des années 2000-2010 : Rootkits de modules de noyau chargeables (LKM)

Alors que les défenseurs sont devenus habiles à repérer les manipulations du domaine utilisateur, les attaquants ont migré dans l'espace du noyau par l'intermédiaire de modules de noyau chargeables (LKM). Bien que les LKM soient des extensions légitimes, les acteurs malveillants les utilisent pour opérer avec tous les privilèges, en accrochant le site sys_call_table, en manipulant le site ftrace ou en modifiant les listes chaînées internes pour cacher les processus, les fichiers, les sockets et même le rootkit lui-même. Bien que les LKM offrent un contrôle approfondi et de puissantes capacités de dissimulation, ils font l'objet d'un examen minutieux dans des environnements renforcés. Ils sont détectables via des états de noyau altérés, des listes sur /proc/modules, ou des scanners LKM spécialisés, et sont de plus en plus entravés par des défenses modernes telles que Secure Boot, la signature de modules et les modules de sécurité Linux (LSM). Parmi les exemples classiques de cette époque, on peut citer Adore-ng (2004+), un LKM capable de se dissimuler et d'accrocher syscall ; Diamorphine (2016), un hooker populaire qui reste fonctionnel sur de nombreuses distributions ; et Reptile (2020), une variante moderne dotée de capacités de porte dérobée.

Fin des années 2010 : Rootkits basés sur l'eBPF

Pour échapper à la détection croissante des menaces basées sur le LKM, les attaquants ont commencé à abuser de l'eBPF, un sous-système conçu à l'origine pour le filtrage sécurisé des paquets et le traçage du noyau. Depuis Linux 4.8+, l'eBPF a évolué pour devenir une machine virtuelle programmable dans le noyau, capable d'attacher du code à des crochets syscall, des kprobes, des tracepoints ou des événements du module de sécurité Linux. Ces implants s'exécutent dans l'espace du noyau mais évitent le chargement traditionnel de modules, ce qui leur permet de contourner les scanners LKM standard tels que rkhunter et chkrootkit, ainsi que les restrictions liées à l'amorçage sécurisé. Parce qu'ils n'apparaissent pas dans /proc/modules et qu'ils sont essentiellement invisibles pour les mécanismes d'audit de module typiques, ils nécessitent CAP_BPF ou CAP_SYS_ADMIN (ou un rare accès non privilégié au FBP) pour être déployés. Cette ère est définie par des outils tels que Triple Cross (2022), une preuve de concept qui injecte des programmes eBPF pour accrocher des appels de système tels que execve, et Boopkit (2022), qui met en œuvre un canal C2 secret entièrement via eBPF, ainsi que de nombreuses présentations Defcon explorant le sujet.

Les années 2025 et au-delà : Rootkits basés sur la technologie io_uring (en cours d'émergence)

L'évolution la plus récente s'appuie sur io_uring, une interface d'E/S asynchrone haute performance introduite dans Linux 5.1 (2019) qui permet aux processus d'effectuer des opérations système par lots via des anneaux de mémoire partagée. Bien qu'il ait été conçu pour réduire la surcharge des syscalls à des fins de performance, les "red teamers" ont démontré que io_uring pouvait être utilisé de manière abusive pour créer des agents userland furtifs ou des rootkits dans le contexte du noyau qui échappent aux EDR basés sur les syscalls. En utilisant io_uring_enter pour effectuer des opérations par lots sur les fichiers, le réseau et les processus, ces rootkits produisent beaucoup moins d'événements syscall observables, frustrant ainsi les mécanismes de détection traditionnels et évitant les restrictions imposées aux LKM et à l'eBPF. Bien qu'ils soient encore expérimentaux, des exemples comme RingReaper (2025), qui utilise io_uring pour remplacer furtivement des appels de système courants comme read, write, connect et unlink, et les recherches menées par ARMO montrent qu'il s'agit d'un vecteur très prometteur pour le développement de futurs rootkits difficiles à repérer sans instrumentation personnalisée.

La conception du rootkit Linux s'est constamment adaptée en réponse à l'amélioration des défenses. Le chargement de LKM devenant plus difficile et l'audit de syscall plus avancé, les attaquants se sont tournés vers d'autres interfaces telles que eBPF et io_uring. Avec cette évolution, la bataille n'est plus seulement une question de détection, mais de compréhension des mécanismes utilisés par les rootkits pour s'intégrer au cœur du système, à commencer par leurs stratégies d'accrochage et leur architecture interne.

##Rootkit Internals and Hooking Techniques (techniques d'accrochage)

Comprendre l'architecture des rootkits Linux est essentiel pour la détection et la défense. La plupart des rootkits suivent une conception modulaire avec deux composants principaux :

  • Chargeur: Installe ou injecte le rootkit et peut établir une persistance. Bien qu'il ne soit pas strictement nécessaire, un composant de chargement distinct est souvent utilisé dans les chaînes d'infection de logiciels malveillants qui déploient des rootkits.
  • Charge utile: Effectue des actions malveillantes telles que le masquage de fichiers, l'interception d'appels système ou de communications secrètes.

Les charges utiles s'appuient fortement sur des techniques d'accrochage pour modifier le flux d'exécution et parvenir à la furtivité.

Composant Rootkit Loader

Le chargeur est le composant responsable du transfert du rootkit dans la mémoire, de l'initialisation de son exécution et, dans de nombreux cas, de l'établissement de la persistance ou de l'escalade des privilèges. Son rôle est de combler le fossé entre l'accès initial (par exemple, via un exploit, un hameçonnage ou une mauvaise configuration) et le déploiement complet du rootkit.

Selon le modèle de rootkit, le chargeur peut fonctionner entièrement dans l'espace utilisateur, interagir avec le noyau par le biais d'interfaces système standard ou contourner complètement les protections du système d'exploitation. D'une manière générale, les chargeurs peuvent être classés en trois catégories : les droppers basés sur des logiciels malveillants, les initialisateurs de rootkits en zone utilisateur et les chargeurs personnalisés de l'espace noyau. En outre, les rootkits peuvent être chargés manuellement par un attaquant à l'aide d'outils de l'espace utilisateur tels que insmod.

Droppers basés sur des logiciels malveillants

Les droppers de logiciels malveillants sont des programmes légers, souvent déployés après l'accès initial, dont le seul but est de télécharger ou de décompresser la charge utile d'un rootkit et de l'exécuter. Ces droppers opèrent généralement dans l'espace utilisateur, mais escaladent les privilèges et interagissent avec les fonctions au niveau du noyau.

Les techniques les plus courantes sont les suivantes :

  • Injection de module: Écriture d'un fichier .ko malveillant sur le disque et invocation de insmod ou modprobe pour le charger en tant que module du noyau.
  • Enveloppe de l'appel de service : Utilisation d'un wrapper autour de init_module() ou finit_module() pour charger un LKM directement par le biais d'appels de système.
  • Injection en mémoire: Exploitation d'interfaces telles que ptrace ou memfd_create, ce qui permet souvent d'éviter les artefacts de disque.
  • Chargement basé sur le BPF: Utilisation d'utilitaires tels que bpftool, tc, ou d'appels syscall directs bpf() pour charger et attacher les programmes eBPF aux tracepoints du noyau ou aux crochets LSM.

Chargeurs Userland

Dans le cas des rootkits à objets partagés, le chargeur peut se limiter à modifier la configuration de l'utilisateur ou les paramètres de l'environnement :

  • Abus de l'éditeur de liens dynamiques: Le réglage de LD_PRELOAD=/path/to/rootkit.so permet à l'objet partagé malveillant de remplacer les fonctions de libc lors de l'exécution du binaire cible.
  • Persistance via la modification du profil: L'insertion de configurations de préchargement dans .bashrc, .profile ou dans des fichiers globaux tels que /etc/profile garantit une exécution continue entre les sessions.

Bien que la mise en œuvre de ces chargeurs soit triviale, ils restent efficaces dans les environnements faiblement défendus ou dans le cadre de chaînes d'infection en plusieurs étapes.

Chargeurs de noyau personnalisés

Les rootkits avancés peuvent inclure des chargeurs de noyau personnalisés conçus pour contourner entièrement les chemins de chargement de modules standard. Ces chargeurs interagissent directement avec les interfaces de bas niveau du noyau ou les périphériques de mémoire pour écrire le rootkit dans la mémoire, en échappant souvent aux journaux d'audit du noyau ou à la vérification de la signature du module.

Par exemple, Reptile inclut un binaire userspace en tant que chargeur, ce qui lui permet de charger le rootkit sans invoquer insmod ou modprobe; cependant, il s'appuie toujours sur le syscall init_mod pour charger le module en mémoire.

Capacités supplémentaires du chargeur

Le chargeur de logiciels malveillants joue souvent un rôle plus important que la simple initialisation, devenant un élément multifonctionnel de la chaîne d'attaque. Une étape clé pour ces chargeurs avancés est l'élévation des privilèges, au cours de laquelle ils cherchent à obtenir un accès root avant de charger la charge utile principale, souvent en exploitant des vulnérabilités locales du noyau, une tactique courante illustrée par la vulnérabilité "Dirty Pipe" (CVE-2022-0847). Une fois les privilèges obtenus, le chargeur est chargé de brouiller les pistes. Il s'agit d'un processus d'effacement des preuves d'exécution en supprimant les entrées des fichiers critiques tels que bash_history, les journaux du noyau, les journaux d'audit ou la page principale du système syslog. Enfin, pour garantir la réexécution lors du redémarrage du système, le chargeur assure la persistance en installant des mécanismes tels que systemd units, cron jobs, udev rules, ou des modifications des scripts d'initialisation. Ces comportements multifonctionnels brouillent souvent la distinction entre un simple chargeur "" et un logiciel malveillant à part entière, en particulier dans le cas d'infections complexes en plusieurs étapes.

Composant de la charge utile

La charge utile offre des fonctionnalités essentielles : furtivité, contrôle et persistance. Il existe plusieurs méthodes principales qu'un attaquant peut utiliser. Les charges utiles de l'espace utilisateur, souvent appelées rootkits SO, fonctionnent en détournant des fonctions de la bibliothèque C standard telles que readdir ou fopen via l'éditeur de liens dynamiques. Cela leur permet de manipuler les résultats d'outils système courants tels que ls, netstat, et ps. Bien qu'ils soient généralement plus faciles à déployer, leur portée opérationnelle est limitée.

En revanche, les charges utiles de l'espace noyau fonctionnent avec tous les privilèges du système. Ils peuvent cacher des fichiers et des processus directement à partir de /proc, manipuler la pile de réseau et modifier les structures du noyau. Une approche plus moderne implique des rootkits basés sur l'eBPF, qui exploitent le bytecode dans le noyau attaché à des tracepoints syscall ou à des crochets du module de sécurité Linux (LSM). Ces kits offrent la furtivité sans nécessiter de modules hors arborescence, ce qui les rend particulièrement efficaces dans les environnements dotés de politiques de démarrage sécurisé ou de signature de modules. Des outils tels que bpftool simplifient leur chargement, compliquant ainsi leur détection. Enfin, les charges utiles basées sur io_uringexploitent la mise en lots d'E/S asynchrones via io_uring_enter (disponible dans Linux 5.1 et les versions ultérieures) pour contourner la surveillance traditionnelle des appels de service. Cela permet des opérations furtives sur les fichiers, les réseaux et les processus tout en minimisant l'exposition à la télémétrie.

Rootkits Linux - Techniques d'accrochage

En partant de cette base essentielle, nous nous tournons maintenant vers le cœur de la plupart des fonctionnalités des rootkits : le hooking. Par essence, le hooking consiste à intercepter et à modifier l'exécution de fonctions ou d'appels système afin de dissimuler une activité malveillante ou d'injecter de nouveaux comportements. En détournant le flux normal du code, les rootkits peuvent cacher des fichiers et des processus, filtrer les événements de sécurité ou surveiller secrètement le système, souvent sans laisser d'indices évidents. Le crochetage peut être mis en œuvre à la fois dans l'espace utilisateur et dans l'espace noyau, et au fil des ans, les attaquants ont mis au point de nombreuses techniques de crochetage, qu'il s'agisse d'anciennes méthodes ou de manœuvres d'évasion modernes. Dans cette partie, nous ferons une plongée en profondeur dans les techniques de crochetage courantes utilisées par les rootkits Linux, en illustrant chaque méthode par des exemples et des échantillons de rootkits réels (tels que Reptile, Diamorphine, PUMAKIT et, plus récemment, FlipSwitch) afin de comprendre leur fonctionnement et la manière dont l'évolution du noyau les a remises en question.

Le concept de crochet

À un niveau élevé, le hooking consiste à intercepter l'invocation d'une fonction ou d'un appel système et à la rediriger vers un code malveillant. Ce faisant, un rootkit peut modifier les données ou le comportement renvoyés afin de dissimuler sa présence ou d'altérer les opérations du système. Par exemple, un rootkit peut crocheter la syscall qui liste les fichiers d'un répertoire (getdents), en lui faisant sauter tous les noms de fichiers qui correspondent aux propres fichiers du rootkit, rendant ainsi ces fichiers "invisibles" pour les commandes utilisateur comme ls.

Le crochetage n'est pas limité aux éléments internes du noyau ; il peut également se produire dans l'espace utilisateur. Les premiers rootkits Linux fonctionnaient entièrement dans le domaine utilisateur en injectant des objets partagés malveillants dans les processus. Des techniques telles que l'utilisation de la variable d'environnement LD_PRELOAD de l'éditeur de liens dynamiques permettent à un rootkit de remplacer les fonctions standard de la bibliothèque C (par exemple, getdents, readdir et fopen) dans les programmes d'utilisateurs. Cela signifie que lorsqu'un utilisateur exécute un outil tel que ps ou netstat, le code injecté par le rootkit intercepte les appels à des processus de liste ou à des connexions réseau et filtre les appels malveillants. Ces crochets ne requièrent aucun privilège du noyau et sont relativement simples à mettre en œuvre.

Parmi les exemples notables, citons JynxKit (2012) et Azazel (2014), des rootkits en mode utilisateur qui accrochent des dizaines de fonctions libc pour cacher des processus, des fichiers, des ports réseau et même activer des portes dérobées. Cependant, les crochets du userland présentent des limites importantes : ils sont plus faciles à détecter et à supprimer, et ils ne permettent pas un contrôle aussi approfondi que les crochets du niveau du noyau. Par conséquent, la plupart des rootkits Linux modernes et "dans la nature" sont passés au crochet de l'espace noyau, malgré la complexité et le risque plus élevés, parce que les crochets du noyau peuvent tromper complètement le système d'exploitation et les outils de sécurité à un bas niveau.

Dans le noyau, l'accrochage consiste généralement à modifier les structures de données ou le code du noyau de sorte que lorsque le noyau tente d'exécuter une opération particulière (par exemple, ouvrir un fichier ou effectuer un appel système), il invoque le code du rootkit à la place (ou en plus) du code légitime. Au fil des ans, les développeurs du noyau Linux ont mis en place des protections renforcées contre les modifications non autorisées, mais les attaquants ont réagi en proposant des méthodes de piratage de plus en plus sophistiquées. Nous examinerons ci-dessous les principales techniques de crochetage dans l'espace du noyau, en commençant par les anciennes méthodes (aujourd'hui largement obsolètes) et en progressant vers les techniques modernes qui tentent de contourner les défenses contemporaines du noyau. Chaque sous-section expliquera la technique, montrera un exemple de code simplifié et discutera de son utilisation dans les rootkits connus et de ses limites compte tenu des protections actuelles de Linux.

Techniques d'accrochage dans le noyau

Accrochage de la table des descripteurs d'interruption (IDT)

L'une des premières astuces de crochetage du noyau sous Linux consistait à cibler la table des descripteurs d'interruption (IDT). Sous Linux x86 32 bits, les appels système étaient invoqués via une interruption logicielle (int 0x80). L'IDT est un tableau qui associe les numéros d'interruption aux adresses des gestionnaires. En modifiant l'entrée IDT pour 0x80, un rootkit pourrait détourner le point d'entrée de l'appel système avant que le distributeur d'appel système du noyau ne prenne le contrôle. En d'autres termes, lorsqu'un programme déclenche un appel syscall via int 0x80, le processeur passe d'abord au gestionnaire personnalisé du rootkit, ce qui permet à ce dernier de filtrer ou de rediriger les appels au niveau le plus bas. Vous trouverez ci-dessous un exemple de code simplifié d'accrochage d'IDT (à des fins d'illustration) :

// Install the IDT hook
static int install_idt_hook(void) {
    // Get pointer to IDT table
    idt_table = get_idt_table();

    // Save original syscall handler (int 0x80 = entry 128)
    original_syscall_entry = idt_table[0x80];

    // Calculate original handler address
    original_syscall_handler = (void*)(
        (original_syscall_entry.offset_high << 16) |
        original_syscall_entry.offset_low
    );

    // Install our hook
    idt_table[0x80].offset_low = (unsigned long)custom_int80_handler & 0xFFFF;
    idt_table[0x80].offset_high =
        ((unsigned long)custom_int80_handler >> 16)
        & 0xFFFF;

    // Keep same selector and attributes as original
    // idt_table[0x80].selector and type_attr remain unchanged

    printk(KERN_INFO "IDT hook installed at 0x80\n");
    return 0;
}
Exemple de code de détournement d'IDT

Le code ci-dessus définit un nouveau gestionnaire pour l'interruption 0x80, redirigeant le flux d'exécution vers le gestionnaire du rootkit avant toute manipulation de syscall. Cela permet au rootkit d'intercepter ou de modifier le comportement des appels syscall entièrement en dessous du niveau de la table des appels syscall. Le hooking IDT est utilisé par des rootkits éducatifs et plus anciens tels que SuckIT.

Le crochet IDT est désormais une technique historique. Il ne fonctionnait que sur les anciens systèmes Linux qui utilisent le mécanisme int 0x80 (noyaux x86 32 bits antérieurs à Linux 2.6). Le système Linux 64 bits moderne utilise les instructions sysenter/syscall au lieu de l'interruption logicielle, de sorte que l'entrée IDT pour 0x80 n'est plus utilisée pour les appels système. En outre, l'accrochage IDT est très spécifique à l'architecture (x86 uniquement) et n'est pas efficace sur les noyaux modernes avec x86_64 ou d'autres architectures.

Syscall Accrochage de table

Le crochetage de la table des appels système est une technique classique de rootkit qui consiste à modifier la table de répartition des appels système du noyau, connue sous le nom de sys_call_table. Cette table est un tableau de pointeurs de fonctions où chaque entrée correspond à un numéro de syscall spécifique. En écrasant un pointeur dans cette table, un pirate peut rediriger un appel syscall légitime, tel que getdents64, kill, ou read, vers un gestionnaire malveillant. Un exemple est présenté ci-dessous.

asmlinkage int (*original_getdents64)(
    unsigned int,
    struct linux_dirent64 __user *,
    unsigned int);

asmlinkage int hacked_getdents64(
    unsigned int fd,
    struct linux_dirent64 __user *dirp,
    unsigned int count)
{
    int ret = original_getdents64(fd, dirp, count);
    // Filter hidden entries from dirp
    return ret;
}

write_cr0(read_cr0() & ~0x10000); // Disable write protection
sys_call_table[__NR_getdents64] = hacked_getdents64;
write_cr0(read_cr0() | 0x10000); // Re-enable write protection
Exemple de code de détournement de la table Syscall

Dans l'exemple, pour modifier la table, un module du noyau devrait d'abord désactiver la protection en écriture sur la page de mémoire où réside la table. Le code d'assemblage suivant (tel qu'il apparaît dans Diamorphine) montre comment le 20e bit (Write Protect) du registre de contrôle CR0 peut être effacé, même si la fonction write_cr0 n'est plus exportée vers les modules :

static inline void
write_cr0_forced(unsigned long val)
{
    unsigned long __force_order;

    asm volatile(
        "mov %0, %%cr0"
        : "+r"(val), "+m"(__force_order));
}
Exemple de code d'effacement du registre de contrôle (cr0)

Lorsque la protection en écriture est désactivée, l'adresse d'un appel de système dans le tableau peut être remplacée par l'adresse d'une fonction malveillante. Après la modification, la protection en écriture est réactivée. Parmi les exemples notables de rootkits utilisant cette technique, on peut citer Diamorphine, Knark et Reveng_rtkit. L'accrochage à la table Syscall présente plusieurs limites :

  • Durcissement du noyau (depuis la version 2.6.25) cache sys_call_table.
  • Les pages de mémoire du noyau ont été mises en lecture seule (CONFIG_STRICT_KERNEL_RWX).
  • Les fonctions de sécurité telles que Secure Boot et le mécanisme de verrouillage du noyau peuvent entraver les modifications apportées à CR0.

L'atténuation la plus définitive a été apportée par le noyau Linux 6.9, qui a fondamentalement modifié la manière dont les appels de service sont envoyés sur l'architecture x86-64. Avant la version 6.9, le noyau exécutait les appels de service en recherchant directement le gestionnaire dans le tableau sys_call_table:

// Pre-v6.9 Syscall Dispatch
asmlinkage const sys_call_ptr_t sys_call_table[] = {
    #include <asm/syscalls_64.h>
};
Exécution de syscall dans les noyaux Linux antérieurs à la version 6.9

À partir du noyau 6.9, le numéro de l'appel de service est utilisé dans une instruction de commutation pour trouver et exécuter le gestionnaire approprié. Le site sys_call_table existe toujours, mais il n'est renseigné que pour des raisons de compatibilité avec les outils de traçage et n'est plus utilisé dans le chemin d'exécution de la syscall.

// Kernel v6.9+ Syscall Dispatch
long x64_sys_call(const struct pt_regs *regs, unsigned int nr)
{
    switch (nr) {
    #include <asm/syscalls_64.h>
    default: return __x64_sys_ni_syscall(regs);
    }
};
Exécution de syscall dans les noyaux Linux après la version 6.9

En raison de ce changement architectural, l'écrasement des pointeurs de fonction dans le site sys_call_table sur les noyaux 6.9 et plus récents n'affecte pas l'exécution des syscalls, ce qui rend la technique totalement inefficace. Alors que nous pensions que le patching de la table syscall n'était plus viable, nous avons récemment publié la technique FlipSwitch, qui démontre que ce vecteur est loin d'être mort. Cette méthode exploite des gadgets spécifiques de manipulation de registres pour désactiver momentanément les mécanismes de protection en écriture du noyau, ce qui permet à un attaquant de contourner l'immutabilité "" du chemin syscall moderne et de réintroduire des crochets même dans ces environnements renforcés.

Au lieu de cibler la fonction sys_call_table basée sur les données, FlipSwitch se concentre sur le code machine compilé de la nouvelle fonction de distribution de syscall du noyau, x64_sys_call. Étant donné que le noyau utilise désormais une déclaration switch-case massive pour exécuter les syscalls, chaque syscall dispose d'une instruction call codée en dur dans le binaire du répartiteur. FlipSwitch analyse la mémoire de la fonction x64_sys_call pour localiser la signature spécifique "" d'un appel de système cible, généralement un opcode 0xe8 (l'instruction CALL ) suivi d'un décalage relatif de 4 octets qui pointe vers le gestionnaire légitime d'origine.

Une fois ce site d'appel identifié dans le répartiteur, le rootkit utilise des gadgets pour effacer le bit Write Protect (WP) dans le registre de contrôle CR0, accordant ainsi un accès temporaire en écriture aux segments de code exécutable du noyau. Le décalage relatif d'origine est alors remplacé par un nouveau décalage pointant vers une fonction malveillante, contrôlée par l'adversaire. Cela permet à "de basculer l'interrupteur" au point d'envoi, garantissant que chaque fois que le noyau tente d'exécuter le syscall cible par le biais de son chemin d'état d'interrupteur moderne, il est redirigé vers le rootkit à la place. Cela permet une interception fiable et précise des syscalls qui persiste malgré le durcissement de l'architecture du noyau 6.9.

Accrochage en ligne / Fonction Prologue Patching

L'accrochage en ligne est une alternative à l'accrochage via les tables de pointeurs. Au lieu de modifier un pointeur dans un tableau, le hooking inline corrige le code de la fonction cible elle-même. Le rootkit écrit une instruction de saut au début (prologue) d'une fonction du noyau, ce qui détourne l'exécution vers le propre code du rootkit. Cette technique s'apparente au hot-patching de fonctions ou à la manière dont fonctionnent les crochets en mode utilisateur sous Windows (par exemple, en modifiant les premiers octets d'une fonction pour passer à un détour).

Par exemple, un rootkit peut cibler une fonction du noyau comme do_sys_open (qui fait partie de la gestion de l'appel de système du fichier ouvert). En remplaçant les premiers octets de do_sys_open par une instruction x86 JMP vers un code malveillant, le rootkit s'assure que chaque fois que do_sys_open est appelé, il saute dans la routine du rootkit à la place. La routine malveillante peut alors exécuter ce qu'elle veut (par exemple, vérifier si le nom de fichier à ouvrir figure sur une liste de fichiers cachés et en refuser l'accès), et éventuellement appeler l'adresse do_sys_open d'origine afin de suivre le comportement normal pour les fichiers non cachés.

unsigned char *target = (unsigned char *)kallsyms_lookup_name("do_sys_open");
unsigned long hook = (unsigned long)&malicious_function;
int offset = (int)(hook - ((unsigned long)target + 5));
unsigned char jmp[5] = {0xE9};
memcpy(&jmp[1], &offset, 4);

// Memory protection omitted for brevity
memcpy(target, jmp, 5);

asmlinkage long malicious_function(
    const char __user *filename,
    int flags, umode_t mode) {
    printk(KERN_INFO "do_sys_open hooked!\n");
    return -EPERM;
}
Exemple de code d'accrochage en ligne

Ce code écrase le début de l'instruction do_sys_open() par une instruction JMP qui redirige l'exécution vers un code malveillant. Le rootkit open-source Reptile utilise largement le patching de fonctions en ligne via un cadre personnalisé appelé KHOOK (dont nous parlerons bientôt).

Les crochets en ligne de Reptile ciblent des fonctions telles que sys_kill et d'autres, permettant des commandes de porte dérobée (par exemple, l'envoi d'un signal spécifique à un processus déclenche l'élévation des privilèges ou le masquage du processus par le rootkit). Un autre exemple est celui de Suterusu, qui a également appliqué des correctifs en ligne pour certains de ses crochets.

Le hooking inline est fragile et très risqué : l'écrasement du prologue d'une fonction est sensible aux différences de version du noyau et de compilateur (les hooks nécessitent donc souvent des correctifs par construction ou un désassemblage en cours d'exécution), il peut facilement faire planter le système si les instructions ou l'exécution simultanée ne sont pas gérées correctement, et il nécessite de contourner les protections modernes de la mémoire (W^X, CR0 WP, module signing/lockdown) ou d'exploiter des vulnérabilités pour rendre le texte du noyau accessible en écriture.

Accrochage au système de fichiers virtuels

La couche du système de fichiers virtuel (VFS) de Linux fournit une abstraction pour les opérations sur les fichiers. Par exemple, lorsque vous lisez un répertoire (comme ls /proc), le noyau appellera éventuellement une fonction pour itérer sur les entrées du répertoire. Les systèmes de fichiers définissent leurs propres file_operations avec des pointeurs de fonction pour des actions telles que iterate_shared (pour lister le contenu des répertoires) ou read/write pour les entrées/sorties de fichiers. Le crochetage de VFS consiste à remplacer ces pointeurs de fonction par des fonctions fournies par le rootkit afin de manipuler la manière dont le système de fichiers présente les données.

Par essence, un rootkit peut s'accrocher au VFS pour cacher des fichiers ou des répertoires en les filtrant de la liste des répertoires. Une astuce courante : accrocher la fonction qui parcourt les entrées du répertoire et lui faire sauter tous les noms de fichiers qui correspondent à un certain modèle. La structure file_operations des répertoires (en particulier dans /proc ou /sys) est une cible fréquente, car la dissimulation de processus malveillants implique souvent la dissimulation d'entrées sous /proc/<pid>.

Considérez cet exemple de crochet pour une fonction d'inscription dans un annuaire :

static iterate_dir_t original_iterate;

static int malicious_filldir(
    struct dir_context *ctx,
    const char *name, int namelen,
    loff_t offset, u64 ino,
    unsigned int d_type)
{
    if (!strcmp(name, "hidden_file"))
        return 0; // Skip hidden_file
    return ctx->actor(ctx, name, namelen, offset, ino, d_type);
}

static int malicious_iterate(struct file *file, struct dir_context *ctx)
{
    struct dir_context new_ctx = *ctx;
    new_ctx.actor = malicious_filldir;
    return original_iterate(file, &new_ctx);
}

// Hook installation
file->f_op->iterate = malicious_iterate;
Exemple de code d'accrochage VFS

Cette fonction de remplacement permet de filtrer les fichiers cachés lors des opérations de listage des répertoires. En s'accrochant au niveau du VFS, le rootkit n'a pas besoin d'altérer les tables d'appels système ou l'assemblage de bas niveau ; il se contente de s'appuyer sur l'interface du système de fichiers. Adore-NG, un rootkit Linux autrefois très populaire, utilisait le crochet VFS pour dissimuler des fichiers et des processus. Il a corrigé les pointeurs de fonction pour l'itération des répertoires afin de dissimuler les entrées pour des PID et des noms de fichiers spécifiques. De nombreux autres rootkits du noyau disposent d'un code similaire pour se dissimuler ou dissimuler leurs artefacts par l'intermédiaire de crochets VFS.

L'accrochage au VFS est encore largement utilisé, mais il présente des limites en raison des changements dans les décalages de structure du noyau entre les versions, ce qui peut entraîner la rupture des accrochages.

Accrochage basé sur les traces

Les noyaux Linux modernes incluent un puissant cadre de traçage appelé ftrace (function tracer). Ftrace est destiné au débogage et à l'analyse des performances. Il permet d'attacher des crochets (callbacks) à presque toutes les entrées et sorties de fonctions du noyau sans modifier directement le code du noyau. Il fonctionne en modifiant dynamiquement le code du noyau au moment de l'exécution d'une manière contrôlée (souvent en patchant un trampoline léger qui appelle le gestionnaire de traçage). Il est important de noter que ftrace fournit une API permettant aux modules du noyau d'enregistrer des gestionnaires de traces, pour autant que certaines conditions soient remplies (comme le fait que le noyau soit construit avec le support ftrace et que l'interface debugfs soit disponible).

Les rootkits ont commencé à abuser de ftrace pour implémenter des hooks d'une manière moins évidente. Au lieu d'écrire manuellement un JMP dans une fonction, un rootkit peut demander à la machinerie ftrace du noyau de le faire en son nom, ce qui a pour effet de "légitimer" le crochet. Cela signifie que le rootkit n'a pas besoin de trouver l'adresse de la fonction ou de modifier les protections de page ; il enregistre simplement un rappel pour le nom de la fonction qu'il veut intercepter, et le noyau installe le crochet.

Voici un exemple simplifié de l'utilisation de ftrace pour accrocher le gestionnaire d'appels système mkdir:

static int __init hook_init(void) {
    target_addr = kallsyms_lookup_name(SYSCALL_NAME("sys_mkdir"));
    if (!target_addr) return -ENOENT;
    real_mkdir = (void *)target_addr;

    ops.func = ftrace_thunk;
    ops.flags = FTRACE_OPS_FL_SAVE_REGS
        | FTRACE_OPS_FL_RECURSION_SAFE
        | FTRACE_OPS_FL_IPMODIFY;

    if (ftrace_set_filter_ip(&ops, target_addr, 0, 0)) return -EINVAL;
    return register_ftrace_function(&ops);
}
Exemple de code d'accrochage Ftrace

Ce crochet intercepte la fonction sys_mkdir et la redirige vers un gestionnaire malveillant. Des rootkits récents tels que KoviD, Singularity et Umbra ont utilisé des crochets basés sur ftrace. Ces rootkits enregistrent des callbacks ftrace sur diverses fonctions du noyau (y compris les syscalls) afin de les surveiller ou de les manipuler.

Le principal avantage du crochet ftrace est qu'il ne laisse pas d'empreintes évidentes dans les tables globales ou dans le code corrigé. L'accrochage se fait par l'intermédiaire d'interfaces légitimes du noyau. Pour un œil non averti, tout semble normal ; sys_call_table est intact, les prologues des fonctions ne sont pas écrasés manuellement par le rootkit (ils sont écrasés par le mécanisme ftrace, mais il s'agit d'un phénomène courant et autorisé dans un noyau où le traçage est activé). En outre, les crochets ftrace peuvent souvent être activés/désactivés à la volée et sont intrinsèquement moins intrusifs que les correctifs manuels.

Bien que le crochet ftrace soit puissant, il est limité par l'environnement et les limites de privilèges (s'il est utilisé depuis l'extérieur du noyau). Elle nécessite un accès à l'interface de traçage (debugfs) et aux privilèges CAP_SYS_ADMIN, qui peuvent être indisponibles sur des systèmes renforcés ou conteneurisés où même l'UID 0 est restreint par des espaces de noms, des LSM ou des politiques de verrouillage de Secure Boot. Debugfs peut également être démonté ou en lecture seule en production pour des raisons de sécurité. Ainsi, alors qu'un utilisateur root disposant de tous les privilèges peut généralement utiliser ftrace, les défenses modernes désactivent ou limitent souvent ces capacités, ce qui réduit l'utilité des crochets basés sur ftrace dans les environnements hautement renforcés.

Kprobes Crochet

Kprobes est une autre fonctionnalité du noyau destinée au débogage et à l'instrumentation, que les attaquants ont réaffectée pour l'accrochage de rootkits. Les Kprobes permettent de s'introduire dynamiquement dans presque toutes les routines du noyau au moment de l'exécution en enregistrant un gestionnaire de sonde. Lorsque l'instruction spécifiée est sur le point d'être exécutée, l'infrastructure kprobe enregistre l'état et transfère le contrôle au gestionnaire personnalisé. Après l'exécution du gestionnaire (vous pouvez même modifier les registres ou le pointeur d'instructions), le noyau reprend l'exécution normale du code original. En termes plus simples, les kprobes vous permettent d'attacher un rappel personnalisé à un point arbitraire du code du noyau (entrée de fonction, instruction spécifique, etc.), un peu comme un point d'arrêt avec un gestionnaire.
L'utilisation de kprobes pour un crochetage malveillant implique généralement l'interception d'une fonction pour l'empêcher de faire quelque chose ou pour obtenir des informations. Une utilisation courante dans les rootkits modernes : étant donné que de nombreux symboles importants (comme sys_call_table ou kallsyms_lookup_name) ne sont plus exportés, un rootkit peut déployer une kprobe sur une fonction qui a accès à ce symbole et le voler. La structure et l'enregistrement d'un kprobe sont présentés ci-dessous.

// Declare a kprobe targeting the symbol "kallsyms_lookup_name"
static struct kprobe kp = {
    .symbol_name = "kallsyms_lookup_name"
};

// Function pointer type matching kallsyms_lookup_name
typedef unsigned long
    (*kallsyms_lookup_name_t)(const char *name);

// Global pointer to the resolved kallsyms_lookup_name
kallsyms_lookup_name_t kallsyms_lookup_name;

// Register the kprobe; kernel resolves kp.addr
// to the address of the symbol
register_kprobe(&kp);

// Assign resolved address to our function pointer
kallsyms_lookup_name =
    (kallsyms_lookup_name_t) kp.addr;

// Unregister the kprobe (only needed it once)
unregister_kprobe(&kp);
Exemple de code d'accrochage Kprobes

Cette sonde est utilisée pour récupérer le nom du symbole pour kallsyms_lookup_name, typiquement un précurseur du crochetage de la table syscall. Bien qu'elle ne soit pas présente dans les commits initiaux, une mise à jour récente de Diamorphine a utilisé cette technique. Il place un kprobe pour saisir le pointeur de kallsyms_lookup_name lui-même (ou utilise un kprobe sur une fonction connue pour obtenir indirectement ce dont il a besoin). De même, d'autres rootkits utilisent une kprobe temporaire pour localiser des symboles, puis la désenregistrent une fois que c'est fait, pour passer à l'exécution de crochets par d'autres moyens. Les Kprobes peuvent également être utilisés pour accrocher directement un comportement (et pas seulement pour trouver des adresses). Une jprobe (une kprobe spécialisée) peut également rediriger entièrement une fonction. Cependant, l'utilisation de kprobes pour remplacer complètement une fonctionnalité est délicate et peu répandue, car il est plus simple de patcher ou d'utiliser ftrace si vous souhaitez détourner une fonction de manière cohérente. Les sondes K sont souvent utilisées pour des accrochages intermittents ou auxiliaires.

Les sondes K sont utiles mais limitées : elles augmentent la charge d'exécution et peuvent déstabiliser les systèmes si elles sont placées sur des fonctions de bas niveau très chaudes ou restreintes (les sondes récursives sont supprimées), de sorte que les attaquants doivent choisir les points de sonde avec soin ; elles sont également auditables et peuvent déclencher des avertissements du noyau ou être enregistrées par l'audit du système, et les sondes actives sont visibles sous /sys/kernel/debug/kprobes/list (les entrées inattendues sont donc suspectes) ; certains noyaux peuvent être construits sans prise en charge de kprobe/debug.

Cadre d'accroche du noyau

Comme nous l'avons déjà mentionné, avec le rootkit Reptile, les attaquants créent parfois des cadres de niveau supérieur pour gérer leurs crochets. Kernel Hook (KHOOK) est un cadre de ce type (développé par l'auteur de Reptile) qui fait abstraction du travail salissant des correctifs en ligne et fournit une interface plus propre aux développeurs de rootkits. Essentiellement, KHOOK est une bibliothèque qui vous permet de spécifier une fonction à accrocher et votre remplacement, et qui gère la modification du code du noyau tout en fournissant un trampoline pour appeler la fonction d'origine en toute sécurité. Pour illustrer, voici un exemple de la façon dont on pourrait utiliser une macro de type KHOOK (basée sur l'utilisation de Reptile) pour accrocher l'appel syscall kill :

// Creates a replacement for sys_kill:
// long sys_kill(long pid, long sig)
KHOOK_EXT(long, sys_kill, long, long);

static long khook_sys_kill(long pid, long sig) {
    // Signal 0 is used to check if a process
    // exists (without sending a signal)
    if (sig == 0) {
        // If the target is invisible (hidden by
        // a rootkit), pretend it doesn't exist
        if (is_proc_invisible(pid)) {
            return -ESRCH; // No such process
        }
    }

    // Otherwise, forward the call to the original sys_kill syscall
    return KHOOK_ORIGIN(sys_kill, pid, sig);
}
Exemple de code KHOOK

KHOOK opère par le biais d'un patching de fonction en ligne, en écrasant les prologues des fonctions avec un saut vers les gestionnaires contrôlés par l'attaquant. L'exemple ci-dessus montre comment sys_kill() est redirigé vers un gestionnaire malveillant si le signal kill est égal à 0.

Bien que KHOOK simplifie la correction en ligne, il hérite de tous ses inconvénients : il modifie le texte du noyau pour insérer des jump stubs, de sorte que des protections telles que le verrouillage du noyau, Secure Boot ou W^X peuvent le bloquer. Ils dépendent également de l'architecture et de la version (ils sont généralement limités à x86 et ne fonctionnent pas avec le noyau 5.x+), ce qui les rend fragiles d'une version à l'autre.

Techniques d'accrochage dans l'espace utilisateur

Le hooking de l'espace utilisateur est une technique qui cible la couche libc, ou d'autres bibliothèques partagées accessibles via l'éditeur de liens dynamiques, afin d'intercepter les appels d'API courants utilisés par les outils de l'utilisateur. Des exemples de ces appels sont readdir, getdents, open, fopen, fgets, et connect. En interposant des fonctions de remplacement, un attaquant peut manipuler des outils userland ordinaires tels que ps, ls, lsof, et netstat pour renvoyer des vues" modifiées ou "aseptisées. Elle est utilisée pour dissimuler des processus, des fichiers, des sockets ou pour cacher des preuves de code malveillant.

Les méthodes courantes de mise en œuvre reflètent la manière dont l'éditeur de liens dynamiques résout les symboles ou impliquent la modification de la mémoire du processus. Ces méthodes comprennent l'utilisation de la variable d'environnement LD_PRELOAD ou de LD_AUDIT pour forcer le chargement anticipé d'un fichier d'objets partagés (.so) malveillant, la modification des entrées DT_* de l'ELF ou des chemins de recherche des bibliothèques pour donner la priorité à une bibliothèque hostile, ou l'exécution d'écrasements GOT/PLT à l'intérieur d'un processus. L'écrasement du GOT/PLT implique généralement de modifier les paramètres de protection de la mémoire (mprotect), d'écrire le nouveau code (write), puis de rétablir les paramètres d'origine (restore) après l'injection.

Une fonction accrochée appelle généralement le symbole libc réel en utilisant dlsym(RTLD_NEXT, ...) pour son fonctionnement normal. Il filtre ou modifie ensuite les résultats uniquement pour les cibles qu'il souhaite cacher. Vous trouverez ci-dessous un exemple de base d'un filtre LD_PRELOAD pour la fonction readdir().

#define _GNU_SOURCE       // GNU extensions (RTLD_NEXT)
#include <dlfcn.h>        // dlsym(), RTLD_NEXT
#include <dirent.h>       // DIR, struct dirent, readdir()
#include <string.h>       // strstr()

// Pointer to the original readdir()
static struct dirent *(*real_readdir)(DIR *d);

struct dirent *readdir(DIR *d) {
    if (!real_readdir) // resolve original once
        real_readdir =
            dlsym(RTLD_NEXT, "readdir");
    struct dirent *ent;
    // Fetch next dir entry from real readdir
    while ((ent = real_readdir(d)) != NULL) {
        // If name contains the secret marker,
        // skip this entry (hide it)
        if (strstr(ent->d_name, ".secret"))
            continue;
        return ent; // return visible entry
    }
    return NULL; // no more entries
}

Cet exemple remplace readdir() en cours de traitement en fournissant une bibliothèque résolue avant le véritable libc, masquant ainsi les noms de fichiers qui correspondent à un filtre. Les outils historiques de dissimulation en mode utilisateur et les "rootkits" légers ont utilisé LD_PRELOAD ou les correctifs GOT/PLT pour dissimuler des processus, des fichiers et des sockets. Les attaquants injectent également des objets partagés dans des services spécifiques pour atteindre une furtivité ciblée sans avoir besoin de modules du noyau.

L'interposition dans l'espace utilisateur n'affecte que les processus qui chargent la bibliothèque malveillante (ou dans lesquels elle est injectée). Il est fragile pour la persistance à l'échelle du système (les fichiers de service/d'unité, les environnements assainis, les binaires setuid/statiques compliquent les choses). La détection est simple par rapport aux crochets du noyau : vérifiez les entrées suspectes LD_PRELOAD/LD_AUDIT, les objets partagés mappés inattendus dans /proc/<pid>/maps, les incohérences entre les bibliothèques sur disque et les importations en mémoire, ou les entrées GOT modifiées. Les outils d'intégrité, les superviseurs de service (systemd) et une simple inspection de la mémoire des processus permettent généralement de mettre en évidence cette technique.

Techniques de crochetage utilisant l'eBPF

Un modèle de mise en œuvre de rootkit plus récent implique l'utilisation abusive de l'eBPF (extended Berkeley Packet Filter). L'eBPF est un sous-système de Linux qui permet aux utilisateurs privilégiés de charger des programmes en bytecode dans le noyau. Bien qu'elle soit souvent décrite comme une VM sandboxée "," sa sécurité repose en fait sur un vérificateur statique qui s'assure que le bytecode est sûr (pas de boucle infinie, pas d'accès illégal à la mémoire) avant qu'il ne soit compilé par JIT en code machine natif pour une exécution avec une latence proche de zéro.

Au lieu d'insérer un LKM pour modifier le comportement du noyau, un attaquant peut charger un ou plusieurs programmes eBPF qui s'attachent aux événements sensibles du noyau. Par exemple, on peut écrire un programme eBPF qui s'attache à l'entrée de l'appel système pour execve (via un kprobe ou un tracepoint), ce qui lui permet de surveiller ou de manipuler l'exécution du processus. De même, l'eBPF peut s'accrocher à la couche LSM (comme les notifications d'exécution de programme) pour empêcher certaines actions ou les masquer. Un exemple est présenté ci-dessous.

// Attach this eBPF program to the tracepoint for sys_enter_execve
SEC("tp/syscalls/sys_enter_execve")
int tp_sys_enter_execve(struct sys_execve_enter_ctx *ctx) {
    // Get the current process's PID and TID as a 64-bit value
    // Upper 32 bits = PID, Lower 32 bits = TID
    __u64 pid_tgid = bpf_get_current_pid_tgid();

    // Delegate handling logic to a helper function
    return handle_tp_sys_enter_execve(ctx, pid_tgid);
}
Exemple de code d'accrochage de l'eBPF

TripleCross et Boopkit en sont deux exemples publics notoires. TripleCross a fait la démonstration d'un rootkit qui utilisait l'eBPF pour accrocher des appels de système comme execve pour la persistance et la dissimulation. Boopkit a utilisé l'eBPF comme canal de communication secret et comme porte dérobée, en joignant des programmes eBPF capables de manipuler les tampons des sockets (ce qui permet à une partie distante de communiquer avec le rootkit par le biais de paquets élaborés). Il s'agit de projets de validation, mais ils ont prouvé la viabilité de l'eBPF dans le développement de rootkits.

Les principaux avantages sont que l'accrochage de l'eBPF ne nécessite pas le chargement d'un LKM et qu'il est compatible avec les protections modernes du noyau. Pour les noyaux supportés par l'eBPF, il s'agit d'une technique efficace. Mais si elles sont puissantes, elles sont aussi limitées. Ils nécessitent des privilèges élevés pour être chargés, sont limités par les contrôles de sécurité du vérificateur, sont éphémères lors des redémarrages (ce qui nécessite une persistance distincte) et sont de plus en plus faciles à découvrir par les outils d'audit et de criminalistique. L'utilisation de l'eBPF sera particulièrement visible sur les systèmes qui n'utilisent généralement pas l'outil eBPF.

Techniques d'évasion utilisant io_uring

Bien que io_uring ne soit pas utilisé pour le hooking, il mérite une mention honorable en tant qu'ajout récent aux techniques d'évasion EDR utilisées par les rootkits. io_uring est une API d'E/S asynchrone, basée sur des tampons en anneau, qui permet aux processus de soumettre des lots de demandes d'E/S (SQE) et de récolter des achèvements (CQE) avec une surcharge minimale de syscall. Il ne s'agit pas d'un cadre de crochetage, mais sa conception modifie la surface de visibilité des syscalls et expose de puissantes primitives orientées vers le noyau (tampons enregistrés, fichiers fixes, anneaux mappés) dont les attaquants peuvent abuser pour des E/S furtives, des flux de travail évitant les syscalls ou, lorsqu'ils sont combinés à une vulnérabilité, comme primitive d'exploitation conduisant à l'installation de crochets à une couche inférieure.

Les schémas d'attaque se répartissent en deux catégories : (1) évasion/abus de performance: un processus malveillant utilise io_uring pour effectuer de nombreuses opérations de lecture/écriture/métadonnées par lots, de sorte que les détecteurs traditionnels par appel de système voient moins d'événements ou des schémas atypiques ; et (2) exploitation: les bogues dans les surfaces io_uring (mappages d'anneaux, ressources enregistrées) ont historiquement été le vecteur d'une escalade des privilèges, après quoi un attaquant peut installer des crochets dans le noyau par des moyens plus traditionnels. io_uring contourne également certaines enveloppes libc si le code soumet des opérations directement, de sorte que le hooking du userland qui intercepte les appels libc peut être contourné. Un flux simple de soumission/récupération est illustré ci-dessous :

// Minimal io_uring usage (error handling omitted)

// io_uring context (SQ/CQ rings shared with kernel)
struct io_uring ring;

// Initialize ring with space for 16 SQEs
io_uring_queue_init(16, &ring, 0);

// Grab a free submission entry (or NULL if full)
struct io_uring_sqe *sqe =
    io_uring_get_sqe(&ring);

// Prepare SQE as a read(fd, buf, len, offset=0)
io_uring_prep_read(sqe, fd, buf, len, 0);

// Submit pending SQEs to the kernel (non-blocking)
io_uring_submit(&ring);

struct io_uring_cqe *cqe;
// Block until a completion is available
io_uring_wait_cqe(&ring, &cqe);
// Mark the completion as handled (free slot)
io_uring_cqe_seen(&ring, cqe);

L'exemple ci-dessus montre une file d'attente de soumission alimentant de nombreuses opérations de fichiers dans le noyau avec un seul ou quelques appels syscall io_uring_enter, réduisant ainsi la télémétrie des appels syscall par opération.

Les adversaires intéressés par la collecte furtive de données ou l'exfiltration à haut débit peuvent passer à io_uring pour réduire le bruit des appels système. io_uring n'installe pas de crochets globaux ou ne modifie pas le comportement d'autres processus ; il s'agit d'un processus local, à moins qu'il ne soit associé à une escalade des privilèges. La détection est possible en instrumentant les appels de système de io_uring (io_uring_enter, io_uring_register) et en observant les schémas anormaux : lots anormalement importants, nombreux fichiers/buffers enregistrés ou processus qui effectuent des opérations de métadonnées lourdes par lots. Les différences de version du noyau sont également importantes : les fonctionnalités de io_uring évoluent rapidement, de sorte que les techniques d'attaque peuvent dépendre de la version. Enfin, comme io_uring nécessite un processus malveillant en cours d'exécution, les défenseurs peuvent souvent l'interrompre et inspecter ses anneaux, ses fichiers enregistrés et ses mappages de mémoire pour découvrir une utilisation abusive.

Conclusion

Les techniques d'accrochage sous Linux ont beaucoup évolué depuis le simple écrasement d'un pointeur dans une table. Nous voyons maintenant des attaquants exploiter des cadres légitimes d'instrumentation du noyau (ftrace, kprobes, eBPF) pour implanter des crochets qui sont plus difficiles à détecter. Chaque méthode, des patchs de tables IDT et syscall aux crochets en ligne et aux sondes dynamiques, a ses propres compromis en termes de furtivité et de stabilité. Les défenseurs doivent être conscients de tous ces vecteurs possibles. Dans la pratique, les rootkits modernes combinent souvent plusieurs techniques d'accrochage pour atteindre leurs objectifs. Par exemple, PUMAKIT utilise un crochet direct de table d'appel système et des crochets ftrace, et Diamorphine utilise des crochets d'appel système ainsi qu'une sonde kprobe pour contourner le masquage des symboles. Cette approche par couches signifie que les outils de détection doivent vérifier de nombreuses facettes du système : les entrées IDT, les tables syscall, les registres spécifiques au modèle (pour les crochets sysenter), l'intégrité des prologues de fonctions, le contenu des pointeurs de fonctions critiques dans les structures (VFS, etc.), les opérations ftrace actives, les kprobes enregistrés et les programmes eBPF chargés.

Dans la deuxième partie de cette série, nous passons de la théorie à la pratique. Armés de la compréhension de la taxonomie des rootkits et des techniques de hooking abordées ici, nous nous concentrerons sur l'ingénierie de la détection, en construisant et en appliquant des stratégies de détection pratiques pour identifier ces menaces dans des environnements Linux réels.

Partager cet article