# Runbook : reproduction pas à pas (stager BlackLotus, mars 2023)

## A. Recherche de l'échantillon (MalwareBazaar)

```bash
KEY=$(cat ../.mb_api_key)
curl -s -X POST https://mb-api.abuse.ch/api/v1/ -H "Auth-Key: $KEY" \
  -d "query=get_taginfo&tag=BlackLotus&limit=1000" | python3 -c "
import json,sys
d=json.load(sys.stdin)
for e in d.get('data', []):
    print(e.get('first_seen'), e.get('sha256_hash'), e.get('file_size'), e.get('signature'))
"
```

Un seul échantillon disponible sous ce tag :
`d68f668b4240f9518e4f80499d93d8c5a1eddece0771658c33ae916cc54f5a66`
(2023-03-11, 504 832 octets). Recherche parallèle du tag `CosmicStrand` : zéro
résultat, piste abandonnée faute de source récupérable.

```bash
h=d68f668b4240f9518e4f80499d93d8c5a1eddece0771658c33ae916cc54f5a66
curl -s -X POST https://mb-api.abuse.ch/api/v1/ -H "Auth-Key: $KEY" \
  -d "query=get_file&sha256_hash=$h" -o "binaries/$h.zip"
cd binaries && 7z x -pinfected -y "$h.zip" && cd ..
```

## B. Triage initial

```python
import pefile
pe = pefile.PE(f"binaries/{h}.exe", fast_load=True)
pe.parse_data_directories()
for s in pe.sections:
    print(s.Name.decode(errors="ignore").strip("\x00"), round(s.get_entropy(), 2), hex(s.SizeOfRawData))
try:
    for e in pe.DIRECTORY_ENTRY_IMPORT:
        print(e.dll.decode())
except AttributeError:
    print("aucune table d'import")
```

Résultat : PE32+ x86-64, 5 sections, **aucune table d'import**, section
`.data` à entropie 8.0/8.0 sur 461 Ko (sur 504 Ko de fichier total). Signale
une résolution d'API dynamique et un payload chiffré/compressé embarqué,
plutôt qu'un simple packer générique.

## C. Décompilation Ghidra headless (PyGhidra), vue d'ensemble

```bash
source ~/.venvs/ctf-reverse/bin/activate
python3 tools/entry_and_overview.py binaries/$h.exe tools/overview.txt
```

`tools/entry_and_overview.py` (nouveau, pour cette analyse) liste le point
d'entrée, les fonctions triées par taille et les blocs mémoire. Résultat :
123 fonctions seulement, point d'entrée `entry` à `140001320`, `.data`
(`14000c000`, RW non-X) confirmé comme la zone du payload embarqué.

```bash
python3 tools/decompile_addr.py binaries/$h.exe tools/decomp_entry_and_top.txt \
  0x140001320 0x140002e50 0x140004a20 0x140002240 0x1400073a8 0x140005cb0
```

`entry()` enchaîne une trentaine d'appels d'initialisation puis
`FUN_140002240()` (résolution d'API) et `FUN_140002e50()` (le corps
principal : extraction et installation du payload).

## D. Cassage de l'obfuscation de chaînes

`FUN_1400057d4` (chaînes wide/UTF-16) et `FUN_14000564c` (chaînes ASCII)
décodent, à l'appel, une table statique encodée en `.rdata` : lecture à
l'envers, XOR conditionnel (`+0xa0` si octet/unité ≥ 0x80, sinon inchangé),
XOR chaîné avec l'unité déjà décodée, permutation par paires adjacentes, puis
miroir complet.

```bash
python3 tools/decompile_addr.py binaries/$h.exe tools/decomp_strhelpers.txt \
  0x14000564c 0x1400057d4 0x140002888 0x1400028f4 0x140002b98
```

Réimplémentation Python de l'algorithme (`tools/decode_strings.py`), testée
sur les deux variants : attention à la condition du variant ASCII (`>= 0x80`,
pas `< 0x80`, qui sort du texte non imprimable). Le variant wide décode
"AES", "ChainingModeCBC", "bcrypt.dll", etc. Résultat :
décodage en clair de la clé AES utilisée plus loin :

```bash
python3 tools/decode_strings.py binaries/$h.exe a 0x140009f80 33
# -> '6jxkPUHKRK5rgdg0PkhLhuS844uMYgrm'
```

Décodage en lot de toutes les chaînes référencées par `FUN_140002e50`
(extraction des paires adresse/longueur directement depuis
`tools/decomp_entry_and_top.txt`) : noms de DLL (`user32.dll`,
`advapi32.dll`, `Rpcrt4.dll`, `bcrypt.dll`, `ole32.dll`, `Cabinet.dll`),
chemins de fichiers EFI/registre, l'hôte et les chemins réseau (§G), et
plusieurs chaînes système (`SeBackupPrivilege`, `SeRestorePrivilege`,
`\BaseNamedObjects\`, etc.). Voir `blacklotus-uefi-bootkit.md` §3 pour la liste complète.

## E. Résolution d'API par hash

`FUN_140002b98` : parcourt `PEB->Ldr` (liste des modules déjà chargés) par
hash de nom, pour amorcer la résolution sans appeler `LoadLibrary`.
`FUN_140002888` : `LoadLibrary` classique sur un nom de DLL décodé (une fois
la chaîne en clair obtenue via `FUN_1400057d4`).
`FUN_1400028f4` : parcourt la table d'export du module résolu, hash chaque
nom exporté et compare à une constante ; gère aussi la redirection API Set
(`api-*`/`ext-*`) par forwarder PE, donc pas une simple resolution-by-hash
générique copiée-collée mais une implémentation qui respecte les
redirections modernes de Windows 10+.

## F. Déchiffrement des blobs embarqués (AES-256-CBC, clé statique)

Dans `FUN_140002e50`, quatre appels à `FUN_1400010e0`/`FUN_140006954`
déchiffrent chacun un blob à une adresse `.data` fixe, tous avec la même
chaîne clé décodée à l'étape D. Décompilation de `FUN_1400010e0` : séquence
`BCryptOpenAlgorithmProvider("AES") -> BCryptSetProperty(ChainingModeCBC)
-> BCryptGetProperty(ObjectLength) -> BCryptGenerateSymmetricKey(secret=32
octets) -> BCryptDecrypt(pbIV=NULL, dwFlags=BCRYPT_BLOCK_PADDING)`, confirmée
en décodant les chaînes d'algorithme CNG passées en paramètre (§D).

```bash
python3 tools/decrypt_payload.py binaries/$h.exe decrypted/
```

Résultat (AES-256-CBC, IV nul, clé `6jxkPUHKRK5rgdg0PkhLhuS844uMYgrm`,
retrait manuel du padding PKCS7 équivalent à `BCRYPT_BLOCK_PADDING`) :

| Blob (adresse `.data`) | Chiffré | Clair | Identifié comme |
|---|---|---|---|
| `0x1400734e0` | 9 632 o | 9 624 o | PE64 natif (driver noyau) |
| `0x140075a80` | 12 304 o | 12 288 o | ruche de registre Windows |
| `0x140078a90` | 16 400 o | 16 384 o | ruche de registre Windows |
| `0x140021fc0` | 333 088 o | 333 084 o | encore compressé (§H) |

Trois des quatre blobs sont directement exploitables après ce seul
déchiffrement AES. Le quatrième (`0x140021fc0`, le plus gros) nécessite une
étape supplémentaire, repérée dans le code : `FUN_140006954` appelle
`FUN_140007198` juste après le déchiffrement AES.

## G. Décodage du chemin réseau alternatif

Décompilation de `FUN_1400079ac` (requête HTTPS) et des chaînes associées :
connexion à `msdl.microsoft.com` sur le port 443, trois requêtes GET vers
les chemins `/download/symbols/bootmgfw.efi/7144BCD31C0000/bootmgfw.efi`,
`/download/symbols/bootmgr.efi/98B063A61BC000/bootmgr.efi` et
`/download/symbols/hvloader.efi/559F396411D000/hvloader.efi`, soit le format
d'URL standard du serveur de symboles public de Microsoft. Ce chemin de code
récupère donc des copies saines et légitimes des fichiers de démarrage
visés, directement depuis l'infrastructure Microsoft, plutôt qu'un
quelconque C2 attaquant.

## H. Décompression du blob principal (LZMS)

`FUN_140007198` résout et appelle trois fonctions de `Cabinet.dll` :
`CreateDecompressor(Algorithm=5, ...)`, `Decompress(...)`,
`CloseDecompressor(...)`. Algorithme 5 = `COMPRESS_ALGORITHM_LZMS` (API
Windows Compression). Le code appelle `Decompress` une première fois avec un
buffer de sortie nul pour obtenir la taille requise, puis une seconde fois
pour décompresser réellement : signature caractéristique du mode "buffer"
(sans le flag `COMPRESS_RAW`), où le flux compressé encode lui-même sa
structure interne (par opposition au mode "block"/`COMPRESS_RAW`, où
l'appelant gère tout en externe).

**Tentative avec wimlib (échec, format incompatible)** : `wimlib` (paquet
Arch officiel `extra/wimlib`, installé via `pacman`)
implémente LZMS pour le format WIM et expose une fonction bas niveau
`wimlib_decompress()` documentée comme équivalente au mode **raw**
(`COMPRESS_ALGORITHM_LZMS | COMPRESS_RAW`) de l'API Windows, pas au mode
"buffer" utilisé ici. Test de non-régression du binding ctypes (compression
puis décompression d'un texte de test avec l'API wimlib elle-même) : succès,
donc le binding est correct. Balayage de tailles candidates (`tools/lzms_bruteforce_size.py`)
de 16 octets à 4 Mio sur le blob complet : zéro succès sur toute la plage,
y compris pour des tailles minuscules, ce qui confirme un problème de format
dès le premier octet, pas une mauvaise taille de sortie.

**Reconstruction empirique du format "buffer mode"** : aucune documentation
publique trouvée sur le format exact de ce mode (je n'ai eu accès qu'aux
extraits de résultats de recherche, pas aux pages elles-mêmes). Inspection directe des 24 premiers octets du blob déchiffré
par AES (`0x1400734e0` reste hors sujet ici) : un champ à l'offset 4
(`u16`) vaut `0x18` (= 24, taille d'en-tête plausible), un champ à l'offset
8 (`u32`) vaut `928592`, un champ à l'offset 16 (`u32`) vaut `65536` (une
puissance de deux, plausible comme taille de chunk), et un champ à l'offset
24 (`u32`) vaut `19284`. Hypothèse testée directement : offset 24 = taille
compressée du premier chunk, chunk lui-même décodable comme un bloc LZMS
**raw** standard (donc via `wimlib_decompress`) produisant exactement
`65536` octets de sortie.

```bash
python3 tools/lzms_container_decompress.py decrypted/blob_main_0x140021fc0.bin decrypted/blob_main_decompressed.bin
```

Premier essai : succès immédiat, sortie commençant par `4d 5a` ("MZ").
Généralisation à la totalité du blob (boucle sur des chunks
`[u32 taille_compressee][donnees]`, 65 536 octets de sortie par chunk sauf
le dernier) : 15 chunks, 928 592 octets décompressés au total, un PE32+
EFI application complet. Format documenté en tête de
`tools/lzms_container_decompress.py`.

## I. Identification du contenu final

```bash
file decrypted/blob_0x1400734e0.bin decrypted/blob_main_decompressed.bin
sha256sum decrypted/*.bin
strings decrypted/blob_0x1400734e0.bin | grep -iE 'efi|boot|hive'
python3 -c "
import re
data = open('decrypted/blob_main_decompressed.bin','rb').read()
for m in re.finditer(rb'(?:[\x20-\x7e]\x00){5,}', data):
    print(m.group(0).decode('utf-16le'))
" | grep -iE 'shim|mok|grub|debug'
```

Le driver (`0x1400734e0`) référence en clair `EFI\Microsoft\Boot\bootmgfw.efi`,
`EFI\Microsoft\Boot\bootload.efi`, `MokList`, `system32\hvloader.efi`,
`system32\BCD`. Le blob principal décompressé est un exécutable EFI
application (sous-système PE `10`), identifié par sa chaîne de débogage
`add-symbol-file /usr/lib/debug/usr/share/shim/x64-15.4-5.fc33/shimx64.efi.debug`
comme un binaire du projet **shim** (rhboot/Fedora), avec ses chaînes
natives de gestion du Machine Owner Key (`MokList`, `MokListRT`,
`import_one_mok_state`, `mirror_mok_db`, `install_shim_protocols`,
`init_grub`, `\\grubx64.efi`).

## Environnement

Identique aux autres analyses de ce dépôt : Ghidra headless via PyGhidra,
`pefile`, venv `~/.venvs/ctf-reverse`, clé API MalwareBazaar dans
`.mb_api_key` à la racine. Spécifique à cette analyse : `wimlib`
(paquet Arch `extra/wimlib`, fournit `libwim.so.15`, utilisé uniquement pour
son décodeur LZMS bas niveau via `ctypes`, pas pour ses outils WIM de haut
niveau) et `pycryptodome` (AES-256-CBC) dans le même venv.
