Technologie rootkit sous Linux/Unix
Signature : | Mis en ligne le : 05/02/2009
Catégorie(s) :
  • GNU/Linux Magazine HS
  • | Domaine :
    Commentez
    Article publié dans :
    Achetez
    Linux Magazine HS 32 :
    Version Papier

    Jusqu’à cette année, la prolifération de codes malveillants prédite n’a pas eu lieu. Pourtant, le danger n’est pas écarté au vu du nombre conséquent de serveurs sous Linux/Unix. Ce parc de machines constitue une cible de plus en plus attrayante et en particulier pour l’installation de rootkits. Le but de cet article n’est pas d’en présenter les techniques les plus récentes afin de divulguer clé en main une attaque reproductible. Il s’agit, au travers d’un exemple décortiqué, d’expliquer leur fonctionnement global et de mettre en lumière les problèmes techniques inhérents à leur conception : connaissance pointue du noyau, programmation fortement concurrentielle et risques pour la machine de test.

    1. Introduction aux rootkits

    Contrairement aux idées reçues, un rootkit n’est pas un code malveillant à proprement parler. Au-delà de tout aspect moral, il devrait plutôt être défini de manière neutre comme un ensemble de techniques de furtivité et de camouflage au sein d’un système d’information [Chap. 7-01]. Le camouflage considère la dissimulation de données statiques telles que les fichiers, tandis que la furtivité revêt un aspect dynamique avec la dissimulation de données actives dans un contexte d’exécution. Comme pour les virus, c’est l’usage frauduleux qui en a été fait qui a faussé notre perception du problème. De manière générale, le principe de furtivité est très difficile à formaliser et n’a fait l’objet d’études théoriques que très récemment d’abord par Z. Zuo et M. Zhou, puis par E. Filiol [02, 03]. Il faut bien comprendre que sous le terme de rootkit se cachent des techniques et non des programmes. C’est le déploiement de ces techniques qui se traduit de manière concrète par des " modifications du système d’information ", ces modifications pouvant prendre différentes forme [04]. Il peut s’agir de la simple installation de programmes autonomes, mais aussi d’altérations d’éléments logiciels de l’environnement, et même parfois matériels au travers des firmwares [05]. L’utilisation du terme anglais kit traduit cet agglomérat de modifications qui peut être vu comme un panel d’outils. Le terme root, quant à lui, provient d’une conception erronée liée à l’utilisation illégitime des rootkits afin de maintenir dans le temps un contrôle privilégié sur le système. Si les principes fondamentaux de la furtivité sont génériques, leur mise en œuvre au travers des modifications est fortement liée à la plate-forme et à son système d’exploitation. Pour les lecteurs intéressés, le livre de référence sur les rootkits, écrit par G. Hoglund et J. Butler, donne une vue très complète des différentes techniques existantes [06]. En revanche, les exemples d’implémentation sont spécifiques à Windows. Nous allons donc adopter, pour cet article, une démarche de tutoriel similaire, mais orientée vers les systèmes Linux/Unix. Après cette courte introduction, nous traiterons donc dans la seconde partie de la migration historique des rootkits vers le noyau. Ce phénomène nous amènera directement au cœur du sujet. Nous y introduirons, dans un premier temps, les bases de la programmation noyau avant de dérouler le cycle de vie d’un rootkit au travers d’un exemple commenté.

    2. Migration du monde utilisateur vers le noyau

    Les premiers rootkits sont apparus sous UNIX, dans un contexte purement utilisateur, par remplacement d’applications légitimes avec des versions altérées ou bien par modification directe de leur code. Les cibles privilégiées ont bien évidemment été les utilitaires mis à disposition par le système pour sa configuration et sa surveillance. L’exemple donné le plus souvent est celui de tr0n. Au moment de son installation, ce dernier commence par stopper le démon syslogd pour qu’aucun enregistrement ne vienne trahir ses actions. Pour effacer toute trace de son installation et de son exécution, il remplace alors un certain nombre d’utilitaires par des versions malicieuses : du, find, ifconfig, login, ls, netstat, ps, top, sz ainsi que syslogd avant de le redémarrer. Dissimulé au travers de ces applications, il amorce des services distants tels que telnetd ou rsh et configure inetd pour un démarrage automatique, tout ceci dans le but de faciliter l’accès à attaquant. Ce qui ressort finalement de cet exemple, c’est le nombre important de modifications à apporter et la pertinence dans le choix des cibles. Au final, ces modifications restent facilement détectables par de possibles inconsistances dans le croisement des informations obtenues auprès des différents outils, ou tout simplement par le déploiement d’un contrôle d’intégrité [07]. En réponse, les attaquants ont peu à peu modifié leur angle d’approche en visant dans un premier temps les bibliothèques dynamiques partagées de l’espace utilisateur, puis les modules du noyau. Cette évolution suit une progression tout à fait logique : migration vers le bas niveau qui offre le contrôle des couches supérieures et tentative d’action le plus tôt possible dans la chaîne d’exécution afin de prendre de vitesse les protections [Chap. 7-01]. Cette évolution respecte un principe de mise en commun qui permet de diminuer l’ampleur des modifications nécessaires [04]. La majorité des techniques actuelles se situent au niveau du noyau et vont donc constituer le fond de notre article. À terme, cette évolution est amenée à se poursuivre avec l’engouement actuel pour les techniques de virtualisation. Elles constituent, à n’en pas douter, la prochaine étape de cette migration comme l’illustre le rootkit concept SubVirt [08].

    3. Développement et chargement de modules noyau

    La migration des rootkits vers le noyau a modifié fondamentalement la conception de ces codes malveillants. Leur élaboration requiert maintenant des outils et des méthodes de tests plus complexes, adaptés au développement de modules noyau. Nous allons donc présenter succinctement les bases nécessaires à la compréhension des analyses qui vont suivre. Pour ceux qui maîtrisent déjà ces fondamentaux, ils sont invités à continuer directement avec la partie suivante. Des informations plus détaillées que celles présentées ici sont disponibles dans la littérature correspondante [09].

    3.1 Particularité de la programmation noyau

    À première vue, le code d’un module noyau est très similaire à celui d’une application utilisateur écrite en C. Le fichier de source donné dans l’exemple ci-dessous fournit un squelette basique de module sous une plate-forme Linux. Il définit principalement deux routines : une de chargement et une de déchargement, qui sont enregistrées auprès des services disponibles à l’aide des primitives module_init et module_exit.

    La réelle particularité de la programmation noyau réside dans l’approche événementielle. Un module propose un certain nombre de services au travers d’interfaces prédéfinies. Une fois enregistré auprès du système, il se place en écoute sur ces interfaces à la manière d’un serveur et traite les différentes requêtes reçues. Pour chaque interface supportée, une routine de traitement doit être associée. Le premier exemple donné est très basique, puisque les seuls services offerts par le module sont son chargement et son déchargement. La définition de services supplémentaires, illustrée dans un second exemple, passe par la définition de nouvelles routines dont les signatures sont contraintes par l’interface considérée (open, read, write, ioctl…). Depuis le mode utilisateur, les requêtes sont alors transmises au module à l’aide d’appels système de manière identique à la manipulation de fichiers.

    Partie serveur en mode noyau

     

      Partie client en mode utilisateur

     

    Le traitement de ces requêtes par le module n’impose pas de séquentialité et se place dans un contexte fortement concurrentiel. Les routines de traitement doivent donc être réentrantes afin de supporter simultanément de multiples exécutions dans différents contextes. Ceci implique de lourdes précautions sur la manipulation des structures de données, à savoir la gestion d’accès concurrents dans le cas de données partagées et le cloisonnement parfait dans le cas d’instances locales. Ces données doivent conserver une taille limitée sachant que le code noyau s’exécute dans une pile dédiée dont la taille varie entre 4 et 8 ko. Dans le cas de structures plus importantes, l’allocation devra se faire dynamiquement. Dans le monde utilisateur, il est monnaie courante, lorsqu’on développe, de faire appel aux différentes fonctions offertes par les bibliothèques telles que libc. Ces bibliothèques ne sont pas disponibles depuis le noyau. Le module devra donc recourir uniquement aux fonctions exportées par celui-ci. Ces fonctions, ainsi que les différentes structures modélisant les objets noyau (processus, threads…), sont principalement déclarées dans les headers spécifiques des répertoires include/linux et include/asm. La liste de ces fonctions et leur description est disponible auprès de différents sites [10]. Nous ne détaillerons que celles nécessaires au long des prochaines parties.

    3.2 Compilation

    Dans les versions les plus récentes du noyau (2.6.x), la compilation de modules a été grandement simplifiée. Les instructions de compilation doivent être définies dans deux fichiers de configuration sous le répertoire sources : kbuild et makefile. Des fichiers de compilation pour un module éclaté en trois fichiers sources sont donnés à titre d’exemple. Si la structure de ces fichiers n’est fondamentalement pas différente des makefile traditionnels, la méthode de compilation diffère par le fait que la compilation s’arrête avant l’édition de lien. Cette dernière phase sera effectuée dynamiquement au chargement du module. À noter que les fichiers objets obtenus présenteront l’extension .ko et non .o.

     

    /img-articles/lmhs/32/art-1/t0.jpg

     

     

    3.3 Chargement de module

    Une fois le module compilé, la phase de test est très importante pour vérifier sa stabilité. Le mécanisme de LKM (Loadable Kernel Module), disponible sur les systèmes Linux récents, met à disposition du développeur un certain nombre d’outils de gestion des modules. L’ensemble de ces programmes, dont les principaux sont listés un peu plus loin, est disponible dans le package Modutils [11]. Pour assurer leur fonctionnement, le chargement dynamique de modules doit être préalablement activé à la configuration du noyau : Il existe alors un certain nombre d’outils pour la gestion du noyau dont voici des exemples. Cette liste n’est pas exhaustive, mais donne un aperçu des outils les plus utiles pour débuter. Charge un LKM à l’intérieur du noyau. Il résout auprès de l’instance en mémoire l’ensemble des symboles non résolus à partir de la table d’export du noyau. La liste de ces symboles exportés est notamment disponible à l’aide de la commande ksyms. insmod lance alors l’initialisation du module par appel de la routine enregistrée. Appelle la routine de nettoyage associée et décharge un LKM du noyau. Liste les modules actuellement chargés. Permet de configurer le chargement de certains modules. Les informations contenues dans son fichier de configuration /etc/modules.conf vont permettre d’enregistrer les disponibilités et dépendances des modules présents dans la hiérarchie /lib/modules. Combiné avec le démon kerneld, il permet de charger et décharger automatiquement des modules dès qu’une requête leur est destinée, sans aucune intervention manuelle.

    4. Cycle de vie d’un rootkit

    Dans le cadre de cet article, nous ne pouvons pas nous appesantir sur les techniques d’intrusion qui permettent d’introduire le rootkit sur la machine compromise et de lancer son exécution dans un contexte administrateur. Il existe un certain nombre d’exploits, décrits de manière détaillée au travers de nombreux articles, permettant ce type d’opération. Nous allons considérer que le cycle de vie d’un rootkit commence avec son exécution. SuckIt, publié dans le journal Phrack en 2001, est un exemple particulièrement représentatif de mise en œuvre de techniques de furtivité. Il va nous permettre d’illustrer les différentes phases du cycle de vie du rootkit, au travers d’analyses de code détaillées [12]. Les portions de code qui sont citées au cours de cet article ont été recommentées et, dans certains cas, simplifiées. Les quatre phases les plus cruciales vont maintenant être abordées en détail.

    4.1 Collecte d’information

    Afin de s’adapter au système et de pouvoir s’exécuter dans des conditions optimales, il est nécessaire au rootkit d’obtenir des informations sur la machine qu’il tente de compromettre. Ces informations vont s’avérer particulièrement utiles dans sa phase initiale d’installation et de configuration. De fait, la collecte d’information reste souvent une étape préalable aux modifications qui constituent le corps du rootkit.

    4.1.1 Accès aux informations du système

    Les informations les plus utiles pour compromettre un système sont bien souvent les plus critiques et sont donc stockées dans l’espace mémoire réservé au noyau. Dans le cas où le mécanisme de LKM (cf. 3.3) est supporté, certaines de ces informations sont accessibles au travers des symboles exportés par le noyau. Utilisées par insmod pour l’édition de liens, il existe des fonctions permettant d’accéder à la table de ces symboles, comme le montre cette fonction extraite des sources de SuckIt.

    Mais ce qui fait la particularité de SuckIt par rapport à la majorité des rootkits sous Linux, c’est sa volonté d’indépendance par rapport à ce type de mécanisme. Si le support des LKM offre une grande flexibilité, il peut ne pas avoir été activé lors de la compilation du noyau. Pour contourner cette limitation, SuckIt utilise le périphérique virtuel /dev/kmem capable de manipuler la mémoire virtuelle du système. Au travers de ce module, il devient possible d’accéder à la mémoire réservée au noyau moyennant des privilèges administrateurs. Comme nous l’avons évoqué dans la courte introduction à la programmation noyau (cf. 3.1), la communication depuis le monde utilisateur avec ce module est établie à l’aide des primitives système particulières, utilisées pour la manipulation de fichiers. Les routines définies ci-dessous sont utilisées dans SuckIt afin d’accéder en lecture et en écriture aux plages de mémoire virtuelle. À partir de ces routines, il devient possible de récupérer les informations recherchées sous peine de connaître leur localisation précise.

     

    4.1.2 Accès à la table des appels système

    La localisation des informations dans la mémoire virtuelle n’est pas forcément aussi aisée qu’elle y paraît, comme nous allons le voir au travers de cet exemple d’utilisation. Une des premières informations que SuckIt tente de localiser est la table des appels système. Les appels système constituent une passerelle du monde utilisateur vers le noyau et se révèlent donc une cible de choix pour l’installation de modifications. L’interception de ces appels système offre notamment d’importantes facilités de capture et de contrôle des données échangées. Pour bien comprendre les différentes stratégies adoptées par les rootkits, une visualisation globale de l’architecture système est nécessaire. Une vue schématique est donc rappelée en figure 1.

    /img-articles/lmhs/32/art-1/fig-1.jpg

    Figure 1 : Architecture du système d’exploitation. Gestion des appels système.

    Ce schéma nous donne un début de solution pour la localisation de la table des appels. On s’aperçoit que le basculement du mode utilisateur vers le mode noyau est réalisé par des instructions bien spécifiques : l’interruption INT 80h ou la commande sysenter des processeurs Intel. L’interruption 80h est utilisée comme point d’entrée de notre recherche. Il existe en mémoire une table unidimensionnelle appelée IDT ou Interruption Descriptors Table. Elle contient notamment les adresses des 256 routines de traitement associées. L’adresse de cette table en mémoire est stockée dans le registre IDTR que l’on accède à l’aide de l’instruction sidt. À l’aide de ces informations basiques, nous sommes capables de retrouver l’adresse de la routine de traitement selon le numéro d’interruption affecté. Nous pouvons donc localiser le gestionnaire d’appel système en mémoire, mais nous ne disposons toujours pas de la table en elle-même. Par désassemblage, on s’aperçoit que l’adresse de la table des appels système est utilisée lors d’un appel indirect dans la routine. Par recherche de pattern dans le code, il est possible de localiser l’instruction de saut et, par conséquent, l’adresse de la table. Pour une meilleure compréhension, le processus de localisation a été schématisé en figure 2 avant de présenter le code commenté. La localisation de la table des appels système n’est finalement qu’une première étape permettant de cibler les portions de code noyau à modifier lors de l’installation du rootkit.

    /img-articles/lmhs/32/art-1/fig-2.jpg

    Figure 2 : Localisation de la table des appels système. Analyse de la routine de traitement de l’interruption INT 80h.

    4.2 Déploiement des composantes

    Cette seconde partie traite du déploiement opérationnel des différentes composantes du rootkit et en particulier de l’injection de modules dans le noyau. À noter que l’emplacement de ces modifications n’a pu être déterminé que grâce à la phase préliminaire de collecte d’information.

    4.2.1 Modules embarqués

    Lors de son introduction dans un système, SuckIt se présente sous la forme d’un unique exécutable autonome appelé sk. En réalité, le programme contient en interne un module noyau stocké sous la forme d’un tableau de caractères hexadécimaux. Pour comprendre comment ce module appelé core est embarqué dans l’application, il faut remonter, par analyse du fichier Makefile, au processus de compilation représenté dans sa globalité en figure 3. Dans un premier temps, le module est compilé en omettant la phase finale d’assemblage (1). L’application d’un premier outil parser reconstruit une structure simplifiée du code assembleur en ne conservant que sous une seule section fusionnée, les sections .text contenant le code et .bss contenant les variables globales et statiques (2). La section .bss est relogée dans la sous-section bss_start, tandis que les autres sections sont tout simplement supprimées. On procède alors de manière différée à l’assemblage du code afin d’obtenir une version finale du module (3). Le code objet obtenu est ensuite soumis à un second outil rip qui a la charge de supprimer l’en-tête ELF du fichier pour n’en conserver que l’image minimale à charger dans le noyau (4). Ce code minimal est alors réécrit sous la forme d’un tableau d’hexadécimaux par l’outil bin2hex (5). Le header obtenu par cette dernière opération est finalement inclus dans le projet principal, prêt à être injecté au cours de l’exécution (6).

    4.2.2 Injection dans le noyau

    Le module est maintenant embarqué au cœur de l’application, mais reste l’étape la plus délicate, à savoir le chargement de ce module dans le noyau.

    /img-articles/lmhs/32/art-1/fig-3.jpg

    Figure 3 : Intégration du module noyau aux sources du rootkit. Outils spécifiques de compilation.

     La méthode de chargement majoritairement utilisée par les rootkits reste le mécanisme de LKM (cf. 3.3), mais, ce, de manière détournée. Plutôt que de charger directement le module malveillant en espace noyau  pour plus de discrétion, l’attaquant va infecter un module légitime avant son chargement [13]. Malgré tout, cette approche reste extrêmement surveillée. Ce mécanisme constitue un point de contrôle aisé, augmentant de fait la probabilité de détection. Comme nous l’avons souligné précédemment, SuckIt se distingue des autres rootkits par son utilisation du périphérique mémoire kmem, afin de faire preuve d’une plus grande furtivité. En contrepartie, la procédure de chargement s’en trouve complexifiée d’autant. Un certain nombre d’opérations devient nécessaire pour pouvoir allouer l’espace noyau, résoudre les symboles du module core et charger son code : (1) La première étape est donc de localiser la fonction kmalloc exportée par le noyau. Dans le cas où les LKM sont activés, un simple appel à get sym est suffisant (cf. 4.1.1). Dans le cas contraire, une solution alternative est la recherche de patterns en espace mémoire. Le code de la fonction de localisation de kmalloc ne sera pas détaillé, car il ne présente pas un intérêt majeur. En revanche, nous pouvons préciser le type de pattern recherché :

    (2) kmalloc ne peut être exécuté en espace utilisateur. Il va donc falloir définir une routine spécifique qui sera exécutée en espace noyau afin de procéder à l’allocation de mémoire. Afin d’exécuter cette routine, la solution est de la substituer à un appel système. L’appel système olduname est la cible parfaite, puisqu’il s’agit d’une primitive obsolète conservée pour des raisons de compatibilité. Sa modification devrait donc passer inaperçue. Nous connaissons son adresse, puisque nous avons à notre disposition la table des appels système. Il devient donc possible de l’écraser avec notre routine au travers du périphérique kmem. Un simple appel à olduname depuis l’espace utilisateur va finalement permettre l’allocation de mémoire noyau. L’argument passé à olduname contient une structure définissant les paramètres de l’allocation et l’adresse de kmalloc. (3) Une fois la mémoire allouée, la prochaine étape avant le chargement est la relocation des symboles. Cette procédure est normalement réalisée lors de l’édition de lien par insmod (cf. 3.3), mais, dans notre cas de figure, nous avons choisi de le contourner. Il va donc falloir le faire manuellement. Les variables globales et statiques ainsi que leurs offsets sont déclarés dans la sous-section bss_start du module core (cf. 4.2.1). Avant l’édition de lien, on ne trouve à ces offsets que des adresses relatives. Pour obtenir les adresses absolues et ainsi mettre à jour les symboles, il suffit d’additionner ces adresses relatives avec l’adresse mémoire de base précédemment allouée pour le chargement module. (4) Le module est maintenant prêt à être chargé en mémoire par simple recopie au travers du périphérique kmem. Une fois le chargement effectué, le code de l’appel système olduname est rétabli.

    4.2.3 Installation permanente

    À l’origine, le rootkit SuckIt, bien qu’opérationnel, reste un code preuve de concept. S’il est capable de modifier dynamiquement le système. Il n’est hélas pas permanent : il suffit de redémarrer le système pour rétablir la mémoire noyau dans son état original et annuler les modifications apportées par le rootkit. Dans le cadre d’une attaque réelle, l’attaquant souhaite le plus souvent maintenir dans le temps son contrôle. Deux aspects supplémentaires doivent donc impérativement être considérés dès la conception : la persistance en mémoire et le redémarrage automatique. Bien que nous ne les abordions pas dans le cadre de cet article, il existe des techniques pour rendre l’installation permanente. Le rootkit ne se limite alors plus à une installation dynamique en mémoire volatile, mais modifie des éléments statiques du système pour survivre au redémarrage. Un exemple d’implémentation opérationnelle repose sur la corruption de l’image statique du noyau utilisée au cours du démarrage de la machine [14]. De manière générale, les chaînons du processus de démarrage peuvent être utilisés de manière détournée pour charger automatiquement le rootkit [Chap. 7-01].

    4.3 Mise à disposition de services

    La première étape de déploiement des composantes constitue déjà un challenge technique en soi, mais elle reste une étape préliminaire. La finalité réelle du rootkit réside dans le maintien d’un contrôle frauduleux par l’attaquant. Afin d’assurer un accès aux services, deux niveaux de communication sont nécessaires pour établir le canal de commande : entre l’attaquant et la machine compromise dans un premier temps, puis entre les composantes du rootkit [04]. Selon ce principe, SuckIt déploie une architecture trois tiers client-serveur, décrite en figure 4. Nous allons maintenant donner plus de détails sur la nature des services offerts, ainsi que sur l’établissement du canal de commande.

    /img-articles/lmhs/32/art-1/fig-4.jpg

    Figure 4 : Architecture trois-tiers du rootkit SuckIt. Client distant – Client local – Module noyau.

    4.3.1 Types de services offerts

    Un rootkit installé offre un pannel de services possibles très étendu, dont la seule limite est finalement l’imagination de l’attaquant. On peut néanmoins les classer dans deux catégories distinctes : les services passifs qui sont de simples fonctionnalités d’espionnage et les services actifs qui impactent sur le comportement du système [04]. Au sein des services actifs, une seconde distinction peut être faite entre les services autonomes, qui sont en réalité de nouveaux services apportés par le rookit (exécution distante de code, attaques par déni de service), et les altérations de services, qui ne font que modifier un comportement existant (dissimulation d’information). Si l’on poursuit sur notre exemple, SuckIt se situe plutôt dans cette dernière catégorie. Dans la majorité des systèmes d’exploitation, les services offerts par le noyau sont accessibles au travers des appels système (cf. 4.1.2). La mise en place des services du rootkit passe alors par l’interception des appels système, aussi appellé hooking, afin de les rediriger vers des versions corrompues. De nouvelles primitives système doivent être définies dans l’espace noyau avec une signature identique à la fonction d’origine. La redirection est réalisée par écrasement des adresses originales dans la table des appels système, elle-même localisée dans la mémoire noyau.

    Les techniques de hooking sont communément utilisées pour la dissimulation d’informations et, en particulier, les fichiers et processus [06]. Prenons un exemple concret. La primitive système getdents est utilisée pour lister les fichiers associés à un répertoire. Des commandes basiques telles que ls y font appel. De manière similaire, la liste des processus est obtenue par un simple appel à getdents sur le répertoire /proc qui contient un sous-répertoire associé à chaque processus actif, nommé selon son identifiant.  L’utilitaire ps a donc un fonctionnement très proche de ls. Pour cacher l’existence de fichiers et de processus aux applications du monde utilisateur, il est donc nécessaire de filtrer les résultats de getdents. Une fois l’interception mise en place, l’élaboration d’une routine de filtrage est très basique comme l’illustre l’extrait de code qui suit. La version hookée de getdents commence simplement par un appel à la fonction originale. Elle parcourt ensuite la liste des structures dirent retournées. Selon le nom des entrées, certaines seront supprimées, en accord avec la configuration du service. SuckIt maintient une liste d’identifiants de processus et de fichiers à dissimuler que l’attaquant peut modifier comme bon lui semble. Ce premier exemple est suffisamment explicite pour expliquer le mécanisme, mais il ne représente qu’un morceau de l’iceberg. La dissimulation totale des fichiers et processus demande l’altération d’autres primitives de manipulation des fichiers et des processus. Bien qu’elles ne soient pas détaillées ici, SuckIt fournit des versions malveillantes de nombreuses fonctions système comme open, read, mais également fork pour dissimuler automatiquement les processus fils ou kill pour interdire la terminaison d’un processus caché.

    4.3.2 Communication entre l’attaquant et la machine corrompue

    La communication entre l’attaquant et la machine corrompue constitue la première interface de l’architecture trois tiers du rootkit SuckIt. Un canal de commande est établi de manière distante au travers d’une connexion réseau. Du côté de l’attaquant, la connexion est initialisée à l’aide d’un paquet spoofé pour passer le firewall. La construction de ce paquet factice est réalisée par une application cliente utilisant des sockets brutes. Du côté de la machine compromise, lors de sa première exécution, la partie utilisateur sk de SuckIt installe le module noyau, puis lance un processus autonome de backdoor avant de se terminer. Le nouveau processus se place alors dans une boucle infinie en attente de requêtes réseau. À la réception d’une commande, le processus relance alors sk avec les arguments extraits du paquet. Le module core étant déjà installé, sk ne lance pas de réinstallation, mais transmet la requête à exécuter, comme nous allons le voir maintenant.

    4.3.3 Communication entre l’espace utilisateur et le noyau

    Nous avons vu jusqu’à présent que l’exécution des services du rootkit requiert des privilèges importants et qu’il était nécessaire bien souvent d’installer ou de modifier un module noyau pour injecter le code malveillant. Mais une fois chargé en mémoire, il est important de conserver un canal de communication avec celui-ci pour transmettre les commandes. Dans le cas de SuckIt, le module core est installé sans l’aide des LKM ; aucune interface n’est donc enregistrée (cf. 3.1). Pour contourner le problème, l’interception des appels système va de nouveau être utilisée, prouvant ainsi la puissance et la souplesse d’utilisation de cette technique (cf. 4..3.1). Pour les mêmes raisons que celles évoquées précédemment, la primitive olduname, permettant d’obtenir des infos sur le noyau, est une cible adéquate. Le buffer passé en argument pour stocker le résultat va servir à transmettre les commandes, ainsi que leurs paramètres. L’extrait de code suivant décrit l’établissement du canal de commande depuis le mode utilisateur. La mise en place du hook, venant d’être présentée, ne sera pas rappelée.

    4.4 Survie au sein du système compromis

    Comme tout code malveillant, le rootkit risque d’être la cible des mesures de protection déployées sur la machine compromise. Durant la conception, l’attaquant doit donc considérer sa propre protection pour éviter toute tentative de détection ou d’éradication. Plusieurs stratégies peuvent être mises en place. La solution la plus primaire est la défense proactive où le rootkit adopte un comportement agressif envers les applications de protection (suppression de fichiers tels que la base de signature d’un antivirus ou terminaison du processus d’analyse). La deuxième solution, plus subtile, est la dissimulation des modifications apportées par le rootkit, ainsi que de son activité. Nous venons de voir lors de la partie précédente comment dissimuler des processus et des fichiers, mais ce n’est pas la seule mesure qu’il est possible de mettre en œuvre.

    4.4.1 Surveillance de la table des appels système

    La table des appels système est un élément critique du système. Son intégrité est donc scrupuleusement surveillée par bon nombre de logiciels de protection. La modification directe des adresses présentée précédemment reste facilement détectable par de telles mesures (cf. 4.3.1). Pour éviter ce risque, SuckIt met en place une astuce assez simple. En réalité, la mise en place du hook n’est pas réalisée sur la véritable table en mémoire, comme nous l’avons décrit dans un premier temps, mais sur une copie. Afin de ne pas embrouiller le lecteur, suivant la progression logique de l’article, seule la technique basique a été décrite. Nous allons maintenant préciser cette nuance, sachant que le mécanisme dans sa globalité reste quasi identique. Les mêmes fonctions de localisation et de lecture sont utilisées pour accéder à la table et la recopier. Sans que l’utilisation de kmem soit nécessaire, l’écrasement des adresses est réalisé dans la copie locale. La table factice est ensuite intégrée à l’image du module core, avant qu’il ne soit chargé en mémoire noyau. La table originale n’a finalement subi aucune modification. Il faut maintenant faire en sorte que le système utilise cette nouvelle table plutôt que l’originale. Lors de la localisation de la table, nous avons conservé l’adresse du saut dans la routine de traitement INT 80h (cf. 4.1.2). Il suffit finalement de patcher la valeur du saut avec l’adresse de la nouvelle table. La détection de cette modification requerrait la mise en place d’un contrôle d’intégrité plus complexe travaillant au niveau du code et non plus des données.

    4.4.2 Surveillance de l’activité réseau

    La communication entre l’attaquant et la machine est sans doute l’un des signes les plus visibles de la compromission. Il est donc important de dissimuler au maximum cette activité réseau. Si le processus de backdoor est déjà dissimulé par le rootkit, il est important que les sockets qu’il utilise le soient aussi. Dans le cas contraire, un simple appel à netstat permettrait de déceler une activité anormale. De manière générale, SuckIt dissimule toutes les sockets ouvertes par des processus cachés. Pour chaque processus, le répertoire /proc/[pid]/fd liste les fichiers ouverts par celui-ci ; il contient donc, entre autres, les fichiers associés aux sockets. Afin d’établir une table référençant les sockets à dissimuler, le rootkit définit une première fonction explorant ce répertoire pour les différentes entrées de la table des processus cachés. À chaque fichier socket rencontré, une nouvelle entrée est ajoutée dans la table. Les numéros de socket ainsi répertoriés devront donc être filtrés à l’aide d’une seconde fonction de manière très similaire à celle utilisée pour dissimuler les processus. Lorsqu’une application ou un utilisateur souhaitera obtenir la liste des sockets ouvertes sur le système, il devra procéder à la lecture des fichiers /proc/net/tcp, udp et raw. Cette fonction de filtre peut donc être déployée dans une version hookée de l’appel système read, détectant toute tentative de lecture de ces trois fichiers spécifiques. Le code de ces deux fonctions est détaillé par la suite. En revanche, il est important de noter que cette mesure ne modifie pas l’activité réseau perçue par une sonde externe surveillant les paquets transitant ou la distribution statistique de l’activité réseau. Ces techniques de détection requièrent des mesures plus avancées comme le chiffrement des paquets ou la simulation statistique d’une activité normale.

    Conclusion

    Même si certaines techniques ont pu évoluer depuis la création de SuckIt, les principes fondamentaux de la conception d’un rootkit restent les mêmes. Au travers de cet exemple, les principaux concepts ont pu être introduits, même si d’autres techniques d’injection en espace noyau, de communication ou même de furtivité existent [Chap. 7 – 03]. A priori, vous avez maintenant à votre disposition les fondamentaux nécessaires à la compréhension du fonctionnement d’autres techniques que nous n’avons pu aborder ici. Au-delà de cette analyse, il est surtout important de comprendre que la menace est bien réelle et que l’installation d’outils de protection appropriés est indispensable (chkrootkit, rootkithunter…). RÉFÉRENCES
    • [01] FILIOL (E.), Techniques Virales Avancées, Éditions Springer, collection IRIS, ISBN : 2-287-33887-8 – 2007.
    • [02] ZUO (Z.) ET ZHOU (M.), " Some Further Theoretical Results about Computer Viruses ", The Computer Journal, Vol. 47, N° 6, 2004.
    • [03] FILIOL (E.), " Formal Model Proposal for (Malware) Program Stealth ", Virus Bulletin Conference, 2007.
    • [04] LACOMBE (E.), RAYNAL (F.) et NICOMETTE (V.), " De l’invisibilité des rootkits : application sous Linux ", Actes du Xème Symposium sur la Sécurité des Technologies de l’Information et de la Communication (SSTIC), 2007.
    • [05] HEASMAN (J.), " Firmware Rootkits and the Threat to the Enterprise ", Black Hat, DC 2007.
    • [06] HOGLUND (G.) et BUTLER (J.), Rootkits, Subverting the Windows kernel, Addison-Wesley Professional, ISBN : 0-321-29431-9, 2006.
    • [07] Tripwire Products : http://www.tripwire.com/products/enterprise/ost/
    • [08] KING (S. T.), CHEN (P. M.), WANG (Y-M.), VERBOWSKI (C.), WANG (H. J.) et LORCH (J. R.), " SubVirt: Implementing Malware with Virtual Machines ", Proceedings of the 2006 IEEE Symposium on Security and Privacy (S&P’06), p. 314-327, 2006.
    • [09] CORBET (J.), RUBINI (A.) et KROAH-HARTMAN (G.), Linux Device Drivers, 3ème édition, Éditions O’Reilly Media, ISBN : 0-596-00590-3, 2006.
    • [10] Linux Kernel 2.6.20 API: http://www.gnugeneration.com/mirrors/kernel-api/book1.html
    • [11] Page de téléchargement/package Modutil : http://www.kernel.org/pub/linux/utils/kernel/modutils/
    • [12] Sd et Devik, " Linux on-the-fly Kernel Patching without LKM ", Phrack 58, Article 0x07, 2001.
    • [13] Truff, " Infecting Loadable Kernel Modules ", Phrack 61, Article 0x0A, 2003.
    • [14] Jbtzhm, " Static Kernel Patching ", Phrack 60, Article 0x08, 2002.
    Vous souhaitez commenter cet article ?
    Brèves Flux RSS
    Édito : GNU/Linux Magazine N°170
    Édito : GNU/Linux Magazine Hors-Série N°71
    Édito : MISC N°72
    Édito : Open Silicium N°10
    Édito : GNU/Linux Magazine N°169
    Communication RSS Com. RSS Presse
    HACKITO ERGO SUM
    GNU/Linux Magazine, partenaire du SymfonyLive Paris
    Opensilicium, partenaire de RTS EMBEDDED
    Linux Pratique et Linux Essentiel, Partenaire de l’Open World Forum
    Gnu/Linux Magazine, Partenaire des JDEV 2013
    Rechercher un article dans notre base documentaire :
    En kiosque Flux RSS

    Le tout nouveau GNU/Linux Magazine est disponible dès maintenant chez votre marchand de journaux et sur notre site marchand.

    Découvrez le sommaire de ce numéro et un aperçu de ce magazine...

    Lire la suite...

    Le tout nouveau GNU/Linux Magazine HS est disponible dès maintenant chez votre marchand de journaux et sur notre site marchand.

    Découvrez le sommaire de ce numéro et un aperçu de ce magazine...

    Lire la suite...

    Le tout nouveau Misc est disponible dès maintenant chez votre marchand de journaux et sur notre site marchand.

    Découvrez le sommaire de ce numéro et un aperçu de ce magazine...

    Lire la suite...

    Le tout nouveau Open Silicium est disponible dès maintenant chez votre marchand de journaux et sur notre site marchand.

    Découvrez le sommaire de ce numéro et un aperçu de ce magazine...

    Lire la suite...

    Le tout nouveau Linux Pratique est disponible dès maintenant chez votre marchand de journaux et sur notre site marchand.

    Découvrez le sommaire de ce numéro et un aperçu de ce magazine...

    Lire la suite...