Runbook Prince (Prince-Ransomware)
Contexte du choix de cible#
Recherche initiale sur Anubis (RaaS Go, décembre 2024, ECIES via
github.com/ecies/go) : aucun échantillon exploitable sur MalwareBazaar,
le tag/signature Anubis étant entièrement pollue par un cheval de Troie
bancaire Android homonyme (dizaines d'APK 2021-2026) et un unique exe de
2022 (20 Ko, trop ancien, predate le RaaS). Trend Micro note que la
bibliothèque ECIES d'Anubis est similaire à celle utilisée par
EvilByte/Prince. Prince a de vrais échantillons disponibles : bascule sur
cette cible, même piste technique (bibliothèque cryptographique publique
réutilisée), corpus réel.
Acquisition#
curl -s -X POST https://mb-api.abuse.ch/api/v1/ \
-H "Auth-Key: $(cat ../.mb_api_key)" \
-d "query=get_taginfo&tag=Prince&limit=100"
# 5 hashs, dont un tague explicitement golang+Ransomware :
# 84715cf1d82e5139d39d8aadb9580ba80393dfa0f63fa14974e4f74a3e69752e
# (2458624 octets, exe, first_seen 2025-02-22)
curl -s -X POST https://mb-api.abuse.ch/api/v1/ \
-H "Auth-Key: $(cat ../.mb_api_key)" \
-d "query=get_file&sha256_hash=84715cf1d82e5139d39d8aadb9580ba80393dfa0f63fa14974e4f74a3e69752e" \
-o binaries/84715cf1.zip
7z x -pinfected -y -obinaries binaries/84715cf1.zip
Confirmation : binaire Go non stripé, symboles complets#
file binaries/*.exe
# PE32+ executable for MS Windows 6.01 (GUI), x86-64, 8 sections
strings -n 6 binaries/*.exe | grep -iE "^go1\.|golang.org|ecies|prince"
Chemins source complets présents en clair (le binaire n'a pas été
strippe) :
- Encryptor/main.go, Encryptor/configuration/configuration.go,
Encryptor/encryption/encryption.go, Encryptor/filewalker/filewalker.go
- github.com/ecies/go@v1.0.1/{ecies,privatekey,publickey}.go
- golang.org/x/crypto@v0.24.0/chacha20/*.go
Clé publique de la campagne embarquée en clair dans les flags de build :
-ldflags="-H=windowsgui -s -w -X 'Prince-Ransomware/configuration.PublicKey=049054dc5ba95cfcb7fea790425f2ef2cf4565ffe0a6ad47913cde6112c3a47be10fd8c69bf1463d57fa794c4600dec3874ef5de853d4a3d4120360a13e9f3bd05'"
(point EC non compressé, préfixe 04, 65 octets : courbe secp256k1 d'après
la documentation publique de github.com/ecies/go).
Go 1.23.3, module Prince-Ransomware (devel).
Localisation directe des fonctions clé (noms Go récupérés nativement par Ghidra)#
python3 tools/list_functions.py binaries/84715...52e.exe /tmp/out.txt \
"ecies" "encryption" "configuration" "filewalker" "Encryptor"
Pas besoin de chasse à l'adresse comme sur les binaires strippés : Ghidra
récupère directement les noms paquet.Fonction via son support natif Go
(pclntab). Fonctions clé :
- Prince-Ransomware/encryption.generateKey @ 0x5019a0
- Prince-Ransomware/encryption.generateNonce @ 0x501a20
- Prince-Ransomware/encryption.EncryptFile @ 0x501aa0
- github.com/ecies/go.Encrypt @ 0x4fee80
- github.com/ecies/go.GenerateKey @ 0x4ff260
Génération clé/nonce par fichier : saine#
python3 tools/decompile_by_name.py binaries/84715...52e.exe /tmp/out.txt \
"Prince-Ransomware/encryption.generateKey" "Prince-Ransomware/encryption.generateNonce"
generateKey (encryption.go:30) : crypto/rand.Read sur un slice de 32
octets. generateNonce (encryption.go:38) : crypto/rand.Read sur un
slice de 24 octets (cohérent avec un nonce XChaCha20). Les deux passent
par le CSPRNG système de Go (crypto/rand, pas math/rand), sans graine
prédictible. Rien d'anormal à ce niveau.
EncryptFile (encryption.go:46) : découverte du vrai problème#
python3 tools/decompile_by_name.py binaries/84715...52e.exe /tmp/out.txt \
"Prince-Ransomware/encryption.EncryptFile"
Flux général (confirmé par lecture du décompilé complet) :
1. generateKey() / generateNonce() : matériel frais par fichier (voir
ci-dessus).
2. ecies.Encrypt(cle_publique_campagne, cle_aes) et
ecies.Encrypt(cle_publique_campagne, nonce) : chaque valeur est
enveloppée séparément via ECIES (secp256k1, clé éphémère par appel
d'après github.com/ecies/go.GenerateKey, puis AES-256-GCM sur le
secret partagé ECDH côté bibliothèque : crypto/aes.NewCipher suivi de
crypto/cipher.newGCMWithNonceAndTagSize(cipher,0x10,0x10)). Rien
d'evidemment cassé dans cette bibliothèque publique elle-même (courbe
standard, GCM standard, clé éphémère par appel) : ce n'est pas la piste
qui aboutit ici, contrairement à l'hypothèse de départ sur Anubis.
3. chacha20.newUnauthenticatedCipher(etat, cle, nonce) initialise le
flux ChaCha20 avec le matériel EN CLAIR (avant enveloppe ECIES).
4. Lecture intégrale du fichier en mémoire (boucle os.File.Read par
blocs de 0x10000 octets, accumulés dans un buffer).
5. Boucle de "chiffrement" du contenu, le vrai problème :
for (uVar3 = 0; (int)uVar3 < (int)local_188; uVar3 = uVar3 + 3) {
// ... XORKeyStream() applique UNIQUEMENT sur 1 octet ...
if (uVar3 + 1 < local_188) { local_18[uVar3+1] = local_90[uVar3+1]; } // copie brute
if (uVar3 + 2 < local_188) { local_18[uVar3+2] = local_90[uVar3+2]; } // copie brute
}
Vérification par désassemblage précis (le décompilateur élide les arguments de registre du call sur cet ABI Go interne, ne pas se fier uniquement au pseudo-C) :
objdump -d -M intel --start-address=0x501aa0 --stop-address=0x5022e0 binaries/*.exe \
| grep -B25 "call.*5007e0" # 0x5007e0 = XORKeyStream
501f00: mov ecx,0x1 ; dst.len = 1
501f05: mov r8,rcx ; src.len = 1 (copie depuis ecx)
501f08: mov rax,rsi ; receiver (cipher) -> AX
501f0b: mov rbx,r12 ; dst.ptr -> BX
501f0e: mov rdi,r11 ; dst.cap -> DI
501f11: mov rsi,r13 ; src.ptr -> SI
501f14: call 0x5007e0
Mapping ABIInternal Go (amd64) pour func (s *Cipher) XORKeyStream(dst, src []byte) :
receiver=AX, dst.ptr=BX, dst.len=CX, dst.cap=DI, src.ptr=SI, src.len=R8,
src.cap=R9. CX et R8 (dst.len et src.len) valent explicitement 1 :
chaque appel ne traite qu'un seul octet. Confirmé indépendamment par la
structure du pseudo-C Ghidra (incrément de boucle par 3, deux copies
brutes explicites des octets suivants).
Conclusion : Prince ne chiffre réellement qu'un octet sur trois du contenu de chaque fichier. Les deux autres octets sur trois sont copiés tels quels (en clair) à leur position d'origine dans le fichier "chiffré". Ce n'est pas du chiffrement intermittent par blocs (technique légitime utilisée par plusieurs familles réelles pour accélérer le chiffrement de gros volumes tout en rendant les fichiers inutilisables), c'est un entrelacement octet par octet qui laisse les deux tiers du contenu directement lisibles sans aucune clé.
Format du fichier produit#
Après la boucle, le programme réécrit le fichier en place (Truncate +
Seek + Write) avec la structure suivante :
[enveloppe ECIES de la cle AES][0x7c 0x7c]["||"][enveloppe ECIES du nonce][0x7c 0x7c][contenu "chiffre" par octet]
(0x7c 0x7c = "||" ASCII, marqueur de séparation en clair, facilement
repérable pour localiser le début exact du contenu et donc l'alignement
du motif 1-octet-chiffre/2-octets-clairs).
Environnement#
Ghidra headless via PyGhidra (pyghidra.start(install_dir="/opt/ghidra")),
venv ~/.venvs/ctf-reverse. Outils génériques copiés depuis
gunra-time-seed-rng/tools/ (list_functions.py, decompile_by_name.py,
find_api_callers.py, decompile_addr.py, find_string_refs.py), tous
directement utilisables sans adaptation : le support natif Go de Ghidra
(pclntab) récupère les noms de fonctions complets paquet.Fonction, comme
déjà observé sur l'analyse Hive de ce dépôt.
Extraction de la configuration (extension, note de rançon, listes d'exclusion)#
filewalker.EncryptDirectory.func1 (filewalker.go:14) révèle deux choses
en plus du chiffrement lui-même : l'écriture de la note de rançon
(os.WriteFile, chemin construit via path/filepath.Join) et trois
listes de contrôle (fichiers/extensions à exclure). func1.1
(filewalker.go:40) révèle le renommage post-chiffrement
(os.rename(ancien_nom, ancien_nom + extension)).
python3 tools/decompile_by_name.py binaries/84715...52e.exe /tmp/out.txt \
"main.main" "Prince-Ransomware/filewalker.EncryptDirectory.func1" \
"Prince-Ransomware/filewalker.EncryptDirectory.func1.1"
# reperage des adresses de donnees (pointeur+longueur) des chaines et
# tableaux impliques, puis resolution directe via decompile_addr.py
python3 tools/decompile_addr.py binaries/84715...52e.exe /tmp/out.txt \
00645d30 00645d38 0053bb35 00645d50 00645d58 \
006462b0 006462b8 006462d0 006462d8 006462f0 006462f8
Extraction finale des octets bruts (pefile, lecture directe par RVA) :
- extension : .MOONMAN (8 octets @ 0x53a43c)
- nom de la note de rançon : READTHISNOW.txt (15 octets @ 0x53bb35)
- contenu de la note : 979 octets @ 0x545e92, signée "MoonMan/UwU/Sprunki/LockBitch",
rançon 1488$ en crypto, contacts sn33ds3curity@tutanota.com /
vitollebonzi@gmail.com, adresse 357a3So9CbsNfBBgFYACGvxxS6tMaDoa1P
- liste d'exclusion (fichiers propres au malware, à ne jamais chiffrer) :
TOTAL_TRANNY_DEATH.png, READTHISNOW.txt
- extensions systématiquement exclues : .exe, .dll, .sys, .lnk
Contexte : builder open-source, deuxième échantillon vérifié#
Chemin de build C:/MoonMan/Prince-Ransomware-main/... (suffixe -main
caractéristique d'un ZIP GitHub) : recherche web confirme que
Prince-Ransomware est un générateur de ransomware open-source publie sur
GitHub (sources : halcyon.ai, cyberpress.org, cybersecuritynews.com,
gbhackers.com). Plusieurs variantes connues : "Black (Prince)", "Wenda",
"UwU" (celle analysée ici).
Vérification croisée sur un deuxième échantillon du même tag MalwareBazaar,
tagué crazyhunter :
curl -s -X POST https://mb-api.abuse.ch/api/v1/ \
-H "Auth-Key: $(cat ../.mb_api_key)" \
-d "query=get_file&sha256_hash=f72c03d37db77e8c6959b293ce81d009bf1c85f7d3bdaa4f873d3241833c146b" \
-o binaries/f72c03d3.zip
7z x -pinfected -y -obinaries binaries/f72c03d3.zip
strings -n 6 binaries/f72c03d3*.exe | grep -iE "prince|hunter"
# chemin de build : C:/Users/fake/Downloads/Prince/Prince v1.0.0/Encryptor/...
# fond d'ecran : "---------- Hunter Ransomware ----------"
# meme dependance github.com/ecies/go@v1.0.1, meme structure de packages
python3 tools/list_functions.py binaries/f72c03d3...46b.exe /tmp/out.txt \
"EncryptFile" "XORKeyStream"
objdump -d -M intel --start-address=0x4fb6e0 --stop-address=0x4fbfa0 binaries/f72c03d3...46b.exe \
| grep -B8 "call.*4f88e0"
# meme motif : mov ecx,0x1 ; mov r8,rcx juste avant l'appel XORKeyStream
Le même défaut de chiffrement partiel est confirmé à l'identique sur ce second échantillon. Des sources publiques (recherche web) rapportent qu'un ransomware nomme CrazyHunter a touché l'hôpital Mackay Memorial à Taïwan en février 2025 (600+ appareils chiffrés, infection initiale par clé USB). Le rattachement précis de ce hash à cet incident repose sur un recoupement (nom "Hunter" + tag MalwareBazaar "crazyhunter"), pas sur une source qui le confirmerait explicitement.
Preuve de concept#
poc/simulate_prince_format.py reproduit le format de fichier (sans
réimplémenter le vrai chiffrement, un flux pseudo-aléatoire quelconque
suffit à démontrer que la faille est structurelle) ;
poc/poc_recover_plaintext.py démontré la récupération. Test sur un
fichier texte de 256 octets : 170 octets récupérés sans aucune clé
(66,4%), texte reconstitué largement lisible malgré l'absence d'un
caractère sur trois.
python3 poc/simulate_prince_format.py test_plain.txt test_encrypted.moonman
python3 poc/poc_recover_plaintext.py test_encrypted.moonman test_recovered.txt
poc/poc_infer_gaps.py explore en complément une inférence heuristique
(pas cryptographique) du tiers restant, sur du contenu textuel
uniquement : le motif laisse toujours les deux voisins immédiats de
chaque octet manquant en clair, ce qui permet parfois de deviner une
lettre manquante par recherche dans un dictionnaire. Dictionnaires
hunspell locaux trouvés via les paquets OnlyOffice déjà installés
(/opt/onlyoffice/desktopeditors/dictionaries/{fr_FR,en_US}/), pas
besoin d'installation supplémentaire. Résultat sur le même fichier de
test : 9 mots à une seule lacune, 2 à solution unique, gain de
66,4% à 67,2%. Rappel important : ceci ne casse pas XChaCha20, c'est de
la devinette contextuelle qui échoue silencieusement sur du contenu à
haute entropie (voir avertissement en tête du script).
python3 poc/poc_infer_gaps.py test_recovered.txt test_inferred.txt
Confirmation par le code source réel (GitHub public)#
Le dépôt source du builder est public (github.com/oakkaya/Prince-Ransomware,
également forke sur github.com/Kmaahan/Prince-Ransomware). Recupere via
l'API GitHub brute (pas de gh auth login configure ici) :
curl -s "https://api.github.com/repos/oakkaya/Prince-Ransomware/contents"
curl -s "https://raw.githubusercontent.com/oakkaya/Prince-Ransomware/main/Encryptor/encryption/encryption.go"
curl -s "https://raw.githubusercontent.com/oakkaya/Prince-Ransomware/main/Builder/main.go"
curl -s "https://raw.githubusercontent.com/oakkaya/Prince-Ransomware/main/Decryptor/decryption/decryption.go"
Encryptor/encryption/encryption.go correspond exactement, instruction
pour instruction, à la boucle de "chiffrement" retrouvée par décompilation
et désassemblage (voir "EncryptFile" plus haut). Levée de toute ambiguïté
résiduelle : ce n'est pas une erreur de lecture du binaire.
Builder/main.go montre que la paire de clés ECIES est générée une seule
fois par build via eciesgo.GenerateKey() (donc crypto/rand, sain), puis
compile deux exécutables à partir de cette même paire : l'Encryptor
(clé publique embarquée via ldflags) et un Decryptor distinct, ou la clé
privée est embarquée de la même manière
(-X 'Prince-Decryptor/configuration.PrivateKey=%s'). Si un Decryptor est
un jour obtenu (partagé par une victime, saisie, fuite), la clé privée
complète s'en extrait aussi trivialement que la clé publique de
l'Encryptor : déchiffrement intégral possible, y compris le tiers de
contenu actuellement inaccessible. Aucun Decryptor trouvé sur
MalwareBazaar au moment de cette analyse (attendu, ce n'est pas un
exécutable malveillant, rarement soumis à ces plateformes).
Decryptor/decryption/decryption.go confirme aussi que le décrypteur
officiel localise le contenu par strings.SplitN(contenu, "||", 3),
exactement la même méthode (et la même fragilité théorique face à une
coïncidence dans les enveloppés ECIES à haute entropie) que celle utilisée
dans poc/poc_recover_plaintext.py : ce n'est donc pas une simplification
de notre part, c'est la méthode réelle du projet.
Vérification des 3 échantillons restants du tag Prince#
for h in a205f794058c59a19322debb1e96a6133ebbab01cb57ae159c2ddb3c7e97a922 \
4d0be6135596f7ec27f4b071f8225b6423132d51e44320f251cbe7036f447165 \
b44af07de17ed16af4d6699b9462bee6855a0a0207ec787d41f00fbed81e1c13; do
curl -s -X POST https://mb-api.abuse.ch/api/v1/ \
-H "Auth-Key: $(cat ../.mb_api_key)" \
-d "query=get_file&sha256_hash=$h" -o "binaries/$h.zip"
7z x -pinfected -y -obinaries "binaries/$h.zip"
done
strings -n 6 binaries/*.exe | grep -oE "[A-Za-z0-9_/. :-]*Encryptor/[a-z]*\.go"
Résultat par chemin de build source embarqué :
- a205f794... : EByte-Ransomware-main/Encryptor/main.go, fond d'écran
"Zéro Day Locker" -> projet différent (EByte-Ransomware, pas
Prince-Ransomware), list_functions.py filtre "EncryptFile" ->
0/5620 match, code de chiffrement différent, non vérifié plus avant.
- 4d0be613... : /home/lucy/Desktop/GoConti/Encryptor/main.go ->
encore un projet différent ("GoConti"), décompilation complète non
tentée (le fichier a 5620+ fonctions, coût disproportionné pour ce
degré de confirmation).
- b44af07d... : path Prince-Ransomware, wallpaper "Hello, we are
AnonWorld.", clé publique différente -> code Prince-Ransomware non
modifié, vérifié :
python3 tools/list_functions.py binaries/b44af07d...c13.exe /tmp/out.txt \
"EncryptFile" "XORKeyStream"
# -> memes adresses exactes que sur l'echantillon UwU (0x501aa0, 0x5007e0) :
# module de chiffrement compile a l'identique, seule la cle publique differe
objdump -d -M intel --start-address=0x501aa0 --stop-address=0x5022e0 binaries/b44af07d...c13.exe \
| grep -B8 "call.*5007e0"
# -> mov ecx,0x1 ; mov r8,rcx identique, meme defaut confirme
Bilan : 3 des 5 échantillons du tag Prince sont du code non modifié du
générateur (tous confirmés vulnérables au même défaut), les 2 restants
sont des forks a codé différent, non vérifiés.
Contexte additionnel (recherche web)#
- Prince première observation publique rapportée : 2024-12-01, cohérent avec le corpus MalwareBazaar (dates first_seen de nov. 2024 à fev. 2025).
- PCRisk note que le générateur Prince ne met en place aucun identifiant unique par victime : les opérateurs ne peuvent pas savoir qui a payé, ce qui suggère que certains déploiements pourraient ne jamais restituer l'accès même en cas de paiement.
- Aucun décrypteur public trouvé (No More Ransom vérifié explicitement, aucune entrée pour Prince/Hunter/Black Prince/CrazyHunter/UwU).
- Chiffrés CrazyHunter corrigés par rapport à la première recherche web (sources : WithSecure Labs, TeamT5, presse taïwanaise) : plus de 500 postes touchés (pas 600), jusqu'à 32,5 Go de données patients exfiltrées, vecteur USB puis BYOVD (pilote Zemana AntiMalware vulnérable pour désactiver l'EDR). Opérateur identifié par les autorités taïwanaises (ressortissant chinois, province du Zhejiang), au moins 11 attaques similaires (hôpitaux, écoles, entreprises cotées) en février-mars 2025.
Pistes ouvertes#
- ~~Valider empiriquement le motif 1/3 sur un vrai fichier chiffré~~ : fait par PoC reproduisant le format (voir "Preuve de concept" ci-dessus), pas encore sur un vrai fichier de victime. Reste ouvert si un tel fichier est retrouvé publiquement.
- ~~Écrire un script de récupération partielle~~ : fait
(
poc/poc_recover_plaintext.py). - ~~Déterminer l'extension et le contenu de la note de rançon~~ : fait (voir "Extraction de la configuration" ci-dessus).
- ~~Confirmer si le même défaut existe dans les 3 échantillons restants
du tag
Prince~~ : fait, voir "Vérification des 3 échantillons restants" ci-dessus (3/5 confirmés vulnérables, 2/5 forks a codé différent non vérifiés). Reste ouvert seulement pour Anubis (toujours aucun échantillon exploitable) et pour les 2 forks (EByte-Ransomware, GoConti) si un intérêt spécifique s'y porte un jour. - Rediger un prince-ecies-go.md final une fois ces points tranches : fait, intégré l'ensemble des points ci-dessus.
- Chercher un Decryptor.exe (voir "Confirmation par le code source réel" ci-dessus) : source de récupération bien plus directe que le défaut de chiffrement partiel si trouve, clé privée extractible en clair. Vérifié sur No More Ransom (aucune entrée Prince/variantes) et MalwareBazaar (aucun résultat) : pas trouvé. Pistes non essayées : forums de victimes, rapports d'incident response payants/privés sur CrazyHunter, VirusTotal (nécessite une clé API non configurée ici).