# Prince : un octet sur trois seulement est chiffré

**Catégorie :** Ransomware

**Analyste :** Gordon PEIRS

**Famille :** Prince (Prince-Ransomware), un RaaS en Go. Des chercheurs tiers notent que sa bibliothèque de chiffrement à clé publique (ECIES) ressemble à celle d'Anubis/EvilByte.

**Méthode :** reverse statique (Ghidra headless via PyGhidra, décompilation, support natif des binaires Go), confirmé par un désassemblage au niveau de l'ABI interne de Go pour lever une ambiguïté du décompilateur sur un site d'appel critique. L'échantillon n'est jamais exécuté.

---

## Résumé

La génération de la clé et du nonce par fichier (`crypto/rand.Read`) et leur enveloppement par ECIES (secp256k1, clé éphémère par appel, AES-256-GCM sur le secret ECDH) sont sains. Côté primitives, rien à signaler. Le défaut est dans la boucle de chiffrement du contenu, écrite par les auteurs du ransomware. Elle avance par pas de trois octets et n'applique le flux XChaCha20 qu'au premier octet de chaque triplet. Les deux suivants sont copiés **en clair, à leur position d'origine**. Je l'ai confirmé en désassemblant au niveau de l'ABI interne de Go (le décompilateur restitue mal cet appel), puis en comparant instruction pour instruction avec le code source du projet, qui est public. Ce n'est donc ni une erreur de lecture ni un bug. C'est un choix de conception, appliqué à une granularité qui le rend presque inutile comme protection.

**Conséquence : deux tiers du contenu de chaque fichier chiffré par Prince restent lisibles sans clé.** Un PoC (§8) récupère 66,4 % d'un fichier de test sans aucune clé, et 67,2 % en y ajoutant une reconstruction heuristique des lettres isolées qui manquent dans des mots (recherche dans un dictionnaire).

Prince-Ransomware est un générateur open source, public sur GitHub. Trois des cinq échantillons connus sous ce tag correspondent au code non modifié du générateur et ont tous le même défaut, que j'ai vérifié par décompilation sur chacun. L'un d'eux est très probablement le ransomware **CrazyHunter**. Des tiers (WithSecure Labs, TeamT5, presse taïwanaise) lui attribuent l'attaque du 6 février 2025 contre l'hôpital Mackay Memorial à Taïwan (plus de 500 postes paralysés, jusqu'à 32,5 Go de données patients exfiltrées) et au moins 11 attaques similaires contre des hôpitaux, des écoles et des entreprises taïwanaises.

> Tout est reproductible depuis [`runbook.md`](writeups/prince-ecies-go/runbook.md). Les scripts sont dans [`tools/`](tools.html?slug=prince-ecies-go).

## 1. Contexte

Le point de départ est une bibliothèque publique de chiffrement à courbe elliptique (`github.com/ecies/go`), que des chercheurs tiers disent réutilisée telle quelle par plusieurs familles de ransomware en Go. Je voulais savoir si cette bibliothèque, ou la façon de s'en servir, avait une faiblesse exploitable. L'échantillon n'est pas strippé : Ghidra retrouve directement les chemins source complets et les noms de fonctions `paquet.Fonction`, sans reconstruction manuelle.

La bibliothèque ECIES (secp256k1, clé éphémère générée à chaque appel, AES-256-GCM sur le secret partagé ECDH) ne montre aucune anomalie évidente dans le code décompilé. La faille est ailleurs, et elle est bien plus grave : elle se trouve dans la routine qui chiffre le contenu des fichiers, écrite par les auteurs du ransomware.

## 2. Échantillon

| Champ | Valeur |
|---|---|
| SHA256 | `84715cf1d82e5139d39d8aadb9580ba80393dfa0f63fa14974e4f74a3e69752e` |
| Taille | 2 458 624 octets |
| Type | PE32+ (Windows x64), Go 1.23.3, non strippé |
| Première soumission | 2025-02-22 |

Module Go : `Prince-Ransomware (devel)`. Il existe quatre autres échantillons sous le même tag sur MalwareBazaar, que je n'examine pas en détail ici (voir §7).

## 3. Génération de clé et de nonce par fichier : saine

Chaque fichier reçoit une clé AES de 32 octets et un nonce de 24 octets (ce qui correspond à XChaCha20), tous deux tirés de `crypto/rand.Read`, le générateur cryptographiquement sûr de la bibliothèque standard Go. Pas de graine prévisible, pas de repli sur `math/rand`. Le problème ne vient pas de là.

## 4. La clé et le nonce sont ensuite protégés correctement par ECIES

La clé et le nonce sont chacun enveloppés séparément par `github.com/ecies/go.Encrypt`, avec la clé publique de la campagne. Celle-ci est embarquée en clair dans les métadonnées de compilation du binaire (`-X 'Prince-Ransomware/configuration.PublicKey=04...'`, un point non compressé sur secp256k1). La bibliothèque utilise une clé éphémère fraîche à chaque appel et un AES-256-GCM standard. Je n'ai trouvé aucune anomalie dans cette construction.

## 5. Le contenu du fichier : un octet sur trois seulement

C'est ici que ça se gâte. Le fichier est lu en entier en mémoire, puis « chiffré » par une boucle qui avance de trois octets en trois octets :

```c
for (i = 0; i < taille; i += 3) {
    XORKeyStream(sortie[i:i+1], entree[i:i+1]);  // un seul octet chiffré
    if (i+1 < taille) { sortie[i+1] = entree[i+1]; }  // copie brute, en clair
    if (i+2 < taille) { sortie[i+2] = entree[i+2]; }  // copie brute, en clair
}
```

Ghidra omet certains arguments passés par registre sur cet appel, parce qu'il reconnaît mal la convention d'appel interne de Go. J'ai donc vérifié au désassemblage. Juste avant l'appel à `XORKeyStream` :

```asm
mov ecx, 0x1     ; longueur du buffer de sortie = 1 octet
mov r8, rcx      ; longueur du buffer d'entrée = 1 octet (copiée depuis ecx)
call <XORKeyStream>
```

D'après l'ABI interne de Go, un appel à `func (s *Cipher) XORKeyStream(dst, src []byte)` passe le receiver, puis dst.ptr/dst.len/dst.cap, puis src.ptr/src.len/src.cap, dans cet ordre de registres. `CX` et `R8` sont donc les longueurs des tranches `dst` et `src`, et les deux valent `1`. Indépendamment du pseudo-code, ça confirme que chaque appel au flux ChaCha20 ne transforme qu'un octet et que les deux suivants sont copiés tels quels dans le fichier de sortie.

**Conséquence : deux tiers du contenu de chaque fichier chiffré par Prince restent en clair, à leur position d'origine, lisibles sans aucune clé.** Ça n'a rien à voir avec le chiffrement intermittent que pratiquent plusieurs familles, qui sautent de gros blocs contigus pour aller plus vite sur de gros volumes tout en rendant les fichiers inutilisables. Ici l'entrelacement se fait à l'octet, et la quasi-totalité de la structure et du contenu utile d'un fichier reste directement lisible.

J'ai ensuite comparé cette lecture, obtenue par décompilation puis confirmée par le désassemblage de l'ABI Go, au code source du projet, qui est public. Il correspond instruction pour instruction :

```go
ciphertext := make([]byte, len(fileContent))
for i := 0; i < len(fileContent); i += 3 {
    if i < len(fileContent) {
        cipher.XORKeyStream(ciphertext[i:i+1], fileContent[i:i+1])
    }
    if i+1 < len(fileContent) {
        ciphertext[i+1] = fileContent[i+1]
    }
    if i+2 < len(fileContent) {
        ciphertext[i+2] = fileContent[i+2]
    }
}
```

Il n'y a donc plus d'ambiguïté. Ce n'est ni une erreur d'interprétation du binaire ni un off-by-one : c'est un choix de conception, une forme de chiffrement intermittent sans doute pensée pour aller plus vite, mais appliquée à l'octet plutôt qu'au bloc, ce qui la rend à peu près inutile comme protection.

## 6. Format du fichier produit, extension, note de rançon

Le fichier est réécrit en place (troncature puis écriture) avec la structure suivante :

```
[enveloppe ECIES de la clé][ || ][enveloppe ECIES du nonce][ || ][contenu, un octet sur trois chiffré]
```

Le séparateur `||` (deux octets `0x7c`, en clair) permet de repérer exactement où commence le contenu, donc l'alignement du motif un octet chiffré / deux octets en clair.

Le fichier chiffré est ensuite renommé avec l'extension `.MOONMAN` (une constante extraite des données du binaire), et une note de rançon `READTHISNOW.txt` est déposée dans chaque répertoire traité. L'extension et le nom de la note sont des constantes de configuration, que le projet source (voir §7) permet de personnaliser pour chaque variante.

Cette variante liste deux noms de fichiers à ne jamais chiffrer (`TOTAL_TRANNY_DEATH.png` et `READTHISNOW.txt`, la note elle-même) et exclut de façon générale les extensions `.exe`, `.dll`, `.sys`, `.lnk`.

## 7. Contexte : un builder open-source, deux usages très différents

Le chemin source embarqué dans le binaire (`C:/MoonMan/Prince-Ransomware-main/...`) porte le suffixe `-main` typique d'une archive ZIP téléchargée depuis GitHub. Des sources publiques indépendantes le confirment : Prince-Ransomware est un **générateur de ransomware open source**, publié librement sur GitHub. N'importe qui peut produire un exécutable fonctionnel en modifiant un fichier de configuration (clé publique, extension, texte de la note). Plusieurs variantes connues en dérivent sous d'autres noms.

L'échantillon principal de cette analyse (§2) est une variante qui s'appelle elle-même `UwU ransomware` dans son code et signe aussi `MoonMan`/`Sprunki`/`LockBitch` dans sa note de rançon. Le contenu de la note (rançon symbolique de 1 488 $ en cryptomonnaie, références à des communautés en ligne rivales comme BonziWORLD, CollabVM, OWOT, Sprunki, contenu haineux) évoque du vandalisme entre communautés internet plutôt qu'une opération criminelle organisée contre des cibles professionnelles. L'impact de cette variante sur de vraies organisations est sans doute négligeable.

Le code du générateur (`Builder/main.go`, public lui aussi) montre que la paire de clés ECIES est générée une fois par build avec la même fonction sûre (`eciesgo.GenerateKey()`, donc `crypto/rand`). Je n'ai trouvé aucune faiblesse de prévisibilité de la clé privée à ce niveau. **Mais le générateur compile deux exécutables à partir de cette paire : l'Encryptor, qui embarque la clé publique (c'est celui étudié ici), et un Decryptor, qui embarque la clé privée de la même manière** (`-X 'Prince-Decryptor/configuration.PrivateKey=%s'`, exactement le mécanisme de compilation utilisé pour la clé publique de l'Encryptor). Ce second exécutable n'est en principe remis qu'aux victimes qui ont payé. S'il tombe un jour entre les mains d'un tiers (victime qui le partage, saisie, fuite), la clé privée de la campagne s'en extrait aussi facilement que la clé publique extraite au §2, et elle permet de tout déchiffrer, y compris le tiers aujourd'hui inaccessible (§5), pour tous les fichiers de la campagne. Je n'ai trouvé aucun Decryptor sur MalwareBazaar au moment de l'analyse. C'est attendu : ce n'est pas à proprement parler un exécutable malveillant, il est donc rarement soumis aux plateformes d'échantillons.

**Le même code source a pourtant servi à des attaques sérieuses et documentées.** Un second échantillon du même tag MalwareBazaar (`f72c03d37db77e8c6959b293ce81d009bf1c85f7d3bdaa4f873d3241833c146b`, tag `crazyhunter`) porte le chemin de compilation `C:/Users/fake/Downloads/Prince/Prince v1.0.0/...` et le texte de fond d'écran `Hunter Ransomware`. Sa structure de code est strictement identique : mêmes packages, même dépendance `github.com/ecies/go@v1.0.1`, mêmes adresses de fonctions dans le binaire compilé. D'après des sources publiques indépendantes (WithSecure Labs, TeamT5, presse taïwanaise), un ransomware nommé CrazyHunter, construit avec le générateur Prince-Ransomware, a frappé le 6 février 2025 l'hôpital Mackay Memorial à Taïwan (plus de 500 postes paralysés, jusqu'à 32,5 Go de données patients exfiltrées). L'attaque est passée par une clé USB puis par une technique BYOVD (un pilote signé vulnérable de Zemana AntiMalware) pour désactiver l'EDR. Les autorités taïwanaises ont identifié l'opérateur, un ressortissant chinois de la province du Zhejiang, responsable d'au moins 11 attaques similaires contre des hôpitaux, des écoles et des entreprises cotées à Taïwan en février-mars 2025. **J'ai vérifié le défaut de chiffrement partiel (§5) sur ce second échantillon, avec la même méthode de désassemblage : il est identique.** Le code compilé est le même que celui du premier, à la clé publique embarquée près.

Un troisième échantillon du même tag (`b44af07de17ed16af4d6699b9462bee6855a0a0207ec787d41f00fbed81e1c13`, une campagne qui se présente comme « AnonWorld ») a été vérifié de la même façon : **code compilé identique, même défaut**, seule la clé publique de campagne change. Sur les cinq échantillons disponibles sous le tag `Prince`, trois correspondent donc au code non modifié du générateur, et ils sont tous vulnérables. Les deux autres sont des projets dérivés au code différent (l'un se présente comme `EByte-Ransomware`, variante « Zero Day Locker », l'autre comme un projet nommé `GoConti`). Je ne les ai pas vérifiés.

Le lien entre l'échantillon `crazyhunter` et le payload utilisé contre l'hôpital Mackay Memorial repose sur un recoupement de nom et de tag, pas sur une source qui confirmerait ce hash. S'il est exact, la faille décrite ici touche au moins une partie d'une série d'attaques réelles contre des établissements de santé, des écoles et des entreprises, et pas seulement un usage anecdotique.

Une source tierce (PCRisk) note par ailleurs que le générateur Prince n'attribue aucun identifiant unique aux victimes, ce qui empêche les opérateurs de savoir qui a payé. Plusieurs analystes en concluent que certains déploiements de ce code pourraient ne jamais rendre l'accès aux données, même après paiement, que ce soit voulu ou non. Au moment de l'analyse, je n'ai trouvé aucun décrypteur public pour Prince, ses variantes nommées ou CrazyHunter, ni sur No More Ransom ni ailleurs.

## 8. Preuve de concept : récupération sans clé

Pour montrer que le défaut du §5 est exploitable sans exécuter l'échantillon ni disposer d'un vrai fichier de victime, deux scripts autonomes reproduisent le format de fichier et la récupération (`tools/simulate_prince_format.py`, `tools/poc_recover_plaintext.py`). Le premier ne réimplémente pas le vrai chiffrement (ECIES/XChaCha20). Il utilise un flux pseudo-aléatoire quelconque pour le tiers d'octets transformé, ce qui suffit à montrer que la faille ne dépend pas de la solidité du chiffre appliqué à ce tiers.

Sur un fichier texte de test de 256 octets :

```
170/256 octets récupérés sans aucune clé (66,4 %)
```

Le texte reconstitué (les octets non récupérés sont affichés en `.`) reste lisible et reconnaissable, même avec un caractère sur trois en moins :

```
.ec. e.t .n .oc.me.t .e .es. r.pr.se.ta.if.d'.n .ic.ie. d. v.ct.me.
```

("Ceci est un document de test representatif d'un fichier de victime.")

**Le tiers restant n'est pas récupérable de cette façon, et il ne peut pas l'être sans la clé privée de la campagne.** Ces octets sont vraiment chiffrés par XChaCha20, un chiffre de flux où chaque octet de sortie est indépendant des autres : connaître le clair aux positions voisines n'apprend rien, mathématiquement, sur l'octet manquant. Un troisième script (`tools/poc_infer_gaps.py`) explore une piste complémentaire, purement heuristique et sans rapport avec la cryptographie. Comme le motif laisse toujours en clair les deux voisins de chaque octet manquant, on peut parfois deviner une lettre à l'intérieur d'un mot en cherchant dans un dictionnaire (français et anglais, via `hunspell`), comme une case vide de mots croisés. Sur le même fichier de test, 9 mots avaient exactement une lettre manquante. Seuls 2 ont une solution unique (les 7 autres admettaient plusieurs mots valides), ce qui fait passer le taux de reconstruction de 66,4 % à 67,2 %. Le gain dépend beaucoup du contenu : sensible sur un long texte au vocabulaire courant, nul sur des données binaires, des identifiants ou du contenu déjà compressé, où le script s'abstient plutôt que de deviner.

## 9. Portée et limites

- **Le motif n'a pas été validé sur un vrai fichier de victime**, faute d'avoir un fichier `.MOONMAN` réel. La conclusion repose sur une lecture croisée du binaire (décompilation, désassemblage de l'ABI, résultat identique sur un second échantillon indépendant, §7), sur le code source du projet (§5, correspondance instruction pour instruction) et sur un PoC qui reproduit le format sans réimplémenter le vrai chiffrement (§8). Je n'ai pas observé de sortie réelle du malware. Mais comme le binaire compilé et le code source publié disent la même chose, une erreur d'interprétation est très improbable.
- **Trois échantillons sur cinq (le code non modifié du générateur) ont été vérifiés en détail et ont le même défaut.** Les deux autres sont des projets dérivés au code différent (§7), que je n'ai pas examinés.
- Le rattachement du second échantillon (§7) à l'incident CrazyHunter/Mackay Memorial Hospital repose sur un recoupement de nom (`Hunter Ransomware` en fond d'écran, tag `crazyhunter` sur MalwareBazaar) et de structure de code. Aucune source ne confirme explicitement ce hash comme le payload de cet incident.
- La génération d'aléa (§3) et la bibliothèque ECIES (§4) sont saines. Le problème est propre à l'implémentation de la boucle de chiffrement du contenu, pas au choix des primitives.

## 10. IOCs

| Type | Valeur |
|---|---|
| SHA256 (échantillon principal, variante "UwU") | `84715cf1d82e5139d39d8aadb9580ba80393dfa0f63fa14974e4f74a3e69752e` |
| SHA256 (second échantillon, variante "Hunter", tag `crazyhunter`) | `f72c03d37db77e8c6959b293ce81d009bf1c85f7d3bdaa4f873d3241833c146b` |
| SHA256 (troisième échantillon, campagne "AnonWorld") | `b44af07de17ed16af4d6699b9462bee6855a0a0207ec787d41f00fbed81e1c13` |
| SHA256 (projets dérivés à code différent, non analysés) | `a205f794058c59a19322debb1e96a6133ebbab01cb57ae159c2ddb3c7e97a922` (EByte-Ransomware / "Zero Day Locker"), `4d0be6135596f7ec27f4b071f8225b6423132d51e44320f251cbe7036f447165` ("GoConti") |
| Extension ajoutée (variante "UwU") | `.MOONMAN` |
| Nom de la note de rançon (variante "UwU") | `READTHISNOW.txt` |
| Clé publique de campagne, variante "UwU" (secp256k1, point non compressé) | `049054dc5ba95cfcb7fea790425f2ef2cf4565ffe0a6ad47913cde6112c3a47be10fd8c69bf1463d57fa794c4600dec3874ef5de853d4a3d4120360a13e9f3bd05` |
| Clé publique de campagne, variante "Hunter" (secp256k1, point non compressé) | `04a6ac1e60b24b6261a3dbfa73b13c62bea4fa16476b6df4cbff891cf2d8242f6f6c81b418ee0036851908480601b45ad6a82423d261836fb7aec17d3c0b39f414` |
| Clé publique de campagne, "AnonWorld" (secp256k1, point non compressé) | `04f64a958d4f19de417d99398036fd22436872f8746374571a51029da694b89069317f464dcc9337b54318f3c2cb446a08b73c47ca9046b021defa5cddd6b4ccdc` |
| Contacts (variante "UwU") | `sn33ds3curity@tutanota.com`, `vitollebonzi@gmail.com` |
| Adresse de paiement (variante "UwU", chaîne au format Bitcoin, non vérifiée on-chain) | `357a3So9CbsNfBBgFYACGvxxS6tMaDoa1P` |

## 11. Divulgation et usage

Les échantillons étudiés sont publics (MalwareBazaar). Ils sortent d'un générateur de ransomware open source dont au moins une variante a servi à une attaque réelle documentée par des tiers (§7). Le défaut (un tiers seulement du contenu est chiffré) permet de récupérer directement les deux tiers du contenu original de tout fichier chiffré par ce code, sans la clé privée de la campagne. C'est une information utile aux victimes pour évaluer ce qu'elles peuvent récupérer sans payer ni coopérer avec les opérateurs. Le PoC fourni (`tools/`) ne reproduit que le format de fichier décrit ici, à des fins de démonstration, et ne contient aucun code de chiffrement réutilisable pour construire un ransomware. Mon but est de documenter, code décompilé et désassemblé à l'appui, une faiblesse d'implémentation qui ne semble publiée nulle part ailleurs à ce jour.

## 12. Reproduire l'analyse

Le journal détaillé est dans [`runbook.md`](writeups/prince-ecies-go/runbook.md). Le PoC est dans [`tools/`](tools.html?slug=prince-ecies-go) (`simulate_prince_format.py`, `poc_recover_plaintext.py`, `poc_infer_gaps.py`). La règle YARA : [`prince_ecies_go.yar`](writeups/prince-ecies-go/prince_ecies_go.yar). Le guide de détection et de remédiation pour une victime : [`remediation.md`](writeups/prince-ecies-go/remediation.md).
