Runbook : reproduction pas à pas (Clop, MT19937/GetTickCount)
A. Acquisition#
h=389e03b1a1fd1c527d48df74d3c26a0483a5b105f36841193172f1ee80e62c1b
curl -s -X POST https://mb-api.abuse.ch/api/v1/ -H "Auth-Key: $(cat .mb_api_key)" \
-d "query=get_file&sha256_hash=$h" -o sample.zip
7z x -pinfected sample.zip
sha256sum $h.exe
file $h.exe
Retour : PE32 executable for MS Windows 4.00 (GUI), Intel i386, 3 sections, hash conforme.
B. Triage import PE : pourquoi ce n'est pas le payload#
python3 -c "
import pefile
pe = pefile.PE('$h.exe', fast_load=True); pe.parse_data_directories()
for e in pe.DIRECTORY_ENTRY_IMPORT:
print(e.dll.decode(), [i.name.decode() if i.name else i.ordinal for i in e.imports])
"
Retour : aucun import CryptoAPI (ADVAPI32.dll/CRYPT32.dll absents), GetTickCount seule fonction temporelle/aléa présente. Indice que la génération de clé, si elle existe, est artisanale et encore chiffrée/compressée à ce stade.
C. Décompilation Ghidra headless (PyGhidra)#
pip install /opt/ghidra/Ghidra/Features/PyGhidra/pypkg/dist/pyghidra-3.1.0-py3-none-any.whl
python3 - <<'EOF'
import pyghidra
pyghidra.start(install_dir="/opt/ghidra")
from ghidra.app.decompiler import DecompInterface
from ghidra.util.task import ConsoleTaskMonitor
with pyghidra.open_program("389e03b1....exe", analyze=True) as flat_api:
# xref sur GetTickCount, decompilation des fonctions appelantes
...
EOF
Retour : FUN_00401000 (appelée depuis main) fait VirtualAlloc + une boucle de décodage XOR/ROL9/soustraction d'un blob embarqué (&DAT_0040831c, 0x564 DWORD, clé DAT_00408318), puis saute dedans. GetTickCount ici n'alimente rien : anti-sandbox isolé (voir clop-mt19937-killchain.md §2).
D. Rejouer le décodage de l'étage 0, identifier l'étage 1#
python3 - <<'EOF'
import pefile, struct
# lire DAT_00408318 (cle) et &DAT_0040831c (blob), appliquer
# ((in^cle) rol 9 ^cle) - index sur 0x564 DWORD, ecrire stage2_decoded.bin
EOF
r2 -a x86 -b 32 -q -c "pd 60" -n stage2_decoded.bin
Retour : code x86 valide (pas du bruit), construction de chaînes sur la pile pour résoudre dynamiquement GetProcAddress/VirtualAlloc/VirtualProtect/LoadLibraryA/VirtualFree/VirtualQuery/TerminateThread. Confirme un reflective PE loader.
E. Décompilation de l'étage 1, découverte du second blob et de la décompression aPLib#
Décompiler stage2_decoded.bin (même méthode PyGhidra que C, base 0, langage x86:LE:32:default) donne la fonction principale complète : lecture d'un contexte (hKernel32, pointeur vers un blob à 0x004098b0, taille 0x19ed8, clé XOR à 0x004098ac, taille destination à 0x00423788), dé-entrelacement (saute 2 octets sur 3), même transformation XOR/ROL9/soustraction, puis un appel à une fonction dont la structure (lecteur de bits MSB, décodeur de longueur gamma, seuils de distance 31999/0x4ff/0x80) est celle de l'algorithme aPLib publié.
Portage direct en Python (tools/aplib_depack.py), puis pipeline complet (tools/unpack_clop.py) :
python3 tools/unpack_clop.py 389e03b1....exe -o clop_unpacked.bin
Retour :
132096 octets ecrits dans clop_unpacked.bin
MZ ? True
SHA256 : f59e2dde0fc19129329b3e950e27f4107536c5538c6413e7004c653c3b342941
F. Confirmation indépendante du déballage#
curl -s -X POST https://mb-api.abuse.ch/api/v1/ -H "Auth-Key: $(cat .mb_api_key)" \
-d "query=get_info&hash=389e03b1a1fd1c527d48df74d3c26a0483a5b105f36841193172f1ee80e62c1b"
Champ vendor_intel.UnpacMe : deux hashs listés, dont f59e2dde0fc19129329b3e950e27f4107536c5538c6413e7004c653c3b342941, identique au résultat de l'étape E obtenu indépendamment. vendor_intel.ReversingLabs : Win32.Ransomware.ClopCrypt, MALICIOUS, 24/29 (82,76 %).
G. Confirmation MT19937 sur le payload déballé#
python3 -c "
import struct
data = open('clop_unpacked.bin','rb').read()
for name, val in [('matrix_A',0x9908b0df), ('init_mult',0x6c078965)]:
print(name, data.count(struct.pack('<I', val)))
"
Retour : matrix_A x1, init_mult x5. Décompilation de FUN_00403940/FUN_00403840 (mêmes outils qu'en C) : init_genrand/genrand_int32 standard, sans variation. FUN_00402b20 : seed GetTickCount() (une fois par valeur de tick distincte), tirage genrand_int32() % range + min. FUN_00402d10 : 117 tirages FUN_00402b20(0, 0x100), indexation d'une table de substitution, repli sur une clé statique si les premiers octets sont nuls (voir clop-mt19937-killchain.md §3/§5).
H. Confirmation RC4 + enveloppe RSA#
Décompilation de FUN_004032f0 (KSA), FUN_00403270 (PRGA) : RC4 standard sans variante. FUN_00401420 : CryptEncrypt sur un handle RSA importé via CryptImportPublicKeyInfo/CryptDecodeObjectEx, la clé de 117 octets n'est jamais transmise à la CryptoAPI pour être générée, seulement pour être enveloppée après coup (voir clop-mt19937-killchain.md §4).
I. PoC de récupération par brute-force#
python3 tools/mt19937_key_poc.py
Retour : simulation d'un seed GetTickCount(), chiffrement d'un texte de test avec la clé réellement produite pour ce seed, brute-force sur une fenêtre de 100 001 candidats (20,15 s, ~4 960 candidats/s en Python pur mono-cœur), seed et fichier en clair retrouvés à l'identique. Détail et mise en garde sur la portée du PoC : voir clop-mt19937-killchain.md §8 et §9.
J. YARA#
Règles dans clop_mt19937.yar : IOC sur les deux hashs (stub + payload déballé) et signature structurelle sur les constantes MT19937 + la taille de clé 0x75, validées sur l'échantillon de ce dossier.
K. Validation bit-exacte via Unicorn (vrai code machine, pas la transcription)#
python3 tools/validate_mt19937_unicorn.py clop_unpacked.bin
Exécute réellement FUN_00403940/FUN_00403840/FUN_00402b20/FUN_004032f0/FUN_00403270 du payload déballé (PE32 i386, cdecl), avec uniquement GetTickCount hooké (patch du slot IAT vers un stub ret dont Unicorn fixe la valeur de retour). Retour :
6/6 etats internes (624 mots) bit-exact.
2600/2600 tirages 32 bits bit-exact (650 tirages/seed, dont la torsion #2).
appel 1/2/3 (cache de seed sur tick) : OK
8/8 paires KSA+PRGA bit-exact.
TOUT CONFIRME bit-exact contre le vrai code machine du payload deballe.
Détail et portée de cette validation : voir clop-mt19937-killchain.md §7.