BlackLotus : dans le stager, un shim Fedora authentique
Catégorie : Bootkit
Analyste : Gordon PEIRS
Famille : BlackLotus, bootkit UEFI vendu sur des forums de hacking depuis octobre 2022, capable de contourner UEFI Secure Boot sur des systèmes Windows 11 entièrement à jour.
Méthode : reverse statique (Ghidra headless via PyGhidra), sans jamais exécuter l'échantillon. Le déchiffrement et la décompression sont faits hors ligne, à partir du seul contenu du binaire.
Résumé
L'échantillon est l'installeur de BlackLotus, le « stager ». Pas de table d'import, et une seule section .data à entropie maximale qui occupe presque tout le fichier : la signature typique d'une résolution d'API dynamique avec un payload chiffré embarqué. J'ai cassé par décompilation la résolution d'API par hash et l'obfuscation de chaînes, ce qui m'a donné en clair la clé AES-256 statique qui protège quatre blobs embarqués. Je les ai ensuite déchiffrés.
Trois de ces blobs sont un driver noyau natif compilé en septembre 2022 et deux ruches de registre Windows. Le quatrième, le plus gros, demandait une décompression en plus : du LZMS, l'algorithme natif de l'API Windows Compression. Je n'ai trouvé aucune documentation publique de son format exact (le mode « buffer »), donc je l'ai reconstruit à tâtons à partir de la structure du blob. Une fois décompressé, c'est un exécutable EFI complet et fonctionnel : un bootloader shim authentique du projet Fedora/rhboot (x64-15.4-5.fc33), avec toutes ses fonctions de gestion du Machine Owner Key (MOK). Ce résultat, obtenu uniquement en statique, colle avec le mécanisme de persistance décrit par ESET (enrôler une clé MOK contrôlée par l'attaquant pour gagner la confiance de Secure Boot sans signature Microsoft) et le précise : le stager n'a pas à écrire son propre code d'enrôlement MOK, il embarque un shim authentique et se sert de ce qu'il sait déjà faire.
Tout est reproductible depuis
runbook.md. Les scripts sont danstools/.
1. Échantillon
| Champ | Valeur |
|---|---|
| SHA256 | d68f668b4240f9518e4f80499d93d8c5a1eddece0771658c33ae916cc54f5a66 |
| Taille | 504 832 octets |
| Type | PE32+, x86-64, 5 sections, aucune table d'import |
| MalwareBazaar | tag BlackLotus, première soumission 2023-03-11 (seul échantillon disponible sous ce tag au moment de cette analyse) |
La section .data (461 Ko sur les 504 Ko du fichier) a une entropie de 8,0/8,0, le maximum possible. Elle contient donc un ou plusieurs blobs chiffrés, pas du code ni des données structurées.
2. Anti-analyse : chaînes obfusquées et résolution d'API par hash
Le binaire n'importe aucune fonction Windows. Toutes les API sont résolues à l'exécution :
- Chaînes obfusquées : deux routines presque identiques (une pour l'ASCII, une pour l'UTF-16) décodent à la demande une table statique rangée en
.rdata. L'algorithme lit la chaîne à l'envers, transforme chaque octet ou unité sous condition (+0xa0si la valeur est supérieure ou égale à 0x80, rien sinon), la XORe avec l'unité décodée juste avant (un chaînage façon chiffrement par flux), permute les paires adjacentes, puis retourne toute la chaîne. Je l'ai réimplémenté en Python et validé en décodant des chaînes attendues (« AES », « ChainingModeCBC », « bcrypt.dll », des noms de DLL, etc.). - Résolution de module : le code parcourt directement le PEB (
PEB->Ldr, la liste des modules déjà chargés) en comparant des hashs de noms, ce qui lui permet d'amorcer la résolution sans appelerLoadLibrarytant qu'aucune fonction n'est résolue. Les DLL supplémentaires (user32.dll,advapi32.dll,Rpcrt4.dll,bcrypt.dll,ole32.dll,Cabinet.dll, noms tous récupérés en clair par désobfuscation) sont ensuite chargées normalement. - Résolution de fonction : le code parcourt la table d'export du module, hache chaque nom exporté et le compare à une constante embarquée. Il gère même la redirection API Set de Windows 10+ (forwarders
api-*/ext-*), ce qui trahit un code soigné et pas un gabarit de résolution par hash copié-collé.
3. Quatre blobs, une seule clé AES-256 statique
Le code appelle quatre fois la même séquence de déchiffrement (CNG / bcrypt.dll) : BCryptOpenAlgorithmProvider("AES"), BCryptSetProperty(ChainingModeCBC), BCryptGenerateSymmetricKey avec un secret de 32 octets, puis BCryptDecrypt avec pbIV=NULL (IV nul) et le flag BCRYPT_BLOCK_PADDING (le padding PKCS7 est retiré automatiquement). Ce secret de 32 octets est lui aussi une chaîne obfusquée, que décode la routine du §2 :
6jxkPUHKRK5rgdg0PkhLhuS844uMYgrm
Cette clé unique, statique et codée en dur protège les quatre blobs suivants (adresses dans la section .data du stager) :
| Blob | Chiffré | Clair (AES) | Contenu identifié |
|---|---|---|---|
0x1400734e0 |
9 632 o | 9 624 o | Driver noyau PE64 natif, compilé le 2022-09-17 |
0x140075a80 |
12 304 o | 12 288 o | Ruche de registre Windows (regf) |
0x140078a90 |
16 400 o | 16 384 o | Ruche de registre Windows (regf) |
0x140021fc0 |
333 088 o | 333 084 o | Encore compressé (LZMS, voir §4) |
Le driver contient en clair des chaînes qui correspondent exactement aux fichiers de démarrage que BlackLotus manipule d'après la documentation publique : EFI\Microsoft\Boot\bootmgfw.efi, EFI\Microsoft\Boot\bootload.efi, MokList, system32\hvloader.efi, system32\bootmgr.efi, system32\BCD, ainsi que mcupdate_AuthenticAMD.dll/mcupdate_GenuineIntel.dll. Pour les deux ruches de registre, je me suis arrêté à la confirmation de leur format (regf) : fouiller leurs clés sort du périmètre de cette analyse.
4. Le blob principal : décompression LZMS et reconstruction d'un format non documenté
Après le déchiffrement AES, le blob de 333 084 octets reste illisible. Le stager appelle ensuite CreateDecompressor/Decompress/CloseDecompressor de Cabinet.dll avec l'algorithme COMPRESS_ALGORITHM_LZMS de l'API Windows Compression, en mode « buffer » (sans le flag COMPRESS_RAW).
wimlib, une bibliothèque libre qui réimplémente LZMS pour le format WIM, ne décode pas ce blob directement : sa fonction bas niveau wimlib_decompress() équivaut au mode raw de l'API Windows (COMPRESS_ALGORITHM_LZMS | COMPRESS_RAW), pas au mode « buffer » utilisé ici. Un balayage des tailles de sortie candidates, de 16 octets à 4 Mio, donne 100 % d'échecs, même pour des tailles minuscules. Le problème vient donc du format du flux dès le premier octet, pas de la taille de sortie.
Le mode « block »/COMPRESS_RAW est documenté, le mode « buffer » non, en tout cas je n'ai rien trouvé. J'ai donc reconstruit le format en regardant les premiers octets du blob : un en-tête de 24 octets (taille d'en-tête, taille totale décompressée, taille de chunk), puis une suite de chunks, chacun précédé de sa taille compressée sur 4 octets. Une fois cette structure comprise, chaque chunk s'avère être un bloc LZMS raw standard, que wimlib_decompress() décode directement. Le premier chunk le confirme tout de suite : il se décompresse en exactement 65 536 octets qui commencent par un en-tête PE valide (MZ). Sur l'ensemble du blob, ça donne 15 chunks et 928 592 octets décompressés.
5. Le contenu final : un vrai bootloader shim, réutilisé pour l'enrôlement MOK
Le blob décompressé est une application EFI (PE32+, sous-système EFI, 9 sections). Sa chaîne de débogage l'identifie précisément :
add-symbol-file /usr/lib/debug/usr/share/shim/x64-15.4-5.fc33/shimx64.efi.debug
C'est un shim authentique, le petit bootloader du projet Fedora/rhboot. Son rôle légitime : étendre la confiance de Secure Boot à des chargeurs que Microsoft n'a pas signés (typiquement GRUB, pour le double amorçage Linux) grâce à un mécanisme de certificats appelé Machine Owner Key (MOK). Le binaire a gardé toutes ses chaînes fonctionnelles d'origine : MokList, MokListRT, MokListX, import_one_mok_state, mirror_mok_db, install_shim_protocols, init_grub, \\grubx64.efi, AuthenticodeVerify(vendor_cert).
D'après ESET, la persistance de BlackLotus repose sur l'enrôlement d'une clé MOK contrôlée par l'attaquant, pour que les composants suivants (renommés grubx64.efi) soient acceptés par Secure Boot sans signature Microsoft directe. En extrayant et en identifiant le binaire embarqué, on voit le mécanisme concret derrière cette description : le stager n'implémente pas la logique d'enrôlement MOK, il dépose un shim authentique, légitimement signable, et le pilote avec ses fonctions existantes. Le code du shim n'est pas modifié, c'est un composant open source légitime réutilisé tel quel.
6. Le chemin réseau alternatif : récupération de fichiers sains depuis Microsoft, pas un C2
Une branche du code, déclenchée par un paramètre que je n'ai pas exploré, ouvre une connexion HTTPS vers msdl.microsoft.com sur le port 443 puis envoie trois requêtes vers :
/download/symbols/bootmgfw.efi/7144BCD31C0000/bootmgfw.efi
/download/symbols/bootmgr.efi/98B063A61BC000/bootmgr.efi
/download/symbols/hvloader.efi/559F396411D000/hvloader.efi
Ce sont les chemins standard du serveur de symboles public de Microsoft (nom de fichier, horodatage et taille combinés en hexadécimal, nom de fichier à nouveau). Cette branche ne contacte donc aucune infrastructure de l'attaquant : elle va chercher chez Microsoft des copies saines des fichiers de démarrage que le stager s'apprête à manipuler. Mon hypothèse, plausible mais non vérifiée : ces copies propres servent de référence pour une sauvegarde ou une restauration ultérieure.
7. Ce qui n'a pas été résolu
- Je n'ai pas analysé clé par clé le contenu des deux ruches de registre, seulement confirmé leur format
regf. - Je n'ai pas localisé dans ce binaire l'exploitation de CVE-2022-21894 (« Baton Drop », la vulnérabilité du Windows Boot Manager qu'ESET et Microsoft donnent comme point d'entrée du contournement de Secure Boot). Le gros blob identifié ici est le shim qui sert à l'enrôlement MOK, pas forcément le vecteur de la vulnérabilité, et je n'ai retrouvé aucune copie vulnérable du Boot Manager dans les quatre blobs.
- La branche réseau (§6) n'est analysée que jusqu'à l'identification des requêtes. Je n'ai pas tracé ce que le code fait des fichiers récupérés (comparaison, sauvegarde, autre chose).
- Le format du mode « buffer » de l'API Windows Compression est reconstruit en observant les données, faute de documentation Microsoft. Les noms de champs de l'en-tête (§4) sont des hypothèses de travail qui fonctionnent, pas la lecture d'une spécification officielle.
8. Indicateurs de compromission
| Type | Valeur |
|---|---|
| SHA256 (stager) | d68f668b4240f9518e4f80499d93d8c5a1eddece0771658c33ae916cc54f5a66 |
| SHA256 (driver noyau extrait) | c769330b4381788cf2909b49bbb0c3eb1bc28c638156015dbd69a2f9a3a32d94 |
SHA256 (bootloader shim extrait, x64-15.4-5.fc33) |
b37c6b726eeed8948682c3d20182ed0bbf6c62947f6093f79995d8c850527531 |
| Clé AES-256 statique embarquée | 6jxkPUHKRK5rgdg0PkhLhuS844uMYgrm |
| Hôte contacté (récupération de fichiers légitimes, pas un C2) | msdl.microsoft.com:443 |
| CVE associée à la famille (documentation publique) | CVE-2022-21894 ("Baton Drop") |
9. Divulgation et usage
L'échantillon est public (MalwareBazaar), le bootkit est vendu depuis 2022 et il a déjà été analysé en détail par ESET et Microsoft. Je ne révèle donc aucune nouvelle vulnérabilité. L'intérêt de ce travail est ailleurs : montrer, en déchiffrant et en décompressant pour de bon à partir du seul binaire, comment les API sont résolues, avec quel algorithme et quelle clé les ressources embarquées sont chiffrées, et quel composant exact sert à l'enrôlement MOK (un shim Fedora authentique, réutilisé plutôt que réimplémenté). Le tout est vérifiable, IOC à l'appui, avec un niveau de détail qu'on trouve rarement en public pour ce composant.
10. Reproduire l'analyse
Le journal détaillé est dans runbook.md. Les scripts de déchiffrement AES, de décompression LZMS et de désobfuscation de chaînes sont dans tools/ (decrypt_payload.py, lzms_decompress.py, lzms_container_decompress.py, decode_strings.py). Le guide de détection et de remédiation : remediation.md.