Prinz Eugen : un mot de passe en dur derrière Argon2id
Catégorie : Ransomware
Analyste : Gordon PEIRS
Famille : ransomware Prinz Eugen. Son nom de projet interne, « scorched-earth-ausfc », se retrouve dans les chemins de compilation embarqués et ThreatDown le cite comme IOC. C'est un RaaS documenté publiquement (voir §10) : usurpation de marque (Standard Bank), leak-site Tor, accès initial par identifiants RDP compromis. Ce binaire-ci (fc.exe, hash différent de l'échantillon ThreatDown) m'a été transmis directement pour analyse, hors de toute plateforme d'échantillons, avec un fichier réellement chiffré à récupérer.
Type : analyse statique uniquement, le binaire n'a jamais été exécuté. Ghidra headless via PyGhidra, décompilation par nom de symbole Go. Le déchiffrement d'un vrai fichier victime est confirmé par une réimplémentation Python de toute la chaîne cryptographique.
Tout est reproductible depuis
runbook.md. Les scripts (déchiffrement, restauration) sont danstools/.
Résumé
On m'a transmis le binaire fc.exe avec un fichier qu'il avait chiffré et qu'il fallait récupérer, sans mot de passe ni clé au départ. Il est écrit en Go et n'est pas strippé (table de symboles COFF intacte). Son chemin de compilation interne, scorched-earth-ausfc, le rattache au ransomware Prinz Eugen, un RaaS documenté publiquement (usurpation de la marque Standard Bank, leak-site Tor, accès initial par RDP compromis). J'ai décodé champ par champ son format de fichier maison chv1 et vérifié le résultat sur le vrai fichier. Le schéma cryptographique (Argon2id, HKDF-SHA256, ChaCha20-Poly1305, sel et nonce tirés du générateur du système) est correctement construit. Le problème est ailleurs : le mot de passe est généré une fois à la compilation puis figé en dur dans le binaire. On peut donc l'extraire et déchiffrer le fichier fourni sans casser la moindre primitive. Je livre deux outils autonomes : un déchiffreur pour un fichier isolé, et un outil qui restaure un dossier entier en vérifiant l'intégrité de chaque fichier avant de supprimer l'original.
1. Contexte et échantillon
Le binaire fc.exe m'a été confié avec un objectif concret : récupérer un fichier chiffré par cet outil, sans mot de passe ni fichier-clé connus au départ.
| Champ | Valeur |
|---|---|
| SHA256 | 88e63d1f5f5478bb98269bb48c7508c6fc90f1f203f37aa7b30b801080b55fbd |
| Type | PE32+, x86-64, 16 sections, console |
| Taille | 3 673 600 octets |
| Runtime | Go 1.26.2 |
| Chemin de compilation embarqué | C:/Users/User/source/scorched-earth-ausfc/cmd/encrypter/main.go |
| Module Go | scorched-earth-ausfc (devel), dépendances golang.org/x/crypto v0.50.0, golang.org/x/sys v0.43.0 |
Le binaire n'est pas strippé. La table de symboles COFF garde tous les noms de packages et de fonctions Go (internal/crypto, internal/chv1, internal/fswalk, cmd/encrypter). On peut donc naviguer par nom de symbole au lieu de reconstruire la structure du programme depuis zéro.
Le chemin cmd/encrypter/main.go indique un dépôt Go qui produit plusieurs binaires, et celui-ci n'est que le module de chiffrement. L'échantillon analysé par ThreatDown (servertool.exe, voir §10) vient probablement d'un autre cmd/ du même dépôt, tourné vers l'orchestration et le C2. Ça explique que ce binaire-ci n'ait aucune capacité réseau ni persistance (confirmé au §3).
On m'a aussi fourni un fichier chiffré par cet outil : HelloWorld.txt.prinzeugen (86 octets). Il sert de référence pour toutes les vérifications de cette analyse.
2. Outillage
pefile,objdump,stringspour le triage initial et l'extraction de la table de symboles COFF.- Ghidra headless via PyGhidra pour la décompilation, avec un outil écrit pour l'occasion (
decompile_by_name.py, danstools/) qui retrouve une fonction par son nom de symbole Go complet plutôt que par une adresse calculée à la main. - Python (
argon2-cffi,cryptography) pour réimplémenter la chaîne de dérivation de clé et valider le déchiffrement.
3. Triage : un binaire Go non strippé, sans capacité réseau
Le PE n'importe directement que kernel32.dll, ce qui est trompeur : les binaires Go font la plupart de leurs appels système sans passer par la table d'imports classique. C'est la table de symboles qui dit ce qu'est vraiment ce binaire. On y voit des sections aux noms longs (/4, /19, /32...) typiques du COFF avec table de chaînes, une section .symtab complète, et des chaînes qui ne laissent aucun doute sur le Go (go1.26.2, golang.org/x/crypto/argon2, golang.org/x/crypto/chacha20poly1305, golang.org/x/crypto/hkdf).
Un point important pour la suite : la table de symboles ne contient aucune capacité réseau. Ni net.Dial, ni net/http, ni crypto/tls (les seules occurrences de « tls » dans le binaire concernent le thread-local storage du runtime Go, rien à voir). Le seul appelant de os.Getenv est le runtime Go lui-même (résolution de PATH pour os/exec, options GODEBUG), jamais le code de l'outil. Ce chiffreur n'envoie donc rien à un serveur de commande et contrôle : toute la logique de clé reste locale au processus.
Autre comportement à noter : la fonction main.selfDelete relance le processus via os/exec.Command pour se supprimer une fois le travail fini. C'est ce que fait un ransomware, pas un utilitaire de sauvegarde.
4. Architecture du programme
La table de symboles COFF liste directement les packages internes :
internal/crypto:DeriveKeyArgon2id,deriveFinalKey,DeriveKeyMaterial,EncryptFileTo,DecryptTo,VerifyEncryptedMatchesFile.internal/chv1:EncodeHeaderV1,DecodeHeaderV1,ReadHeaderV1,WriteHeaderV1(format de fichier maison).internal/fswalk:IterFiles,isEncrypted,isTmp,shouldExclude(parcours récursif de répertoire).cmd/encrypter:main.main,main.encryptOne,main.selfDelete.
main.main accepte un flag -delete (supprimer les originaux après chiffrement et vérification) et une liste de chemins en argument. Pour chaque fichier renvoyé par fswalk.IterFiles, un pool de goroutines (une par cœur CPU) appelle main.encryptOne. Cette fonction ouvre le fichier, appelle crypto.EncryptFileTo, synchronise et renomme le résultat puis, si demandé, vérifie l'intégrité avec crypto.VerifyEncryptedMatchesFile avant de supprimer l'original.
5. Le format de fichier chv1, décodé et vérifié champ par champ
chv1.ReadHeaderV1 lit un header de taille fixe, exactement 0x35 = 53 octets, placé avant tout contenu chiffré. chv1.DecodeHeaderV1 valide dans l'ordre le magic, la version, l'identifiant de KDF, la longueur du sel, l'identifiant de chiffrement et la longueur du nonce de base. Chaque champ invalide a son message d'erreur explicite ("unsupported kdf", "unsupported base nonce length", etc.), présent en clair dans le binaire.
Voici le décodage complet, où chaque champ a été confronté au vrai fichier HelloWorld.txt.prinzeugen :
offset taille champ
0 4 magic "CHV1"
4 1 version (=1)
5 1 identifiant KDF (=1, Argon2id)
6 4 coût mémoire Argon2 en KiB (u32 little-endian)
10 4 coût temps Argon2 / itérations (u32 little-endian)
14 4 parallélisme Argon2 (u32 little-endian)
18 1 longueur du sel (=16, fixe)
19 16 sel (aléatoire, crypto/rand.Read, frais par fichier)
35 1 identifiant de chiffrement (=2, ChaCha20-Poly1305)
36 1 longueur du nonce de base (=4, fixe)
37 4 nonce de base (aléatoire, crypto/rand.Read, frais par fichier)
41 4 taille de chunk (u32 little-endian)
45 8 taille du fichier original (u64 little-endian)
puis, répété par chunk :
4 longueur du chunk chiffré (u32 little-endian)
N texte chiffré + tag d'authentification Poly1305 (16 octets)
Sur HelloWorld.txt.prinzeugen : Argon2id avec un coût mémoire de 262144 KiB (256 Mo), 3 itérations, un parallélisme de 2, et un seul chunk de taille 1048576 (1 Mo, largement assez pour les 13 octets du fichier original). Ce décodage explique chacun des 86 octets du fichier, sans reste.
Le nonce de chaque chunk est la concaténation des 4 octets aléatoires du header et d'un compteur de chunk sur 8 octets, ce qui garantit qu'il est unique à l'intérieur d'un fichier. Le sel est lui aussi tiré au hasard pour chaque fichier, donc la clé dérivée change d'un fichier à l'autre. Il n'y a pas de réutilisation nonce/clé entre fichiers, même avec seulement 32 bits de nonce aléatoire par fichier.
6. Chaîne de dérivation de clé
Je l'ai reconstituée en décompilant crypto.deriveFinalKey, crypto.DeriveKeyArgon2id et crypto.DeriveKeyMaterial :
- Argon2id(mot de passe, sel, temps=3, mémoire=262144 KiB, parallélisme=2) produit 32 octets.
- SHA-256 de ce résultat (et, si un fichier-clé est fourni, SHA-256 de son contenu concaténé au précédent).
- HKDF-SHA256 de ce condensé, avec le même sel de 16 octets et une chaîne d'info fixe :
scorched-earth key v1(21 octets), produit la clé finale de 32 octets utilisée directement comme clé ChaCha20-Poly1305.
Les paramètres Argon2id (256 Mo, 3 itérations, parallélisme 2) sont des constantes codées en dur dans main.main, on ne peut pas les changer en ligne de commande. Ils sont sérieux : avec 256 Mo de mémoire par tentative, un brute-force sur GPU ou ASIC coûte très cher. Avec un sel et un nonce vraiment aléatoires, on aurait un schéma solide si le mot de passe qui alimente la chaîne était lui-même secret et fort.
7. Le mot de passe est codé en dur dans le binaire
Aucun flag -password n'est enregistré (il n'y a que -delete), aucune saisie interactive masquée (golang.org/x/term.ReadPassword est absent de la table de symboles), aucune variable d'environnement propre au programme. Il ne reste qu'un candidat, visible dans main.main : un buffer de 32 octets construit une seule fois avant la boucle sur les fichiers et réutilisé à chaque appel à encryptOne.
Ghidra nomme mal cette zone de données : il donne au buffer le nom d'une chaîne d'erreur voisine dans .rdata. En lisant directement les octets à l'adresse virtuelle que référence le décompilateur (avec pefile.get_data()), le contenu apparaît en clair :
SUPERCLEDEMORTQUITUE (valeur réelle remplacée pour cette publication)
32 caractères alphanumériques imprimables : c'est un mot de passe généré aléatoirement à la compilation, puis embarqué en dur. Comme il n'est lu qu'une fois avant la boucle de traitement, il est le même pour tous les fichiers chiffrés par cet exécutable. Je ne publie pas sa valeur ici (elle a été transmise à part à la personne concernée). Le mécanisme d'extraction et sa validation par déchiffrement (§8) sont, eux, entièrement documentés.
8. Déchiffrement réel confirmé
J'ai réimplémenté toute la chaîne en Python (Argon2id avec argon2-cffi, HKDF-SHA256 et ChaCha20-Poly1305 avec cryptography) et je l'ai appliquée au sel et au nonce extraits du vrai fichier HelloWorld.txt.prinzeugen, avec le mot de passe retrouvé plus haut :
argon2_out = hash_secret_raw(password, salt, time_cost=3, memory_cost=262144,
parallelism=2, hash_len=32, type=Type.ID)
material = hashlib.sha256(argon2_out).digest()
key = HKDF(algorithm=hashes.SHA256(), length=32, salt=salt,
info=b"scorched-earth key v1").derive(material)
nonce = base_nonce + struct.pack("<Q", 0)
ChaCha20Poly1305(key).decrypt(nonce, ciphertext_avec_tag, None)
Le tag Poly1305 est valide et le clair obtenu fait exactement la taille annoncée dans le header (13 octets) : Hello World !. Le fichier est authentifié et déchiffré, le mot de passe retrouvé est donc le bon.
Un outil autonome, tools/decrypt_chv1.py, généralise cette chaîne à n'importe quel fichier produit par ce binaire, y compris ceux qui s'étendent sur plusieurs chunks.
9. Confirmation externe : ransomware Prinz Eugen (ThreatDown)
Le nom de projet Go « scorched-earth-ausfc » (§1) est cité tel quel comme IOC dans une analyse publique de ThreatDown (Malwarebytes), parue sous le nom « Prinz Eugen ransomware » et portant sur un autre échantillon (servertool.exe, SHA256 686213cc11d36af764de824801bced9366dfca3823fe0d51b752f74149bcf1f4). Un nom de projet aussi spécifique ne peut pas être une coïncidence : c'est bien la même famille.
Ce que ThreatDown documente et que je n'ai pas observé (recoupement externe, non vérifié sur ce binaire) :
| Type | Valeur |
|---|---|
| Infrastructure C2 | 212.80.7.74 (AS215439, Play2go International, Francfort), HTTPS/443 |
| Domaines de phishing | stndrdbnk.cc (usurpation Standard Bank), g-captchafestung.sbs (faux CAPTCHA), festung-e.duckdns.org |
| Leak-site Tor | prinzfkbjiazbrur4mjje6mntjc4vydx3iatkkzycufoylqcoo4y7pqd.onion (actif), 6cudc5cqa2bjpwdhcwm2lj6dbqejjjqzeo6ipwvmbazr6cgu7vfk3dad.onion (inactif) |
| Persistance observée | net user admin germania /add |
| Handles d'acteur | ROOTBOY, avtokz, GERMANIA |
| Contacts | prinzeugen@mail2tor.co, standardbankcc@cock.li, TOX 496187425B2944D73FBB17CAF3F9FD569B9ED3A08A497A8314CB4F27A51E65081ACEE1E22F21 |
| Bitcoin | bc1q2ztpcvqdaptej6uu2ywt9mrlatx6envu34rf0v |
| Vecteur d'accès initial | Identifiants RDP compromis, abus de l'outil RMM RemotePC (IDrive) |
Ce que cette analyse confirme, au niveau du code plutôt que du comportement : ThreatDown décrit le même schéma cryptographique (Argon2id → SHA-256 → HKDF-SHA256, ChaCha20-Poly1305, magic CHV1, chiffrement par chunk). C'est une confirmation indépendante du format chv1 décodé au §5 et de la chaîne de clé du §6.
Une divergence sur la clé : sur leur échantillon, ThreatDown affirme qu'il n'y a pas de mot de passe codé en dur (« clé maître zéroïsée avant sortie »). Sur fc.exe, j'en ai extrait un et j'ai confirmé qu'il fonctionne en déchiffrant un vrai fichier (§7-8). Deux explications possibles, que je ne peux pas départager sans leur échantillon : un défaut de build propre à cet exemplaire (cohérent avec le §10, le mot de passe étant régénéré à chaque compilation), ou une clé présente chez eux aussi mais qu'ils n'ont pas localisée. Dans les deux cas, la voie de récupération décrite dans remediation.md n'est démontrée que pour un binaire qui a ce défaut, pas pour toute la famille.
Ce que cette analyse ne couvre pas : fc.exe correspond au chemin cmd/encrypter/main.go (§1), donc au seul module de chiffrement de fichiers. Toute l'infrastructure listée plus haut (C2, persistance, phishing, RMM) vient d'un autre binaire (servertool.exe) que je n'ai pas examiné.
10. Conclusion
Le schéma cryptographique (Argon2id, HKDF-SHA256, ChaCha20-Poly1305, sel et nonce tirés du générateur aléatoire du système) est bien construit et n'a aucune faiblesse structurelle. Toute la faille est dans la gestion de la clé : un mot de passe généré une fois à la compilation, figé en dur dans l'exécutable, le même à chaque exécution de ce binaire. Quiconque récupère ce fc.exe peut en sortir le mot de passe et déchiffrer tous les fichiers qu'il a produits, sans casser la moindre primitive.
Cette faiblesse est propre au binaire qui embarque ce mot de passe. Vu son caractère aléatoire, il est sans doute régénéré à chaque compilation : un autre exemplaire aurait alors un mot de passe différent, à retrouver par la même méthode.
Le rattachement à Prinz Eugen (§9) donne une portée pratique à cette faille. Une organisation victime de cette famille qui met la main sur le binaire de chiffrement (fc.exe ou équivalent), et pas seulement sur le C2, a une vraie chance d'y trouver le même défaut et d'éviter de payer ou de perdre ses données.