Wargame (CTF)

Write-ups de challenges CTF / wargame (repris de Wiki.js le 09/11/2026). Livre public : lisible sans compte.

Modèle de write-up CTF

🚩 Write-up CTF : [Nom du Challenge]

Fiche technique Détails
Événement [Nom du CTF, ex: Root-Me / Hack The Box / PicoCTF]
Catégorie Web / Reverse / Pwn / Crypto / Forensics / OSINT
Difficulté 🟢 Facile / 🟡 Moyen / 🔴 Difficile
Auteur [Nom du créateur de l'épreuve]
Points 500 pts

📝 Description

[Coller ici la description officielle de l'épreuve]

Fichiers / Liens fournis :


🔍 1. Reconnaissance & Analyse

Explication des premières étapes de scan ou d'inspection.

nmap -sC -sV -p- 10.10.10.X

---
*Page reprise de l'ancien Wiki.js (https://wikijs.htv.nasspc.fr/WARGAME, créée le 2026-09-16, modifiée le 2026-09-16) le 09/11/2026.*

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é

Deux jeux de mots identifiés dès le départ :


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 :

  1. Calculer l'adresse physique de l'ancre connue (phys = file_off - 0x1cb720 + 0x200000)
  2. 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
  3. 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 :

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() :

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 :

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


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


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.