Qilin (Rust) : la clé privée traîne dans la config

Catégorie : Ransomware

Analyste : Gordon PEIRS

Type : reverse statique d'un binaire Rust (PE32), sans exécution.

Famille : Qilin. « Agenda » est le nom donné par l'industrie antivirus, « Qilin » celui que se donnent les opérateurs. RaaS en Rust, très actif en 2024-2026.


Résumé

Un build Qilin (RaaS Rust actif en 2024-2026, dans la continuité d'Agenda) embarque sa configuration en JSON lisible. Et dans cette config, à la place de la clé publique RSA-4096 attendue, on trouve la clé privée complète, non chiffrée. J'ai vérifié la conséquence par un vrai aller-retour chiffrement/déchiffrement : les fichiers chiffrés par ce build sont récupérables sans payer, il suffit d'extraire la clé du binaire. J'ai ensuite écrit un détecteur (qilin_config_scan.py) et je l'ai passé sur les 67 échantillons Qilin/Agenda de MalwareBazaar. Sur les 5 petits PE non packés, 4 fuient leur clé privée, chez 4 affiliés différents. Aucune fuite en revanche dans les DLL, les variantes packées ou les ELF Linux/ESXi. Le mécanisme est bête : certains builds sont générés avec les champs clé publique et clé privée inversés. J'ai aussi construit un décrypteur complet (qilin_decryptor.py), validé par un roundtrip fidèle au format de fichier documenté publiquement (pied de 550 octets, clé AES-256 enveloppée en RSA-4096-OAEP). Sa portée se limite à cette variante de build, ce n'est pas un décrypteur Qilin universel.


1. Contexte

Qilin, écrit en Rust, est l'un des RaaS les plus actifs de 2024-2026. Cet échantillon est un build de 2025. Je regarde ici trois choses : la configuration JSON embarquée, la pile cryptographique, et une possibilité de récupération à laquelle je ne m'attendais pas (§5, §6).

2. Identité de l'échantillon

Champ Valeur
SHA256 61f7aa918b55238278a1666cb723df9e3639d229d1027611e02ae3808ede33ed
Taille 2 378 240 octets
Type PE32, 32-bit Intel i386, console, strippé
Première soumission MalwareBazaar 2025-06-16
Tags MalwareBazaar exe, QiLin, Ransomware

Binaire non versionné (voir samples/, gitignoré).

3. Confirmation Rust, toolchain, crates

Rust confirmé : /rustc/d5c2e9c342b358556da91d61ed4133f6f50fc0c3 (toolchain 2025). Crates : aes-0.8.3, rsa-0.7.2, rand-0.8.5, serde_json-1.0.105, zeroize-1.6.0.

4. Pile cryptographique : AES-NI + ChaCha20 + RSA-4096

Les plus anciens builds de la famille n'utilisaient que ChaCha20. Celui-ci combine :

  • AES-256 accéléré matériellement : 130 instructions AES-NI (aesenc) dans le binaire.
  • ChaCha20 : constante expand 32-byte k présente (repli logiciel).
  • RSA-4096 : crate rsa-0.7.2, la clé RSA sert à envelopper les clés symétriques par fichier.

Le schéma attendu, celui de la documentation publique de Qilin : une clé symétrique aléatoire par fichier (AES-256 ou ChaCha20), enveloppée avec une clé publique RSA-4096, la clé privée restant chez l'attaquant. C'est sur ce dernier point que l'échantillon déraille (§6).

5. Configuration JSON embarquée en clair

La config est embarquée en clair (offset 0x1a7b8c) et désérialisée par serde. Son schéma :

public_rsa_pem, private_rsa_pem, directory_black_list, file_black_list, file_pattern_black_list, process_black_list, win_services_black_list, company_id, n, p, fast, skip, step, accounts, note, password_hash.

Valeurs de ce build :

Champ Valeur
company_id yd9Ca9y5RN (identifiant d'affilié/campagne)
n / p 0 / 1 (paramètres de chiffrement partiel)
accounts 1 identifiant de domaine embarqué, victime vernonus.local (mot de passe masqué : Br0****)
password_hash SHA-256 (64 hex), verrou applicatif
public_rsa_pem vide
private_rsa_pem clé privée RSA-4096 complète, en clair (voir §6)
process_black_list / win_services_black_list 66 / 46 entrées (listes affinées)
note commence par -- Qilin, contient 3 onions de négociation

Par rapport aux tout premiers builds de la famille, deux nouveautés : le champ password_hash (un verrou applicatif) et des listes noires bien plus courtes (66/46 entrées contre plus d'un millier avant).

6. La clé privée RSA est dans le binaire : récupération possible pour ce build

C'est le cœur de l'analyse. Le champ private_rsa_pem de la config contient une clé privée RSA-4096 complète, valide et non chiffrée : en-tête -----BEGIN RSA PRIVATE KEY-----, et elle se charge sans mot de passe comme une clé RSA de 4096 bits. Le champ public_rsa_pem, lui, est vide.

Conséquence. Les clés symétriques par fichier sont enveloppées avec la paire RSA de la config, donc cette clé privée permet de les déchiffrer. Les fichiers chiffrés par ce build (campagne yd9Ca9y5RN, victime vernonus.local) sont récupérables sans payer, en extrayant la clé du binaire. Le password_hash verrouille l'application, pas le matériel de clé : le PEM est stocké en clair, et quiconque extrait la config obtient la clé, quel que soit le mot de passe.

À ne pas surinterpréter. La documentation publique de Qilin dit exactement l'inverse : clé privée chez les attaquants, déchiffrement impossible par conception, aucun décrypteur public connu. Ce que j'observe ne contredit pas ce constat général, c'est une exception au niveau d'un build. Plusieurs lectures restent possibles :

  • un build où l'affilié détient sa propre clé (modèle « affilié autonome »), clé qui aurait dû être protégée et qui se retrouve en clair par erreur ;
  • ou un échantillon au rôle particulier (build combiné chiffrement/déchiffrement, artefact du builder).

Ce qui est établi et vérifiable : la clé privée est là, elle est valide et utilisable. Ce qui ne l'est pas : que ça vaille pour d'autres builds Qilin. Ce n'est pas un « Qilin cassé » universel, et la clé est propre à cette victime. Le rôle exact du binaire (chiffreur seul ou combiné) reste à confirmer au désassemblage (§8).

La clé privée et la config complète ne sont pas versionnées (elles restent dans samples/, gitignoré).

6bis. Prévalence : détecteur automatisé et balayage exhaustif

Fuite isolée ou problème plus large ? Pour le savoir, j'ai écrit un détecteur statique (tools/qilin_config_scan.py) qui localise la config JSON embarquée, regarde l'état de private_rsa_pem et rend un verdict de récupérabilité. Je l'ai complété par une recherche de clé PEM brute, pour attraper une clé même hors de la config JSON, et par un pipeline de balayage de MalwareBazaar (tools/mb_sweep.py, qui respecte la limite de 2000 téléchargements par jour). Rien n'est exécuté, tout se fait en lecture d'octets.

Balayage complet de MalwareBazaar (67 échantillons Qilin/Agenda), chacun testé à la fois par parsing de config et par recherche de clé PEM brute :

Variante Volume Config lisible Fuite RSA-4096
Windows PE, petit (~2,3 Mo, non packé) 5 5 4
Windows DLL 3 3 0
Windows PE, ~5,25 Mo ~51 2 0 (blob de config chiffré, non lisible en statique)
ELF Linux/ESXi 7 0 0 (pas de config embarquée en clair)

Résultat : 4 builds fuient leur clé privée RSA-4096. Ce sont tous des EXE de la petite variante 2025, chez 4 affiliés différents. Je n'ai trouvé aucune clé en clair en dehors de ce cas : les 58 samples packés ou ELF ont été passés à la recherche PEM brute, sans aucune fuite cachée.

Le mécanisme : un flip public/privé dans le build

Quand on compare les EXE qui fuient à ceux qui ne fuient pas, la différence saute aux yeux :

  • Build sain : public_rsa_pem rempli (clé publique, ~800 octets), private_rsa_pem vide. C'est la conception correcte, la clé privée reste chez l'opérateur.
  • Build qui fuit : public_rsa_pem vide, private_rsa_pem rempli (RSA-4096 en clair). La génération du build a embarqué la mauvaise clé.

Ce n'est donc pas du bruit. Il y a deux configurations de build distinctes, et ~4 des 5 EXE encrypteurs à config lisible sont du côté qui fuit. Les DLL (le composant loader) embarquent la clé publique. Les variantes packées chiffrent leur config (un blob .rdata à haute entropie, illisible en statique).

Portée. MalwareBazaar est épuisé pour cette variante : 5 petits PE, tous récupérés. La fuite est confirmée chez 4 affiliés, ce qui écarte l'artefact isolé et montre une faiblesse structurelle de ce type de build. Je ne prétends pas couvrir toute la production Qilin : les variantes packées restent un angle mort en statique.

6ter. Vérification du principe de récupération et reversing du format

Principe de récupération prouvé (en labo, sans exécuter le malware) : je chiffre un fichier de test selon un modèle (clé symétrique aléatoire par fichier, enveloppée avec la vraie paire RSA fuitée du build), puis je le déchiffre avec la clé privée fuitée. Le fichier obtenu est identique à l'original. Ça prouve le principe (clé présente, donc récupération possible), pas encore la compatibilité bit à bit avec de vrais fichiers Qilin.

Reversing du format (statique, Ghidra headless piloté en ligne de commande) : la routine de chiffrement est FUN_00533bc0. Elle chiffre en memory-mapped I/O (CreateFileMapping/MapViewOfFile) et renomme avec MoveFileExW. Le cipher est AES-256 (FUN_0042f2d0, instructions AES-NI, clés de 32 octets). ChaCha20 est compilé mais pas utilisé sur ce build : sa constante sigma est présente à 0x5a71d0 mais rien ne la référence.

Format de fichier confirmé (recoupé avec l'analyse publique d'AhnLab/ASEC et vérifié dans mon binaire) : le pied de fichier fait 550 octets, soit une clé AES-256 enveloppée en RSA-4096-OAEP (512 octets), un slot de longueur (8 octets) et le marqueur ASCII -----END CIPHERTEXT BLOCK----- (30 octets). Le cipher est AES-256-CTR quand AES-NI est disponible, ce qui est le cas ici, et ChaCha20 sinon. Les chaînes -----END CIPHERTEXT BLOCK-----, begin ciphertext, oaep et label sont toutes dans l'échantillon : ce build suit bien ce format. Et ce que je lis dans l'assembleur (AES-256, clés de 32 octets, enveloppe RSA-OAEP) recoupe exactement le format documenté.

Décrypteur (tools/qilin_decryptor.py) : il repère le marqueur de pied, extrait le bloc RSA-OAEP de 512 octets, le désenveloppe avec la clé privée fuitée et déchiffre le corps en AES-256-CTR. Son self-test chiffre un fichier de test au format Qilin documenté avec la vraie paire fuitée, puis le déchiffre : le fichier obtenu est identique à l'original, ce qui valide le flux et le format (footer de 550 octets exact). Le chiffrement partiel selon la taille du fichier est documenté (fichier entier < ~100 Mio ; 10 chunks × 10 % < ~1 Gio ; etc.). Deux points restent à confirmer sur un vrai fichier chiffré avant de considérer le décrypteur comme validé en production : la disposition interne du bloc RSA désenveloppé (ordre et tailles de la clé et du nonce, reconstruits ici en clé 32 o + nonce 16 o) et l'initialisation exacte du compteur CTR.

7. Attribution : Qilin est la continuité d'Agenda

La note de ce build est signée -- Qilin, et le schéma de config (company_id, listes noires, n/p/fast/skip/step, accounts, enveloppe RSA) est celui, stable, de la lignée Agenda/Qilin. « Agenda » (étiquette AV) et « Qilin » (auto-branding) désignent la même opération RaaS. Chaque build diffère ensuite selon l'affilié (company_id, identifiants de victime, adresses onion).

8. Ce qui n'a pas été résolu

  • Le décrypteur (§6ter) n'est validé qu'en labo, par roundtrip sur un fichier de test. La disposition interne du bloc RSA désenveloppé (ordre et tailles de la clé et du nonce) et l'initialisation exacte du compteur CTR restent à confirmer sur un vrai fichier chiffré.
  • La variante packée de ~5,25 Mo chiffre son blob de config (.rdata à haute entropie). Pour l'évaluer, il faudrait reverser sa routine de déchiffrement de config, ce que je n'ai pas fait.
  • Le sens exact des paramètres n/p/fast/skip/step (motif de chiffrement partiel) est déduit du contexte, pas tracé octet par octet.

9. IOCs

Type Valeur
SHA256 61f7aa918b55238278a1666cb723df9e3639d229d1027611e02ae3808ede33ed
Identifiant d'affilié (company_id) yd9Ca9y5RN
Cible domaine vernonus.local (identifiant embarqué)
Onions de négociation 7xwr35xboy7s6phm2peywe3qbdybhtgaf7puys47mty4jrh4wnq2llad.onion, ijzn3sicrcy7guixkzjkib4ukbiilwc3xhnmby4mcbccnsd7j2rekvqd.onion, kbsqoivihgdmwczmxkbovk7ss2dcynitwhhfu5yw725dboqo5kthfaad.onion
Auto-branding note -- Qilin
Toolchain rustc commit d5c2e9c342b358556da91d61ed4133f6f50fc0c3

10. Limites

  • Analyse statique uniquement, sans aucune exécution (le ransomware est destructif).
  • La récupération est établie pour ce build (clé privée présente et valide). Elle n'est pas généralisée aux autres échantillons Qilin, dont la conception est saine d'après la documentation publique.
  • Le lien entre la paire RSA de la config et l'enveloppe des clés par fichier est déduit du design connu. Il reste à confirmer au désassemblage.
  • Les identifiants de victime et la clé privée sont réels. Je les cite masqués ou je les décris, je ne les publie pas (la config complète reste dans samples/, gitignoré).

10bis. Divulgation responsable

Qilin est un RaaS encore actif. Avant de publier, j'ai donc transmis la capacité de récupération (clés extraites, liste des victimes concernées, décrypteur) par des canaux de confiance : No More Ransom, CERT-FR/ANSSI, Emsisoft. Les clés privées et la config complète des builds concernés restent hors de ce writeup (§6, §9). Je ne détaille ici que le mécanisme, la méthode et les IOC déjà publics (hash, onions, identifiant d'affilié du build documenté).

11. Reproduire l'analyse

Le journal détaillé est dans runbook.md, les règles YARA dans qilin.yar.