# 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`](writeups/scorched-earth-ausfc/runbook.md). Les scripts (déchiffrement, restauration) sont dans [`tools/`](tools.html?slug=scorched-earth-ausfc).

---

## 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`, `strings` pour 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`](writeups/scorched-earth-ausfc/tools/decompile_by_name.py), dans [`tools/`](tools.html?slug=scorched-earth-ausfc)) 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` :

1. Argon2id(mot de passe, sel, temps=3, mémoire=262144 KiB, parallélisme=2) produit 32 octets.
2. SHA-256 de ce résultat (et, si un fichier-clé est fourni, SHA-256 de son contenu concaténé au précédent).
3. 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 :

```python
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`](writeups/scorched-earth-ausfc/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`](writeups/scorched-earth-ausfc/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.
