WARGAME — Trouvez la phrase top secrète
WARGAME — Trouvez la phrase top secrète
Writeup complet de l'investigation
Challenge : HESD4, par Thomas J (Nantes)
Fichier fourni : trouve-la-phrase.zip → trouve-la-phrase.dmp (dump RAM ELF core, 2 181 953 312 octets, VirtualBox/QEMU, machine Windows)
Format attendu du flag : ESD{...}
0. Indices de l'énoncé
- Compile des programmes C/C++ sous Windows
- Adore les raccourcis clavier, surtout Windows+R
- Regarde des vidéos de « Jamy et Michelle »
- Collectionne les « artéfacts de volatiles »
Deux jeux de mots identifiés dès le départ :
- « artéfacts de volatiles » → clin d'œil à Volatility (framework de forensic mémoire) + volatile = oiseau en français
- « Jamy et Michelle » → probablement « Jacquie et Michel » (site français bien connu), indice dont la finalité exacte n'a jamais été confirmée malgré une recherche exhaustive
1. Mise en place
unzip -P password trouve-la-phrase.zip # -> trouve-la-phrase.dmp
file trouve-la-phrase.dmp # ELF 64-bit core file (dump physique VM)
Analyse de l'en-tête ELF (readelf -l) : un unique gros segment PT_LOAD avec
p_offset=0x1cb720, p_paddr=0x200000, taille ~2 Go → mapping linéaire entre offset fichier et adresse physique invité :
phys = file_offset - 0x1cb720 + 0x200000
file_offset = phys - 0x200000 + 0x1cb720
(Vérifié : les deux constantes sont multiples de 16, donc phys mod 16 == file_offset mod 16 — propriété réutilisée plus tard pour la recherche de PTE.)
2. Windows+R → identifiants KeePass
Recherche de la structure registre RunMRU dans le dump (clé HKCU\...\Explorer\RunMRU).
Une entrée révèle la commande tapée par le chercheur :
"c:\Users\passurmoi\Desktop\Package\KeePass-2.51.1\KeePass.exe"
c:\Users\passurmoi\Desktop\Package\OpenMe.kdbx
-pw:Poulet
-keyfile:"c:\Users\passurmoi\Documents\satz\beau_fichier"
→ Mot de passe KDBX : Poulet (jeu de mot avec « volatiles »)
→ Fichier-clé requis : beau_fichier
3. Récupération du fichier-clé (beau_fichier)
Recherche de structures NTFS $MFT (enregistrements FILE) contenant le nom beau_fichier, avec extraction de l'attribut $DATA résident (petit fichier stocké directement dans le MFT) :
<?xml version="1.0" encoding="utf-8"?>
<KeyFile>
<Meta><Version>2.0</Version></Meta>
<Key><Data Hash="57E83CAE">
2B57F876 20618481 E7009C6E CB89B51A
B5E88F9B C6BF5FFB 8698F1BD 85B22FC1
</Data></Key>
</KeyFile>
Checksum vérifié (SHA256(clé)[:4] == 57E83CAE) ✅
4. Déchiffrement de OpenMe.kdbx — la vraie difficulté technique
4.1 Premier essai (échec) : lecture physique « à plat »
En-tête KDBX (KDBX 3.1, AES-256, GZip, 60 000 tours AES-KDF) localisé deux fois dans le dump. Une lecture contiguë après l'en-tête donne un déchiffrement partiellement correct (les premiers Ko), puis corrompu — cause : fragmentation physique du buffer applicatif (les pages virtuelles contiguës du process ne sont pas contiguës en RAM physique).
4.2 Percée : reconstruction par table de pages Windows
Méthode développée et réutilisée tout au long du challenge :
- Calculer l'adresse physique de l'ancre connue (
phys = file_off - 0x1cb720 + 0x200000) - Chercher dans tout le dump les PTE (Page Table Entries, 8 octets) dont les bits 12-51 encodent ce PFN, avec le bit Present (0x1) ou Transition (0x800) actif
- Si une correspondance unique est trouvée dans une table de 512 entrées majoritairement valides → lire la table de pages complète, et reconstituer les pages virtuelles consécutives dans l'ordre logique, même si elles sont éparpillées physiquement
Appliqué à la 2ᵉ copie du KDBX (mappée via des PTE en état Transition) → reconstruction complète. Hash SHA-256 du bloc de données vérifié conforme à la valeur attendue dans l'en-tête KDBX → déchiffrement garanti correct à 100 %.
5. Contenu de la base KeePass
Après décompression Gzip du XML, puis déchiffrement des champs protégés (algorithme Salsa20, nonce = E830094B97205D2A, clé = SHA256(ProtectedStreamKey)) appliqué dans l'ordre du document :
- Groupe OpenMe → entrées
Credits ==>(auteur), Flag,valeurchamp1..5, groupe Corbeille (8 objets supprimés, contenu non récupérable) - L'entrée Flag explique le mécanisme :
Renseigner Username/Password/champ3/champ4 avec les vraies valeurs de valeurchamp1/2/3/4, puis exécuter :
staz.exe valeurchamp2 valeurchamp1 valeurchamp3 VALEURCHAMP4
Découverte des 4 valeurs (chacune cachée par un procédé différent)
| Champ | Valeur | Méthode de découverte |
|---|---|---|
| champ1 (username) | kiwis |
Historique de l'entrée encodé en T9 (clavier téléphone) : 888 2 555 33 88 777 222 44 2 6 7 3 33 88 99 0 → K-I-W-I-S |
| champ3 | zinzolins |
Texte stylisé caché dans le champ Notes, image PNG reconstituée via {PASSWORD}{NOTES} |
| champ4 (MAJUSCULE) | RACHEL |
Geohash 9qtwnjng4sz → coordonnées (37.645, -115.742) → ville la plus proche d'Area 51, Nevada |
| champ2 (mot de passe) | hash NTLM du compte Windows passurmoi |
Voir section 6 — jamais résolu avec certitude |
| valeurchamp5 | — | Juste un indice texte : « Il n'y a que 4 valeurs à renseigner » |
6. satz.exe — extraction et rétro-ingénierie
6.1 Extraction depuis une archive ZIP cachée en RAM
Recherche de signatures PK\x03\x04 (en-têtes locaux ZIP) : découverte du satz.zip original entièrement caché en mémoire (probablement le buffer de téléchargement/extraction), contenant satz.exe (deflate), beau_fichier (stocké), OpenMe.kdbx (stocké).
satz.exe reconstruit via la même technique de table de pages (CRC-32 et taille décompressée vérifiés ✅).
6.2 Désassemblage (objdump)
Le binaire contient des symboles de debug DWARF (bibliothèque MinGW uniquement, pas notre code).
Analyse du flux de main() :
- Charge
argv[1..4](aucune vérification d'argc) - Concatène des
std::string: fragments littéraux + arguments, aucun calcul, aucun hash, aucune validation — confirmé ligne par ligne en assembleur x86-64
Template du flag confirmé avec certitude absolue :
ESD{Les_[champ2]_cultivent_des_kiwis_zinzolins_pr3s_de_RACHEL}
7. Extraction du hash NTLM — la chaîne SAM/SYSTEM
7.1 Bootkey (ruche SYSTEM)
Recherche des cellules nk réelles Lsa\{JD,Skew1,GBG,Data}, extraction de leurs noms de classe (8 caractères hex chacun) via un même schéma de cellule adjacente -24.
JD = 8ed06ec3
Skew1 = 7f4f716b
GBG = b2f0295b
Data = 8a7deba8
Permutation standard [8,5,4,2,0xB,9,0xD,3,0,6,1,0xC,0xE,0xA,0xF,7] appliquée à la concaténation :
bootkey = b24f7f6e5bf07dc38e71d08aeb29a86b
7.2 PEK (ruche SAM)
RID de passurmoi confirmé = 1000 (0x3E8, NextRid=1001 trouvé dans le blob F du domaine).
Blob Account\F (240 octets) extrait, Key0 parsé (SAM_KEY_DATA, revision=1, RC4) :
tmpKey = MD5(Salt + QWERTY_CONST + bootkey + DIGITS_CONST)
hashedBootKey = RC4(tmpKey, Key + CheckSum)[:16]
Checksum de contrôle vérifiée (MD5(PEK+DIGITS+PEK+QWERTY) == CheckSum stocké) ✅
PEK = aa4f433bd3e23f9d7f524207c6eea8fa
7.3 Valeur V (hash NT chiffré) — introuvable
Cellule vk "V" du RID 1000 localisée (DataLength=0x110=272), mais son contenu (offset hive-relatif 0x1730) s'est révélé physiquement absent du dump : recherche exhaustive (par signature de structure, par empreinte RC4 calculée à partir de la PEK, sur les 2 Go entiers) → 0 résultat. Abandon de cette voie.
8. Les secrets LSA — percée majeure puis mur technique
8.1 Découverte via journal de transaction (TxR/CLFS)
Recherche de Policy\Secrets\DefaultPassword\CurrVal encodé en UTF-16 (les journaux de transaction du registre stockent les chemins en Unicode, contrairement aux cellules brutes en ASCII — angle mort de nos recherches précédentes).
→ Découverte d'un enregistrement de log auto-contenu avec la structure complète LSA_SECRET (Version+GUID+Algo+Flags+Data).
8.2 Déchiffrement de PolEKList → clé LSA
Algorithme AES vérifié contre le code source officiel de Volatility 3 (lsadump.py, fonction decrypt_aes) :
sha = SHA256(); sha.update(bootkey)
for _ in range(1000): sha.update(secret[28:60]) # salt
aeskey = sha.digest()
for chaque bloc de 16 octets à partir de l'offset 60:
AES-CBC(aeskey, IV=0) déchiffre indépendamment chaque bloc
→ Structure de clé LSA décodée, checksum interne cohérent.
LSAKey = 4c12960aa7a1117b3a999e9a90481282e1929e5af6cbc8d5f1c6413d78cff34
8.3 OldVal déchiffré avec succès → extraterrestres (PIÈGE)
Même algorithme appliqué à Policy\Secrets\DefaultPassword\OldVal → déchiffrement intégral et propre :
"extraterrestres" (15 caractères, 30 octets UTF-16, Length=30 confirmé)
Thématiquement parfait (Area 51 / Rachel) mais soumis et rejeté par le wargame.
8.4 Le vrai CurrVal (22 caractères) — mystère non résolu
CurrVal (mot de passe actuel) a Length=44 (22 caractères) — différent d'OldVal, donc extraterrestres est bien la mauvaise valeur.
3 copies indépendantes de CurrVal localisées en mémoire (sels différents : bbabe5b8..., 4a0a673d..., 8a58b233...), résolution d'adresse non-ambiguë vérifiée via la vraie liste de valeurs du registre (grâce à des pages hbin à offset hive-relatif connu).
Phénomène reproductible et inexpliqué sur les 3 copies :
- Le premier bloc AES (16 octets) se déchiffre parfaitement à chaque fois (
Length=44+ 12 octets nuls — motif si spécifique qu'un test de calibration sur 2000 clés aléatoires n'a donné aucun faux positif) - Tous les blocs suivants (contenant le mot de passe lui-même) restent illisibles, quel que soit le mode testé : ECB, CBC (IV=0 / IV=sel), CFB, OFB, CTR, RC4, AES-128 (clé tronquée), ordres de hachage alternatifs, sel décalé, etc.
L'algorithme a été validé ligne par ligne contre le code source réel de Volatility 3 — aucune divergence trouvée. Conclusion : la clé et la méthode sont prouvées correctes ; les octets du mot de passe au-delà du premier bloc sont absents du dump, probablement une perte de données réelle à la capture, pas un bug de déchiffrement.
9. Pistes explorées sans succès pour trouver/deviner le CurrVal
- Recherche exhaustive (2 Go) de tout bloc déchiffrable en texte UTF-16 lisible avec la clé confirmée (ECB et CBC chaîné) → 0 résultat
- ~13 000 combinaisons thématiques testées par vérification cryptographique (chiffrement du candidat + comparaison aux octets réels, pas du bruteforce aveugle) → 0 correspondance
- Historique de frappe URL (
TypedURLs), dossiers Vidéos/Téléchargements, Corbeille ($R...), cache DNS, navigateurs installés → aucune trace de fichier vidéo ou de site visité - Indice « Jacquie et Michel » → aucune trace littérale dans le dump ; probablement juste une inspiration thématique pour les autres champs (kiwis, zinzolins, Rachel) sans artefact direct à trouver
- Blob DPAPI (
Credentials\) → uniquement un artefact par défaut du modèle de ruche Windows, pas de vraies données - Autres secrets LSA (
$MACHINE.ACC,NL$KM,DPAPI_SYSTEM) pour valider la méthode sur une référence → non trouvés en mémoire - Commandes
reg add/netplwiz→ uniquement des signatures antivirus et enregistrements COM système
10. État final
Flag quasi complet, un seul élément manquant :
ESD{Les_[HASH_NTLM_DE_22_CARACTERES]_cultivent_des_kiwis_zinzolins_pr3s_de_RACHEL}
Confirmé avec certitude : champ1=kiwis, champ3=zinzolins, champ4=RACHEL, template exact, mécanisme complet.
Non résolu : la valeur du mot de passe Windows actuel de passurmoi (22 caractères), et donc son hash NTLM (champ2), à cause d'une donnée physiquement absente de ce dump mémoire précis — malgré une chaîne cryptographique intégralement reconstruite et vérifiée (bootkey → PEK → clé LSA → structure du secret).
Pistes pour la suite
- Un dump mémoire plus complet ou plus récent de la même VM
- Accès à un outil Volatility 3 complet (
windows.hashdump,windows.lsadump) sur une machine dédiée, au cas où son parcours d'adressage virtuel (VAD) résout ce que notre reconstruction manuelle de tables de pages n'a pas pu résoudre - Contact direct avec le créateur du challenge pour un indice complémentaire sur
champ2
Page reprise de l'ancien Wiki.js (https://wikijs.htv.nasspc.fr/WARGAME_Trouvez_la_phrase_top_secrete, créée le 2026-09-16, modifiée le 2026-09-16) le 09/11/2026.
Aucun commentaire à afficher
Aucun commentaire à afficher