# 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](https://github.com/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`](writeups/hybridpetya-uefi-bootkit/runbook.md). Les scripts sont dans [`tools/`](tools.html?slug=hybridpetya-uefi-bootkit). 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 :

- **`b949e951` est un dump de `c75a0c76`.** Sa DLL déchiffrée est identique à celle extraite de `c75a0c76`, et la valeur d'IV qu'il contient en mémoire est exactement le dernier bloc chiffré du blob de `c75a0c76`, effet de bord connu du déchiffrement CBC en place.
- **`c25e5f72` est la même DLL en mémoire.** Même horodatage que celle de `c75a0c76`, 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.efi` en `bootmgfw.efi.old`, dépôt du bootkit à sa place, écriture des fichiers `config` et `verify`, 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 :

```c
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é) :

```c
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.

1. **Saut de 0x20 secteurs par extent.** La borne de départ vaut `spc * décalage + 0x20` et la borne de fin `spc * longueur - 0x20` (`imul r8, rdx` puis `add r8, 0x20` en 0xe34 et 0xe3c, `sub rax, 0x20` en 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.
2. **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, edx` en 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.
3. **Fixups ignorés.** Entre la lecture de l'enregistrement 0 (`ReadBlocks` en 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 (`ja` en 0xe51, `jbe` en 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 `verify` XOR `0x07` est candidat. Il est retenu si au moins 90 % des enregistrements chiffrés du `$MFT` deviennent 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 `$MFT` au-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 `$MFT` de 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`](writeups/hybridpetya-uefi-bootkit/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`, `counter` ou `bootmgfw.efi.old` dans `\EFI\Microsoft\Boot\` ;
- `bootmgfw.efi` sans 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.efi` absent ;
- `cloak.dat` ou `cloak64.dat` sur 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`](writeups/hybridpetya-uefi-bootkit/iocs.csv). La conduite à tenir pour une victime est décrite dans [`remediation.md`](writeups/hybridpetya-uefi-bootkit/remediation.md).

## 12. Reproduire l'analyse

Tous les outils sont dans [`tools/`](writeups/hybridpetya-uefi-bootkit/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`](writeups/hybridpetya-uefi-bootkit/runbook.md).
