# Runbook : reproduction pas à pas (RagnarLocker/VMProtect, identification de cible)

## A. Écarter la première piste (INC Ransom) : vérification directe contredit le rapport tiers

Le sample cité par le rapport Acronis comme "protégé VMProtect 3.X" a été identifié par recoupement de marqueurs de chaînes (`curve25519-dalek`, `SALSA20`/`EncryptionAlgo`, `BCryptGenRandom`, `OpenSCManagerW`, `GetDriveTypeW`, `GetSystemInfo`) : SHA256 `c3d8c86844d7bb86dea31bcb0510542fb59bc3bc67a4e3df59f72da5c8509f54`. Vérification directe (`pefile`, entropie de section) : entropie normale sur toutes les sections, IAT complète et lisible, chaînes de debug Rust en clair. Contredit l'affirmation initiale du rapport, confirme sa propre section technique détaillée ("no code virtualization employed"). Piste abandonnée, aucun travail de dévirtualisation entamé dessus.

## B. Recherche d'un vrai échantillon ransomware protégé par VMProtect (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=VMProtect&limit=1000" -o /tmp/vmprotect_tag.json
python3 -c "
import json
d = json.load(open('/tmp/vmprotect_tag.json'))
for e in d['data']:
    print(e.get('first_seen'), e.get('sha256_hash')[:16], e.get('file_type'), e.get('file_size'), e.get('signature'), e.get('tags'))
"
```
58 échantillons taggés `VMProtect` sur MalwareBazaar. La plupart sont des RAT/stealers/loaders (FatalRAT, ValleyRAT, LummaStealer, Rhadamanthys, Cobalt Strike...), aucune famille de ransomware directement nommée dans ce jeu de résultats brut. Deux entrées se distinguent par leur taille inhabituelle (5-6 Mo contre 40-120 Ko pour un RagnarLocker typique) et leurs tags additionnels `CAPCOM`/`CAMPARIGROUP` : recoupement avec le tag `RagnarLocker`.

```bash
curl -s -X POST https://mb-api.abuse.ch/api/v1/ -H "Auth-Key: $KEY" \
  -d "query=get_taginfo&tag=RagnarLocker&limit=100" -o /tmp/rl_tag.json
python3 -c "
import json
d = json.load(open('/tmp/rl_tag.json'))
for e in sorted(d['data'], key=lambda x: x.get('first_seen','')):
    print(e.get('first_seen'), e.get('sha256_hash'), e.get('file_size'), e.get('tags'))
"
```
Confirme : `9416e5a57e6de00c685560fa9fee761126569d123f62060792bf2049ebba4151` (CAPCOM, 5 359 456 octets) et `afab912c41c920c867f1b2ada34114b22dcc9c5f3666edbfc4e9936c29a17a68` (CAMPARIGROUP, 6 026 592 octets), tous deux tagués `RagnarLocker` + `VMProtect` + `packed`, première soumission 2020-11-26, contre 38-120 Ko pour les échantillons RagnarLocker "normaux" non protégés du même tag. L'écart de taille correspond exactement à ce qu'ajoute un enrobage VMProtect (section virtualisée de plusieurs Mo).

## C. Acquisition et vérification directe (ne pas se fier au tag seul)

```bash
h1=9416e5a57e6de00c685560fa9fee761126569d123f62060792bf2049ebba4151
h2=afab912c41c920c867f1b2ada34114b22dcc9c5f3666edbfc4e9936c29a17a68
for h in $h1 $h2; 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.zip"
  cd binaries && 7z x -pinfected -y "$h.zip" && cd ..
done
```

```python
import pefile
for f in [f"binaries/{h1}.exe", f"binaries/{h2}.exe"]:
    pe = pefile.PE(f, 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), s.SizeOfRawData, hex(s.VirtualAddress))
    for e in pe.DIRECTORY_ENTRY_IMPORT:
        print("DLL", e.dll.decode(), "n=", len(e.imports))
    print("EntryPoint RVA", hex(pe.OPTIONAL_HEADER.AddressOfEntryPoint))
```

Retour :
- CAPCOM : sections `.ctr0` (vide, `SizeOfRawData=0`) / `.ctr1` (5 338 112 octets, entropie 7,95).
- CAMPARIGROUP : sections `.vmp0` (vide) / `.vmp1` (6 003 200 octets, entropie 7,96), nommage VMProtect par défaut, non renommé.
- IAT fragmentée sur les deux (KERNEL32.dll apparaît 3 fois dans la table avec des tailles différentes), point d'entrée situé à l'intérieur de la section virtualisée sur les deux.

## D. Octets d'entrée : motif d'entrée VMProtect confirmé

```python
import pefile
pe = pefile.PE(f"binaries/{h2}.exe")
ep = pe.OPTIONAL_HEADER.AddressOfEntryPoint
print(pe.get_memory_mapped_image()[ep:ep+32].hex())
print(pe.FILE_HEADER.TimeDateStamp)
```
Retour : `68 06d2495d e8 32b1f6ff ...` (`push imm32` puis `call rel32`, motif d'entrée dispatcher VMProtect classique). `TimeDateStamp = 1603207057` (2020-10-20 15:17:37 UTC), **identique sur les deux fichiers** : même build protégé, redéployé sur les deux victimes ~5 semaines plus tard.

## E. Harnais de traçage dynamique Unicorn (`tools/vmp_trace.py`)

```bash
python3 tools/vmp_trace.py binaries/afab912c41c920c867f1b2ada34114b22dcc9c5f3666edbfc4e9936c29a17a68.exe --budget 20000000 --dump-ring --ring-size 150
```

Construit un GDT minimal (FS -> faux TEB/PEB) car `UC_X86_REG_FS_BASE` est un no-op silencieux en mode 32 bits sur Unicorn 2.1.4 (piège à connaître, cf. avertissement de dépréciation à l'exécution). Patch chaque slot IAT avec une adresse synthétique hookée en exécution ; construit une fausse DLL en mémoire (en-tête PE + table d'export) à la volée pour chaque `LoadLibraryA`/`GetModuleHandleA` par nom, afin que le parcours manuel de table d'export (bootstrap classique de protecteur pour retrouver `kernel32`/`ntdll` sans IAT) résolve vers les mêmes stubs qu'un `GetProcAddress` normal.

Deux bugs de harnais trouvés et corrigés par l'échec observé (crash sur adresse aléatoire ou comportement incohérent) :
1. La valeur de retour par défaut de la table de stubs n'était pas câblée (toujours 0 au lieu du défaut déclaré), faisant échouer silencieusement `GetProcessWindowStation()` (retour NULL au lieu d'un handle factice), ce qui ressemble à un déclenchement de détection anti-sandbox.
2. `WTSSendMessageW` déclaré avec 9 arguments stdcall au lieu de 10 (vraie signature MSDN) : désalignement de pile silencieux qui provoque un crash sur une adresse aléatoire une fois les instructions suivantes exécutées.

Résultat après corrections : émulation stable sur ~4,8 millions d'instructions, jusqu'à `WTSSendMessageW` puis un `ExitProcess(0xDEADC0DE)` contrôlé.

Fragment de trace capturé (`--dump-ring`) juste avant l'appel à un stub : révèle le handler VM générique d'appel de fonction externe (boucle de dépilement d'un tableau d'arguments virtuel, empilage stdcall, puis `call eax`), un détail de fonctionnement de cette protection jamais documenté publiquement pour RagnarLocker.

## F. De `ExitProcess(0xDEADC0DE)` au run complet « A à Z », `ExitProcess(0)` propre

`ExitProcess(0xDEADC0DE)` sans aucun message affiché est la terminaison silencieuse standard de VMProtect (`loader_status` interne resté à `LOADER_ERROR`, `FreeImage()` → `ExitProcess(0xDEADC0DE)`, jamais un `LoaderMessage()`), pas un blocage propre à RagnarLocker. Plusieurs bugs de harnais, franchis un par un, ont fait progresser l'exécution jusqu'au vrai point d'entrée applicatif :

- `peb->OSBuildNumber` jamais résolu (aucune valeur ne correspond à `IS_KNOWN_WINDOWS_BUILD()`) → `LoaderMessage(mtDebuggerFound)` inconditionnel. Corrigé par `--realistic-os-version` (`PEB+0xAC=19041`, `WINDOWS_10_20H1`).
- Deux instructions `SYSENTER` natives non interceptées (stub d'import factice `\xC3` bare-RET) plutôt qu'un vrai numéro de syscall (SSN) résolu et dispatché.
- `HeapCreate`, `GetModuleHandleW`/`LoadLibraryW`, `TlsAlloc`/`TlsSetValue`/`TlsFree`, `EncodePointer`/`DecodePointer`, `Interlocked*`, `GetCurrentThreadId`, `HeapDestroy`, absents ou mal typés dans les tables d'arité du harnais, chacun bloquant silencieusement la phase bootstrap CRT/VMProtect à un stade différent. Corrigés un par un, par ordre d'apparition du blocage suivant.

Commande finale (échantillon CAPCOM) :
```bash
python3 tools/vmp_trace.py binaries/9416e5a57e6de00c685560fa9fee761126569d123f62060792bf2049ebba4151.exe \
  --wide-api-sweep --real-seh --realistic-os-version --budget 2000000000
```
Volontairement **sans** `--survive-exitprocess` : ce flag neutralise le premier `ExitProcess` rencontré pour "voir au-delà", utile pour explorer, mais il masque la vraie fin naturelle du programme dès qu'elle est atteinte. S'en passer laisse le harnais s'arrêter proprement dessus (confirmé identique, à l'instruction près, entre les deux modes une fois ce point atteint).

Résultat : arrêt contrôlé sur `ExitProcess(0)` : **1 751 245 135 instructions exécutées, 3075 appels d'API interceptés, aucun crash.** Séquence complète en §3 du writeup.

Deux bugs de harnais supplémentaires, trouvés en creusant les deux threads secondaires (`CreateThread`, §4 du writeup) :
- `lstrcatW`/`lstrcatA`/`lstrcpyW`/`lstrcpyA`/`wsprintfW`/`wsprintfA` absentes des tables d'arité (comportement par défaut = aucune écriture mémoire réelle), tronquaient silencieusement `lpApplicationName`/`lpCommandLine` du second `CreateProcessAsUserW` (celui qui relance `notepad.exe` sur la note de rançon). Corrigées avec de vraies implémentations (concaténation/copie fidèle à l'encodage W/A, parsing minimal de format `%s`/`%c`/`%d`/`%x`/`%X`).
- Les threads imbriqués s'exécutaient de façon synchrone en consommant tout le budget restant : un watchdog de surveillance de process (boucle `CreateToolhelp32Snapshot`/`Process32First`/`NextW`, jamais vu avant ce correctif) épuisait ainsi la totalité du budget sans jamais rendre la main au thread principal. Corrigé par un plafond dédié (`--nested-thread-cap`, défaut 20 000 000 instructions), appliqué via un vrai fetch invalide plutôt qu'une exception Python : une exception levée dans un hook imbriqué fuyait vers l'appel `emu_start()` englobant à cause d'un attribut d'état partagé (`_hook_exception`) jamais réinitialisé dans les bindings Python d'Unicorn 2.1.4 (voir `run_nested_thread()` dans `tools/vmp_trace.py`).

Après ces deux correctifs, le run ci-dessus révèle son vrai contenu : la commande exacte du second `CreateProcessAsUserW` (`notepad.exe` + chemin de la note de rançon `!!!_READ_ME_<ID hexadécimal 8 chiffres>_!!!.txt`) ; détail complet en §3.7 du writeup.
