Personal Safe
crackmes.one (LubimPiskoty)
Le programme est écrit en ASM, on trouve le nom du fichier
(safe.asm
) lorsque l'on fait un strings
dessus. Lorsque je l'ouvre
avec Ghidra, cela explique pourquoi je me retrouve avec des fonctions
syscall
dans le code décompilé. Il est possible de déchiffrer un à
un tous ces appels (un Wikibook dessus) mais Ghidra a un script pour
ça (ResolveX86orX64LinuxSyscallsScript.java
). Après son exécution,
tous les appels systèmes sont correctement convertis dans le
décompilateur.
En lisant rapidement le code décompilé, je comprends que le
décompilateur ne va pas mettre utile ici. Il arrive à trouver des
appels à OpenSSL alors qu'il n'en est rien. Je me concentre sur
l'ASM. La seule partie à retenir du décompilateur est l'appel système
à read
. Le mot de passe fait 16 caractères et est stocké à l'adresse
0x00402038
.
read(0,&password,0x10);
La fonction verify
est ensuite appelée plusieurs fois dans l'ASM. Je
vais voir ce qu'elle fait avant de me pencher sur les différentes
opérations entre chaque appel.
MOV RAX,password CALL _verify MOV R12,R15 MOV RAX,DAT_00402040 CALL _verify CMP R12,R15 JNZ _jmpwrong MOV RAX,DAT_0040203c CALL _verify MOV R14,R15 MOV RAX,DAT_00402044 CALL _verify CMP R14,R15 JNZ _jmpwrong
Elle est très courte. Je vous mets le code avec des commentaires qui
explique ce qui se passe dedans en détail. Si vous avez la flemme de
le lire : le registre EBX
est utilisé comme un compteur (mis à 0
puis incrémenté et comparé en sorti de boucle). Il s'arrête
à 4. Le byte se trouvant dans l'adresse mémoire de RAX
va être
ajouter dans un compteur à chaque tour de boucle. L'adresse de RAX
va être ensuite incrémenté, pour se placer sur le byte suivant. Le
compteur final est ensuite ajouté dans un compteur global.
;; sauvegarde de tbs PUSH tbs ;; réinitialisation de R15, tbs et mise à zéro de EBX XOR tbs,tbs XOR R15,R15 MOV EBX,0x0 _verifyLoop ;; incrémentation de RBX, dont EBX est une partie. INC RBX ;; récupération du byte se trouvant dans l'adresse mémoire de ;; RAX MOV tbs,byte ptr [RAX] INC RAX ;; on l'ajoute dans R15 ADD R15,tbs CMP RBX,0x4 JNZ _verifyLoop ;; on ajoute notre total dans R13 ADD R13,R15 POP tbs RET
En se repenchant sur les appels de la fonction, l'adresse du mot de
passe est mis dans le registre RAX
, qui est ensuite utilisé dans
verify
pour pouvoir récupérer les bytes du mot de passe. Entre
chaque appel, le registre change d'adresse. Il passe de l'index 0 de
password
à l'index 8 puis 4 et enfin 12.
Découpons les appels différents et ce qui se passe :
- les 4 premiers bytes de
password
sont additionés et le résultat est stocké dansR12
- les bytes 9 à 12 sont additionnés et le résultat est comparé avec la somme précédente
- idem que 1. avec les bytes 5 à 8 dont le résultat est stocké dans
R14
- idem que 2. avec les bytes 13 à 16 et 3. à la place de 1.
À la fin, nous avons :
R13
contient la somme totale de tous les caractères depassword
R12
la somme des 4 premiers bytesR14
celle des 4 suivantsR15
celle des 4 derniers, dont le résultat doit être le même queR14
Il est donc possible de découper le mot de passe en 4 blocs de 4 bytes
dont le premier et le troisième doivent être égaux ainsi que le
deuxième et quatrième : XXXXYYYYXXXXYYYY
.
Attaquons le dernier bloc de vérification.
CMP R13,0x42e JNZ _jmpwrong ADD R12,0xb CMP R12,R15 JNZ _jmpwrong CALL _correct
En reprenant la liste des registres, il faut avoir un mot de passe
dont la somme vaut au final 0x42e
et dont la somme des premiers
bytes est légèrement inférieur à celle des derniers. Les opérations
peuvent être résumées en une équation qui se résout rapidement.
En divisant par 4 \(x\) et \(y\), on peut retrouver des nombres à virgules
"ronds" (0.25 et 0.5). Après quelques calculs, je trouve des nombres
qui sont contenus dans la table ASCII. Je fini par écire un mdp qui
ressemble à AAACDDDEAAACDDDE
. Il est sûrement possible d'écire un
autre mot de passe qui respecte les règles précédentes mais celui-là
est le plus simple.