RagnarLocker sous VMProtect 3.5.1 : tout le processus rejoué par émulation
Catégorie : Ransomware
Analyste : Gordon PEIRS (@ozer0x777)
Famille : RagnarLocker, build protégé VMProtect 3.5.1 (campagne Capcom, 2020).
Méthode : émulation CPU (Unicorn Engine + Triton) sur un harnais maison écrit pour cette analyse. Pas de machine virtuelle, pas de bac à sable, et l'échantillon n'est jamais exécuté pour de vrai. Chaque appel d'API Windows est intercepté et simulé par du Python écrit à la main. Le harnais ne connaît le comportement de chaque API que par sa documentation publique et par recoupement avec des traces d'exécution tierces (voir §5).
Résumé
L'échantillon (9416e5a5..., campagne dite « Capcom », 2020-10-20) est un build de RagnarLocker protégé par VMProtect 3.5.1. Il pèse 5,9 Mo et l'essentiel de son code se trouve, virtualisé ou chiffré, dans deux sections .ctr0/.ctr1 (les .vmp0/.vmp1 par défaut, renommées). Avec ce genre de protection, l'analyse statique classique (Ghidra ou IDA sur le fichier disque) ne donne presque rien : le vrai code n'existe en clair qu'en mémoire, pendant l'exécution, et seulement pour les portions réellement atteintes.
J'ai donc construit un harnais d'émulation CPU (Unicorn Engine pour exécuter le x86 instruction par instruction, Triton pour l'analyse symbolique) qui rejoue le binaire sans jamais le lancer. Chaque appel Windows (allocation mémoire, accès disque, registre, création de processus...) est intercepté au niveau de l'IAT et simulé en Python, sans toucher au système hôte. Il a fallu régler plusieurs détails de fidélité : exposer une version d'OS réaliste dans le PEB, gérer correctement les exceptions structurées x86, retrouver l'arité exacte de quelques appels système peu documentés. Une fois ces points réglés, le harnais rejoue tout le flux d'exécution du processus, de son point d'entrée à sa terminaison propre (ExitProcess(0), code de sortie 0) : 1 751 245 135 instructions x86 et 3075 appels d'API interceptés, sans crash ni arrêt anormal.
Ce flux confirme en dynamique, sur ce build protégé, le comportement documenté publiquement pour RagnarLocker : suppression des clichés instantanés, désactivation de la récupération au démarrage, relance dans la session utilisateur interactive. Je l'ai recoupé avec un rapport de sandbox public qui porte sur le hash exact de cet échantillon (§5) : il confirme de son côté que ce flux correspond bien à une exécution réelle sous Windows.
1. Échantillon
| SHA256 | Soumission publique | Taille | Protection |
|---|---|---|---|
9416e5a57e6de00c685560fa9fee761126569d123f62060792bf2049ebba4151 |
2020-11-26 (rapport Triage, exécution réelle antérieure) | 5,9 Mo | VMProtect 3.5.1, sections .ctr0/.ctr1 |
Le renommage en .ctr0/.ctr1 (au lieu de .vmp0/.vmp1) ne sert qu'à échapper aux signatures basées sur le nom de section. Il ne change rien à la protection, qui reste du VMProtect standard.
2. Méthodologie : pourquoi l'émulation plutôt que l'exécution réelle
Exécuter un vrai ransomware, même dans une VM isolée, c'est accepter un risque de fuite qu'aucune isolation logicielle n'élimine à 100 % : évasion de VM, exfiltration réseau avant la coupure, erreur de manipulation. Je me suis donc fixé une règle sans exception : l'échantillon n'est jamais exécuté pour de vrai, ni en VM ni en conteneur. Il est seulement rejoué instruction par instruction dans un émulateur CPU logiciel (Unicorn Engine), sans accès au système d'exploitation hôte.
En pratique, ça oblige à réimplémenter à la main chaque fonction Windows appelée par le programme (allocation mémoire, accès fichier, énumération de lecteurs, gestion de jetons, création de processus...) au lieu de laisser un vrai Windows s'en charger. C'est plus long à mettre en place qu'un bac à sable, mais le risque d'exécution réelle disparaît. Et on y gagne une vue complète de l'état interne (registres, mémoire, pile) à chaque instruction, ce qu'aucun rapport de sandbox public ne donne.
Pour que l'échantillon passe ses propres vérifications d'environnement, le harnais doit être assez fidèle. Il expose notamment une version de Windows 10 réaliste dans le PEB, une vraie gestion des exceptions structurées x86 (SEH) et pas une simple journalisation passive, et une table d'imports large (plusieurs milliers de fonctions résolues, kernel32/ntdll) pour ne jamais bloquer sur un appel imprévu.
3. Comportement complet capturé, de bout en bout
Voici la séquence observée, du point d'entrée du processus à sa terminaison :
- Vérification d'environnement. Le programme regarde s'il tourne dans une station de fenêtrage visible, c'est-à-dire un poste de travail interactif et non un service Windows sans interface. Sur un poste normal, ce contrôle passe sans rien changer à la suite.
- Écriture d'un fichier de configuration/log local (
CreateFileW/WriteFile, contenu encodé en base64 viaCryptBinaryToStringA), puis copie vers un second emplacement. - Énumération des lecteurs locaux (
GetLogicalDrives/GetVolumeInformationW/GetDriveTypeW), pour préparer le ciblage des volumes. - Manipulation de jeton d'accès (
OpenProcessToken/DuplicateTokenEx/SetTokenInformation) combinée àWTSGetActiveConsoleSessionId. C'est la technique connue pour relancer le processus dans la session utilisateur interactive active plutôt que dans le contexte, souvent restreint, du processus initial. - Relance du processus dans cette session utilisateur (
CreateProcessAsUserW). Le harnais capture la ligne de commande en clair. - Suppression des sauvegardes du système, avec ces commandes shell construites puis passées à
CreateProcessAsUserW: -wmic.exe shadowcopy delete-bcdedit /set {default} recoveryenabled No-bcdedit /set {default} bootstatuspolicy IgnoreAllFailures-bcdedit /set {globalsettings} advancedoptions false - Ouverture automatique de la note de rançon : un second appel
CreateProcessAsUserWlanceC:\Windows\System32\notepad.exesur un fichier nommé\!!!_READ_ME_<ID hexadécimal 8 chiffres>_!!!.txt. C'est le motif de nommage déjà relevé par le rapport de sandbox public sur ce même hash (§5). Le processus affiche donc lui-même la demande de rançon à la victime, sans intervention de l'opérateur. - Terminaison propre du processus parent (
ExitProcess(0), code de sortie 0, donc un succès).
Aucune de ces étapes n'a demandé de contournement ni de simulation partielle. Chaque appel a renvoyé un résultat cohérent avec celui d'un vrai Windows, et le processus s'est arrêté tout seul, normalement, sans planter.
Les commandes wmic/bcdedit ci-dessus sont exactement la technique d'anti-récupération documentée pour cette famille : suppression des clichés instantanés et neutralisation de la réparation au démarrage. Ici, on la voit au niveau de l'appel système et de sa ligne de commande complète, pas par simple déduction.
4. Deux threads secondaires, dont un watchdog de surveillance de processus
Le processus lance deux threads (CreateThread) tôt dans son exécution. Le harnais les rejoue tous les deux, chacun avec sa pile et son contexte de registres.
Le premier se termine après quelques dizaines de milliers d'instructions sur un accès mémoire invalide, sans rien faire d'observable. Le second est plus intéressant. Il énumère en boucle tous les processus actifs (CreateToolhelp32Snapshot/Process32First/NextW), compare le nom de chacun à une valeur de référence embarquée dans le binaire, attend dix secondes et recommence. C'est un watchdog classique, qui vérifie la présence d'un processus cible ou l'unicité de l'instance. Dans l'environnement synthétique du harnais, il ne trouve jamais de correspondance et la boucle ne s'arrête donc jamais d'elle-même. Je la borne par un plafond d'instructions pour laisser le thread principal avancer, ce qui reste fidèle à la réalité : sur un vrai Windows, ce watchdog tournerait en tâche de fond sans bloquer le reste du processus. Aucun des deux threads n'a d'effet observable sur la séquence du thread principal décrite au §3.
5. Recoupement avec une exécution réelle indépendante
Il existe un rapport de sandbox public pour le SHA256 exact de cet échantillon (Triage, exécution réelle sous Windows, score de malveillance maximal). Ce rapport, obtenu sans aucun lien avec mon harnais, décrit les mêmes actions : écriture du MBR, bcdedit, suppression des clichés instantanés, dépôt d'une note de rançon. Quand une exécution réelle faite par un tiers et un rejeu par émulation convergent sur le même binaire, c'est la meilleure validation qu'on puisse avoir sans exécuter l'échantillon soi-même.
6. Constantes cryptographiques présentes dans le code natif
Une partie du code de ce build n'est pas virtualisée par VMProtect et reste en clair dans le fichier. On y retrouve les primitives cryptographiques déjà documentées sur des échantillons RagnarLocker non protégés de la même année (voir le writeup compagnon « RagnarLocker : un seul flux Salsa20 pour tous les fichiers ») :
- Une clé publique RSA-2048 des attaquants (PEM standard, exposant
65537), dont j'ai vérifié la validité cryptographique. - Le cœur de Salsa20 (10 doubles-tours, constantes de rotation 7/9/13/18 conformes à la spécification), de même structure que sur les builds non protégés.
- Une routine d'initialisation BLAKE2b (constantes IV standard), cohérente avec le mécanisme de dérivation de clé déjà documenté.
La couche cryptographique de RagnarLocker n'a donc pas changé entre les builds non protégés et celui-ci. VMProtect a simplement été ajouté par-dessus le même code applicatif.
7. Portée et limites
L'analyse couvre le processus tracé de bout en bout, jusqu'à sa terminaison normale. Le seul appel CreateProcessAsUserW observé lance notepad.exe pour afficher la note de rançon (§3.7) ; ce flux ne relance pas de seconde instance du ransomware. Je n'ai donc pas observé en dynamique la boucle de chiffrement de masse des fichiers. Elle se trouve très probablement dans le bytecode virtualisé par VMProtect (.ctr0/.ctr1), encore presque entièrement chiffré quand l'exécution tracée se termine. Autrement dit, c'est un chemin de code que cette exécution n'a pas emprunté, pas un processus séparé que j'aurais oublié d'émuler.
Complément statique, mené à part et maintenant terminé. J'ai rétro-ingénié en entier le mécanisme de chiffrement du bytecode VM et je l'ai vérifié au bit près sur les instances de VM présentes dans le binaire. Ça m'a permis de reconstruire en assembleur x86 lisible douze blocs de code virtualisé (sept séquences métier et cinq occurrences d'un même mécanisme d'appel générique), sans exécuter le programme pour cette partie non plus. Ce résultat confirme et complète ce qui précède : les fonctions cryptographiques natives du §6 (clé publique RSA-2048, cœur Salsa20, initialisation BLAKE2b) n'ont jamais été virtualisées. Elles vivent en code natif clair, à des adresses fixes, et les portions protégées les appellent par le mécanisme d'appel indirect reconstruit dans les cinq blocs génériques. C'est cohérent avec la façon dont VMProtect compile un appel de fonction virtualisé vers une cible externe.
En revanche, je n'ai pas localisé la boucle de chiffrement de masse, ni dans le flux dynamique tracé ici ni dans les douze blocs reconstruits en statique. Il faut être clair sur les limites du volet statique : le code reconstruit est une séquence plate d'instructions, sans vrai branchement conditionnel. J'ai testé une à une plus de 1400 unités de code sans trouver la moindre logique de comparaison locale dans ces douze blocs. Je n'y vois pas une limite de la méthode. Ça indique plutôt que la logique de décision de RagnarLocker, si elle existe sous forme virtualisée, n'est pas dans les portions de ce binaire que l'une ou l'autre approche permet d'atteindre.
8. Indicateurs de compromission
| SHA256 | Notes |
|---|---|
9416e5a57e6de00c685560fa9fee761126569d123f62060792bf2049ebba4151 |
RagnarLocker, VMProtect 3.5.1, campagne Capcom (2020-10-20) |
Commandes shell observées (technique d'anti-récupération) : wmic.exe shadowcopy delete, bcdedit /set {default} recoveryenabled No, bcdedit /set {default} bootstatuspolicy IgnoreAllFailures, bcdedit /set {globalsettings} advancedoptions false.
Note de rançon : nommage !!!_READ_ME_<ID hexadécimal 8 chiffres>_!!!.txt, ouverte automatiquement via notepad.exe.
9. Reproduire l'analyse
Le harnais d'émulation et les scripts d'analyse sont dans tools/. Pour le détail de l'algorithme de chiffrement de fichiers, voir le writeup compagnon sur les builds non protégés.