Exploitation – stack buffer overflow: Blind Return Oriented Programming
La dernière fois, nous avions parlé d’un scénario entièrement à distance c’est-à-dire : exploiter un programme, sans en posséder l’exécutable ni le code source. Exploiter le binaire est effectivement possible dans ce type de scénario, mais seulement dans certains cas. Je m’explique:
Pour exploiter, il va falloir émettre des hypothèses et déterminer si oui ou non chaque hypothèse est bonne. C’est ce qu’on appelle une approche heuristique. Lorsque l’hypothèse est fausse, le programme va probablement crasher (par exemple via SEGFAULT
). En admettant que le binaire soit compilé avec les protections suivantes:
– PIE: Adressage aléatoire des fonctions de librairies partagées (PLT)
– Stack-Protector: Ajout d’un stack cookie permettant de valider que l’adresse de retour n’a pas été écrasée par un buffer overflow
À chaque crash, les adresses seront à nouveau aléatoires et le cookie sera généré à nouveau, sauf dans le cas où le programme redémarre via un fork. Par exemple, un programme qui écoute sur un socket, accepte une connexion, et enfin traite les données le tout dans une seule boucle, ne sera pas exploitable. En revanche, un programme qui écoute sur un socket, accepte une connexion et délègue le traitement des données à un fork, sera exploitable tant que le crash est dans le processus fils. Comme nous n’avons pas le code source ou l’exécutable, il va falloir déterminer tout cela.
La démarche sera donc la suivante (pas d’inquiétude, je décris tout dans les parties dédiées à chaque étape):
- Trouver un crash
- Déterminer si le programme est exploitable à partir de ce crash
- Bruteforcer les octets autour de la fin de notre buffer et déterminer de quelle nature sont ces octets (adresse de stack, stack cookie, adresse de retour d’une fonction)
- Trouver un comportement reproductible en manipulant l’adresse de retour (on appellera cette nouvelle adresse “stop gadget”)
- Trouver l’adresse d’un gadget exécutant “
pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
“. (Ce gadget sera appelé le “brop gadget”) - Trouver le numéro de socket qui nous est associé
- FACULTATIF: Trouver
strcmp
dans la PLT du programme - Trouver
send
ou write dans la PLT du programme - Leak l’exécutable en enchaînant des appels à
send/write
- Leak la libc
- Exploiter le programme via une ropchain classique telle qu’on a vu dans le précédent article.
Alors il y a beaucoup d’étapes ici, mais si on les découpe en briques élémentaires ça passe tout seul.
Pour pouvoir mettre tout ça en pratique, nous vous proposons un petit challenge. Il vous suffira de l’exécuter et de tenter de l’exploiter sur le port 5555 ! Amusez-vous bien !
docker run -p 5555:5556 -ti --rm registry.gitlab.com/vaelio/blindrop:docker-ci
Notes : Nous allons faire pas mal de bruteforce, il est bon d’avoir un système de cache dans son exploit pour éviter de relancer l’exploit complet entre deux tentatives. (Par exemple, en utilisant jsonpickle
). De même, certains passages risquent de bloquer le programme. Si c’est seulement le fork qui est bloqué, il sera plus intéressant de tuer uniquement le processus de ce fork
plutôt que le processus parent qui invaliderait toutes les adresses déjà identifiées. Enfin, il est très fortement conseillé d’utiliser la lib pwntools
1/ Trouver le crash
Normalement cette étape, si vous en êtes à vous renseigner sur le BlindROP, vous connaissez. Pour résumer, on envoie une chaîne de caractères toujours plus longue jusqu’à avoir un crash. On appellera ce nombre de caractères,”offset”.
Attention, cette étape n’est pas à prendre à la légère. Avant de passer à la suite, nous vous recommandons de bien identifier les comportements de l’application : quelle entrée provoque quel résultat (lenteur, crash, fin de transmission des données…).
2/ Déterminer si le programme est exploitable à partir de ce crash
Si on reprend notre introduction, pour que le programme soit exploitable il faut qu’il soit sans protection, ou que le crash intervienne dans un processus fils ayant été généré par un fork.
Pour vérifier ça, nous allons envoyer une chaîne de caractère d’un octet de plus que la précédente (donc offset+1) et on va tenter toutes les combinaisons du dernier caractère jusqu’à ce que le programme ne crashe plus. Si en renvoyant la même chaîne le programme crashe, alors le programme n’est pas exploitable.
Dit autrement, on bruteforce un octet à côté de notre buffer. Si en envoyant une nouvelle requête avec l’octet bruteforcé, le programme crashe, alors le programme n’est pas exploitable. Autre solution, on bruteforce le premier octet et on tente de bruteforcer le deuxième. Si aucune tentative ne fonctionne pour le deuxième octet c’est que le programme n’est pas exploitable.
3/ Bruteforcer les quelques octets après notre buffer et les identifier
Ici, on reproduit la démarche précédente, octet par octet, jusqu’à trouver une adresse qui semble être une adresse de retour (donc en 64 bits avec du PIE, une adresse commençant par 0x55
). (Si vous trouvez une adresse qui commence par 0x7f
et qui n’est pas de la forme 0x7fff*
ni 0x7ffe*
, vous avez trouvé une adresse de librairie)
Dans le challenge, vous devriez tomber sur 2 valeurs 64 bits avant de tomber sur une adresse commençant par 0x55
ou 0x56
.
- Une valeur qui semble aléatoire mais qui termine par
0x00
(le stack cookie). Par exemple0x21b4655a9f053f00
- Une adresse qui devrait commencer par
0x7fff
ou0x7ffe
(un pointeur vers la stack, souvent appelé stack_frame). Par exemple,0x7fff07fc0910
L’adresse qui devrait commencer par 0x55
ou 0x56
devrait être l’adresse de retour de la fonction dans laquelle nous avons un point d’entrée. Par exemple, 0x561320715020
.
Vous l’aurez compris, il est important d’avoir une idée claire de la stack. Voici un schéma représentant la stack dans la situation où le buffer vulnérable est la dernière variable sur la stack et que le programme est compilé avec les protections dont on a parlé au début de l’article.
4/ Trouver le stop gadget
Ici, rien de bien compliqué, il faut itérer sur les adresses autour de l’adresse de retour que nous avons bruteforcée. L’objectif c’est de trouver une adresse qui permette facilement d’afficher plusieurs fois un comportement identifiable. Si le programme envoie par exemple “FIN” à chaque fin de connexion, il est possible de trouver une adresse qui permettra de renvoyer “FIN” plusieurs fois à la suite.
Par exemple, si on obtient le même comportement 4 fois en faisant ce test:
BUFFER + COOKIE + STACKFRAME + ADDRTEST + ADDRTEST + ADDRTEST + ADDRTEST
Alors on peut utiliser ADDRTEST comme stop gadget.
Cette étape est très importante, car elle permettra de tirer des conclusions quant au comportement du programme.
Voilà comment s’utilise par exemple un stop gadget:
BUFFER + COOKIE + STACKFRAME + ADDRTEST + STOP_GADGET
Voilà à quoi devrait ressembler la stack dans cet exemple:
Si le message “FIN” n’est pas reçu, on sait que ADDRTEST est une adresse qui pose problème dans ce contexte.
Voilà un exemple où j’utilise 4 fois une adresse, ce qui provoque 4 affichages du message “Bye”. Ceci ferait un bon stop gadget.
5/ Trouver le BROP Gadget
Cette étape mérite qu’on s’y attarde un peu car ce gadget est très intéressant. De base, ce gadget fait uniquement la suite d’instructions pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
. En revanche, si on prend cette adresse + 7, alors la suite d’instruction devient pop rsi; pop r15; ret
, et si on prend cette adresse + 9, on obtient pop rdi; ret
. Ce gadget à lui seul nous permet de contrôler les registres RDI et RSI. Voici une petite explication via un désassembleur:

En fait, les instructions étant codées sur plusieurs octets, en décalant notre pointeur on change l’interprétation des instructions.
Pour pouvoir trouver ce gadget, il va falloir lutter un peu. Déjà, vu que ce gadget enchaîne 6 pop, un premier test peut être:
BUFFER + COOKIE + STACKFRAME + ADDRTEST + JUNKDATA + JUNKDATA + JUNKDATA + JUNKDATA + JUNKDATA + JUNKDATA + STOP_GADGET
Si nous mettons 6 fois JUNKDATA, c’est parce que nous nous plaçons sur la stack, et comme pour une ropchain classique si le gadget exécute 6 pop, nous devons avoir 6 valeurs avant le gadget suivant.
Si cette tentative réussit, alors nous sommes sur une bonne piste. Si elle échoue, il faut passer à une autre adresse.
On peut également imaginer que les tentatives suivantes doivent toutes les deux réussir:
BUFFER + COOKIE + STACKFRAME + (ADDRTEST+7) + JUNKDATA + JUNKDATA + STOP_GADGET
et
BUFFER + COOKIE + STACKFRAME + (ADDRTEST+9) + JUNKDATA + STOP_GADGET
Voilà trois heuristiques qui peuvent aider à confirmer qu’ “ADDRTEST” est bien le brop gadget. À ma connaissance, il n’existe qu’une occurrence de ce gadget par exécutable et il est à la fin du programme. Si vous avez plusieurs candidats, imaginez de nouvelles heuristiques pour en invalider certains. Les heuristiques présentées ne sont pas forcément les plus fiables possibles. Je vous laisse tester par vous-même les décalages et détecter les heuristiques qui impactent le moins les registres importants.
Pour rappel, les arguments des fonctions C, sont stocké dans l’ordre dans les registres suivants : RDI, RSI, RDX, RCX
Le BROP Gadget permet donc de contrôler les deux premiers arguments qui seront envoyés à une fonction.
6/ Trouver le numéro de socket qui nous est attribué
Cette étape va permettre de fiabiliser notre exploit et la détection de comportements. Sur une application peu sollicitée comme dans le cas d’un challenge hors ligne, comme ici, le numéro de socket est souvent 4. Il est possible que votre stop gadget ne paramètre pas le numéro de socket de lui-même. Dans ce cas, vous pouvez trouver le numéro de socket en paramétrant RDI de manière incrémentale jusqu’à ce que votre stop gadget fonctionne.
Effectivement, si votre RDI est mal configuré, lors de l’appel à send ou write de votre stop gadget, vous n’aurez pas de retour puisque send prends le numéro de socket en premier argument et write prend le “file descriptor”
7/ Trouver strcmp dans la PLT
Cette étape est facultative mais fort intéressante parce qu’elle permet de contrôler au moins partiellement le registre RDX. De fait, lorsque strcmp compare deux chaînes de caractères, le compteur de caractères est stocké dans RDX. On peut donc utiliser ce comportement pour avoir une valeur de RDX arbitraire.
Il va falloir également chercher quelques heuristiques. Par exemple, il faut que
BUFFER + COOKIE + STACKFRAME + (BROPGADGET+7) + PTRVALIDE + JUNKDATA + (BROPGADGET+9) + PTRVALIDE2 + ADDRTEST + STOPGADGET
fonctionne.
Note : Si on reprend ce qu’on a vu plus tôt sur le brop gadget, ça veut dire qu’on fait une ropchain pour paramétrer RDI et RSI (les deux premiers arguments d’un call en C sur Linux) puis pour appeler strcmp.
Vous pouvez également tester avec un seul pointeur valide, par exemple, pour enlever les faux positifs. L’important une fois strcmp
trouvée est de prendre deux fois la même chaîne de caractère et idéalement la plus longue possible
Il existe aussi une approche un peu différente, c’est-à-dire chercher la PLT directement puis la parcourir dans un second temps. Pour ce faire, il suffit de se baser sur la structure de la PLT. Chaque entrée de la PLT est de la forme :
On a donc une nouvelle entrée tous les 16 octets, et ADDRTEST +0
et ADDRTEST+6
doivent être valides.
La difficulté dans cette approche est que nous ne connaissons pas quelle fonction nous allons appeler et donc comment paramétrer RDI/RSI.
Note: La PLT se trouve avant la section de code en mémoire.
8/ Trouver send ou write dans la PLT
Si vous avez trouvé strcmp, alors vous avez trouvé la PLT. Grâce à la PLT, vous avez accès à pas mal de fonctions. Entre autres, autour de strcmp, vous trouverez des fonctions comme send ou write. Ces fonctions sont bien pratiques, car elles vont nous permettre de leak le binaire à distance. En fait, pas mal de fonctions qui prennent en paramètre un “file descriptor” sont intéressantes. Attention toutefois à la fonction send : En 64 bits lors d’un syscall, le kernel va sauvegarder RIP dans RCX (registre du 4ᵉ argument), ce qui complique grandement la tâche. 🙂
Ici plusieurs options : Comme dit précédemment,
- soit vous avez déjà strcmp et donc la PLT,
- soit il va falloir détecter la PLT comme décrit plus tôt,
- soit vous pouvez directement chercher la fonction send (avec un peu de chance
RDX != 0 && RCX == 0
).
Si vous connaissez votre numéro de socket, vous pouvez paramétrer RDI (numéro de socket), RSI (une adresse de buffer valide), éventuellement RDX via strcmp pour que RDX != 0
, éventuellement RCX pour que RCX == 0 et itérer sur la PLT jusqu’à ce que vous ayez un retour.
9/ Leak le binaire et éventuellement la libc
Normalement si vous êtes arrivés ici alors ça va être rapide. On paramètre l’appel à send et on itère sur les adresses pour récupérer le binaire. Si c’est un gros exécutable, vous devriez avoir pléthores de gadgets. Dans le cas où vous ne trouveriez pas tout ce qu’il vous faut, vous pouvez reproduire la même chose pour la libc (Petite astuce pour trouver la base d’un binaire en partant d’une adresse: il suffit de leak (addr & 0xfffffffffffff000
) et ensuite diminuer de 0x1000
en 0x1000
jusqu’à tomber sur ‘\x7fELF
‘).
Vous pouvez également utiliser DynELF
pour simplifier votre code, mais vous devrez avoir une fonction de leak très efficace.
10/ ROP The Stack as usual
Vous avez désormais tous les gadgets qu’il vous faut. Vous n’avez plus qu’à exploiter l’exécutable comme d’habitude.
Conclusion
Nous avons donc vu comment déterminer la nature de certaines adresses au travers d’heuristiques et comment établir des hypothèses à partir d’un comportement reproductible de l’exécutable.
Nous avons vu qu’il est possible d’exploiter cet exécutable dans le cas où vous n’avez ni une copie de l’exécutable, ni son code source, même si les conditions pour y arriver sont rares. Cette technique reste néanmoins intéressante car elle permet d’exploiter un exécutable compilé avec toutes les protections actuelles.
N’hésitez pas à nous faire savoir si d’autres sujets de ce type vous intéressent et si vous avez des questions ou suggestions!
(Merci à toute l’équipe :D)