HybridPetya : le bootkit UEFI chiffre tous les clusters du $MFT avec le même flux Salsa20, le volume se restaure sans la clé
Catégorie : Ransomware
Analyste : Gordon PEIRS (@ozer0x777)
Famille : HybridPetya (EFI/Diskcoder.A, Win32/Filecoder.OSK selon ESET), imitation de Petya/NotPetya capable de chiffrer le $MFT depuis l'UEFI. Échantillons soumis à MalwareBazaar le 2025-09-13 ; ESET n'a pas observé de déploiement en conditions réelles.
Méthode : analyse statique (Ghidra headless via PyGhidra, capstone) et émulation du vrai code machine avec Unicorn (32 bits, 64 bits et mode réel 16 bits). Aucun échantillon n'a été exécuté, aucune infrastructure n'a été contactée. Les démonstrations de récupération utilisent des volumes NTFS de test créés localement et une image forensique publique (Digital Corpora).
Résumé
HybridPetya chiffre le disque de deux façons : avec un chargeur BIOS/MBR repris de NotPetya, ou avec un bootkit UEFI qui remplace bootmgfw.efi puis chiffre le $MFT au démarrage suivant. L'analyse publique de référence (ESET) décrit le bootkit, sa clé Salsa20 de 32 octets et son nonce de 8 octets, sans détailler la façon dont le flux de chiffrement est consommé. C'est là que j'ai trouvé la faille principale.
Le bootkit UEFI chiffre tous les clusters avec le même flux (§4). Chaque cluster du $MFT est chiffré par un appel séparé à la routine Salsa20, avec la même clé, le même nonce et un compteur de blocs remis à zéro. Tous les clusters sont donc chiffrés avec exactement le même flux. Le fichier \EFI\Microsoft\Boot\verify, 512 octets de 0x07 chiffrés avec ce même flux, en livre directement les 512 premiers octets. Je l'ai confirmé en émulant la routine du bootkit face à une implémentation de référence.
Le volume se restaure sans la clé (§6). J'ai écrit un outil qui reconstruit le flux, puis le volume. Méthode exacte dès qu'un cluster nul a été chiffré (cas observé dans tous les chiffrements menés à terme), méthode statistique guidée par la structure NTFS sinon. Six scénarios de test, dont une image Windows publique et un $MFT très fragmenté, sont restaurés à l'identique de l'original, octet pour octet.
Le parcours du $MFT a deux autres défauts (§5). Le bootkit interprète les décalages d'extents NTFS en absolu au lieu de relatif, et lit l'enregistrement 0 sans appliquer les corrections de secteur (fixups). Sur un $MFT fragmenté, il chiffre des clusters de données à la place des fragments du $MFT ; sur un $MFT très fragmenté, une longueur d'extent corrompue le fait boucler jusqu'à la fin du volume. Ces dommages restent réversibles avec le même flux.
Le chemin BIOS garde la faille de NotPetya (§7). Le chargeur 16 bits est celui de NotPetya, corrigé de sa constante Salsa20 détournée mais pas de son chargement de mots 16 bits défectueux (flux de période 4 Mio, faille déjà exploitée publiquement pour NotPetya). Nouveau défaut : l'état de la fonction de hachage de la clé saisie n'est jamais remis à zéro, si bien qu'après une seule erreur de saisie la bonne clé est refusée jusqu'au redémarrage.
Le reste tient (§3). La protection de la graine par ECDH secp192k1 et le chiffrement des fichiers utilisateur (AES-256-CBC, IV aléatoire par fichier) ne présentent pas de faiblesse exploitable ; seule la clé privée de l'opérateur permet de les défaire. J'ai vérifié le schéma de bout en bout en émulant la vraie fonction avec une paire de clés de test.
Tout est reproductible depuis
runbook.md. Les scripts sont danstools/. Les échantillons et les composants extraits restent hors paquet.
1. Échantillons et filiation
Huit fichiers portent le tag HybridPetya sur MalwareBazaar. En les déballant sans les exécuter (§2), j'ai pu les ordonner et de séparer les vrais builds des captures mémoire.
| SHA-256 (début) | Nature | Date de compilation de la DLL | Rôle établi |
|---|---|---|---|
65f77a21 |
installeur PE32 | 2025-01-30 | DLL sans bootkit EFI (chemin MBR seul) ; clé AES en clair dans .data |
f3cc2284 |
installeur PE32 | 2025-02-03 | DLL dont la section de code est identique à celle de 65f77a21 ; clé préfixée au blob |
01b57ae9 |
installeur PE32 | 2025-02-11 | premier build avec branche GPT/UEFI ; bootkit « 0211 » |
ccdad8f0 |
installeur PE32 | 2025-02-13 | ajout d'une liste de dossiers exclus ; bootkit « 0214 » |
c75a0c76 |
installeur PE32 | 2025-02-14 | même logique que ccdad8f0 ; bootkit « 0214 » |
b949e951 |
capture mémoire | (de c75a0c76) |
image relogée en 0xf20000, DLL déjà déchiffrée en place |
c25e5f72 |
capture mémoire | (de c75a0c76) |
DLL prise juste après le décodage de sa configuration, sans aucune clé |
20fbf95c |
EFI x64 signé | 2017-07-27 | reloader.efi vulnérable (CVE-2024-7344) de la variante Secure Boot |
Deux observations fixent cette filiation :
b949e951est un dump dec75a0c76. Sa DLL déchiffrée est identique à celle extraite dec75a0c76, et la valeur d'IV qu'il contient en mémoire est exactement le dernier bloc chiffré du blob dec75a0c76, effet de bord connu du déchiffrement CBC en place.c25e5f72est la même DLL en mémoire. Même horodatage que celle dec75a0c76, configuration déjà en clair, mais bootkit et chargeur MBR encore masqués : la capture a été prise avant toute génération de clé et ne contient donc pas de matériel cryptographique résiduel.
Le bootkit extrait du build du 14 février a pour SHA-1 bd35908d..., identique à l'indicateur publié par ESET pour EFI/Diskcoder.A. Le bootkit du 11 février (a9d81bbe...) n'apparaît pas dans les indicateurs publics. cloak.dat et le bootkit qu'il contient (variante Secure Boot) ne sont pas disponibles publiquement.
J'ai comparé les builds fonction par fonction sur le pseudo-code Ghidra, en masquant les adresses :
- 30 janvier → 11 février. La routine d'installation disque passe de 1504 à 2510 octets : c'est l'ajout de la branche GPT/UEFI (recherche de la partition système EFI par son GUID, remplacement de
bootmgfw.efi). - 11 → 13 février. Seule la routine de chiffrement des fichiers change : ajout d'une liste de dossiers ignorés (
Windows,$Recycle.Bin,Microsoft,Mozilla Firefox,Opera,Internet Explorer,Temp,Local,LocalLow,Chrome). - 13 → 14 février. Même logique ; seule la structure compilée de la comparaison d'extensions diffère.
- Bootkits 0211 et 0214. Seul l'emplacement d'un saut de ligne autour de la barre de 80 caractères
█de l'écran de rançon change. La logique cryptographique est identique.
2. Déballage et comportement
Installeur. Un petit PE32 dont la section .data (entropie 8,0) contient une DLL chiffrée en AES-256-CBC. La première fonction de l'installeur appelle une expansion de clé déchiffrante de 256 bits puis un déchiffrement CBC en place ; on lit la clé, l'IV, l'adresse et la taille du blob directement dans les opérandes push. Deux formes existent : clé et IV stockés en clair dans .data (65f77a21), ou 32 + 16 octets placés en tête du blob et recopiés en mémoire juste avant l'appel (tous les autres builds). Dans les deux cas la clé voyage avec le chiffré, ce qui permet une extraction purement statique (tools/extract_installer_payload.py). L'installeur cherche ensuite dans la DLL l'export dont le nom contient ReflectiveLoader (chaîne masquée par XOR) et lui passe la main.
DLL. Elle masque ses blocs sensibles par un XOR simple, octet[i] ^= (0x6b81 mod (i+1)) : la configuration de 0x328 octets (note de rançon, montant de 1000 dollars, portefeuille, adresse de contact), le MBR et le chargeur 16 bits (section .xxxx), et dans les builds de février le bootkit EFI de 0x5aa0 octets. Le compilateur produit une boucle reconnaissable pour chaque bloc, ce qui permet de tous les retrouver et décoder automatiquement (tools/extract_dll_blobs.py).
Au démarrage, la DLL chiffre toujours les fichiers utilisateur (§3), quel que soit son niveau de privilège. Si elle tourne en administrateur, elle prépare en plus le chiffrement du disque, selon le schéma de partition :
- Disque MBR. Écriture du MBR et du chargeur 16 bits (secteurs 0 à 0x12), d'un secteur de configuration (0x20), d'un secteur de vérification rempli de
0x07(0x21) et du MBR d'origine masqué par XOR 7 (0x22), puis écran bleu forcé. - Disque GPT. Sur la partition système EFI : suppression de
\EFI\Boot\bootx64.efi, renommage de\EFI\Microsoft\Boot\bootmgfw.efienbootmgfw.efi.old, dépôt du bootkit à sa place, écriture des fichiersconfigetverify, puis écran bleu forcé.
Le fichier config de l'ESP contient un octet d'état (0 prêt, 1 chiffré, 2 déchiffré), la clé Salsa20 de 32 octets, un nonce de 8 octets, le portefeuille et la clé d'installation de 96 caractères.
3. Schéma de clés : sain côté opérateur
Toute la cryptographie de la DLL part d'une graine R de 16 octets tirée par CryptGenRandom. En administrateur, une seconde graine est tirée pour le disque, avec sa propre clé d'installation, celle qu'affiche l'écran de démarrage ; les fichiers utilisateur restent liés à la première, citée dans YOUR_FILES_ARE_ENCRYPTED.TXT.
| Élément | Construction |
|---|---|
| Clé des fichiers | SHA-512(R), 32 premiers octets ; AES-256-CBC par blocs de 0x400 octets, IV aléatoire par fichier ; au-delà de 50 Mio, seuls les 5 premiers Mio sont chiffrés |
| Clé du disque (UEFI) | les 32 caractères hexadécimaux minuscules de R, utilisés tels quels comme clé Salsa20 |
| Clé du disque (MBR) | une fonction éponge de type SPONGENT-256 (S-box e d b 0 2 1 4 f 7 a 8 5 9 c 3 6, état de 272 bits) appliquée 129 fois à hex(R) |
| Clé d'installation | ECDH sur secp192k1 avec la clé publique de l'opérateur embarquée ; AES-256-ECB de R sous SHA-512 du secret partagé ; Base58 de (point éphémère, R chiffré, somme de contrôle de 2 octets) |
La clé publique de l'opérateur (49 octets, point non compressé) est :
041acd5c006279b798998e162cba4f04f611e3f1ac519c8bddac580af1d7e0500e427fd2f8dd8bd6c677eaa904bff26485
Une particularité de la dérivation : le secret partagé (24 octets) est haché après un bourrage de (168 − n) octets nuls, n étant le nombre de bits nuls en tête du secret. Le code confond visiblement un nombre de bits et un nombre d'octets, mais le résultat reste déterministe et sans conséquence pour la sécurité.
Vérification de bout en bout (tools/emulate_install_key.py) : la vraie fonction de génération de la DLL est exécutée dans Unicorn, avec la clé publique de l'opérateur remplacée en mémoire par une clé de test dont on connaît la clé privée, et une graine choisie. On décode ensuite la clé d'installation produite en jouant le rôle de l'opérateur.
structure : point ephemere 04||X||Y : True | bourrage nul : True | somme SHA-512[:2] : True
point ephemere sur secp192k1 (y^2 = x^3 + 3) : True
graine retrouvee par l'operateur == graine injectee : True
Huit tirages sur huit retrouvent la graine, dont trois avec un bit nul en tête du secret (191 octets hachés au lieu de 192). La capacité Base58 est juste suffisante : 96 chiffres portent 562,37 bits, et le blob de 71 octets commençant par 0x04 en demande au plus 562,32.
Conclusion pour les victimes : les fichiers chiffrés en mode utilisateur ne sont récupérables qu'avec la clé privée de l'opérateur. Le chiffrement du disque, lui, ne dépend pas de cette protection, car sa faiblesse se situe dans l'usage du flux Salsa20 par le bootkit.
4. Bootkit UEFI : un seul flux Salsa20 pour tous les clusters
Le bootkit (application EFI x64 compilée avec UDK2015, 0x5aa0 octets) lit config au démarrage. Si l'état vaut 0, il recopie la clé et le nonce, efface la clé dans config, chiffre verify, puis parcourt le $MFT de chaque partition NTFS et chiffre ses clusters un par un, en tenant le compte dans counter. Au déchiffrement, après saisie de la bonne clé, il refait le même parcours et s'arrête quand le nombre de clusters traités atteint counter.
La routine de chiffrement (FUN_000006a8, arguments : clé, nonce, tampon, longueur) démarre toujours son compteur de blocs à zéro :
uVar1 = 0; // position dans le flux, remise a 0 a chaque appel
do {
if ((uVar1 & 0x3f) == 0) { // nouveau bloc de 64 octets : compteur = uVar1 >> 6
...
FUN_00000614(param_1, &local_68, local_58); // bloc Salsa20 (cle, nonce || compteur)
}
*param_3 = *param_3 ^ local_58[uVar2 & 0x3f];
...
} while (uVar1 < param_4);
Et la boucle principale l'appelle pour chaque cluster avec les mêmes adresses de clé et de nonce, jamais modifiées entre deux appels (extrait simplifié) :
ReadBlocks(..., lba, taille_cluster, tampon);
FUN_000006a8(0x57c0 /* cle */, &DAT_00005560 /* nonce */, tampon, spc << 9);
WriteBlocks(..., lba, taille_cluster, tampon);
Le fichier verify est chiffré par le même appel, sur 512 octets valant 0x07. Chaque cluster du $MFT est donc combiné par XOR avec le même flux K, et verify XOR 0x07 donne exactement K[0:512].
Le cœur Salsa20 lui-même est standard : la décompilation Ghidra masquait les dix doubles tours, mais le désassemblage montre les rotations 7, 9, 13 et 18 sur 32 bits. Vérification par émulation de la vraie routine (tools/emulate_bootkit_salsa.py), identique sur les deux builds :
[1] flux bootkit == Salsa20(cle, nonce, compteur 0) : True
[2] flux appel 1 == flux appel 2 (compteur remis a 0) : True
c1 ^ c2 == p1 ^ p2 : True
[3] verify ^ 0x07 == flux des clusters [0:512] : True
En pratique, le chiffrement du $MFT équivaut à un masque jetable réutilisé des centaines ou des milliers de fois, sur un contenu très structuré. Le secret de la clé n'a plus d'importance.
5. Le parcours du $MFT : extents mal lus, fixups ignorés
Le bootkit lit l'enregistrement 0 du $MFT (1024 octets) puis prend le dernier attribut $DATA et décode sa liste d'extents. Trois choix d'implémentation, vérifiés au désassemblage, s'écartent de NTFS.
- Saut de 0x20 secteurs par extent. La borne de départ vaut
spc * décalage + 0x20et la borne de finspc * longueur - 0x20(imul r8, rdxpuisadd r8, 0x20en 0xe34 et 0xe3c,sub rax, 0x20en 0xe38). Le premier extent épargne ainsi les 16 premiers enregistrements (fichiers système), mais la règle est réappliquée à chaque extent : avec des clusters de 4 Kio, un fragment de 4 clusters n'est pas chiffré du tout. - Décalages lus en absolu. NTFS code le LCN de chaque extent après le premier comme un décalage relatif au précédent. Le bootkit remet son registre à zéro pour chaque extent (
xor edx, edxen 0xd3f) et l'emploie directement comme LCN. À partir du deuxième fragment, il chiffre donc d'autres clusters que ceux du$MFT, et les vrais fragments restent en clair. Un cluster touché deux fois revient en clair, puisque le même flux est appliqué deux fois. - Fixups ignorés. Entre la lecture de l'enregistrement 0 (
ReadBlocksen 0xcbe) et le parcours de ses attributs (0xcc2), aucune correction de secteur n'est appliquée. Si la liste d'extents franchit l'offset 0x1FE, deux de ses octets sont remplacés par le numéro de séquence de mise à jour et la suite est décodée de travers. Comme les comparaisons de bornes sont non signées (jaen 0xe51,jbeen 0xf8d), une longueur ou une borne aberrante produit une boucle qui avance jusqu'à la fin du volume puis continue indéfiniment sur des lectures en échec.
Je l'ai vérifié sur un volume de test dont le $MFT compte 73 extents (tools/build_fragmented_volume.sh), avec l'ordonnancement exact du bootkit (tools/hp_ntfs.py, qui reproduit l'arithmétique 64 bits, les décalages de bits modulo 64 et l'extension de signe du code d'origine) :
| Effet | Valeur |
|---|---|
| Extents lus par le bootkit / extents NTFS réels | 67 / 73 |
Itérations avant coupure (valeur de counter) |
16 535 |
Clusters chiffrés dans le $MFT |
640 |
Clusters chiffrés hors $MFT (données utilisateur, espace libre) |
15 585 |
| Clusters du premier extent traités deux fois, donc revenus en clair | 155 |
Sur ce volume, la machine afficherait indéfiniment le faux CHKDSK et une grande partie du volume serait chiffrée. Tout reste néanmoins chiffré par le même flux K, à des positions que l'on sait recalculer : la récupération du §6 restaure aussi ces clusters. Sur un $MFT d'un seul tenant, cas le plus courant, seul le premier défaut s'applique.
6. Restaurer le volume sans la clé
tools/recover_mft_without_key.py n'utilise que ce qu'une victime possède : l'image du disque chiffré, les fichiers verify et counter de la partition système EFI, et un volume NTFS sain quelconque comme modèle générique d'enregistrement. Il ne se sert jamais de la clé ; celle-ci n'est lue que pour évaluer le résultat sur les volumes de test.
Étape 1 : savoir quels clusters ont été touchés. L'outil recalcule l'ordonnancement exact du bootkit (§5) à partir de l'enregistrement 0, s'arrête au nombre d'itérations lu dans counter, et tient compte de la parité (un cluster traité deux fois est en clair). Si l'enregistrement 0 a lui-même été chiffré par un balayage, sa première moitié est déchiffrée exactement grâce à verify, et la seconde par l'une des méthodes ci-dessous.
Étape 2 : reconstituer le flux K.
- Méthode exacte. Un cluster entièrement nul, une fois chiffré, est égal à K. Tout cluster dont les 512 premiers octets valent
verifyXOR0x07est candidat. Il est retenu si au moins 90 % des enregistrements chiffrés du$MFTdeviennent cohérents une fois déchiffrés. Dans tous les chiffrements menés à terme de nos tests, de tels clusters existent : la fin de l'allocation du$MFTau-delà de sa taille initialisée, des enregistrements jamais utilisés (image Windows) ou de l'espace libre (volume balayé). - Méthode statistique, si le chiffrement a été interrompu avant d'atteindre un cluster nul. Pour chaque position j au-delà de 512, tous les clusters partagent K[j]. L'outil combine plusieurs sources de votes : la valeur la plus fréquente de chaque colonne comparée à un modèle d'enregistrement, le premier enregistrement de chaque cluster (exact sur 512 octets grâce à
verify), les clairs connus (FILE, numéro d'enregistrement), et surtout les index de répertoires$I30, situés hors du$MFT, qui contiennent une copie des noms et horodatages de chaque fichier. Les positions douteuses sont départagées par un contrôle de cohérence structurelle des enregistrements (chaîne d'attributs, longueurs, extents contenus dans le volume, tailles).
Étape 3 : déchiffrer tous les clusters touchés un nombre impair de fois, y compris ceux chiffrés hors du $MFT.
Je l'ai testé sur six scénarios (volumes ntfs-3g créés localement, image ntfs1-gen2 de Digital Corpora formatée sous Windows) :
| Volume de test | Clusters chiffrés | Méthode | Résultat | Durée |
|---|---|---|---|---|
| 4000 fichiers, clusters de 4 Kio, chiffrement complet | 1015 | exacte | volume identique à l'original | 1 s |
| 6000 fichiers, clusters de 4 Kio, chiffrement complet | 1515 | exacte | identique | 2 s |
$MFT de 73 extents, balayage jusqu'à la fin du volume |
16 225 | exacte | identique | 25 s |
| Windows (Digital Corpora), clusters de 512 octets | 160 | exacte | identique | 1 s |
| 4000 fichiers, chiffrement interrompu | 300 | statistique | identique | 48 s |
| 6000 fichiers, chiffrement interrompu | 800 | statistique | identique | 77 s |
Contrôle de la voie statistique seule, méthode exacte désactivée : 100 % du flux sur les deux volumes complets, mais seulement 91 % sur le volume balayé, dont les clusters hors $MFT contiennent surtout des données aléatoires et dont les index $I30 ont eux-mêmes été chiffrés. Ce cas reste couvert dès qu'un seul cluster nul a été chiffré, ce qui est très probable sur un disque réel.
7. Chemin BIOS : le chargeur de NotPetya, corrigé à moitié
Le MBR et le chargeur 16 bits (0x2400 octets) sont identiques dans tous les builds. Comparé octet par octet au chargeur extrait de NotPetya (perfc.dat, SHA-256 027cc450...), il ne diffère que par :
| Offset | NotPetya | HybridPetya | Portée |
|---|---|---|---|
| 0x16dd et suivants | constante -1nvalid s3ct-id |
expand 32-byte k |
constante Salsa20 standard rétablie |
| 0x1e9e, 0x1f3a | 300 dollars, wowsmith123456@posteo.net |
1000 dollars, wowsmith123457@proton.me |
note de rançon |
| 0x213 | couleur 0x0e | 0x0c | affichage |
| 0x3d5 | recopie de 16 octets de clé | 32 octets | longueur de clé recopiée au déchiffrement |
| 0x1992 | constante LFSR 0xa3 | 0x9e | éponge alignée sur SPONGENT-256 et sur la DLL |
| 0x22b5, 0x23b8 et suivants | octets non nuls | zéros | état initial des données du chargeur |
Défaut hérité : chargement de mots 16 bits. La fonction qui assemble un mot de 32 bits à partir de 4 octets (0x15ec) exécute shl ax, 0x10 et shl ah, 0x10 sur des registres de 16 bits, ce qui les met à zéro : seuls les deux octets de poids faible de chaque mot de l'état Salsa20 comptent, avec extension de signe. Le compteur de blocs n'a donc que 16 bits utiles et le flux se répète tous les 4 Mio. Un modèle Python de ce chargement fautif reproduit exactement la sortie émulée de la routine du chargeur, pour HybridPetya comme pour NotPetya, sur 20 tirages (tools/emulate_legacy_salsa.py). C'est la faille qui a permis la récupération partielle de disques chiffrés par NotPetya (travaux publics de CrowdStrike) ; elle s'applique telle quelle au chemin BIOS d'HybridPetya.
Nouveau défaut : l'état de l'éponge n'est jamais réinitialisé. Le chargeur filtre la clé saisie sur l'alphabet 0123456789abcdef, lui applique 129 fois la fonction éponge, puis vérifie qu'elle déchiffre le secteur 0x21 en 512 octets 0x07. L'état de l'éponge (0x22 octets en 0xa2b2) n'est remis à zéro nulle part, et une erreur de saisie ramène directement à l'invite sans redémarrer (jmp 0x49b). Émulation de la vraie routine de vérification sur un disque simulé, avec la clé produite par la vraie fonction de la DLL (tools/emulate_legacy_keycheck.py) :
A. bonne cle au premier essai : acceptee
B. mauvaise cle : refusee | puis bonne cle sans redemarrer : refusee
C. bonne cle apres remise a zero de l'etat : acceptee
D. bonne cle en majuscules : refusee
La chaîne DLL vers chargeur est donc cohérente, mais une victime qui fait une seule faute de frappe doit redémarrer pour que la bonne clé soit acceptée, et la clé doit être saisie en minuscules.
8. Variante Secure Boot : ce qui est vérifiable
ESET décrit une seconde variante qui dépose, à la place du bootkit, un reloader.efi légitime mais vulnérable (CVE-2024-7344) chargé de lancer un bootkit stocké dans cloak.dat. Seul ce reloader.efi est public (20fbf95c). Sa signature Authenticode remonte à « Microsoft Windows UEFI Driver Publisher » sous l'autorité « Microsoft Corporation UEFI CA 2011 », et il référence les chemins \EFI\Microsoft\boot\cloak.dat et \EFI\boot\cloak64.dat.
Son empreinte Authenticode, calculée localement (tools/authenticode_hash.py), vaut cdb7c90d3ab8833d5324f5d8516d41fa990b9ca721fe643fffaef9057d9f9e48. Elle figure dans la liste de révocation publiée par Microsoft (dbx_info_msft_latest.json, entrée ReLoader64_.efi, Howyar Technologies, ajoutée le 2024-11-01). La variante ne fonctionne donc que sur une machine dont la base DBX n'a pas été mise à jour.
Faute de cloak.dat, le bootkit de cette variante n'a pas pu être analysé. ESET indique qu'il hache la clé saisie avec une fonction proche de SPONGENT-256 ; la réutilisation du flux décrite au §4 n'y est pas vérifiée, même si une réutilisation du code de chiffrement du bootkit installé directement est plausible.
9. Portefeuille et contact
Tous les builds portent la même configuration : 1000 dollars vers 34UNkKSGZZvf5AYbjkUa2yYYzw89ZLWxu2, contact wowsmith123457@proton.me. ESET publie wowsmith1234567@proton[.]me pour la variante installeur ; la valeur extraite ici, identique dans les cinq DLL et dans le bootkit, est wowsmith123457.
Lecture passive de la blockchain (explorateurs publics, lecture seule) :
| Date (UTC) | Montant reçu | Équivalent au cours du jour |
|---|---|---|
| 2025-02-11 | 11 983 sats | environ 11,6 dollars |
| 2025-02-25 | 47 552 sats | environ 42,5 dollars |
| 2025-04-07 | 55 508 sats | environ 42,6 dollars |
| 2025-05-22 | 44 301 sats | environ 49,2 dollars |
Chaque dépôt provient d'une transaction à une entrée et une à 18 sorties, typique d'un retrait groupé, et repart en moins de deux jours. Trois des quatre repartent dans une transaction de 100 entrées vers une même adresse de consolidation ; celui du 25 février, dans une transaction de 23 entrées et 6 sorties. L'adresse de consolidation totalise environ 278 000 transactions et 55 500 BTC reçus. Ce profil est celui d'une adresse de dépôt hébergée par un service de garde à fort volume, donc rattachable à un compte client par ce service. Aucun montant ne correspond à la rançon demandée. Le premier dépôt date du 11 février 2025, jour de compilation de la DLL 01b57ae9.
10. Portée et limites
- Tests sur volumes contrôlés. La récupération a été validée sur des volumes créés par ntfs-3g et sur une petite image formatée sous Windows XP (96 enregistrements). Elle n'a pas été rejouée sur un
$MFTde Windows 10 ou 11 de plusieurs centaines de milliers d'enregistrements. La méthode exacte ne dépend pas du système qui a écrit le volume ; la voie statistique, elle, a été conçue sur des structures standard et devra peut-être s'adapter à des attributs plus variés ($ATTRIBUTE_LIST, flux alternatifs, attributs de réanalyse). - Chiffrement simulé. Le chiffrement des volumes de test n'est pas fait par le bootkit lui-même, qui n'a pas été exécuté. Il est reproduit à partir de sa logique désassemblée, et la routine Salsa20 utilisée a été démontrée identique à celle du bootkit par émulation.
$MFTétendu par$ATTRIBUTE_LIST. Lorsque la liste d'extents ne tient plus dans l'enregistrement 0, NTFS la déporte dans des enregistrements d'extension que le bootkit ne lit pas. Ce cas n'a pas été testé.- Variante Secure Boot. Non analysée, faute d'échantillon de
cloak.dat(§8). - Fichiers chiffrés en mode utilisateur. Hors de portée de cette méthode : ils dépendent de la clé privée de l'opérateur (§3).
- Chemin BIOS. La faille de période 4 Mio est démontrée sur la routine émulée, mais aucun outil de récupération spécifique n'a été écrit pour ce chemin.
11. Détection
hybridpetya.yar contient six règles, testées sur les huit échantillons, sur tous les composants extraits, sur le chargeur de NotPetya (non détecté par la règle du chargeur HybridPetya, qui distingue bien les deux) et sur un corpus négatif d'autres familles (aucune détection) :
| Règle | Cible |
|---|---|
HybridPetya_Installer_AES_Bootstrap |
installeurs : amorce de déchiffrement AES de la DLL embarquée |
HybridPetya_Payload_DLL |
DLL, y compris en mémoire : boucles XOR 0x6b81 et chaînes caractéristiques |
HybridPetya_UEFI_Bootkit |
bootkit EFI : chemins de l'ESP en UTF-16, faux CHKDSK, sous-système EFI |
HybridPetya_Legacy_Loader16 |
chargeur 16 bits dans des secteurs bruts (constante rétablie, LFSR 0x9e) |
HybridPetya_ESP_Config |
fichier config de l'ESP ou secteur 0x20 (portefeuille à l'offset 0x29) |
Vulnerable_Howyar_Reloader_CVE_2024_7344 |
reloader.efi vulnérable, quel que soit son nom sur l'ESP |
Indices simples sur une machine suspecte :
- présence de
config,verify,counteroubootmgfw.efi.olddans\EFI\Microsoft\Boot\; bootmgfw.efisans signature Microsoft de démarrage Windows (autorité « Microsoft Windows Production PCA 2011 » ou « Windows UEFI CA 2023 »), ou de taille très inférieure à l'original (le bootkit fait 23 200 octets) ;\EFI\Boot\bootx64.efiabsent ;cloak.datoucloak64.datsur l'ESP ;- base DBX antérieure à la révocation de novembre 2024.
Les indicateurs complets (hachages, chemins, portefeuille, clé publique de l'opérateur) sont dans iocs.csv. La conduite à tenir pour une victime est décrite dans remediation.md.
12. Reproduire l'analyse
Tous les outils sont dans tools/ et s'exécutent sans lancer aucun échantillon.
| Outil | Rôle |
|---|---|
extract_installer_payload.py |
déchiffrement statique de la DLL embarquée dans un installeur |
extract_dll_blobs.py, decode_xxxx_mbr.py |
décodage de la configuration, du MBR, du chargeur 16 bits et du bootkit EFI |
decompile_all.py |
décompilation Ghidra complète d'un binaire |
emulate_install_key.py |
émulation de la génération de clé d'installation et décodage côté opérateur avec une clé de test |
emulate_bootkit_salsa.py |
émulation de la routine Salsa20 du bootkit, preuve de la réutilisation du flux |
emulate_legacy_salsa.py |
émulation 16 bits de la routine du chargeur MBR, modèle du chargement fautif, période de 4 Mio |
emulate_legacy_keycheck.py |
émulation de la vérification de clé du chargeur sur un disque simulé |
authenticode_hash.py |
empreinte Authenticode, à comparer à la liste DBX |
hp_ntfs.py |
lecture NTFS minimale et ordonnancement exact du bootkit |
build_test_volume.sh, build_fragmented_volume.sh |
création des volumes de test |
simulate_bootkit_encrypt.py |
chiffrement d'un volume de test selon la logique du bootkit |
recover_mft_without_key.py |
récupération sans clé |
Exemple de récupération sur une image de test :
build_test_volume.sh victime.img 256 4000 1337
build_test_volume.sh reference.img 256 3000 4242
simulate_bootkit_encrypt.py victime.img chiffre/
recover_mft_without_key.py chiffre/image.enc chiffre/verify reference.img restaure.img \
--counter chiffre/counter --truth chiffre/truth.key victime.img
Le déroulé détaillé, les commandes et les résultats intermédiaires sont consignés dans runbook.md.