# Runbook : reproduction pas à pas (TAG-150 / NightshadeC2)

## Recherche préalable et choix du point d'entrée

`DenoRAT`, `CastleBot` et `DinDoor` n'ont pas de tag dédié sur MalwareBazaar (`get_taginfo` renvoie `no_results` pour les trois). Trois tags disponibles avec des échantillons réels : `CastleRAT` (44), `CastleLoader` (17), `NightshadeC2` (9). DenoRAT chargeant directement NightshadeC2 d'après les sources publiques consultées (eSentire, Recorded Future), ce tag a été choisi comme point d'entrée le plus court vers la chaîne complète.

## Acquisition

```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=NightshadeC2&limit=100" -o /tmp/nightshadec2_tag.json
```
9 résultats, deux dates groupées (2025-10-06/07/11) et deux plus tardives (2025-12-15). Les 3 premiers (2025-10-06) sont livrés en ZIP, les 6 suivants en PE direct.

```bash
for h in HASH1 HASH2 ...; do
  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}.enc"
done
```

Les 3 premiers sont des ZIP MalwareBazaar (mot de passe `infected`) contenant eux-mêmes un ZIP interne libre :
```bash
7z x -pinfected -olayer1 -y fichier.zip.enc
7z x -y -osample_extrait layer1/interieur.zip
```
Les 6 suivants sont des PE directs sous le même mot de passe MalwareBazaar (`7z x -pinfected`), pas de double couche.

## Triage du kit de DLL sideloading

```bash
ls sample1_consent/consent/
# c1.dll c1xx.dll c2.dll cnt.exe DASMHLP.HLP disco.exe wmsgapi.dll ...
file sample1_consent/consent/cnt.exe sample1_consent/consent/disco.exe
```

Identification formelle via les infos de version PE (`pefile`, `pe.FileInfo`) :
```python
import pefile
pe = pefile.PE('cnt.exe')
for fi in pe.FileInfo:
    for entry in fi:
        if hasattr(entry, 'StringTable'):
            for st in entry.StringTable:
                for k, v in st.entries.items():
                    print(k.decode(), '=', v.decode())
```
`cnt.exe` -> `OriginalFilename = consent.exe`, `CompanyName = Microsoft Corporation` : vrai `consent.exe` UAC Windows 7 SP1. Sa table d'imports (`pe.DIRECTORY_ENTRY_IMPORT`) liste `WMsgAPI.dll` : sideloading confirmé.

Vérification que les autres fichiers du dossier (`c1.dll`/`c1xx.dll`/`c2.dll`/`atlprov.dll`/`disco.exe`) ne sont pas fonctionnellement liés à `cnt.exe` : comparaison croisée de leurs tables d'imports respectives (aucune référence entre eux, chacun importe uniquement des DLL système standard ou son propre runtime, ex. `mspdb100.dll`/`MSVCP100.dll` pour les DLL du compilateur).

`wmsgapi.dll` : taille annoncée ~650 Mo, PE réel tenant dans les deux premiers Mo (sections listées via `pefile`, `PointerToRawData`/`SizeOfRawData`), confirmé par le taux de compression ZIP observé.

Troncature à 4 Mo pour analyse Ghidra pratique (largement suffisant pour couvrir toutes les sections réelles) :
```bash
head -c 4194304 wmsgapi.dll > trimmed/wmsgapi_trimmed.dll
```

```bash
python tools/list_functions.py trimmed/wmsgapi_trimmed.dll tools/funcs_wmsgapi.txt
python tools/decompile_by_name.py trimmed/wmsgapi_trimmed.dll tools/decomp_wmsgapi_entry.txt __DllMainCRTStartup WmsgBroadcastMessage WmsgSendMessage
```
Les 6 exports `Wmsg*` font 1 octet chacun (Ghidra : `Bad instruction`, commentaire `forwarder -> C:\Windows\System32\wmsgapi.XXX`) : purs forwarders vers la vraie DLL système. `__DllMainCRTStartup` = CRT MSVC2010 standard, rien d'ajouté visible directement dedans.

## Triage des 6 échantillons restants

7z liste directement la structure PE de chaque `.exe` (sections/certificat/table COFF) sans extraction préalable :
```bash
7z l fichier.exe
```
`050d061d...` et `df4425b4...` révèlent une `CERTIFICATE` et une `COFF_SYMBOLS` ; les 4 autres n'ont ni l'une ni l'autre.

Identification des chaînes runtime Go (`strings | grep "runtime: panic before malloc heap initialized"`) sur `050d061d...` et `df4425b4...` : confirme des binaires Go 1.24-ish malgré des noms de fichiers/métadonnées ne laissant rien deviner. Les 4 autres échantillons n'ont aucune chaîne Go : natifs C/C++.

```bash
python tools/list_functions.py 050d061d....exe tools/funcs_050d061d.txt main.
# 1937/3932 fonctions matchees -- symptome d'une obfuscation massive du paquet main
```

## Vérification des certificats Authenticode

Extraction brute de la table de certificat (`IMAGE_DIRECTORY_ENTRY_SECURITY`, champ `VirtualAddress` qui est en réalité un offset fichier, pas une RVA) :
```python
import pefile
pe = pefile.PE('050d061d....exe')
d = pe.OPTIONAL_HEADER.DATA_DIRECTORY[4]
raw = pe.write()[d.VirtualAddress+8 : d.VirtualAddress+d.Size]  # +8 saute l'en-tete WIN_CERTIFICATE
open('cert.p7', 'wb').write(raw)
```
```bash
openssl pkcs7 -inform DER -print_certs -in cert.p7 -text -noout | grep -E "Subject:|Not Before|Not After"
```
Révèle la chaîne complète jusqu'au certificat feuille (DeepL SE / Shenzhen Tuozhu selon l'échantillon).

Vérification cryptographique réelle avec `signify` (pas seulement une lecture de la chaîne, une validation) :
```bash
pip install signify
```
```python
from signify.authenticode.signed_file import SignedPEFile
with open('050d061d....exe', 'rb') as f:
    pe = SignedPEFile(f)
    result, e = pe.explain_verify()
    print(result, e)
# AuthenticodeVerificationResult.INVALID_DIGEST
```
(Le chemin d'import correct est `signify.authenticode.signed_file.SignedPEFile`, pas `signify.authenticode.SignedPEFile` qui n'est pas ré-exporté au niveau du paquet.)

## Décompilation de main.main et confirmation du chargeur PE manuel

```bash
python tools/decompile_by_name.py 050d061d....exe tools/decomp_main_050d061d.txt main.main
```

Lecture de `main.main` : séquence `LoadLibrary("ntdll.dll")` + `GetProcAddress("NtUnmapViewOfSection")` + appel via `syscall.Syscall`, puis plusieurs appels à des fonctions renommées mots-salade. Décompilation ciblée de ces fonctions pour identifier leur vrai rôle :
```bash
python tools/decompile_by_name.py 050d061d....exe tools/decomp_helpers_050d061d.txt \
  main.Foliatureoosporic main.Consequentlyscorch main.Serridentinesmystificatory main.Embouchuretorse \
  main.Physonectaepraisefully main.Kaibarthaendosalpingitis main.Biblicismtonsillectome
```
Chacune identifiée par son comportement (pas son nom) : lecture de `e_lfanew`, appel `VirtualAlloc` via `syscall.LazyDLL`, `memcpy`, copie section par section façon `debug/pe.Section.Data()`, `memset`, résolution de table d'imports façon `debug/pe.ImportDirectoryEntry` (détail complet en tag150-denorat-killchain.md §5).

## Extraction et décodage du payload embarqué

Le buffer constant embarqué est repéré dans `main.main` via son type Go récupéré par Ghidra (`_247434_uint8___Array_type`, taille explicite dans le nom). L'adresse `DAT_0058b1c2` référencée par le désassemblage s'est avérée être 10 octets **après** le vrai début du buffer (vérifié en comparant les octets lus à cette adresse aux octets que Ghidra affichait comme des affectations littérales juste avant dans le pseudocode : ils coïncidaient exactement, confirmant que Ghidra avait scindé à tort une seule copie contiguë).

Recherche du bon sens de transformation par tâtonnement (deux directions possibles pour un XOR-like ±0x46 par parité) : la première tentative n'a produit aucun texte lisible ; la tentative opposée a produit 16 octets consécutifs de lettres minuscules ASCII pures (`ytilibatimiksife...`), statistiquement très improbable par hasard, confirmant le bon sens. Ce texte s'est ensuite avéré être des noms de types RTTI C++ lisibles mais inversés caractère par caractère (`.?AVtype_info@@` lu à l'envers) : indice qu'il fallait inverser le buffer entier, ce qui a produit un en-tête `MZ` valide.

```bash
python tools/decode_embedded_payload.py binaries/layer1/050d061d....exe 0x58b1c2 0x3c68a payload_extrait.exe
# 247434 octets ecrits, signature MZ presente : True
```

```bash
sha256sum payload_extrait.exe
# e32b50f1b9ab8ccda64e4046780e002b350ece9c93ff4fa245fe0526ada14a1a
python tools/find_api_callers.py payload_extrait.exe WinHttpConnect tools/c2_winhttpconnect.txt
```
Confirme l'appel `WinHttpConnect` avec un nom de serveur passé en paramètre (pas une chaîne statique) : configuration réseau injectée/déchiffrée à l'exécution, pas trouvée en clair par cette voie.

Même recette appliquée à `df4425b4...` (blob de taille voisine, `0x3c69f` octets, repéré de la même façon) : essai systématique de décalages 0-19 et des deux parités, aucun n'a produit `MZ`. Structure confirmée identique (même approche de chargeur, même ordre de grandeur), payload non extrait.

## Vérification finale (bourrage VS2010, exports des DLL restantes)

```python
import pefile
for f in ['cnt.exe','c1.dll','c1xx.dll','c2.dll','atlprov.dll','disco.exe']:
    pe = pefile.PE(f, fast_load=True)
    pe.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_IMPORT']])
    print(f, [e.dll.decode() for e in pe.DIRECTORY_ENTRY_IMPORT] if hasattr(pe,'DIRECTORY_ENTRY_IMPORT') else [])
```
Confirme l'absence de lien fonctionnel entre les fichiers VS2010 et `cnt.exe`.

```python
for f in ['04a1852a....exe', '8940944e....exe', 'b79878b2....exe', 'e7d7d805....exe']:
    pe = pefile.PE(f, fast_load=True)
    pe.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_EXPORT']])
    print(f, [e.name.decode() if e.name else e.ordinal for e in pe.DIRECTORY_ENTRY_EXPORT.symbols] if hasattr(pe,'DIRECTORY_ENTRY_EXPORT') else [])
```
Confirme que `b79878b2...`/`e7d7d805...` sont bien de la même famille `wmsgapi.dll` (6 exports identiques), et que `04a1852a...`/`8940944e...` sont des EXE natifs sans export (non décompilés en détail à ce stade).

## Recherche du C2 réel dans le payload extrait

Recherche d'empreintes crypto directes (AES S-box Rijndael, table Te0 optimisée, instructions AES-NI via `capstone`, constante d'initialisation SHA-256, constante ChaCha20 `"expand 32-byte k"`) sur le payload extrait : aucune trouvée. Le déchiffrement AES-256-CBC documenté publiquement pour NightshadeC2 ne s'applique donc probablement pas à ce module précis (ou pas à la configuration réseau elle-même).

```bash
python tools/find_api_callers.py payload_extrait.exe WinHttpConnect tools/c2_winhttpconnect.txt
```
Révèle un premier appelant qui interroge `www.ip-api.com` (`line/?fields=147457`, port 80) : une vérification de géolocalisation/statut VPN de la victime, pas le C2 (chaînes `"success"`, `"(VPN)"` lues directement via `decompile_addr.py` sur les adresses référencées).

Recherche large de chaînes ressemblant à un domaine ou une IP dans tout le binaire (pas seulement les appelants d'une API donnée) :
```bash
strings -e l -n 6 payload_extrait.exe | grep -aE "^[a-zA-Z0-9.-]+\.(com|net|org|xyz|top|ru|io)$"
strings -e l payload_extrait.exe | grep -aE "^([0-9]{1,3}\.){3}[0-9]{1,3}$"
```
Révèle une adresse IP en clair : `85.208.84.115`. Localisation de son adresse fichier (`grep -a -t x`), conversion en RVA/VA (`pe.get_rva_from_offset`), puis recherche de la fonction référençant cette adresse :
```bash
python tools/identify_addr.py payload_extrait.exe 0x14003a0e0
```
Révèle `FUN_1400010d0`, la fonction d'initialisation principale du programme (appelée depuis `__scrt_common_main_seh`, donc le vrai point d'entrée). Décompilation complète :
```bash
python tools/decompile_addr.py payload_extrait.exe tools/c2_ip_context.txt 0x1400010d0
```
Révèle en une seule fonction : vérification d'instance unique, persistance par tâche planifiée `JAVA_V3` (commande PowerShell entièrement en clair dans le binaire), lancement du thread keylogger, la vérification `ip-api.com` déjà repérée, collecte d'empreinte machine, et enfin l'appel vers la boucle principale de C2 avec l'adresse `85.208.84.115` copiée dans un buffer juste avant.

Décompilation de cette boucle principale (`FUN_14000cb40`) et de la fonction de connexion (`FUN_140004720`) qu'elle appelle :
```bash
python tools/decompile_addr.py payload_extrait.exe tools/c2_beacon_funcs.txt 0x14000cb40 0x140008cc0 0x1400297a0
python tools/decompile_addr.py payload_extrait.exe tools/c2_connect_func.txt 0x140004720
```
Confirme `getaddrinfo`/`socket`/`connect` par ordinal sur `ws2_32.dll` (pas `WinHttpConnect`, qui ne servait qu'à la géolocalisation), port lu via une chaîne séparée (`identify_addr.py` sur l'adresse référencée, puis lecture brute des octets pour obtenir `"7777"`), et une poignée de main par comparaison de chaîne magique (`"RandOmKey322666B"`, 16 octets) avant tout échange de commandes. La boucle principale lit ensuite un en-tête de commande de 32 octets et distribue vers 13 gestionnaires selon un octet d'opcode (table complète en `tag150-denorat-killchain.md` §6bis, gestionnaires individuels non décompilés).

## Extraction du second payload (df4425b4) et décompilation des gestionnaires de commandes

Nouvelle tentative sur `df4425b4...` : plutôt que de re-tester une plage de décalages/parités à l'aveugle, relecture précise du désassemblage de `main.main` pour ce build précis (`grep -n "local_30->F\|puVar10 = " tools/decomp_main_df4425b4.txt`) : décalage réel de 15 octets (pas 10) entre l'adresse `DAT_xxxxxxxx` et le vrai début du buffer, constante de transformation `0x5e` (pas `0x46`, la constante Go source correspondante `'^'` au lieu de `'F'`). Une fois ces deux paramètres corrigés, extraction réussie du premier coup (pas de recherche exhaustive nécessaire) :
```bash
python3 - <<'PY'
import pefile
pe = pefile.PE('df4425b4....exe', fast_load=True)
off = pe.get_offset_from_rva(0x61cc42 - pe.OPTIONAL_HEADER.ImageBase)
with open('df4425b4....exe','rb') as f:
    f.seek(off - 15); raw = f.read(0x3c69f)
dec = bytearray(len(raw))
for i,b in enumerate(raw):
    dec[i] = (b - 0x5e) & 0xff if i%2==0 else (b + 0x5e) & 0xff
open('payload2.exe','wb').write(bytes(dec)[::-1])
PY
```
Comparaison octet à octet avec le premier payload extrait (`cmp`, puis lecture des octets différents) : seulement 130 octets de différence, tous en toute fin de fichier, tous du texte de bourrage lisible (même thème « mots-salade » que le reste de la campagne). Vérification des chaînes caractéristiques (`85.208.84.115`, `JAVA_V3`, `RandOmKey322666B`) présentes à l'identique dans les deux payloads.

Décompilation des 13 gestionnaires de commandes en un seul appel groupé, adresses collectées depuis le `switch` de `FUN_14000cb40` (`tools/c2_beacon_funcs.txt`) :
```bash
python tools/decompile_addr.py payload1.exe tools/c2_opcode_handlers.txt \
  0x14001d430 0x140002360 0x1400048f0 0x14000e920 0x14000ee40 0x14000e750 0x14000c1b0 \
  0x14000d6f0 0x14000c080 0x140006760 0x140006420 0x140008e50 0x140007e90 0x14000fb20 0x1400079b0
```
Lecture individuelle de chaque fonction (détail complet en `tag150-denorat-killchain.md` §6bis) : identification par les chaînes en clair qu'elles référencent (`u_C__Windows_System32_cmd_exe`, `u__TEMP__Vidosik_mp4`, `u_keylog_txt`, `u_C__Windows_System32_rundll32_exe`) et par les API Windows appelées (`CreateProcessW`+pipes pour les shells, `GetDC`/`CreateCompatibleDC` pour les captures d'écran, `SetWindowsHookExW` déjà repéré dans l'initialisation).

## Triage rapide des deux derniers échantillons (04a1852a, 8940944e)

```bash
strings -e l -n 8 04a1852a....exe | grep -iE "wmsgapi|consent|deepl|java_v3|randomkey|85\.208\.84\.115|vidosik|keylog"
# aucun resultat sur les deux fichiers
python3 -c "
import pefile
pe = pefile.PE('04a1852a....exe')
print(hex(pe.OPTIONAL_HEADER.AddressOfEntryPoint))
for s in pe.sections: print(s.Name, hex(s.Misc_VirtualSize))
print([e.dll.decode() for e in pe.DIRECTORY_ENTRY_IMPORT])
"
```
`.text` volumineux (1,2-1,8 Mo, pas un petit chargeur avec blob embarqué comme les loaders Go), pas d'import réseau (`WS2_32`/`WINHTTP` absents), aucune chaîne caractéristique du reste de la chaîne : rôle exact non déterminé, décompilation complète non menée.

## Rédaction

`tag150-denorat-killchain.md`, `README.md`, `remediation.md` et une règle YARA rédigés à partir des constats ci-dessus, mis à jour après la découverte du C2 réel puis après l'extraction du second payload et la décompilation des gestionnaires de commandes.

## Clôture des 3 derniers points ouverts

### Trace DenoRAT

Recherche web plutôt que MalwareBazaar directement (tags `DenoRAT`/`DinDoor` inexistants sur MalwareBazaar, vérifié par `get_taginfo`). Clarification de la relation exacte entre CastleLoader/CastleBot/CastleRAT/PyNightshade/NightshadeC2/DinDoor/DenoRAT (plusieurs noms provenant d'éditeurs différents pour des composants parfois identiques, parfois distincts) : NightshadeC2 (eSentire) = CastleRAT variante C (Insikt Group), PyNightshade = CastleRAT variante Python. DenoRAT est un composant réellement distinct, basé sur le runtime Deno/JavaScript.

Rapport eSentire (« DinDoor, DenoRAT, and NightshadeC2: Analyzing TAG-150's Evolving Tradecraft ») confirmant la chaîne séquentielle complète avec 5 hashs cités (Griffin20.ps1, DinDoor, DenoRAT stager, DenoRAT, payload NightshadeC2). Vérification systématique sur MalwareBazaar :
```bash
API_KEY=$(cat ../.mb_api_key)
for h in 4ac3e1ecd2c4745f2e846ec7655b92234ee92068bffc7690c3f0184dc25c71da \
         ece42baaca7524460cc5ffc9ae94c2a634a67610decdd2bdc8f134c36fa2dd6c \
         7682e9ac86ff47cc198812719ca54e169c3cb21ce584ad2a278a640f4833de46 \
         3dffe05d9cc49b2598d743301a8991965056d97bb8d429de461ed66c0dbde28b \
         c1e0a0543b12c64726f8bb36389010d7f35e57993a22ce5b73c56b9cc2a87a00; do
  curl -s https://mb-api.abuse.ch/api/v1/ -d "query=get_info&hash=$h" -H "Auth-Key: $API_KEY" | python3 -c "import json,sys; print(json.load(sys.stdin).get('query_status'))"
done
```
Les 5 : `hash_not_found`. Piste close avec réponse documentaire, pas d'échantillon obtenu.

### Origine du certificat donneur

Extraction directe de la table de certificat Authenticode et de l'empreinte SHA1 du certificat feuille DeepL SE :
```bash
python3 -c "
import pefile
p = pefile.PE('050d061d....exe', fast_load=True)
p.parse_data_directories()
sec = p.OPTIONAL_HEADER.DATA_DIRECTORY[4]
data = p.write()[sec.VirtualAddress:sec.VirtualAddress+sec.Size]
open('/tmp/deepl_cert.p7', 'wb').write(data[8:])
"
openssl pkcs7 -inform DER -print_certs -in /tmp/deepl_cert.p7 -out /tmp/deepl_certs.pem
csplit -s -z -f /tmp/deepl_cert_ -b "%02d.pem" /tmp/deepl_certs.pem '/-----BEGIN CERTIFICATE-----/' '{*}'
openssl x509 -in /tmp/deepl_cert_03.pem -noout -subject -fingerprint -sha1
# sha1 Fingerprint=34:BF:69:1C:9E:1D:00:18:F5:C5:9D:A0:26:61:04:0C:1E:AD:6B:E4
```
Recherche de cette empreinte et du numéro de série sur VirusTotal (recherche web + tentative GUI directe), Hybrid Analysis (un résultat trouvé pour un fichier `DeepL.exe`/`ZeroInstall.Stub.exe` mais sans signature valide, donc pas le donneur), et `bazaar.abuse.ch/browse/signature/DeepL%20SE/` (aucun résultat). Vérification indépendante que les données d'identité du certificat sont réelles (recherche du numéro HRB 104617 sur les registres du commerce allemand publics) : confirmé authentique, DeepL SE existe bien à cette adresse à Cologne. Conclusion : copie d'un fichier réel non identifié, pas une fabrication.

### Triage détaillé de 04a1852a et 8940944e

```bash
python3 -c "
import pefile, datetime
for f in ['04a1852a....exe', '8940944e....exe']:
    p = pefile.PE(f)
    print(f, datetime.datetime.fromtimestamp(p.FILE_HEADER.TimeDateStamp, datetime.timezone.utc))
    for e in p.DIRECTORY_ENTRY_IMPORT: print(' ', e.dll.decode())
"
```
Compilés 2023-08-19 et 2024-07-02 (avant l'activité connue de TAG-150). Imports centrés sur `gdiplus.dll`/`GDI32.dll`/`ole32.dll`/`USER32.dll`, aucun réseau. Métadonnées de version extraites (`pefile` + `FileInfo`) : mêmes suites de mots anglais/pseudo-scientifiques sans rapport que les loaders Go (`Gravidae venenately shame`, `Tacos fustiest niches`...), confirmant l'appartenance au même outillage de génération automatique.

Recherche de chaînes ciblées :
```bash
strings -a 04a1852a....exe | grep -iE "CreateDXGIFactory|DuplicateOutput|D3D11CreateDevice"
```
`CreateDXGIFactory` présent dans les deux fichiers (bootstrap de la Desktop Duplication API DirectX, capture d'écran matérielle), absent de la table d'imports statique (résolution dynamique `LoadLibrary("dxgi.dll")` + `GetProcAddress`, cohérent avec le reste de la chaîne qui évite les imports statiques directs). Combiné à `gdiplus.dll` (encodage d'image) et `ole32.dll` (initialisation COM requise par DXGI), conclusion : modules de capture d'écran dédiés, probablement invoqués par le processus principal NightshadeC2 pour les opcodes de diffusion d'écran déjà décompilés (§6bis du writeup). Pas d'exports (`DIRECTORY_ENTRY_EXPORT` absent) : exécutables autonomes, pas des DLL à charger, `CreateMutex`+`GetCommandLine` présents dans les deux (cohérent avec un processus enfant à instance unique recevant des arguments).

Décompilation complète de la logique de réception d'ordre (comment le processus principal déclenche la capture) non menée, jugée hors périmètre.

## Mise à jour finale


## Dernier point ouvert : déclenchement réel de wmsgapi.dll

Triage initial des imports (`pefile`) sur `binaries/trimmed/wmsgapi_74d2910c.dll` (4 Mio, déjà tronquée d'une précédente session) : table d'imports anormalement riche pour un simple relais de forwarders (`USER32.dll`/`GDI32.dll`/`gdiplus.dll` avec des dizaines de fonctions de manipulation de fenêtres/GDI+), et section `.data` à entropie 7,94 (quasi aléatoire, 131 584 octets).

Localisation de `__DllMainCRTStartup` (Ghidra headless) : décompilation confirme un appel à `FUN_180020a00(param_1, param_2, param_3)`, le vrai callback `DllMain` utilisateur, jamais suivi lors du triage initial.

Tentative de décompilation complète de `FUN_180020a00` : échec (`Low-level Error: Flow exceeded maximum allowable instructions`), la fonction couvre une plage d'adresses de 1,7 Mo. Pivot vers radare2 pour l'info de fonction :
```bash
r2 -q -e "bin.cache=true" wmsgapi_74d2910c.dll <<'EOF'
aaa
afi @ 0x180020a00
EOF
```
`size: 1718758` mais `realsz: 65594` (octets d'instructions réels), `num-bbs: 4`, `edges: 3` : signe d'une fonction quasi linéaire malgré sa taille apparente.

Désassemblage complet en texte brut :
```bash
r2 -q -e "bin.cache=true" -e "scr.color=0" wmsgapi_74d2910c.dll <<'EOF' > /tmp/wmsgapi_dllmain_full.asm
aaa
pd 8000 @ 0x180020a00
EOF
```
Repérage du dispatch initial : `cmp dword [rsp + 0x2ac58], 1` (test de `fdwReason == DLL_PROCESS_ATTACH`) suivi d'un `je` vers le bloc principal ou d'un `jmp` de sortie directe. Le corps du bloc principal (5044 occurrences de `mov [rsp+X], ...`) écrit des valeurs éparpillées sur une plage de pile de ~175 Ko (`0x930` à `0x2ac58`), sans structure contiguë identifiable.

Script Python de reconstruction (`/tmp/reconstruct_buffer.py`, non conservé dans `tools/`, résultat négatif) : parcours du désassemblage en ordre d'adresse, suivi de `eax`/`rax` au fil des `mov eax, imm`/`movabs rax, imm`, application de chaque écriture `byte`/`word`/`dword`/`qword` à un buffer simulé. Résultat sur les dix plages les plus denses : aucune ne dépasse 8% de remplissage, aucun contenu ASCII lisible, aucune signature de PE (`MZ` à l'offset 0 absent, chaîne DOS stub absente). Conclusion : pas un payload chiffré reconstruit par petites écritures (contrairement à l'hypothèse de départ), plutôt du bruit délibérément inséré pour gonfler taille et complexité apparente.

Confirmation définitive par recherche exhaustive des références sortantes de la fonction entière (pas seulement l'échantillon désassemblé) :
```bash
r2 -q -e "bin.cache=true" wmsgapi_74d2910c.dll <<'EOF'
aaa
axff @ 0x180020a00
EOF
```
Résultat : exactement 3 références au total dans toute la fonction (1,7 Mo d'adresses) : le dispatch initial (2 sauts internes) et un seul appel, vers la routine de sondage de pile standard `__chkstk` (`fcn.1801c4400`, confirmée par décompilation comme le stack probe MSVC classique). Aucun autre appel, aucune autre branche, nulle part dans la fonction. Conclusion définitive : `wmsgapi.dll` n'exécute aucune action malveillante au-delà du sideloading et du relais forwarder ; le déclenchement réel de la charge n'a pas été localisé parce qu'il n'existe pas dans ce build.

`tag150-denorat-killchain.md` §2, §7 et le résumé mis à jour ; `remediation.md` §4 mis à jour (l'ancienne formulation "déclenchement non localisé" était devenue trompeuse, remplacée par la conclusion négative).

## Clarification des opcodes 0x59 et 0xcd

Décompilation ciblée des trois gestionnaires concernés, adresses déjà connues depuis le premier passage (`tag150-denorat-killchain.md` §6bis) :
```bash
python tools/decompile_addr.py binaries/trimmed/nightshadec2_payload_from_050d061d.exe /tmp/opcode_59_c9_cd.txt \
  0x14000c1b0 0x140007e90 0x1400079b0
```

**`0x59`** (`FUN_14000c1b0`) : lecture attentive de la construction du buffer `local_1d8` juste avant l'envoi final (`Ordinal_19(local_1b0, local_1d8, 10, 0)`) : `local_1d8[0]=5, [1]=0, [2]=0, [3]=1` correspond exactement à l'en-tête d'une réponse SOCKS5 réussie (VER/REP/RSV/ATYP). C'est bien un relais SOCKS5, pas un simple pivot générique supposé.

**`0xcd`** (`FUN_1400079b0`) vs **`0xc9`** (`FUN_140007e90`) : la seule différence de code entre les deux est l'appel à `FUN_140015d60(NULL, 0, FUN_1400074a0, lVar4, 0, local_48)` dans `0xcd`, absent de `0xc9`. Décompilation de `FUN_140015d60` : wrapper CRT autour de `CreateThread` (signature à 6 paramètres correspondant exactement). Décompilation de `FUN_1400074a0` (la routine de thread) :
```bash
python tools/decompile_addr.py binaries/trimmed/nightshadec2_payload_from_050d061d.exe /tmp/opcode_cd_thread.txt \
  0x1400074a0 0x140015d60
```
Boucle de réception de structures `INPUT` Win32 (`sizeof(INPUT)=0x28`), injectées via `SendInput` : coordonnées souris normalisées par `GetSystemMetrics`, clics, clavier, et un type de message personnalisé (`0x97`) qui pousse une chaîne reçue dans le presse-papiers (`OpenClipboard`/`SetClipboardData`) puis simule `Ctrl+V` (séquence `SendInput` avec VK 0x11 et 0x56). Confirme `0xcd` = `0xc9` + canal de contrôle interactif complet.

`tag150-denorat-killchain.md` §6bis (deux lignes de tableau réécrites) et §7 (point ouvert supprimé) mis à jour ; `remediation.md` §3 (liste des capacités) complétée avec le relais SOCKS5.

## Modules DXGI : bootstrap réel confirmé, canal de sortie introuvable

Triage initial de `04a1852a....exe` (`pefile`) : point d'entrée réel à `0x1401b2b2c`. Localisation de `__tmainCRTStartup` et de son appel vers la fonction "main" utilisateur, `FUN_1400035d0`.

Tentative de décompilation complète : échec Ghidra (`Flow exceeded maximum allowable instructions`). Info de fonction radare2 : désaccord avec Ghidra sur les bornes (r2 : 64 Ko de corps réel, zéro branchement ; Ghidra : 1,3 Mo). Vérification directe (au lieu de trancher entre les deux outils) : énumération de toutes les instructions `CALL`/`JMP` réelles via l'API Ghidra `listing.getInstructions()` sur les bornes de Ghidra :
```python
instrs = listing.getInstructions(f.getBody(), True)
# comptage : 4640 CALL, 202 JMP, 241 sauts conditionnels, 227798 autres
```
Confirme un vrai contrôle de flot (pas juste du bourrage comme `wmsgapi.dll`), cohérent avec un control-flow flattening réel plutôt qu'un simple padding.

### Localisation du bootstrap DXGI

Repérage d'une petite table d'adresses fixes (`0x1401bb0xx`-`0x1401bb3xx`) appelées indirectement (`CALL qword ptr [0x1401bbXXX]`) partout dans la fonction : probable table de pointeurs résolus dynamiquement (`LoadLibraryA`/`GetProcAddress`, tous deux importés statiquement), cohérent avec l'absence de `dxgi.dll` dans la table d'imports statique alors que la chaîne `CreateDXGIFactory` est présente dans le binaire.

Confirmation du slot `0x1401bb118` = `GetProcAddress` par lecture du contexte autour de plusieurs sites d'appel (argument `RDX` chargé juste avant par un `LEA RDX,[adresse]`, dont le contenu est un nom de fonction lisible une fois résolu via `pefile.get_data()`, contournement du même bug de lecture mémoire Ghidra déjà rencontré sur Devman et Lynx).

Recherche du site de résolution de `CreateDXGIFactory` spécifiquement (pas les résolutions génériques du CRT hors de la fonction principale) :
```python
# parcours en un seul passage de listing.getInstructions(f.getBody(), True)
# suivi du dernier "LEA RDX,[0x...]" vu, declenchement sur CALL vers le slot GetProcAddress
```
Un seul résultat dans le corps de la fonction principale, à `0x140147271`, résolvant l'adresse `0x14022b900` = `"CreateDXGIFactory\0\0\0\0\0\0\0dxgi.dll"` (vérifié par lecture directe via `pefile`, la chaîne suit immédiatement en mémoire). Désassemblage du contexte immédiat : le pointeur résolu est stocké puis **appelé** quelques instructions plus loin (`CALL qword ptr [RSP+0x19970]`), avec vérification du `HRESULT` de retour (`CMP ...,0 ; JNZ <sortie>`), suivi d'une séquence d'appels de méthodes COM par vtable (`[RAX+0x10]`/`[RAX+0x40]`/`[RAX+0x48]`) cohérente avec `EnumAdapters`/`EnumOutputs`/`QueryInterface`/`DuplicateOutput`.

### Recherche du déclenchement et du canal de sortie (négative, exhaustive)

Fenêtre de 600 instructions avant et 3000 après le bootstrap : aucun `WaitForSingleObject`, `ReadFile`, ou fonction de réception. Recherche des callers de `WriteFile`/`CreateFileW`/`GetStdHandle` dans tout le binaire : `WriteFile` n'a que 7 sites d'appel, tous dans le runtime C interne (`0x1401b3xxx`-`0x1401b8xxx`), aucun depuis `FUN_1400035d0`.

Recherche de chaînes pour des mécanismes alternatifs :
```bash
strings -a 04a1852a....exe | grep -iE "^ws2_32|^wsasend|^wsarecv|^send$|^recv$|^connect$|createnamedpipe|\\\\pipe\\\\"
strings -a 04a1852a....exe | grep -iE "^CreateFileMapping|^MapViewOfFile|^NtWriteFile|^DuplicateHandle"
strings -a 04a1852a....exe | grep -E "^Gdip[A-Z]"
```
Aucun résultat sur les trois recherches : ni réseau (statique ou résolu dynamiquement comme `CreateDXGIFactory`), ni mémoire partagée, ni sauvegarde d'image via l'API C de GDI+. Conclusion : recherche exhaustive de toutes les pistes plausibles, aucun canal de sortie identifié dans le code examiné.

`tag150-denorat-killchain.md` : nouvelle section §6ter, résumé, §7 et table IOC mis à jour. `remediation.md` §4 mis à jour.
