Préliminaires
On ouvre le fichier avec radare2
.
r2 -e bin.relocs.apply=true MagasinSecret
[0x00004eb0]> aaaa
[0x00004eb0]> afl > fonctions
[0x00004eb0]> izz
Le fichier fonctions
contient maintenant les noms des fonctions, notamment :
sym.chall_ecsc_re::generate_secretholders::h514b87a7f53eafe4
sym.chall_ecsc_re::secret_holder::SecretHolder::decrypt_secret::hf5f32b7a9e6bc86c
sym.chall_ecsc_re::secret_holder::SecretHolder::check_password::habf9fdd677886c4e
sym.chall_ecsc_re::main::h91a329e5f7c3415e
On trouve également un certain nombre de chaînes de caractères intéressantes
...
3057 0x0002c990 0x0002c990 703 704 .rodata ascii /usr/src/rustc-1.32 ...
Utilisation: "Nom d'utilisateur" "Mot de passe"\n
3058 0x0002cc90 0x0002cc90 58 61 .rodata utf8 Le mot de passe doit être d'une longueur de 10 caractères\n
3059 0x0002ccd0 0x0002ccd0 144 147 .rodata utf8 Aucun secret n'est associé à ce couple nom d'utilisateur/mot de passe.\n Voici vos secrets:\n
Le programme a certainement été écrit en langage rust. Il doit récupérer un nom d’utilisateur et un mdp (de 10 caractères) en argument. L’analyse de la fonction main va nous le confirmer :
[0x00004eb0]> pdf @sym.chall_ecsc_re::main::h91a329e5f7c3415e
...
0x0000708c ff1526af2300 call qword [sym.std::env::args_os::hce9f729300a1b12b]
La documentation du langage rust nous indique que cette fonction renvoie un itérateur sur les arguments. Les arguments d’index 1 (donc le nom d’utilisateur) et 2 (le mdp) sont récupérés et aux lignes suivantes :
0x000070df e8fcf6ffff call fcn.000067e0
0x000070e4 4883f80a cmp rax, 0xa
nous vérifions que le mdp doit être de 10 (0xa) caractères exactement. En effet la ligne 70df appelle la méthode len
qui renvoie probablement la longueur de la chaîne de caractères.
pdf @ fcn.000067e0
...
└─< 0x000067e0 e98bfaffff jmp sym
... <alloc::vec::Vec<T>>::len::he32f9bfcb9ca7f09
Si la comparaison échoue, le code saute plus bas dans la fonction main, où nous trouvons très rapidement un appel de la fonction exit
.
La fonction generate_secretholders
Immédiatement après la vérification du mdp, nous trouvons :
0x000070f3 e888f8ffff call sym chall_ecsc_re::generate_secretholders::h514b87a7f53eafe4
On se concentre donc sur la fonction generate_secretholder
.
pdf @ sym.chall_ecsc_re::generate_secretholders::h514b87a7f53eafe4
La fonction generate_secretholders commence par répéter 6 fois une série d’instructions assez semblables.
- allocation d’une chaîne de caractères imprimables de lg variable (attribut 1 d’un objet
secret_holder
) - allocation d’une chaîne de 4 caractères imprimables (attribut 2)
- allocation d’une chaîne de 64 caractères hexadécimaux (attribut 3)
- allocation d’une chaîne de caractères hexadécimaux de lg variable (attribut 4)
- appel de la méthode
sym.chall_ecsc_re::secret_holder::SecretHolder::new
avec comme arguments les chaînes allouées
La première chaîne allouée (attribut 1) prend les valeurs successives:
- “MrFlag”
- “Entropie”
- “z3r0”
- “z3r0”
- “LBigBossDu15”
- “LBigBossDu15”.
On reconnaît les personnages de l’énoncé.
Un coup d’œil à la fonction check_password
montre qu’un hachage (lignes 0x00008ce7, 0x00008d5b) est utilisé,
suivi d’un appel à la méthode cmp
( ligne 0x00008db6 ). On suppose donc que le password proposé est haché puis
comparé à un hash stocké quelque part (ce qui est la manière classique de vérifier un mdp).
On examine la méthode check_password
pdf @ sym.chall_ecsc_re::secret_holder::SecretHolder::check_password::habf9fdd677886c4e
...
0x00008c88 call qword [sym.__D_as_digest::digest::Digest_::new::h5e74f92edcde4c94]
...
récursivement :
pdf @ sym.__D_as_digest::digest::Digest_::input::h43159f435b09d51d
...
0x0000acd7 ff159b6c2300 call qword [sym.__sha2::sha256::Sha256_as_digest::Input_::input::ha9fa24ae5920bb74]
Le hachage est de type sha256. Le hash pourrait donc bien être la chaîne de 64 caractères que nous trouvons en 3è attribut de chaque secret_holder
. Comme l’énoncé indique qu’il y a le même mdp pour chacun, et que ces
hash sha256 diffèrent, un sel a probablement été utilisé, ce qui serait cohérent avec le fait que la fonction check_password
utilise une concaténation (ligne 0x00008cbb ). Ce sel pourrait être
la chaîne attribut 2, tj de la même longueur (4 caractères).
On suppose donc qu’un objet de type secret_holder
contient :
- le nom d’utilisateur, par ex. MrFlag, …, z3r0, LBigBossDu15
- le sel pour la vérification du mdp
- le hash sha256 du mdp correct, salé
- un contenu supplémentaire qui contiendrait le ou les secrets à garder.
On constate que les personnages z3r0 et LBigBossDu15 ont tous les deux un secret_holder avec un attribut 4 de exactement 40 caractères ce qui correspondrait au fait qu’ils ont mis tous les deux le même mdp netflix.
Il faudrait donc parvenir à percer le secret du 4è attribut, notamment pour le secret_holder
attribué au personnage MrFlag.
La fonction decrypt_secret
Intéressons-nous à la fonction decrypt_secret
. On y trouve une boucle avec un certain nombre d’appels de fonctions aux noms évocateurs : next
, string.as_bytes
, bitxor
. Cela fait penser à déchiffrement par l’opérateur xor, octet par octet, sur deux chaînes (dont une générée par une concaténation effectuée ligne 0x00008e73). De plus, une instruction de division (ligne 0x00008f4c ) est effectuée avec utilisation du reste, ce qui suggère l’utilisation d’une clé de déchiffrement plus courte que le texte chiffré.
call qword [sym.hex::decode::hc8c2821102ac3f66]
...
call sym _<core::iter::Enumerate<I> as core::iter::iterator::Iterator>::next::hfc69d1dcfc6c26b0
...
call sym alloc::string::String::as_bytes::h8b9bfd04b2c892c7
...
div rcx
cmp rdx, r14
jae 0x8fde
movzx esi, byte [r12 + rdx]
mov rdi, rbp
call sym _<&'a u8 as core::ops::bit::BitXor<u8>>::bitxor::h5e3a781ff899464b
Premiers succès
On tente quelques essais à l’aveugle en faisant des xor entre les attributs 1 et 4 (on exclut 2 et 3 qui seraient utilisés seulement pour le check_password). Les essais sont en python.
# attribut 4 du SecretHolder de LBigBossDu15
a = bytes.fromhex("39381c533a0a164a251c46003416060a4f0c5431")
# attribut 4 du SecretHolder de z3r0
b = bytes.fromhex("0f4907043912005c590012451004195c05557036")
# essais de déchiffrement
bytes(x^y for x, y in zip(a, b"LBigBossDu15"))
b'uzu4xee9aiw5'
bytes(x^y for x, y in zip(b, b"z3r0"))
b'uzu4'
Les résultats des déchiffrements sont égaux sur leurs 4 premiers caractères. Cela pourrait être les 4 premiers caractères du mdp netflix.
c = bytes.fromhex("211a1908000f061f040c5e060a160c50341d7e18090224")
# MrFlag
bytes(x^y for x, y in zip(c, b"MrFlag"))
b'lh_dah'
Le résultat commence par “lh_” ce qui correspondrait au flag.
En poursuivant dans la même idée (nom d’utilisateur XOR attribut 4 du secret_holder
de ce même utilisateur), on trouve "J<3Maman"
pour LBigBossDu15 et “test” pour l’utilisateur “Entropie”.
Il est donc extrêmement probable que l’attribut 4 d’un secret_holder
contienne le ou les secrets de l’utilisateur correspondant, et qu’il se déchiffre avec une clé qui commence par le nom de l’utilisateur. Seulement, pour le secret de “MrFlag”, par ex, nous avons un secret de 40 caractères hexa, donc 20 octets, mais une clé de seulement 6 octets. Il nous manque donc 14 octets de clé. De même pour l’utilisateur z3r0.
Calcul des 12 premiers octets de la clé
Heureusement, les propriétés de l’opérateur XOR nous permettent de deviner les 12 premiers octets de la clé utilisée par z3r0 pour chiffrer le mdp netflix.
clé_z3r0 XOR secret_chiffré_z3r0 = mdp_netflix
donc
mdp_netflix XOR secret_chiffré_z3r0 = clé_z3r0
bytes(x^y for x, y in zip(a, b"LBigBossDu15"))
uzu4xee9aiw5
bytes(x^y for x, y in zip(b, b"uzu4xee9aiw5"))
b'z3r0Awee8iep'
On confirme qu’il s’agit bien d’une clé probable en faisant XOR avec le 2è secret_holder de z3r0 :
bytes(x^y for x, y in zip(bytes.fromhex("195b1d4522050a104c0c"), b"z3r0Awee8iep"))
b'choucroute'
Ce serait peu probable de tomber par hasard sur le nom de ce plat traditionnel alsacien.
On essaye en concaténant avec “MrFlag” :
bytes(x^y for x, y in zip(c, b"MrFlagAwee8iep"))
b'lh_dahGhaifoof'
Ce n’est pas le bon flag… normal, la chaîne à déchiffrer compte 20 octets, et la clé seulement 14 (6 pour “MrFlag”, 8 pour “Awee8iep”). Il manque 6 octets qu’il va falloir trouver quelque part.
On finit à l’aide de la force brute
À partir de là, on regarde d’où vient cette chaîne “Awee8iep”. En analysant le binaire, même sans comprendre le fonctionnement du programme, ce qui apparaît, c’est qu’elle n’est nulle part dans la section .rodata, ni ailleurs dans le fichier. Quelques tests montrent qu’elle n’y est pas non plus sous forme chiffrée avec César ou qqch de simple. On ne trouve aucun code typique de fabrication d’une clé XOR (aléatoire, par ex.).
En fait, comme toutes les autres chaînes de caractères présentes dans le programme sont déjà utilisées autrement, il faut supposer que c’est une chaîne fournie au moment de l’exécution. Comme le nom d’utilisateur a déjà été utilisé, ce serait donc le mdp. Autrement dit, pour déchiffrer le secret d’un utilisateur, il faudrait faire un XOR avec une clé composée du nom d’utilisateur et du mdp corect de cet utilisateur. C’est cohérent, puisque cela évite qu’une analyse du binaire ne suffise à déchiffrer le secret.
Or, nous savons que le mdp correct fait exactement 10 caractères. Nous en avons 8 (“Awee8iep”). Il manque donc 2 B.
Pour trouver le mdp correct (le même pour tous les utilisateurs selon l’énoncé), nous avons le hash, le sel, et 8 caractères sur les 10.
Deux solutions simples sont possibles :
- hash_enregistré = sha256( sel (4 octets) + Awee8iep (8 octets) + ?? (2 octets) )
- hash_enregistré = sha256( (Awee8iep + ?? + sel )
Une attaque en force brute est envisageable pour seulement 2 caractères
alph = bytes(x for x in range(32, int("7e", 16)))
suites = []
for x in alph:
for y in alph:
suites.append(x+y)
for s in suites:
essai = b"Awee8iep" + s.encode("utf-8") + b"@Hf2"
if hex(int.from_bytes(h.sha256(essai).digest())) == objectif:
print(essai)
break
On trouve alors que
‘Awee8iepee@Hf2’ donne bien le hash stocké dans le secret_holder de z3r0
Si l’on suit l’hypothèse que ce serait le mdp, et qu’ils ont tous le même mdp, il suffit de faire
./MagasinSecret LBigBossDu15 Awee8iepee
Voici vos secrets:
J<3Maman
uzu4xee9aiw5uacowe1A
./MagasinSecret MrFlag Awee8iepee
Voici vos secrets:
lh_dahGhaifoofi5yo8thee
C’est le flag.