InterLock : une clé qui dépend d'un seed de 32 bits

Catégorie : Ransomware

Analyste : Gordon PEIRS

Famille : InterLock, groupe de ransomware actif, cité dans l'alerte gouvernementale conjointe CISA/FBI AA25-203A (2025-07-22).

Méthode : reverse statique (Ghidra headless via PyGhidra) et émulation ciblée (Unicorn Engine, CPU seul : chaque appel système ou API est intercepté et simulé à la main, jamais transmis au système de la machine d'analyse). Aucun échantillon, Windows ou FreeBSD, n'a été exécuté en entier dans un environnement réel.


Résumé

InterLock dérive sa clé de chiffrement par fichier (AES-256, enveloppée en RSA-4096) d'un srand() de type MSVCRT initialisé une seule fois par processus, à partir de sources à faible entropie : clock(), un compte de processus et, sur les builds les plus soignés, un vrai CSPRNG dilué dans le mélange. Mais peu importe la qualité de l'entropie en entrée, srand() ne prend qu'un entier de 32 bits. Tout le flux rand() qui suit est un générateur congruentiel linéaire (LCG) public et déterministe. L'espace de recherche (2^32) se brute-force en un temps raisonnable, même sur un seul cœur CPU (voir la preuve de concept plus bas).

J'ai confirmé ce mécanisme en décompilant toute la chaîne de chiffrement (génération de clé, AES-256-CBC, enveloppe RSA, format exact du footer) sur 7 builds distincts, répartis sur 3 architectures ou plateformes (x86 Windows, x64 Windows, x86-64 FreeBSD), compilés entre octobre 2024 et mars 2025. Il n'y a aucune exception et aucun signe de correction sur cette période. Au-delà de mars 2025, je n'ai rien pu confirmer : les échantillons plus récents disponibles se sont révélés être un autre outil (voir plus bas), pas l'encrypteur.

Tout est reproductible depuis runbook.md. Les scripts sont dans tools/.


1. Le mécanisme de la faille

1.1 Génération de clé, formule exacte

Le générateur est initialisé une seule fois par processus :

(*rand_s)(seed_fort);                    // vrai CSPRNG (GetProcAddress "rand_s", repli RtlGenRandom)
uVar1 = clock();
iVar2 = nombre_de_processus_courants();  // K32EnumProcesses() sur Windows, /proc sur FreeBSD
cVar3 = clock();
srand(cVar3 * iVar2 ^ uVar1 ^ seed_fort[0]);

Sur les builds x86-64 les plus simples (le portage FreeBSD notamment), la formule se réduit à trois sources, sans CSPRNG intermédiaire :

srand((uint)time(NULL) ^ (uint)clock() ^ nombre_de_processus_courants());

Le résultat est le même dans les deux cas : srand() ramène une entrée d'entropie moyenne ou élevée à un espace de recherche de 2^32.

La clé de chaque fichier est générée dans un buffer de 48 octets exactement (32 pour la clé AES-256, 16 pour l'IV) :

uVar4 = clock();                                        // capture unique pour ce fichier
for (i = 0; i < 12; i++) cle[i] = rand();                // 1re passe : 12 tirages bruts
for (i = 0; i < 12; i++) cle[i] += (rand() ^ uVar4 ^ taille_fichier);  // 2e passe : 12 tirages de plus

Soit 24 appels à rand() par fichier, mélangés avec un seul clock() et la taille du fichier. La formule est la même, à la variable près, sur les 7 builds décrits en section 2.

1.2 Chiffrement du contenu

find_cipher("aes");
cbc_start(cipher, IV, cle, 0x20, 0, &ctx);   // 0x20 = 32 -> AES-256, pas AES-128
cbc_setiv(IV, 0x10, &ctx);                   // par bloc de 1 Mo lu/écrit
cbc_encrypt(clair, chiffre, longueur, &ctx);

L'API est celle de LibTomCrypt. Trois éléments le confirment : les signatures d'appel, des chaînes de debug internes authentiques ("AES Encrypt", "AES Decrypt", "rijndael", "chc_hash"), et l'exécution. J'ai rejoué telles quelles dans un émulateur les cinq fonctions extraites du binaire (register_cipher, find_cipher, cbc_start, cbc_setiv, cbc_encrypt), avec une clé et un clair de test. Le chiffré obtenu est identique, octet pour octet, à celui d'une implémentation AES-256-CBC de référence indépendante.

1.3 Enveloppe RSA-4096 et format du footer

La clé de 48 octets est enveloppée par rsa_encrypt_key_ex (l'API générique de LibTomCrypt) avec un paramètre de padding à 1, c'est-à-dire PKCS#1 v1.5 et non OAEP. Le générateur pseudo-aléatoire interne de cette opération (chacha20_prng, censé fournir un aléa de qualité) est lui-même initialisé avec 40 octets d'entropie sortis d'une boucle de 40 appels à rand(). C'est la même source faible que pour la clé AES, une fois srand() positionné.

Voici le format du footer, lu directement dans le décompilé et identique sur les 6 builds Windows/FreeBSD entièrement retracés (sections 2.2 et 2.3) :

*(short *)(blob_rsa + longueur_rsa) = longueur_rsa;   // longueur du blob RSA ajoutee juste apres lui, 2 octets
longueur_rsa += 2;
fseeko64(fp, 0, SEEK_END);
fwrite(blob_rsa, 1, longueur_rsa, fp);                // footer ecrit A LA FIN du fichier
fseeko64(fp, 0, SEEK_SET);
cryptBlocks(fp, cle, cle + 0x20, taille_fichier);     // puis chiffrement AES-CBC en place

Le blob de la clé chiffrée en RSA, suivi de sa propre longueur sur 2 octets, est ajouté à la fin du fichier original avant que le contenu ne soit chiffré en place.


2. Builds confirmés vulnérables (l'encrypteur)

2.1 Build x86 (build de référence)

Champ Valeur
SHA256 4a97599ff5823166112d9221d0e824af7896f6ca40cd3948ec129533787a3ea9
Type PE32, Intel i386, stripped, PDB externe, 6 sections
SHA256 payload déballé 9b31237dbf87d06caa3769bec491fad2dae131846e323bed636a6c6b8e24d7ae
Extension de rançon .!NT3RLOCK

Il est empaqueté dans un crypter protégé par obfuscation du flux de contrôle : des constantes de type chaîne cachent des octets calculés, et le vrai CALL est placé après un faux RET pour tromper la décompilation automatique. Une fois déballé (par émulation Unicorn), le payload importe rand/srand/clock depuis msvcrt.dll sans autre obfuscation, ainsi que RstrtMgr.dll (déverrouillage des fichiers ouverts), _findfirst/_findnext (énumération) et CreateProcessA/system.

La ligne de commande complète, d'après la décompilation du parseur d'arguments :

Commutateur Effet
-f / --file <chemin> fichier cible unique
-d / --directory <chemin> dossier cible
-del / --delete active un drapeau de suppression
-s / --system bascule vers un mode différent
-r / --release-files active un second drapeau

L'énumération passe par un pool de threads qui consomme une file de chemins partagée (protégée par une section critique), pas par un simple parcours séquentiel.

2.2 Builds x64 Windows (4)

Ils sont empaquetés dans un crypter à l'obfuscation bien plus lourde : arithmétique mixte booléenne, table d'imports statique volontairement sans rapport, résolution dynamique par GetProcAddress et, sur les builds les plus récents, parcours manuel de PEB->Ldr. Un harnais Unicorn dédié (mapping du PE, un stub par import, simulation de PEB->Ldr) déroule ce déballage sans qu'on ait à comprendre l'obfuscation dans le détail.

SHA256 Compilé le Notes
a26f0a2da63a838161a7d335aaa5e4b314a232acc15dcabdb6f6dbec63cda642 2024-10-11 Le plus ancien x64 confirmé
b85586f95412bc69f3dceb0539f27c79c74e318b249554f0eace45f3f073c039 2024-12-22 Extension de rançon .1nterlock (chiffre "1" à la place du "l"), logique de nouvelle tentative légèrement plus robuste au renommage/ouverture de fichier
33dc991e61ba714812aa536821b073e4274951a1e4a9bc68f71a802d034f4fb9 2025-02-21 Sibling exact du build x86 de référence, compilé 6 secondes après lui le même jour
0fff8fb05cee8dc4a4f7a8f23fa2d67571f360a3025b6d515f9ef37dfdb4e2ea 2025-03-06 Le plus récent x64 confirmé

Sur les 4, j'ai retracé toute la chaîne par décompilation, footer compris (generateKey/rsaCrypt/cryptBlocks, mêmes fonctions LibTomCrypt, même format de footer qu'en 1.3). Le fichier est renommé avec l'extension de rançon avant d'être ouvert pour chiffrement, et son nom d'origine est restauré à chaque point d'échec possible (RSA raté, ouverture impossible). Extensions observées : .!NT3RLOCK sur 2 des 4 builds, .interlock sur 1, .1nterlock sur 1. C'est une variation cosmétique d'une compilation à l'autre, sans effet sur le mécanisme.

Le nettoyage de traces (removeme : une ressource embarquée est déposée dans un fichier temporaire puis relancée par rundll32.exe %s,run %s) est identique à celui de l'outil proxy décrit en section 4. Les deux partagent donc le même utilitaire, et pas seulement le même packer.

2.3 Builds FreeBSD (2)

Deux échantillons ELF64 x86-64, liés statiquement et non strippés (ABI FreeBSD 10.4). Je les ai analysés avec un harnais Unicorn dédié qui intercepte chaque instruction syscall (convention FreeBSD : numéro dans RAX, arguments dans RDI/RSI/RDX/R10/R8/R9, carry flag comme indicateur d'erreur), sans jamais passer un vrai syscall au noyau de la machine d'analyse.

SHA256 Notes
28c3c50d115d2b8ffc7ba0a8de9572fbe307907aaae3a486aabd8c0266e9426f Boot complet à travers l'init CRT/jemalloc/threading, décompilation complète de la chaîne
e86bb8361c436be94b0901e5b39db9b6666134f23cce1e5581421c2981405cb1 Code strictement identique octet pour octet au précédent, aux mêmes adresses

Comme le binaire n'est pas strippé, toute la chaîne se lit directement : main → allLoop/loopdir (parcours récursif depuis /, pool de threads, file d'attente protégée par mutex) → threadMain → startCrypt → generateKey/rsaCrypt/cryptBlocks. À ce niveau de détail, le portage est fonctionnellement identique au crypter Windows : mêmes fonctions LibTomCrypt, même formule de clé, même format de footer. L'extension de rançon, .interlock, est appliquée après un chiffrement complet réussi, alors que Windows renomme avant et annule en cas d'échec.


3. Preuve de concept : récupération par brute-force

tools/interlock_key_poc.py réimplémente fidèlement le LCG rand()/srand() de MSVCRT et la dérivation de clé en deux passes de 12 tirages. Il vise directement l'état interne du LCG au moment des 24 tirages d'un fichier donné, ce qui le rend indépendant du seed d'origine du processus et du nombre de fichiers déjà traités.

Mesure : 100 001 états candidats testés en 0,37 seconde (Python pur, un seul cœur), soit environ 267 000 candidats par seconde, avec l'état exact et le fichier en clair retrouvés sur des données synthétiques. Sur l'espace complet des 32 bits, une parallélisation GPU irait beaucoup plus vite.

Je n'ai aucun fichier réellement chiffré par InterLock. Ce PoC démontre le principe et la vitesse sur des données synthétiques, pas une récupération validée sur un cas réel. À ce jour, il n'existe aucun décrypteur pour cette famille, ni public ni issu de cette analyse.


4. L'outil proxy/backdoor SOCKS5 (pas l'encrypteur)

Sept échantillons du corpus sont empaquetés dans le même crypter que l'encrypteur (même obfuscation, même déballage). Ce ne sont pourtant pas des copies du ransomware : c'est un outil de tunneling et d'exfiltration à part.

SHA256 Compilé le
39539766ae8f5256e6f21d853b8b7ea8f003d29f6d7cd57d1ecb621dc2b97c89 2024-10-04
45a323e010ea6d5b54d2859112925360174507ea3dc039d02ac6680b6aabd073 2024-11-05
5c5598f7fb5bab3e2c18216764dbbff019f7f19d3801083e8fbf638ba02c6bca 2024-12-06
8251186b3196e3fefb0dbfcf71dfccc2c1cd66515686c9af8a6fb48766c739c6 2025-01-14
f02622129e7774b7673e2a9f62bb4a208d4a142b5d925532c7920481549bd07b 2025-01-17
396eb0e817d90cf366b5648f9a97c51bfb37737af13dc4a9e1a768885a867dc5 2026-01-31
5a0d393de439aebc236a87f3ff18c89c5634f8b7b9331c54986b113b75e8df7f 2026-02-13

Les builds les plus anciens exportent start, rundll, Test, GetInstalledPackageInfo, FreeInstalledPackageInfo sous une bibliothèque socks.dll. Ce sont tous des alias vers une seule vraie routine, start(). Le rand/srand qu'il importe sert à du réseau ordinaire (choix aléatoire d'adresse, délais) et n'a rien à voir avec la clé de chiffrement des fichiers.

Génération 2024 (reversé en détail sur 5c5598f7..., décembre 2024)

  • Adresses C2, port 443/TCP : 65.108.80.58, 212.104.133.72, 216.245.184.170 (variantes proches sur les autres builds de cette génération, voir tableau d'IOCs)
  • Boucle infinie : tire un index aléatoire parmi les 3 IP, tente une connexion TCP. Si les trois échouent dans un cycle, relit un fichier de secours local (liste d'IP alternatives) avant de retenter. Délai de nouvelle tentative croissant de 5 minutes jusqu'à plusieurs dizaines d'heures.
  • Handshake : motif de 4 octets magiques suivi d'une chaîne JSON (IP cible, domaine, nom de machine, indicateurs numériques), délai d'attente 300 secondes. Kill switch dès la poignée de main (un octet de réponse du serveur déclenche l'arrêt immédiat).
  • Protocole applicatif : en-tête de 12 octets (commande, session, longueur, clé de déobfuscation), chiffrement de flux à état roulant. Commandes : relais de tunnel local façon SOCKS, fermeture de session, kill switch en session, liste d'adresses de secours poussée par le serveur, redirection dynamique de l'adresse de contact, délai d'attente imposé par le serveur.
  • Contenu réel des commandes de transfert (pas seulement leurs constructeurs) : exfiltration de fichier arbitraire (ouverture locale, envoi taille puis contenu), dépôt de fichier arbitraire (réception nom + contenu, écriture locale), une variante d'exfiltration supprimant en plus le fichier source après transfert.
  • Persistance : fichier temporaire aléatoire, relance via rundll32.exe %s,run %s (fichier %TEMP%/ribdgfj sur l'échantillon reversé en détail, %TEMP%/useyhd sur les 2 builds 5c5598f7.../f0262212...).

Génération 2026 (396eb0e8..., 5a0d393d...)

  • Adresses C2, port 443/TCP : 172.86.68.64, 23.227.203.123, 77.42.75.119 (identiques entre les deux builds 2026, infrastructure entièrement renouvelée par rapport à 2024)
  • Réimplémentation en C++ (conteneurs de bibliothèque standard, vecteur d'adresses construit dynamiquement, gestion de sessions par comptage de références atomique)
  • Nouvelle tentative bornée (quelques heures maximum) plutôt qu'illimitée
  • En-tête protocolaire passé à 25 octets (contre 12), identifiants de commande renumérotés mais mêmes rôles fonctionnels
  • Même paire d'octets de kill switch à travers toute la réécriture (convention délibérément conservée)
  • Les 16 premiers octets de l'en-tête servent de clé à une instance de RC4 standard (confirmée par exécution réelle du code, bit-exact contre une référence indépendante), protégeant à la fois les métadonnées et le corps du message (contre un simple XOR à état roulant en 2024)
  • Handshake équivalent, délai réduit à 30 secondes (contre 300)

5. Builds non analysables (métadonnées corrompues)

Trois échantillons du corpus ont des métadonnées corrompues qui empêchent toute analyse. Ce n'est pas une technique d'évasion : j'ai vérifié que la corruption existait déjà dans la soumission MalwareBazaar et qu'elle ne vient pas de mon pipeline.

SHA256 Type Problème vérifié
dae299469d2d2f836d56b8f8f0a13bd03ba9e6e52e3c9bcddf2c7e1db62e49cf PE32+ x86-64 Une seule section .text couvrant tout le fichier, répertoire d'imports pointant au-delà de la taille réelle du fichier (confirmé indépendamment par pefile et radare2)
85a18f6e3b67c2ebfa27e559515140171348085a4929f95531df15edccd3c4d8 PE32+ x86-64 Aucune table d'imports standard, point d'entrée réel non localisable malgré 4 vérifications ciblées (RVA d'entrée connue vide, signature de trampoline absente, accès PEB/TEB = code CRT standard, aucun pointeur valide en .CRT/.tls)
f00a7652ad70ddb6871eeef5ece097e2cf68f3d9a6b7acfbffd33f82558ab50e ELF64 x86-64 (FreeBSD) Program headers corrompus par un motif de remplacement UTF-8 (ef bf bd, caractère U+FFFD) dès la soumission d'origine, vérifié identique entre le fichier sur disque et une extraction fraîche de l'archive

Aucun des trois ne montre de signe de correction de la faille, mais je ne peux rien conclure dans un sens ou dans l'autre. Ils sont simplement illisibles avec les outils standard, et les reconstruire à la main demanderait un effort disproportionné pour cette analyse.


6. Portée et limites

  • Aucun échantillon (Windows ou FreeBSD) n'a été exécuté en entier. Tout le déballage et l'analyse comportementale reposent sur une émulation Unicorn où chaque appel système ou API est intercepté et simulé à la main.
  • La fenêtre de vulnérabilité confirmée sans interruption va d'octobre 2024 à mars 2025. Rien ne prouve ni n'infirme que la faille persiste après : les échantillons les plus récents disponibles au moment de l'analyse (janvier/février 2026) sont l'outil proxy de la section 4, pas l'encrypteur.
  • Je n'ai obtenu aucun fichier réellement chiffré par InterLock. Le PoC de récupération n'est validé que sur des données synthétiques.
  • 3 échantillons sur 18 sont illisibles (section 5), sans lien établi avec la faille.

7. IOCs consolidés

Type Valeur
Bibliothèque crypto LibTomCrypt (AES-256-CBC, RSA-4096 PKCS#1 v1.5)
Commutateurs CLI (encrypteur) -f/--file, -d/--directory, -del/--delete, -s/--system, -r/--release-files
SHA256 encrypteur x86 4a97599ff5823166112d9221d0e824af7896f6ca40cd3948ec129533787a3ea9
SHA256 payload x86 déballé 9b31237dbf87d06caa3769bec491fad2dae131846e323bed636a6c6b8e24d7ae
SHA256 encrypteurs x64 Windows a26f0a2da63a838161a7d335aaa5e4b314a232acc15dcabdb6f6dbec63cda642, b85586f95412bc69f3dceb0539f27c79c74e318b249554f0eace45f3f073c039, 33dc991e61ba714812aa536821b073e4274951a1e4a9bc68f71a802d034f4fb9, 0fff8fb05cee8dc4a4f7a8f23fa2d67571f360a3025b6d515f9ef37dfdb4e2ea
SHA256 encrypteurs FreeBSD 28c3c50d115d2b8ffc7ba0a8de9572fbe307907aaae3a486aabd8c0266e9426f, e86bb8361c436be94b0901e5b39db9b6666134f23cce1e5581421c2981405cb1
Extensions de rançon observées .!NT3RLOCK, .interlock, .1nterlock (selon le build, même mécanisme)
SHA256 outil proxy (échantillon reversé en détail) bd504862c5c33c81000a9c85dc3a84750687bdb5c59f1aaa1a0b19bf019c0c2e
SHA256 outils proxy (autres builds) 39539766ae8f5256e6f21d853b8b7ea8f003d29f6d7cd57d1ecb621dc2b97c89, 45a323e010ea6d5b54d2859112925360174507ea3dc039d02ac6680b6aabd073, 5c5598f7fb5bab3e2c18216764dbbff019f7f19d3801083e8fbf638ba02c6bca, 8251186b3196e3fefb0dbfcf71dfccc2c1cd66515686c9af8a6fb48766c739c6, f02622129e7774b7673e2a9f62bb4a208d4a142b5d925532c7920481549bd07b, 396eb0e817d90cf366b5648f9a97c51bfb37737af13dc4a9e1a768885a867dc5, 5a0d393de439aebc236a87f3ff18c89c5634f8b7b9331c54986b113b75e8df7f
Adresses C2 proxy, décembre 2024 (port 443/TCP) 65.108.80.58, 212.104.133.72, 140.82.14.117
Adresses C2 proxy, 5c5598f7... (port 443/TCP) 65.108.80.58, 212.104.133.72, 216.245.184.170
Adresses C2 proxy, f0262212... (port 443/TCP) 65.108.80.58, 84.200.24.41, 216.245.184.170
Adresses C2 proxy, janvier-février 2026 (port 443/TCP) 172.86.68.64, 23.227.203.123, 77.42.75.119
Fichier de persistance proxy (build reversé en détail) %TEMP%/ribdgfj
Fichier de persistance proxy (5c5598f7.../f0262212...) %TEMP%/useyhd
Mécanisme de dépôt/relance (encrypteur et proxy) rundll32.exe %s,run %s

Les échantillons étudiés sont publics (MalwareBazaar). La famille est active et déjà documentée par une alerte gouvernementale conjointe (CISA/FBI AA25-203A) et par un chercheur indépendant. Rien ici ne révèle une vulnérabilité inconnue dans un logiciel légitime. Le but est de donner aux équipes de réponse à incident confrontées à un build de la période octobre 2024-mars 2025 une base concrète et vérifiée pour évaluer ce qui est récupérable, en disant clairement qu'il n'existe pas encore d'outil de récupération prêt à l'emploi.