Partie 23 - Annexe n°1 - comment compiler et déboguer un programme avec une version spécifique de la libc
Annexe n°1 : Comment compiler et déboguer un programme avec une version spécifique de la libc
Si vous souhaitez comparer la gestion de la heap dans différentes versions de la glibc, il va falloir apprendre à compiler un programme avec une libc spécifique. Evidemment, cela peut être aussi utile pour d’autres raisons qui ne sont pas forcément liées au tas.
Pour cela nous avons besoin principalement de deux fichiers :
- la bibliothèque : libc. Il s’agit d’un fichier
.soqui est une bibliothèque dynamique nécessaire étant donné que l’on ne compilera pas en statique ; - l’éditeur de liens : ld. C’est ce qui résout les références entre les symboles (fonctions, variables, etc.) et détermine où chaque composant sera chargé en mémoire.
Il existe deux manières de récupérer ces fichiers :
- automatique : en utilisant un script ;
- manuelle : en téléchargeant soi-même la version de la libc que l’on souhaite utiliser.
🦾 Téléchargement automatique d’une version spécifique de la libc
⬇️ Récupération de la libc et de ld
Bonne nouvelle, il existe déjà des outils tout prêts pour le téléchargement de la libc 🥳 ! Nous allons principalement utiliser deux projets libc-database et glibc-all-in-one, chacun ayant des avantages et inconvénients :
- libc-database
- 🟢 Avantages : offre plus de choix (~520 libc pour Ubuntu)
- 🔴 Inconvénient : ne permet pas de télécharger directement les symboles de débogage
- glibc-all-in-one
- 🟢 Avantages : télécharge automatiquement l’éditeur de liens ainsi que le dossier de débogage
.debug - 🔴 Inconvénients : offre moins de choix (~70 libc pour Ubuntu)
- 🟢 Avantages : télécharge automatiquement l’éditeur de liens ainsi que le dossier de débogage
Nous détaillerons un peu plus loin l’utilité du dossier
.debuglorsque l’on utilisera gdb sur le programme compilé.
1️⃣ libc-databases
Commençons par libc-database qui permet de constituer une sorte de base de données d’un grand nombre de libc.
Initialement, ce projet permet de récupérer les symboles d’une version précise de la libc afin de trouver plus facilement les offsets de fonctions intéressantes comme
system,execveet j’en passe.
Après avoir cloné le projet, utilisons la commande suivante :
1
./get ubuntu
Cette commande va télécharger dans le dossier db toutes les libc utilisées dans Ubuntu. Le taille du dossier est inférieure à 1 Go.
D’autres distributions sont disponibles en utilisant une option parmi :
debian,rpm,centos,centos_stream,arch,alpine,kali.
Dans le dossier db, chaque version de la libc dispose de 4 fichiers :
fichier.so: bibliothèque dynamique ;fichier.symbols: liste des offsets des symboles ;fichier.url: lien de téléchargement du paquet contenant la libc. Le paquet inclut bien plus d’éléments que ce qui est téléchargé par libc-database. Par exemple, le paquet contient l’éditeur de lien ld qui n’est pas téléchargé ici ;fichier.info: description succincte de la libc en question.
Dans notre cas, nous avons seulement besoin du fichier .so. Néanmoins il nous manque l’éditeur de liens car la commande ./get ne le télécharge pas. Pour télécharger ld il va d’abord falloir choisir une version de la libc en particulier.
Par ailleurs, pour une version 2.XX de la glibc, plusieurs fichiers sont disponibles. Ceux qui nous intéressent sont ceux de la forme :
libc6_2.23-xxxxxxx_amd64.so: si vous souhaitez compiler le programme en 64 bits ;libc6_2.23-xxxxxxx_i386.so: si vous souhaitez compiler le programme en 32 bits.
Les versions de la forme
libc6-amd64_2.23*,libc6-i386_2.23*etlibc6-x32_2.23*sont des bibliothèques pour faire de la compilation croisée (cross compilation 🇬🇧). Exemple : compiler depuis une machine 32 bits un programme vers une machine 64 bits.
Pour une même version de la libc (ex :
2.23), il peut y avoir plusieurs sous-versions (ex :0ubuntu11.3et0ubuntu3).
Ainsi, pour récupérer le programme ld de la libc libc6_2.23-0ubuntu11.3_amd64 nous pouvons utiliser la commande ./download comme suit :
1
./download libc6_2.23-0ubuntu11.3_amd64
Cette commande télécharge plusieurs bibliothèques dynamiques dans le dossier libs/libc6_2.23-0ubuntu11.3_amd64. Celle qui nous intéresse est ld-2.23.so (ou ld-linux-x86-64.so.2 qui sont exactement les mêmes fichiers).
Le fichier
libc-2.23.soprésent danslibs/libc6_2.23-0ubuntu11.3_amd64est exactement le même que le fichierlibc6_2.23-0ubuntu11.3_amd64.soprésent dans le dossierdb.
2️⃣ glibc-all-in-one
Vous vous demandez peut-être pourquoi évoquer un autre projet qui, en apparence, remplit presque les mêmes fonctions que libc-database. Tout d’abord, comme mentionné précédemment, chaque outil possède ses propres avantages et inconvénients. Présenter deux outils vous offre la possibilité de choisir celui qui répond le mieux à vos besoins.
D’ailleurs, glibc-all-in-one est utilisé par le projet très connu how2heap qui permet de comprendre, dans les grandes lignes, comment fonctionnent certaines techniques d’exploitation dans le tas. Comme cela dépend en grande partie de la version de la glibc, il est nécessaire de pouvoir compiler certaines techniques avec des versions bien précises de la libc.
Enfin, cela enrichit votre boîte à outils : disposer de plusieurs options est toujours utile, notamment si l’un des deux se révèle insuffisant ou inadapté dans une situation donnée.
Après avoir cloné glibc-all-in-one, nous allons lancer la commande ./update_list qui permet de mettre à jour deux listes list et old_list avec une multitude de libc.
Choisissez la version que vous souhaitez utiliser :
- si la version est dans
old_list: il faut utiliser la commande./download_oldpour la télécharger ; - si la version est dans
list: il faut utiliser la commande./download.
Par exemple, si je souhaite télécharger la version 2.24-3ubuntu1_i386 présente dans old_list, la commande à lancer est ./download_old 2.24-3ubuntu1_i386. Lorsque cette commande a terminé son exécution, le dossier libs/2.24-3ubuntu1_i386 est créé avec plusieurs éléments dont la libc, ld et le dossier .debug (utilisé par gdb pour le débogage) :
C’est tout bon, nous sommes désormais prêts pour la compilation !
⚙️ Compilation et exécution
Dans un même dossier, plaçons le fichier main.c à compiler avec les fichiers suivants :
1
2
3
main.c
ld-2.23.so
libc-2.23.so
Vous n’êtes pas obligés de mettre la libc et ld dans le même dossier que le fichier
main.cmais il faudra alors adapter correctement les chemins dans la commande de compilation sinon vous aurez, lors de l’exécution, des erreurs du type :relocation error: .....Ainsi, il vaut mieux mettre tous ces fichiers au même niveau d’arborescence pour ne pas se tromper en copiant-collant les commandes ci-dessous.
Ensuite, pour que le programme puisse s’exécuter correctement après la compilation, nous créons un lien symbolique libc.so.6 vers libc-2.23.so avec :
1
ln -s libc-2.23.so libc.so.6
libc.so.6est le soname utilisé pour la libc. Il s’agit en quelque sorte d’une indication concernant la compatibilité de la libc réellement utilisée. En utilisant la commandestringsune fois le programme compilé, vous constaterez qu’il ne contient pas la chaîne de caractère"libc-2.23.so"mais plutôt"libc.so.6".De ce fait, si le soname n’est pas présent dans le dossier, vous pourrez avoir des erreurs lors de l’exécution du programme 🤕.
Pour ce qui est de la compilation avec gcc, voici la commande que nous allons utiliser :
1
2
3
gcc main.c -o exe \
-Wl,--dynamic-linker=./ld-2.23.so \
-L. -Wl,-rpath=. -l:./libc-2.23.so
Les différentes options utilisées sont :
-L: spécifie à l’éditeur de liens le dossier contenant les bibliothèques à utiliser ;-l:: permet de spécifier explicitement une bibliothèque. En l’occurrence, nous avons seulement besoin d’inclure la libc ;-Wl,-xyz: cette option permet de transmettre l’option-xyzà l’éditeur de liens. Cela revient à l’appeler ainsi :ld -xyz;--dynamic-linker: chemin vers l’éditeur de lien que l’on souhaite utiliser ;-rpath: comme le programme est lié dynamiquement (la libc ne sera chargée dynamiquement qu’à l’exécution) avec une libc différente, ld a besoin de savoir où la trouver lorsque le programme sera exécuté vu que cette libc n’est pas présente dans les dossiers utilisés par défaut par ld (dossiers contenant la libc utilisée par votre machine par exemple). Ainsi, si vous vous trompez dans cette option, vous n’aurez pas d’erreur à la compilation mais plutôt lors de l’exécution carldva râler 🤬.
Vous pouvez désormais lancer votre programme ./exe et le déboguer avec gdb (gdb ./exe) :
Désormais, le tas sera géré par la version 2.23 de la libc. Cela implique, par exemple, l’absence de tcache.
Il est également possible de vérifier la libc et l’éditeur ld utilisés sans avoir à lancer le programme dans gdb avec la commande ldd :
1
2
3
4
5
$ ldd ./exe
linux-vdso.so.1 (0x00007301ab40a000)
libc.so.6 => ./libc.so.6 (0x00007301ab000000)
./ld-2.27.so => /lib64/ld-linux-x86-64.so.2 (0x00007301ab40c000)
Si vous n’arrivez pas à lancer les commandes liées au tas dans gdb, jetez un œil à la section “Importer les symboles de débogage” un peu plus bas.
🔧 Utiliser patchelf
Je me suis rendu compte que la précédente commande gcc ne fonctionne pas très bien avec les libc post 2.35. Voici un exemple de message d’erreur que vous pouvez rencontrer lors de la compilation :
1
2
/usr/bin/ld : ././libc-2.35.so : référence indéfinie vers « __nptl_change_stack_perm@GLIBC_PRIVATE »
collect2: error: ld returned 1 exit status
Une solution est d’utiliser l’outil patchelf après avoir compilé le programme de manière classique :
1
2
3
gcc main.c -o exe # compilation classique
patchelf --set-interpreter ./ld-linux-x86-64.so.2 exe # chemin vers 'ld'
patchelf --set-rpath . exe
Le programme utilisera désormais la libc 2.35.
💪 Téléchargement manuel d’une version spécifique de la libc
Voyons comment télécharger manuellement une version spécifique de la libc. Cela peut arriver dans le cas où vous souhaitez utiliser une version exotique ou non prise en compte par libc-database.
Nous nous limiterons dans cette section à Ubuntu bien que la méthodologie soit similaire pour les autres distributions issues de Debian. Pour le reste des distributions, le principe est identique même si le gestionnaire de paquets peut différer.
Imaginons que l’on veuille télécharger manuellement la libc 2.23 mais en 32 bits cette fois-ci.
⬇️ Récupération de la libc et de ld
Cherchons la version présente sur le site launchpad.net en cherchant sur notre moteur de recherche préféré avec les mots clés : launchpad libc6 2.23 "i386 binary". Le mot clé i386 permet de restreindre la recherche à la version 32 bits.
En navigant dans les premiers liens de la recherche, nous choisissons le premier qui dispose d’une version plus à jour :
En cherchant sur internet, vous pourrez être amenés à tomber sur des libc préfixées par
libc6-dbg_2.*comme ici. Ce sont les variantesdbgde débogage de la libc qui ne sont pas strippées.Il ne s’agit pas de bibliothèques utilisables pour la compilation mais plutôt de symboles de débogages. En téléchargeant une telle libc, vous trouverez bien un fichier
libc-2.XX.soetld-2.XX.somais ils ne permettront pas d’exécuter le programme.Ce sont d’ailleurs ces fichiers qui sont présents dans le dossier
.debug. De ce fait, si vous ne souhaitez pas utiliser glibc-all-in-one ou que vous n’y avez pas trouvé ce que vous cherchez, vous pouvez constituer le dossier.debugà la main à partir du paquetlibc6-dbg*!
Si votre navigateur bloque le téléchargement du lien, il suffit de faire : clic droit ➡️ copier l’adresse du lien ➡️ ouvrir un nouvel onglet ➡️ copier le lien et saisir “entrée”.
Une fois le paquet téléchargé, nous pouvons le décompresser avec dpkg :
1
dpkg-deb -x libc6_2.23-0ubuntu11.3_i386.deb .
Les fichiers qui nous intéressent sont présents dans ./lib/i386-linux-gnu.
En fonction de la version de la libc téléchargée, l’arborescence du dossier risque de quelque peu changer. Néanmoins, avec
findvous trouverez facilement les fichiersld-2*etlibc-2*.
⚙️ Compilation et exécution
Comme précédemment, nous mettons les fichiers ld-2.23.so et libc-2.23.so dans le même dossier que le fichier main.c à compiler et nous ajoutons un lien symbolique libc.so.6 vers la libc.
Pour la compilation, la commande est quasiment identique si ce n’est l’ajout de -m32 pour compiler en 32 bits.
1
2
3
gcc -m32 main.c -o exe \
-Wl,--dynamic-linker=./ld-2.23.so \
-L. -Wl,-rpath=. -l:./libc-2.23.so
Vous pouvez désormais exécuter votre programme 32 bits avec libc 2.23 et le déboguer !
🛠️ Importer les symboles de débogage
J’ai bien réussi à compiler et exécuter le programme, j’arrive d’ailleurs même à le déboguer mais lorsque je tente d’utiliser les commandes liées au tas dans gdb il me dit :
pwndbg will try to resolve the heap symbols via heuristic now since we cannot resolve the heap via the debug symbols..
Lorsqu’un programme est compilé avec une libc qui n’est pas native, le débogueur peut avoir du mal à afficher correctement le tas ainsi que les informations qui lui sont liées ( bins, arènes …). Le débogueur tente alors de retrouver ces informations de manière heuristique.
Cependant, si le débogueur n’arrive pas à afficher correctement le tas et qu’il est dans les choux, il va falloir que l’on importe nous-mêmes les symboles de débogage. Il y a principalement 3 méthodes pour le faire. Voyons ces méthodes de la plus simple à celle qui demande plus d’efforts.
Considérons la version libc6_2.23-0ubuntu11.3_i386 pour laquelle gdb génère un avertissement ⚠️.
1️⃣ Utiliser pwninit
pwninit est un petit outil très utilisé en pwn qui sert à préparer rapidement un binaire pour l’exploitation. Il permet notamment de modifier (ou patcher) la libc et l’éditeur de liens ld utilisée par le programme afin qu’ils soient exactement les mêmes que ceux utilisés dans le vrai contexte d’exécution du programme.
Par exemple, si la machine vulnérable lance le programme avec la glibc 2.21, en téléchargeant le programme et en le lançant sur votre machine, il s’exécutera avec la libc de votre machine qui a sûrement une version supérieure à 2.40. Et puis, on n’exploite pas un programme avec la glibc 2.30 comme on le fait avec la glibc 2.21, surtout pour de l’exploitation dans le tas 😉.
Grâce à pwninit, nous pouvons aller rapidement à l’essentiel dans l’exploitation et ne plus perdre de temps sur la mise en place du bon environnement d’exécution. Pour l’installer, vous pouvez vous référer aux instructions d’installation disponibles sur la page GitHub de l’outil.
Revenons à nos moutons 🐏 : dans le dossier contenant notre exécutable, la libc et ld, utilisons pwninit comme suit :
1
pwninit --bin exe --libc libc-2.23.so --ld ld-2.23.so
pwninit va télécharger la variante dbg de la libc spécifiée afin de l’unstripper, c’est-à-dire ajouter les symboles de débogage. Pour la version libc6_2.23-0ubuntu11.3_i386, pwninit téléchargera libc6-dbg_2.23-0ubuntu11.3_i386.
Le programme exe_patched généré est celui qui utilise la bibliothèque avec les symboles de débogage. Vous n’avez plus qu’à relancer gdb sur exe_patched et vous pourrez utiliser les commandes du tas sans souci. Et là, plus d’avertissement provenant de gdb 😎!
2️⃣ Utiliser glibc-all-in-one
Sachant que pwninit ajoute les symboles de débogage dans la libc, celle-ci est modifiée et ne nécessite pas de dossier
.debug. Afin d’éviter d’avoir un faux positif en utilisant cette autre méthode, nous vous conseillons de repartir de zéro en reprenant la libc 2.23 32 bits qui n’a pas été modifiée.
Comme cela a été vu précédemment, glibc-all-in-one télécharge automatiquement le dossier .debug. Que contient-il ? Téléchargeons la version 2.23 en 32 bits et voyons ce que contient ce dossier.
1
2
3
4
5
6
7
8
9
10
11
12
13
$ ./download 2.23-0ubuntu11.3_i386
$ tree libs/2.23-0ubuntu11.3_i386/.debug
libs/2.23-0ubuntu11.3_i386/.debug
├── lib
│ └── i386-linux-gnu
│ ├── ld-2.23.so <---
│ ├── libanl-2.23.so
│ ├── libBrokenLocale-2.23.so
│ ├── libc-2.23.so <---
│ ├── libcidn-2.23.so
│ ├── libcrypt-2.23.so
...
Le dossier .debug contient une arborescence de dossiers et surtout ld et la libc non strippées :
1
2
3
4
$ file libs/2.23-0ubuntu11.3_i386/.debug/lib/i386-linux-gnu/libc-2.23.so
libc-2.23.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (GNU/Linux), dynamically linked, interpreter *
empty*, BuildID[sha1]=18f761287ed46e213bec29c2e440e73fd72373be, for GNU/Linux 2.6.32, with debug_info, not stripped
Poursuivons en examinant comment utiliser le dossier .debug dans gdb. Déplaçons le dossier .debug vers le dossier contenant notre exécutable. Si ce n’est pas déjà fait, compilez le programme en utilisant la libc initiale, celle qui n’a pas été modifiée par pwninit.
Ouvrons le programme dans gdb et lançons la commande heap. Vous devriez voir un avertissement concernant l’usage d’heuristique. Utilisons la commande set debug-file-directory ./.debug pour spécifier le chemin du dossier .debug.
A présent, tout fonctionne sans souci ni avertissement :
Avant de lancer la commande
heapou une quelconque commande liée à la heap, il est nécessaire de s’assurer que celle-ci est initialisée et présente en mémoire.Par exemple, mettez un point d’arrêt après qu’un premier appel à
mallocsoit terminé.
Si vous avez des erreurs en compilant un programme avec une version spécifique de la libc > 2.30 et que vous n’arrivez pas à les résoudre, vous pouvez faire ceci : compiler le programme normalement (ex :
gcc main.c -o exe) puis utiliser pwninit pour patcher la libc (ex:pwninit --bin exe --libc libc.so.6 --ld ld-linux-x86-64.so.2).Evidemment la libc et le ld donnés en paramètres à pwninit sont ceux que vous avez téléchargés avec glibc-all-in-one.
3️⃣ Télécharger manuellement la version “dbg”
Il s’agit d’une méthode de dernier recours. Voici comment la mettre en œuvre :
- chercher le paquet dbg qui correspond à la libc utilisée (exemple :
libc6_2.23-0ubuntu11.3_i386➡️libc6-dbg_2.23-0ubuntu11.3_i386) ; - décompresser le paquet avec
dpkg-deb -x (...); - créer un dossier
.debuget y mettre la libc non strippée (exemple :libc-2.23.so). Pas besoin d’y mettre ld ; - ouvrir le programme dans gdb et utiliser la commande
set debug-file-directory ./.debug.




