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. Les scripts sont dans tools/.

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 :

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 :

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 :

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. Le PoC est dans tools/ (simulate_prince_format.py, poc_recover_plaintext.py, poc_infer_gaps.py). La règle YARA : prince_ecies_go.yar. Le guide de détection et de remédiation pour une victime : remediation.md.