Hook de lib sous Linux avec LD_PRELOAD

getmyflagyaap authorBonjour à toutes et à tous !

Afin de ne pas laisser la fréquence de publication se rapprocher dangereusement du zéro absolu, et aussi pour réussir mon entrée sur ce magnifique blog, je vous livre aujourd’hui une technique toute simple qui vous permettra peut-être de valider les premiers crackme des sites de challenges en ligne :) (ne vous inquiétez pas, je ferai un effort sur la taille des phrases à l’avenir…).

Dans ce premier billet, nous verrons il est possible de changer complètement le fonctionnement d’un programme à notre avantage. Pour réussir ce tour de force, je vais vous montrer comment redéfinir certaines fonctions utilisées par les programmes sous Unix. Cela peut être utile dans de nombreux cas, et il est toujours intéressant de s’attaquer à ce genre de problématique pour bien comprendre la compilation et la façon dont sont lancés les programmes. Et finalement n’est-ce pas l’essence même de ce blog ?

Exemple simple

Analyse

Pour cette petite démonstration préambulaire, nous prendrons le programme file. Ce programme comme vous le savez, permet d’identifier le type d’un fichier. Sur ma fidèle Debian, il est présent dans le repertoire /usr/bin/, la preuve :

root@debian:~/hook_lib# which file
/usr/bin/file

A l’aide d’un lancement récursif, nous pouvons noter que la commande file est fournie par un binaire 32 bits utilisant des bibliothèques partagées :

root@debian:~/hook_lib# file /usr/bin/file
/usr/bin/file: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=0xdef5410b44a59f16580b0a576a68b8d9aacc5d36, stripped

Pour avoir un poil plus d’informations, nous pouvons utiliser l’utilitaire ltrace (mémo : lib trace) qui permet de tracer les appels à des bibliothèques externe :

root@debian:~/hook_lib# ltrace file /usr/bin/file
__libc_start_main(0x8048d40, 2, 0xbfe28ec4, 0x8049b80, 0x8049b70 
[...]
strlen("/usr/bin/file")                                                              = 13
[...] 

Voir en annexe pour plus d’informations sur les import d’un programme.

Parmi les nombreuses fonctions utilisées, nous voyons que file appelle la fonction strlen. Un “man 3 strlen” précise qu’elle sert à calculer la longueur d’une chaîne.

Exploit

C’est bien beau mais à quoi ça nous sert tout ça me direz-vous ? Et bien c’est très simple mon enfant vous répondrais-je , nous savons que le programme pour fonctionner utilise un morceau de code contenu à un autre emplacement que dans lui même (le binaire file). Ce morceau de code est appelé une librairie, sous Windows elles sont identifiable par leur extension .dll, sous Unix elle sont souvent affublées du suffixe .so. Il est très simple de fabriquer un librairie, imaginez bien que si l’on remplace une librairie par un autre, contenant les mêmes fonctions mais légèrement modifiées, on change le comportement du programme… Et si je vous disais maintenant qu’il n’est même pas nécessaire de bidouiller vos librairies ? intéressez ?

Tout d’abord nous allons réécrire la fonction strlen que l’on a précédemment identifié afin de modifier son comportement. On veillera cependant à ce qu’elle se comporte plus où moins comme la fonction normal (par exemple on retournera un nombre et pas une lettre). Voici notre fichier strlen.c :

#include <stdio.h>

int strlen(const char *c) {
        printf("Le programme veut calculer la taille de la chaine : %s\n", c);
        return 4;
}

Cette fonction strlen custom permet d’afficher la chaîne de caractères dont le programme veut connaitre la taille, puis elle renvoie le nombre 4.

A présent, il suffit de compiler cette fonction avec les arguments adéquats pour créer une bibliothèque partagée :

root@debian:~/hook_lib# gcc -shared -fPIC -Wall strlen.c -o strlen.so
  • -Wall, on active (presque) tous les messages d’avertissement.
  • -shared indique que nous compilons une bibliothèque partagée
  • -fPIC précise qu’il faut générer un binaire dont le code n’est pas dépendant à sa position dans la mémoire – Ça me semble évident pour une bibliothèque partagée mais soit.

Et enfin nous y sommes, la petite chose dans laquelle toute la magie de cet article réside est l’humble variable d’environnement LD_PRELOAD. Cette variable géniale, une fois renseignée permet de préciser les bibliothèques à utiliser avant d’aller les chercher dans les chemins par défaut ! En gros grâce à elle, il vous sera possible de précharger vos librairies perso avant les librairies standards contenues entre autres dans /usr/lib/*.

root@debian:~/hook_lib# LD_PRELOAD=/root/hook_lib/strlen.so file /usr/bin/file
Le programme veut calculer la taille de la chaine : /usr/bin/file
/usr/bin/file:Le programme veut calculer la taille de la chaine : /usr/bin/file
 Le programme veut calculer la taille de la chaine : ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=0xdef5410b44a59f16580b0a576a68b8d9aacc5d36, stripped
ELF

Et voilà, nous avons récupéré des valeurs utilisées en interne par le programme. Dans cet exemple précis je le concède, ça ne sert absolument à rien mais comme vous êtes gourmand, nous allons voir un exemple un peu plus concret. Accrochez-vous ça va secouer (surtout vu la qualité de mon code …).

Exemple utile

Imaginons un challenge où le but serait de récupérer le flag contenu dans la variable a :

#include <stdio.h>
#include <string.h>

int main() {
	char a[10] = "FLAG_toto";
	char b[10] = "";
	printf("Enter your password: ");
	scanf("%9s", b);
	if(strcmp(a, b) == 0) {
		printf("Win!\n");
	} else {
		printf("Fail...\n");
	}
	return 0;
}

Un petit coup de gcc plus tard…

root@debian:~/hook_lib# gcc -Wall -o chall chall.c
root@debian:~/hook_lib# file chall
chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=3dc0245743cb3b719ab08f9e07a6da0f77f54e7a, not stripped

Nous voilà à présent avec un beau binaire compilé dynamiquement, et un challenge parfaitement crédible…  – J’espère que vous voyez où je veux en venir. ;)

root@debian:~/hook_lib# ./chall
 Enter your password: monpassword
 Fail...

Attaquons nous à la partie exploit de ce petit programme. C’est parti pour la redéfinition de strcmp ! – Interrogation personnelle partagée “peut-on parler de surcharge pour un langage non-objet ?”

Comme tout à l’heure, nous allons fabriquer une librairie pirate que l’on préchargera avec LD_PRELOAD :

#include <stdio.h>

int strcmp(const char *s1, const char *s2) {
	printf("s1 = %s\n", s1);
	printf("s2 = %s\n", s2);
	return 0;
}

Nous pouvons à présent retenter notre chall :

root@debian:~/hook_lib# LD_PRELOAD=~/hook_lib/strcmp.so chall
 Enter your password: zut je n'ai pas le password :(
 s1 = FLAG_toto
 s2 = zut 
 Win!

Et c’est la victoire ! Nous récupérons le mot de passe attendu et le programme pense que l’on a rentré le bon mot de passe car nous renvoyons constamment 0 dans notre redéfinition de strcmp !

Habile isn’t it?

Je vous laisse la joie d’étudier LD_LIBRARY_PATH (man ld.so) et les autres moyens d’injecter des librairies custom afin de détourner le déroulement des programmes.

Si vous avez la moindre précision à apporter ou quelques suggestions d’amélioration, je serais honoré de pouvoir en discuter dans les commentaires :)

Annexes

Vous trouverez ci-dessous quelques commandes utiles pour avoir plus d’infos sur un binaire Linux (format ELF).

Afficher la section .interp. Celle-ci contient le chemin vers le chargeur dynamique (dynamic loader ou encore runtime linker) qui se chargera d’aller chercher les bibliothèques externes.

root@debian:~/hook_lib# readelf -p .interp chall

Vidange textuelle de la section « .interp »:
  [     0]  /lib64/ld-linux-x86-64.so.2

Afficher la table des symboles :

root@debian:~/hook_lib# readelf -s chall

Table de symboles « .dynsym » contient 7 entrées:
   Num:    Valeur         Tail Type    Lien   Vis      Ndx Nom
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND strcmp@GLIBC_2.2.5 (2)
     5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __isoc99_scanf@GLIBC_2.7 (3)

Table de symboles « .symtab » contient 68 entrées:
[...]

Afficher la section dynamique :

root@debian:~/hook_lib# readelf -d chall

Section dynamique à l'adresse de décalage 0x880 contient 24 entrées:
  Étiquettes Type                         Nom/Valeur
 0x0000000000000001 (NEEDED)             Librairie partagées: [libc.so.6]
 0x000000000000000c (INIT)               0x400478
 0x000000000000000d (FINI)               0x400704
[...]
 0x0000000000000000 (NULL)               0x0
root@debian:~/hook_lib# readelf -d /usr/bin/file

Section dynamique à l'adresse de décalage 0x3e08 contient 26 entrées:
  Étiquettes Type                         Nom/Valeur
 0x0000000000000001 (NEEDED)             Librairie partagées: [libmagic.so.1]
 0x0000000000000001 (NEEDED)             Librairie partagées: [libz.so.1]
 0x0000000000000001 (NEEDED)             Librairie partagées: [libc.so.6]
 0x000000000000000c (INIT)               0x400ea8
[...]
 0x0000000000000000 (NULL)               0x0

Attention, je ne l’ai pas précisé tout à l’heure mais il est important de faire des lib utilisant la même architecture que le binaire (car le nombre de bits utilisés par l’adressage du processeur peut changer). Par exemple pour créer une lib en 32 bits sur un système en 64 bits, vous pouvez procéder comme ceci :

root@debian:~/hook_lib# gcc -Wall -shared -fPIC -m32 -o strcmp_32.so strcmp.c 
root@debian:~/hook_lib# file strcmp*.so
strcmp_32.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, BuildID[sha1]=150ceccf2b2971d888ab09e1f882875d11d6f2f1, not stripped
strcmp.so:    ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=90ad353cfa60b8acee0169fff1ddb1971ae4db19, not stripped

Et pour ceux qui ont l’œil vif, je vous prie d’excuser les changements d’architecture qui surviennent entre mes exemples, mais je parie que vous ne l’aviez même pas remarqué :p

La bise !