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#

  1. ~~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.
  2. ~~Écrire un script de récupération partielle~~ : fait (poc/poc_recover_plaintext.py).
  3. ~~Déterminer l'extension et le contenu de la note de rançon~~ : fait (voir "Extraction de la configuration" ci-dessus).
  4. ~~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.
  5. Rediger un prince-ecies-go.md final une fois ces points tranches : fait, intégré l'ensemble des points ci-dessus.
  6. 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).