Rusty_rev
Flag: HACKDAY{D0N7_637_rU57Y}
Challenge
Solution
Ce challenge est un premier programme, appelé loader, qui charge en mémoire un second puis l'exécute.
Reverse du loader
Découvrir l'existence du loader
Ici, j'utilise Ghidra pour décompiler le binaire. Dans la partie SymbolTree, on voit que la fonction memfd_create
est utilisée. Un petit tour sur la doc pour voir qu'elle permet de créer un fichier anonyme en mémoire et retourne son file descriptor.

Si l'on regarde où est utilisée cette fonction, on découvre un appel à la fonction write
est fait juste après sur le fichier créé :

Extraction du vrai programme
On va lancer le binaire avec GDB et break à ce niveau-là pour voir ce qui est écrit :
┌──(kali㉿kali)-[~/HackDay 2025]
└─$ gdb rusty_rev
gef➤ r
Starting program: /home/kali/HackDay 2025/rusty_rev
gef➤ b write
Breakpoint 1 at 0x7ffff7ec24d0: file ../sysdeps/unix/sysv/linux/write.c, line 26.
gef➤ r
Starting program: /home/kali/HackDay 2025/rusty_rev
Une fois arrivé au breakpoint, on voit que ce que l'on écrit dans le fichier anonyme commence par ELF. C'est là que l'on comprend que c'est un loader qui charge notre vrai programme en mémoire. On va donc dump
celui-ci, pour ça on utilise l'adresse affichée dans le paramètre buf
et la taille affichée dans nbytes
.
La commande dump
utilise une adresse de début et une adresse de fin, il faut donc additionner la taille à l'adresse de début pour obtenir l'adresse de fin, 0x7ffff7c71010 + 0x5f9c0 = 0x7ffff7cd09d0
:

Reverse du vrai programme
Trouver la fonction principale
Maintenant, je passe sur IDA pour le binaire qu'on vient d'extraire. La décompilation est plus explicite et visuelle pour le WriteUp.
En lançant le programme, on nous affiche la phrase Please input your password
. On commence donc par chercher la string password
pour savoir où nous sommes dans le programme.

En regardant les refs à celle-ci, on arrive sur la fonction sub_8434
:

Comprendre la fonction
En regardant un peu à quoi correspondent les refs utilisées dans la fonction, on peut voir qu'en fonction de v55
, on place GG WP
, Wrong password
, Reboot computer
ou l'ASCII Art AmongUs
en mémoire.
Il y a également une ligne intéressante : on compare v79
à 23
et ensuite, on fait tout un tas de comparaisons avant d'écrire GG WP
.

Aparté concernant v55
, il s'agit du résultat d'une fonction aléatoire, on peut le voir facilement :

Pour attendre le if
qui nous intéresse (celui en vert dans le screen précédent) il ne faut pas que v55
soit égal à 1, 2, 3, ou 4. Donc, à chaque fois qu'on analysera dynamiquement le programme, on va le forcer à 5.
Rebase du program
Toujours concernant l'analyse dynamique, on va devoir rebase notre IDA pour avoir les mêmes adresses que sur notre GDB. Pour ça, on commence par utiliser vmmap
dans gdb et on regarde le start du programme : 0x00555555554000
.

Maintenant, on va setup ça dans IDA : Edit
> Segments
> Rebase program...

Parfait, on a les mêmes adresses dans les deux, ce sera plus simple à suivre. Notre fonction sub_8434
est maintenant à l'adresse sub_55555555C434
.
Analyse dynamique
Comme j'ai dit tout à l'heure, on va fixe notre variable v55
. Pour ça, on va break juste après l'appel à la fonction rand et changer la valeur. Le résultat est dans EAX, donc on change EAX (logique).

gef➤ break *0x55555555CCFF
Breakpoint 1 at 0x55555555ccff
gef➤ run
gef➤ set $eax=5
Taille du mot de passe
Maintenant, on va break sur la comparaison avec 23, c'est à l'adresse 0x55555555CE1F

En lançant le programme avec le mot de passe "test", on voit que c'est rsp+40
qui est comparé à 23 (0x17). Et la valeur dans rsp+40
est 4 :
gef➤ break *0x55555555ccff
Breakpoint 1, 0x000055555555ccff
gef➤ break *0x55555555ce1f
Breakpoint 2, 0x000055555555ce1f
gef➤ run
[#0] Id 1, Name: "hidden_elf", stopped 0x55555555ccff in ?? (), reason: BREAKPOINT
gef➤ set $eax=5
gef➤ continue
[#0] Id 1, Name: "hidden_elf", stopped 0x55555555ce1f in ?? (), reason: BREAKPOINT
→ 0x55555555ce1f cmp QWORD PTR [rsp+0x40], 0x17
gef➤ x $rsp+40
0x7fffffffdb28: 0x00000004
On en déduit facilement que c'est la longueur de notre mot de passe, il faut donc qu'il fasse 23 caractères.
Comparaison du mot de passe
En continuant sur la comparaison qui nous intéresse, on voit que :
rbx
est comparé avec0x5555555A1050
(valeur :A5E7F33F8DE1F5
)rbx+7
est comparé avec0x5555555A1040
(valeur :0FCA1B12650FE105220FCF1436E83C3A
)

De plus, avec notre mot de passe test, RBX contient une valeur sur 4 octets. Avec d'autres mots de passe et en breakant au même endroit, on se rend compte que RBX contient notre mot de passe "chiffré".
gef➤ x /8b $rbx
0x5555555b7d60: 0x78 0xe2 0x42 0xf9 0x00 0x00 0x00 0x00
Le chiffrement est fait caractère par caractère et la position du caractère compte également. On peut le voir assez facilement en faisant différentes tentatives :
# Avec "AAAAAAAAAAAAAAAA"
gef➤ x /16b $rbx
0x5555555b7d60: 0xe4 0xe7 0xe1 0x65 0xa0 0xe1 0x2d 0xe4
0x5555555b7d68: 0xe7 0xe1 0x65 0xa0 0xe1 0x2d 0xe4 0xe7
# Avec "BBBBBBBBBBBBBBBB"
gef➤ x /16b $rbx
0x5555555b7d60: 0xff 0xfc 0xfa 0x7e 0xbb 0xfa 0x36 0xff
0x5555555b7d68: 0xfc 0xfa 0x7e 0xbb 0xfa 0x36 0xff 0xfc
Conclusion : il faut que le résultat de notre mot de passe chiffré donne les deux valeurs qu'on a vues juste avant, c'est-à-dire : A5E7F33F8DE1F50FCA1B12650FE105220FCF1436E83C3A
.
Automatisation
Ce qu'on va faire, c'est tester tous les caractères à toutes les positions possibles et récupérer leur valeur chiffrée. On pourra ainsi comparer avec le résultat attendu pour voir les correspondances. En python, ça donne ce script :
import gdb
file_with_password = 'input.txt'
file_with_result = 'output.txt'
flag_size = 23
# Charger le programme cible
gdb.execute("file ./result.bin")
gdb.execute("break *0x000055555555ccff") # break pour modifier v55
gdb.execute("break *0x000055555555ce1f") # break juste avant la comparaison du mdp
gdb.execute(f"run < {file_with_password}") # Lancer le programme
# On force v55 à 5
gdb.execute('set $eax = 5')
gdb.execute("continue")
# On dump le résultat du chiffrement dans output.txt
gdb.execute(f"dump binary memory {file_with_result} $rbx $rbx+{flag_size}")
gdb.execute("quit")
On a un script Python GDB qui permet de lancer le programme et récupérer le résultat du chiffrement dans output.txt
, maintenant il nous faut un script pour tester tous les caractères :
import subprocess
import string
# Les valeurs récupérées tout à l'heure
expected = bytes.fromhex('a5 e7 f3 3f 8d e1 f5 0f ca 1b 12 65 0f e1 05 22 0f cf 14 36 e8 3c 3a')
# Tous les caractères que l'on va tester
charset = string.digits + string.ascii_letters + string.punctuation
file_with_password = 'input.txt' # Fichier dans lequel on met le mpd à tester
file_with_result = 'output.txt' # Fichier dans lequel on aura le résultat
flag_size = 23
# La fonction met le mdp dans input.txt, lance gdb et retourne le contenu de output.txt
def run(password: str) -> bytes:
with open(file_with_password, 'w') as f:
f.write(password)
out = subprocess.run('gdb -q -x test.py', capture_output=True, shell=True)
with open(file_with_result, 'rb') as f:
result = f.read()
return result
# On commence avec un flag vide
flag = bytearray(b' ' * flag_size)
for c in charset:
password = c * flag_size
result = run(password)
hit = False
for i in range(len(result)):
if result[i] == expected[i]: # Si un des octets est le même qu'attendu
flag[i] = ord(c) # On ajoute le caractère utilisé dans le flag
hit = True
if hit:
print(flag.decode())
Le résultat :
┌──(kali㉿kali)-[~/HackDay 2025]
└─$ python3 solve.py
0
0 3
0 3 5
0 63 5
0 7 637 57
0 7 637 r 57
A A 0 7 637 r 57
AC A 0 7 637 r 57
AC DA D0 7 637 r 57
HAC DA D0 7 637 r 57
HACKDA D0 7 637 r 57
HACKDA D0N7 637 r 57
HACKDA D0N7 637 rU57
HACKDAY D0N7 637 rU57Y
HACKDAY D0N7_637_rU57Y
HACKDAY{D0N7_637_rU57Y
HACKDAY{D0N7_637_rU57Y}
Dernière mise à jour
Cet article vous a-t-il été utile ?