Rusty_rev

Flag: HACKDAY{D0N7_637_rU57Y}

Challenge

Description


Hello agent, a trusted source managed to find one of the most secured app of the black mist crew, but unfortunately, we can't find the password to access it.

We know your talents for reverse engineering, we need you to help us this password.


sha256: 71553d736b4299a40069ff3ae1fbd242b50f88b44c28a49ef559ac34248581d5

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é avec 0x5555555A1050 (valeur : A5E7F33F8DE1F5)

  • rbx+7 est comparé avec 0x5555555A1040 (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 :

run_program.py
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 ?