« Quel serait un flot d’exploitation réaliste, rigoureux et opérationnel avec Ghidra et Cutter/rizin, dans le cadre d’une épreuve de reverse engineering exigeante ? »

Compétences clés

  • Savoir disséquer un exécutable totalement inconnu : sans symboles, sans documentation, avec les protections classiques (NX, RELRO, etc.).
  • Savoir lire des artefacts machine :
    • flot de contrôle ;
    • conventions d’appel ;
    • organisation mémoire, heap/stack ;
    • patterns usuels : structures, pointeurs de fonctions, wrappers, gadgets.
  • Savoir raisonner comme un attaquant orienté exploitation : c’est trouver comment le programme se casse, comment il s’ouvre, où se trouve le levier permettant de renverser le contrôle.

Le flot d’analyse Ghidra

Étape 1 — Ouvrir, analyser automatiquement, mais ne rien croire

Dans Ghidra :

  1. File → New Project
  2. import du binaire, puis analyse automatique ;
  3. laisser Ghidra renommer symboles, signatures, etc.

WARNING

Le décompilateur ment parfois, surtout dans les binaires pédagogiques ou obfusqués. Il faut vérifier tout au niveau ASM.

Étape 2 — Reconstituer le squelette du programme

L’objectif est de savoir l’on est avant de vouloir savoir quoi faire.

Dans Ghidra :

  • aller à entry ;
  • remonter dans _start__libc_start_mainmain ;
  • identifier la topologie :
    • sections .text, .plt, .got, .data, .bss ;
    • fonctions utilisateur (souvent peu nombreuses dans les TP).

Dans Cutter/rizin :

afl         # liste les fonctions
pdf @ main  # display function
V           # flowgraph

On visualise alors l’ossature logique :

  • input utilisateur → stockage → comparaison → bifurcations → appel final vulnérable ;
  • gestion mémoire (malloc/free) ;
  • pointeurs de fonctions → exploitation potentielle ;
  • mélange stack/heap → vecteurs de corruption.

Étape 3 — Identifier les zones vulnérables

Méthode systématique, indépendante du binaire.

3.1. Chercher tous les endroits où des données non contrôlées entrent.

Dans Cutter :

izz            # strings
axt <string>   # qui référence la chaîne ?

Dans Ghidra :

  • Window → Defined Strings
  • double-clic → cross-references (Xref).

Chercher :

  • gets, fgets, scanf, strcpy, strcat, read, recv, sprintf, snprintf, etc. ;
  • malloc/free → vulnérabilités heap possibles ;
  • fonction qui lit un PIN, un mot de passe → comparaison → primitives exploitables.

Étape 4 — Lire l’assembleur « à la main »

Un bon reverse engineer vérifie les points importants en ASM, pas dans le décompilateur.

Les registres :

  • rdi, rsi, rdx, rcx, r8, r9 → arguments ;
  • rbp/rsp → frame pointer et stack layout.

On regarde :

  • où se trouve le buffer ?
  • quelle est sa taille ?
  • quelle est la distance jusqu’au saved RIP / saved RBP ?

Exemple typique :

sub rsp, 0x40
lea rdi, [rbp-0x20]

→ on sait immédiatement où est le buffer.

Étape 5 — Déterminer la primitive d’exploitation

À partir des observations, il faut répondre :

Qu’est-ce que je contrôle dans le programme ? Et qu’est-ce que cela me permet de réécrire ?

Exemples typiques :

  • overflow stack → écriture sur saved RIP ;
  • overflow heap → overwrite du header, unlink, fastbin dup, pointeur de fonction ;
  • format string → arbitrary read/write ;
  • pointeur de fonction dans .data → redirection immédiate du flux ;
  • absence de PIE → gadgets à adresses fixes → ROP facile ;
  • PIE activé → besoin d’un leak ;
  • RELRO full → impossible d’écraser la GOT.

Cela indique si la stratégie sera :

  • ret2text ;
  • ret2plt ;
  • ret2csu ;
  • stack pivot / fake stack ;
  • ret2libc classique.

Étape 6 — Construire l’exploitation (ROP ou autre)

6.1. Avec Cutter/rizin.

/R pop rdi
/ri syscall
/ra    # ROPgadget equivalent dans rizin

6.2. Avec Ghidra.

  • Search → Instructions → "ret" ;
  • filter : pop ;
  • identifier les gadgets manuellement ;
  • vérifier alignement, effets secondaires, clobbering de registres.

6.3. Construire la ROP.

Il faut être capable de :

  • charger RDI, RSI, RDX ;
  • charger RAX pour les syscalls ;
  • chaîner system, execve, ou un syscall ;
  • faire un pivot si l’espace de stack est limité.

Étape 7 — Diff entre analyse théorique et exécution réelle (gdb)

Cette étape est souvent négligée. Un professionnel fait :

  • break main ;
  • x/100gx $rsp ;
  • vmmap ;
  • p &variable → vérifier ses adresses ;
  • si ROP → si / ni pour vérifier la progression des gadgets ;
  • si pivot → surveiller RSP ;
  • si syscall → vérifier rax/rdi/rsi/rdx juste avant.

Il faut mettre en correspondance :

  • ce que dit Ghidra ;
  • ce que voit GDB ;
  • ce qu’exécute réellement le binaire.

C’est cette triangulation qui donne la qualité.

Ce qu’il faut retenir pour l’examen

Faiblesses classiques que je me connais :

  • trop se reposer sur l’exécution (gdb) et pas assez sur l’analyse statique profonde ;
  • absence d’une vision « macro » systémique ;
  • comprendre le ROP par intuition locale, pas globale ;
  • paniquer lorsque la stack ne suffit plus (stack pivot) ;
  • mal maîtriser Ghidra comme outil de raisonnement (flowgraph, structure recover, xrefs).

Ce que Ghidra apporte pour réussir

  1. Reconstruire automatiquement les structures. Ghidra repère les structures sur heap/stack ; Cutter/rizin le fait moins bien.
  2. Identifier les pointeurs de fonction. Crucial dans les exercices de heap overflow.
  3. Décompilation lisible des comparaisons, boucles, branches. On identifie immédiatement la condition qui mène au « ret2win ».
  4. Récupération exacte des offsets. Multipliée par dix en fiabilité par rapport à l’examen à la main.

Le workflow complet à suivre en examen (checklist)

Phase 1 — Statique (15 min)

  • ouvrir binaire dans Ghidra ;
  • aller à main ;
  • identifier l’input path ;
  • lire les allocations heap (malloc/free) ;
  • trouver la vulnérabilité ;
  • déterminer out-of-bound / overflow ;
  • identifier fonction intéressante (login, admin, shell…) ;
  • repérer les gadgets ROP dans .text ;
  • vérifier PIE/NX/RELRO (checksec).

Phase 2 — Dynamique (10 min)

  • break main ;
  • vérifier adresses du buffer ;
  • vérifier adresses rodata/data/bss ;
  • observer layout stack ;
  • tester les crashs ;
  • tester les écritures heap.

Phase 3 — Stratégie d’exploitation (10 min)

  • chercher une primitive ROP viable ;
  • si ret2csu nécessaire → vérifier gadgets ;
  • si pivot → choisir région mémoire sûre ;
  • préparer payload ;
  • tester dans gdb.

Phase 4 — Finalisation (10 min)

  • nettoyer payload ;
  • tester hors gdb ;
  • ajouter sleep/délais si nécessaire ;
  • rédiger note d’exploitation.