# Cl0p : deux étages de crypter, puis un MT19937

**Catégorie :** Ransomware

**Analyste :** Gordon PEIRS

**Famille :** Clop / Cl0p (TA505). L'échantillon analysé correspond à la vague de builds signés d'un faux certificat "Insta Software Solution Inc." documentée par plusieurs éditeurs entre fin 2019 et 2021.

**Méthode :** reverse statique (Ghidra headless, décompilation, réimplémentation Python du pipeline de déballage), puis validation au bit près en exécutant le vrai code machine en isolation (Unicorn Engine). Le sample n'a jamais été lancé en entier.

---

## Résumé

Plusieurs sources publiques disent que les premiers builds Windows de Clop tiraient leur clé RC4 par fichier d'un Mersenne Twister (MT19937), mais aucune ne cite de hash précis ni ne montre l'implémentation. J'ai donc pris un échantillon réel, déroulé son crypter deux étages jusqu'au vrai payload, et vérifié par décompilation puis en exécutant le code machine (Unicorn) que la clé RC4 de 117 octets sort bien d'un MT19937 seedé par `GetTickCount()`. Ce compteur donne les millisecondes écoulées depuis le démarrage de Windows : un entier de 32 bits, prévisible dans une fenêtre de quelques secondes autour de l'heure de l'infection. Un brute-force limité à cette fenêtre retrouve le seed et le fichier en clair en quelques secondes à quelques minutes, selon sa largeur.

Au passage, deux détails d'implémentation que je n'ai vus documentés nulle part : un repli sur une clé statique codée en dur dans de rares cas, et un off-by-one dans la table de substitution qui sert à construire la clé.

> Tout est reproductible depuis [`runbook.md`](writeups/clop-mt19937-killchain/runbook.md). Les scripts sont dans [`tools/`](tools.html?slug=clop-mt19937-killchain).

---

## 1. Identité de l'échantillon

| Champ | Valeur |
|---|---|
| SHA256 (fichier sur disque) | `389e03b1a1fd1c527d48df74d3c26a0483a5b105f36841193172f1ee80e62c1b` |
| Taille | 182 344 octets |
| Type | PE32, Intel i386, GUI, 3 sections (`.text`, `.data`, `.reloc`) |
| Signature | Certificat "Insta Software Solution Inc." (compromis/usurpé) |
| MalwareBazaar | tags `Clop`, `Ransomware`, `signed` ; première soumission 2020-11-24 |
| ReversingLabs | `Win32.Ransomware.ClopCrypt`, MALICIOUS, 24/29 moteurs (82,76 %) |

La table d'imports du fichier sur disque ne contient aucune fonction CryptoAPI (ni `ADVAPI32.dll` ni `CRYPT32.dll`), et la seule fonction importée liée au temps ou à l'aléa est `GetTickCount`. Premier indice : la génération de clé est artisanale, et elle se trouve dans du code encore chiffré ou compressé à ce stade.

## 2. Chaîne de déballage : deux étages avant le vrai payload

Le fichier sur disque n'est pas le ransomware, c'est un crypter/loader. J'ai déroulé ses deux étages et rejoué le tout en Python ([`tools/unpack_clop.py`](writeups/clop-mt19937-killchain/tools/unpack_clop.py)) :

- **Étage 0 (stub sur disque)** : il alloue un buffer exécutable, y décode un blob embarqué avec la transformation `((in ^ clé) rol 9 ^ clé) - index`, puis saute dedans. L'appel à `GetTickCount` juste avant est un simple contrôle anti-sandbox, sans rapport avec la clé de chiffrement.
- **Étage 1 (shellcode décodé, 5520 octets)** : un reflective PE loader complet. Il résout ses API par nom (les chaînes sont construites octet par octet sur la pile), décode un second blob de 106 200 octets avec la même transformation, le décompresse en aPLib (du LZ77, que j'ai réimplémenté depuis le décompilé), puis mappe le résultat à la main comme un PE (en-têtes, sections, relocations, imports) avant de sauter sur son point d'entrée.

Au bout du pipeline, on obtient un PE32 valide de 132 096 octets, SHA256 `f59e2dde0fc19129329b3e950e27f4107536c5538c6413e7004c653c3b342941`. Ses imports ne laissent pas de doute, c'est bien le ransomware : `CryptEncrypt`/`CryptAcquireContextW`/`CryptImportPublicKeyInfo`/`CryptDecodeObjectEx` (RSA), `RmGetList`/`RmStartSession`/`RmShutdown`/`RmRestart`/`RmEndSession` (Restart Manager), `WTSEnumerateSessionsW`/`WTSQueryUserToken`/`CreateProcessAsUserW` (ciblage multi-session), `StartServiceCtrlDispatcherW`/`SetServiceStatus` (exécutable comme service Windows), `FindFirstFileW`/`FindNextFileW`, `GetTickCount`.

**Recoupement indépendant** : la fiche MalwareBazaar du hash renvoie vers une analyse UnpacMe (un service tiers, sans lien avec ce travail) dont le résultat de déballage porte exactement le même SHA256 que celui que j'obtiens en reversant le pipeline à la main.

## 3. Génération de la clé : MT19937 confirmé, seedé par GetTickCount()

Sur le payload déballé, je cherche les constantes propres à MT19937 :

| Constante | Rôle | Présence |
|---|---|---|
| `0x9908b0df` | matrice de torsion ("matrix A") | 1 occurrence |
| `0x6c078965` | multiplicateur d'initialisation (1812433253) | 5 occurrences |

La décompilation montre une implémentation complète et standard, sans aucune modification :

- `init_genrand(seed)` : `mt[0]=seed ; mt[i] = 1812433253*(mt[i-1]^(mt[i-1]>>30)) + i` pour i=1..623, au bit près la référence publique de Matsumoto/Nishimura.
- `genrand_int32()` : la torsion standard sur 624 mots (M=397) et le tempering complet.
- Une fonction de tirage borné : elle appelle `GetTickCount()`, ne re-seede que si la valeur a changé depuis le dernier appel (elle est gardée dans une variable globale), tire `genrand_int32()` et retourne `tirage % (max-min+1) + min`.

Et voici comment la routine de chiffrement de fichier construit la clé :

```c
puVar5 = VirtualAlloc(NULL, 0x75, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);  // 0x75 = 117 octets
memset(puVar5, 0, 0x75);
for (i = 0; i < 0x75; i++) {
    tirage = tirage_borne(0, 0x100);       // MT19937, seed = GetTickCount()
    key[i] = LOOKUP_TABLE[tirage];          // table de substitution
}
if (key[0]==0 && key[1]==0 && key[2]==0 && key[3]==0 && key[5]==0) {
    memcpy(key, FALLBACK_KEY, 0x75);        // repli sur une cle STATIQUE codee en dur
}
```

117 octets : ça colle exactement avec la « 0x75-byte RC4 key » des sources publiques, sauf que cette fois on a le code sous les yeux.

## 4. RC4 standard, enveloppe RSA via la vraie CryptoAPI

Le RC4 (KSA/PRGA) est standard : pas de variante de la boîte S ni du flux. La clé de 117 octets est donc fabriquée entièrement à la main, la CryptoAPI ne la génère jamais. Elle n'intervient qu'après, pour envelopper cette clé avec la clé publique RSA du binaire (`CryptEncrypt` sur un handle importé via `CryptImportPublicKeyInfo`/`CryptDecodeObjectEx`). C'est cohérent avec ce qui est décrit publiquement (« RSA protège la clé RC4 ») : la CryptoAPI sert au transport, pas à la génération.

## 5. Deux détails d'implémentation non documentés ailleurs

- **Repli sur clé statique** : si les 5 premiers octets tirés valent tous zéro (probabilité de l'ordre de 2⁻⁴⁰, donc rarissime mais pas nulle), le code écrase la clé aléatoire par une constante codée en dur dans le binaire. Dans ce cas, pas besoin de brute-force : la clé se lit directement dans l'échantillon.
- **Off-by-one dans la table de substitution** : l'appel réel demande un tirage entre 0 et 0x100 inclus, soit un modulo de 257 et non 256. Le tirage peut donc valoir 256 et indexer l'octet situé juste après la table de 256 entrées. Le binaire se comporte bien ainsi, et le PoC le reproduit tel quel.

## 6. Nuance pour une récupération réelle : état MT19937 global, pas par fichier

La routine de chiffrement est appelée depuis 3 sites distincts (chaque fichier est traité par son propre thread), mais l'état MT19937 (624 mots) et le cache du dernier `GetTickCount()` sont des variables globales, partagées entre tous ces threads. Il n'y a donc qu'un seul flux de tirages MT19937 pour tout le processus, il n'est pas réinitialisé à chaque fichier. Pour retrouver la clé d'un fichier donné, il faut le seed initial plus un décalage borné (un multiple de 117 tirages) qui dépend du nombre de fichiers traités avant lui. C'est un petit paramètre de plus à chercher, pas un second brute-force.

## 7. Validation bit-exacte contre le vrai code machine (Unicorn)

Un décompilé recopié à la main, ça peut contenir une erreur. Pour écarter ce risque, j'ai exécuté les fonctions du payload déballé telles quelles dans Unicorn (PE32 i386, convention cdecl), sans les réécrire, en hookant uniquement `GetTickCount()` :

| Test | Résultat |
|---|---|
| État interne complet (624 mots) après initialisation, 6 seeds | 6/6 identique |
| Sortie de tirage, 650 tirages/seed (franchit la torsion à 624), 4 seeds | 2 600/2 600 identique |
| Cache de seed sur tick identique (pas de re-seed) puis tick différent (re-seed) | identique |
| RC4 KSA + PRGA, 8 combinaisons clé/longueur aléatoires | 8/8 identique |

Ce qui est comparé à la réimplémentation Python, c'est la sortie du code machine du binaire, exécuté pour de bon. Pas un décompilé que j'aurais jugé correct à la lecture.

## 8. Preuve de concept : récupération du seed par brute-force

Le PoC réimplémente fidèlement MT19937, la construction de clé et le RC4 standard, avec les constantes extraites du binaire. Je n'ai pas de fichier chiffré par cet échantillon, donc le PoC simule un seed `GetTickCount()`, chiffre un texte de test avec la clé que produirait le binaire, puis lance le brute-force sur une fenêtre resserrée autour de la vraie valeur.

Mesure : **100 001 seeds candidats testés en 20,15 s** en Python pur sur un seul cœur, soit environ 4 960 candidats par seconde, avec le seed exact et le fichier en clair à l'arrivée. Sur l'espace complet des 32 bits de `GetTickCount()`, il faudrait de l'ordre de 10 jours dans les mêmes conditions. On tombe à quelques minutes en parallélisant (multiprocessing ou GPU), et à quelques secondes dès qu'on peut borner l'heure d'infection à une fenêtre réaliste.

## 9. Ce qui n'a pas été résolu

- Je n'ai pas vérifié le format exact du fichier `.Cllp` (où et comment la clé enveloppée en RSA est écrite dans le fichier de sortie) sur une exécution complète du processus, puisque je n'ai produit aucun fichier chiffré par ce binaire.
- Je n'ai pas observé en pratique l'entrelacement des 3 threads de chiffrement quand plusieurs fichiers sont traités en même temps (l'analyse statique de ce point est au §6).
- Le PoC montre le principe et la vitesse de récupération sur des données synthétiques. Ce n'est pas un décrypteur clé en main validé sur un vrai fichier.

## 10. Indicateurs de compromission

| Type | Valeur |
|---|---|
| SHA256 (stub sur disque) | `389e03b1a1fd1c527d48df74d3c26a0483a5b105f36841193172f1ee80e62c1b` |
| SHA256 (payload déballé) | `f59e2dde0fc19129329b3e950e27f4107536c5538c6413e7004c653c3b342941` |
| Certificat de signature | "Insta Software Solution Inc." (compromis/usurpé) |
| Détection ReversingLabs | `Win32.Ransomware.ClopCrypt` |
| Marqueur de fichier chiffré | `Cllp^_-` (footer) |

---

L'échantillon étudié est public (MalwareBazaar, soumis en 2020) et la famille est active depuis 2019. Rien ici ne révèle une vulnérabilité inconnue dans un logiciel légitime. Ce que j'apporte, code machine à l'appui, c'est le détail d'une faiblesse cryptographique jusqu'ici évoquée en une ligne dans les sources publiques : la clé de chiffrement dépend d'un compteur système prévisible. Le but est de donner aux équipes de réponse à incident qui tombent sur ce build (ou sur une variante proche, avec les mêmes constantes MT19937/RC4) un point de départ concret pour évaluer la récupération de fichiers.
