StealC : du dropper GCleaner au C2 actif, sans exécuter le binaire

Catégorie : Infostealer

Analyste : Gordon PEIRS

Famille : StealC (infostealer, MaaS)

Type : analyse statique uniquement (je n'ai jamais exécuté le binaire). La seule étape qui repose sur une exécution faite par un tiers est expliquée dans la note de méthode du §8.


Résumé

Un dropper GCleaner livre un stub IExpress protégé par Asgard Protector. Ce stub décompresse un interpréteur AutoIt légitime (AutoIt3.exe) et un script compilé (Quotes.a3x), que j'ai décompilé avec autoit-ripper puis désobfusqué avec un outil maison. La résolution des blocs Switch obfusqués est validée sur tout le fichier : 354 blocs, et deux méthodes indépendantes qui tombent d'accord sur 351/351. Le script contient des vérifications anti-VM et anti-sandbox immédiates, une détection de Sleep patché avec des seuils qui dépendent de l'antivirus (20 s pour Avast, 160 s pour Bitdefender), et il adapte son comportement à l'AV présent sur la machine avant même de dépasser le stade du dropper. Il s'installe dans %LocalAppData%\CodeInnovate Technologies Co\ avec une relance par VBS et par le dossier Startup, puis fait un process hollowing (x86 et x64 gérés) pour injecter le vrai payload StealC. Celui-ci est rattaché au botnet euromix/eu1, dont le C2 est confirmé actif (431 signalements). Enfin, un pivot sur l'imphash montre que le crypter Asgard Protector observé ici protège indifféremment des familles sans rapport entre elles (Amadey, LummaStealer, Vidar, QuasarRAT), ce qui relativise sa valeur pour l'attribution.


Ce document rassemble tout le récit de l'enquête (constats, hypothèses, conclusions) pour ses quatre volets. La preuve de travail reproductible (commande, raison, sortie brute) est à part, dans runbook.md, organisé selon les mêmes quatre parties. Les scripts autonomes sont dans tools/.

1. Contexte et choix de l'échantillon

L'échantillon vient de MalwareBazaar (abuse.ch), trouvé par une recherche sur la signature Stealc.

Ce sample n'est pas lié à une campagne précise documentée publiquement. Je l'ai choisi parce qu'il représente un build courant (son imphash est partagé avec plusieurs autres soumissions récentes du même reporter). Première observation le 13/07/2026, tag dropped-by-gcleaner.

À noter (voir §6.4) : cet imphash est partagé par des échantillons de familles sans rapport (Amadey, LummaStealer, Vidar, QuasarRAT...). Il ne caractérise donc pas StealC, mais le crypter (Asgard Protector, voir §6.4), qui protège des payloads de familles variées. Le rattachement à StealC repose uniquement sur le tag signature: Stealc posé par MalwareBazaar ou le reporter. Je ne l'ai jamais vérifié en observant un vol de données, puisque je n'ai jamais obtenu le vrai payload injecté (voir §6.3 et §8).

2. Identité de l'échantillon

Champ Valeur
SHA256 afbeeeaa7952579bc73b5d220ef1a828ecfdb62b80e339b007f07c82c60ab6da
MD5 dc8db3908bec45fc19bfb4d2c4514474
Type PE32 executable, Intel i386, GUI
Taille 2 195 968 octets
Imphash 646167cce332c1c252cdcb1839e0cf48
Reporter (MalwareBazaar) Bitsight
Tags dropped-by-gcleaner, Stealc, exe

3. Outillage

  • file, pefile (Python), pour l'identification et le parsing PE
  • capa 9.4.0 + règles officielles capa-rules, pour la détection de capacités
  • floss 3.1.1, pour l'extraction de chaînes cachées/décodées
  • cabextract, pour l'extraction d'archive CAB embarquée
  • autoit-ripper, pour la décompilation de bytecode AutoIt compilé (.a3x)

Tout le travail est fait en local, sans exécuter le binaire. Je n'avais pas d'environnement sandbox isolé, l'analyse est donc strictement statique.

4. Le stub IExpress : analyse statique

4.1 En-têtes PE

  • Entry point : 0x6ce0, Image base : 0x400000
  • Timestamp de compilation falsifié : valeur brute 2215452883, ce qui correspond au 15/03/2040. Technique d'anti-analyse classique (perturbe le tri chronologique et certains outils de sandboxing).
  • Sections :
Section Taille virtuelle Entropie
.text 0x662c 6.26
.data 0x1aa0 4.97
.idata 0x1052 5.03
.rsrc 0x20f68a 7.99
.reloc 0x888 6.26

.rsrc pèse à elle seule ~95% du binaire, avec une entropie quasi maximale (7.99/8). Le payload est donc très probablement packé ou chiffré et rangé en ressource, pas dans .text.

  • Imports notables : Cabinet.dll (4 fonctions), un indice fort d'une extraction de CAB au runtime, ce qui colle avec l'hypothèse ci-dessus.

4.2 Capa : capacités détectées

Capa classe directement le binaire comme installeur ((internal) installer file limitation), avec 47 capacités remontées, dont les plus significatives :

  • packaged as an IExpress self-extracting archive : confirme le mécanisme. IExpress est un outil natif Windows de création d'auto-extractibles, ce qui explique l'import Cabinet.dll et l'entropie de .rsrc.
  • persist via Run registry key : persistance via Software\Microsoft\Windows\CurrentVersion\RunOnce (clé observée à plusieurs offsets : 0x4020DC, 0x402167, 0x40237D).
  • reference anti-VM strings targeting Xen : chaînes "XeN:", "XeNa", "xeNe" trouvées respectivement à file+0x61FC5, file+0x1D11CE, file+0x49D41. Détection d'hyperviseur Xen (utilisé par certains sandbox d'analyse).
  • reference analysis tools strings : indices de détection d'outils d'analyste (détaillé en §6).
  • Collecte système : disque, version OS, énumération de fichiers/registre, cohérent avec du fingerprinting de machine avant déploiement du payload final.

4.3 Floss

Floss ne remonte presque aucune chaîne cachée ou décodée dans le code du stub (--no static). La vraie logique n'est donc pas dans ce binaire, elle est dans la ressource packée.

4.4 Extraction de la ressource : CAB puis AutoIt

Parsing des ressources PE (pefile) : la ressource de type 10 (RCDATA), taille 1 979 115 octets, commence par la signature MSCF (Microsoft Cabinet File).

extracted/payload.cab: Microsoft Cabinet archive data, many, 1979115 bytes, 2 files,
  "Quotes.a3x" (last modified 2026-07-12 16:52:18)
  "AutoIt3.exe" (last modified 2026-07-12 16:52:40)

Extraction (cabextract), on obtient deux fichiers :

Fichier SHA256 Nature
AutoIt3.exe 92c6531a09180fae8b2aae7384b4cea9986762f0c271b35da09b4d0e733f9f45 Interpréteur AutoIt légitime, abusé comme LOLBin
Quotes.a3x 49ded704632abe3642b76c32c60d46ab99402495624921787e0c57a85f83327d Script AutoIt compilé, logique malveillante réelle

La technique est connue : on livre l'interpréteur AutoIt légitime (souvent whitelisté ou signé) avec un script compilé (.a3x) qui contient la charge utile. Le stub IExpress extrait les deux fichiers et lance AutoIt3.exe Quotes.a3x. Les fichiers sont datés du 12/07/2026, la veille de l'analyse.

4.5 Décompilation du script AutoIt

autoit-ripper a réussi à décompresser le bytecode (EA06 compressed blob) en script source (.au3), 2 724 886 octets, 17 593 lignes.

Le script est fortement obfusqué : - Noms de fonctions/variables : concaténations de mots anglais aléatoires (VIICONSUMPTIONHOLOCAUST, BATHROOMREWARDLIVED...) - Control-flow flattening : boucle While + Switch sur une variable d'état numérique, une seule branche réellement exécutée par itération, le reste noyé dans du code mort (appels à Beep, WinExists, ControlGetHandle sur des arguments factices) - Constantes cassées via BitXOR et arithmétique 32-bit (ex. 867661 + 4294100107) - Déchiffrement de chaînes via une fonction dédiée (BATHROOMREWARDLIVED($hex, $clé)) : décode des blobs hexadécimaux avec une clé, appelée systématiquement pour toute chaîne sensible (noms d'API, chemins, etc.)

Exemple représentatif (extrait brut, non résolu) :

DllCall ( BATHROOMREWARDLIVED ( "0x1E291E013D1A4A5C65270609" , "uLloXvynKCjexQ" ) ,
          BATHROOMREWARDLIVED ( "0x033A29053D" , "gMFwYpqLOwfKme" ) ,
          BATHROOMREWARDLIVED ( "0x302711180F36072713031830" , "wBeTnEsbaq" ) )

Ce niveau d'obfuscation (machine à états, junk code, chiffrement systématique des chaînes) est celui d'un crypter/obfuscateur AutoIt commercial, typique des services de « protection » vendus aux opérateurs de malware-as-a-service. Ça correspond au modèle StealC.


5. Déobfuscation du loader AutoIt (Quotes.a3x)

5.1 Le problème

Le script décompilé (script.au3, 17 593 lignes) est protégé par un obfuscateur/crypter AutoIt de type commercial : - Noms d'identifiants : concaténations de mots anglais aléatoires - Control-flow flattening : chaque bloc réel est noyé dans un While(vrai) / Switch $etat / Case ... où une seule branche par bloc est réellement exécutée, le reste étant du code mort (junk API calls sur arguments factices) - Toutes les chaînes sensibles (noms d'API, chemins, clés de registre) sont chiffrées et décodées à la volée via une fonction dédiée : BATHROOMREWARDLIVED(hex, clé)

5.2 Découverte clé : les "grandes constantes" sont des entiers négatifs mal signés

En analysant à la main BATHROOMREWARDLIVED (la fonction de déchiffrement elle-même obfusquée), on remarque que l'état initial d'un Switch ne correspond à aucune valeur de Case en arithmétique naïve, sauf si on réinterprète les constantes proches de 2³² (et 2⁶⁴) comme des entiers signés 32-bit (ou 64-bit) mal réinterprétés par le décompileur autoit-ripper :

Constante brute Réinterprétée comme
4294967295 -1 (0xFFFFFFFF en int32 signé)
4294966729 -567
18446743537125786376 -536583240 (proche de 2^64)

Vérification manuelle sur le premier bloc de BATHROOMREWARDLIVED : - État initial : 349894 + 38 = 349932 - Case (363652 + 4294960973 + 4294959899), en réinterprétant chaque grande constante comme négative : 363652 + (-6323) + (-7397) = 349932. C'est un match exact, et cette branche contient justement un ExitLoop (les branches "mortes" n'en contiennent jamais), ce qui confirme le raisonnement.

Cet exemple est un cas isolé, vérifié à la main. Pour ne pas me contenter de quelques exemples choisis, j'ai automatisé la même vérification et je l'ai appliquée à tous les blocs Switch du fichier (voir §10 et le runbook). Sur 354 blocs, deux méthodes complètement indépendantes (le calcul arithmétique d'un côté, la présence d'ExitLoop/Return de l'autre) donnent chacune une réponse unique pour 351 d'entre eux, et ces réponses concordent dans les 351 cas. Pour les 3 blocs restants, un seul des deux signaux existe : certains petits Switch servent de simple table de correspondance et ne contiennent jamais d'ExitLoop, ce qui est normal. Je les ai relus à la main et ils sont corrects (l'un d'eux est détaillé au §7.5). On n'est donc plus sur une déduction à partir de 2 exemples, mais sur une validation croisée de tout le fichier.

5.3 L'algorithme de déchiffrement des chaînes

En isolant les deux branches réellement exécutées de BATHROOMREWARDLIVED, l'algorithme se résume à un XOR à clé répétée, octet par octet :

def decrypt(hexstr, key):
    data = bytes.fromhex(hexstr)
    return "".join(chr(b ^ ord(key[i % len(key)])) for i, b in enumerate(data))

5.4 Script de déobfuscation

Outil : tools/deobfuscate.py, en deux passes : 1. Déchiffre chaque appel BATHROOMREWARDLIVED("0xHEX","clé") inline. 2. Pour chaque bloc Switch, réévalue l'expression initiale et celle de chaque Case (avec réinterprétation des grandes constantes), ne garde que la branche correspondante (avec vérification croisée via présence de ExitLoop/Return).

Résultats sur script.au3 :

[pass1] decrypted 11698 BATHROOMREWARDLIVED string(s)
[pass2] resolved 354 Switch block(s), 0 ambiguous/unresolved
Wrote deobfuscated.au3 (3188 lines, was 17594)

Résultat : ~81% de lignes en moins, quasiment tout le code mort éliminé, et 11 698 chaînes en clair.


6. Ce que révèle le script déobfusqué (deobfuscated.au3)

6.1 Chaîne d'appels : process hollowing / RunPE

La séquence d'API remontée est celle d'un process hollowing classique :

Étape API
1. Création du process cible (suspendu) CreateProcessW
2. Démappage de l'image d'origine NtUnmapViewOfSection
3. Mapping de la nouvelle image (payload réel) NtMapViewOfSection
4. Écriture en mémoire NtWriteVirtualMemory (x2 occurrences distinctes)
5. Redirection du point d'entrée du thread NtSetContextThread
6. Reprise d'exécution NtResumeThread

Complété par : - RtlGetCompressionWorkSpaceSize / RtlDecompressFragment : le payload final est stocké compressé et décompressé en mémoire juste avant l'injection. - InitializeProcThreadAttributeList / UpdateProcThreadAttribute : attributs de création de process avancés, ce qui colle avec du spoofing de PPID ou du blocage de politique DLL (évasion EDR). - VirtualAllocExNuma, NtWow64ReadVirtualMemory64, NtWow64QueryInformationProcess64 : primitives d'allocation/lecture distante, y compris variante WOW64 (process 32-bit manipulant un process 64-bit ou inversement).

Lire les noms d'API dans l'ordre ne suffit pas. Pour m'assurer que cette séquence n'est pas une coïncidence, j'ai retracé chaque variable jusqu'à sa définition dans le script désobfusqué :

  • $KNIFEMINACIDCLASSROOM (le flag de création passé à CreateProcessW) vaut 134742020, soit 0x08080004 en hexadécimal. Le bit 0x4 correspond exactement à CREATE_SUSPENDED. Le process est donc bien créé suspendu, ce n'est pas une supposition.
  • Juste avant, le script fait If ProcessExists("avp.exe") Then $KNIFEMINACIDCLASSROOM = 134217732. avp.exe est le nom de process de Kaspersky Anti-Virus. Si Kaspersky tourne, la valeur passe à 0x08000004 : toujours CREATE_SUSPENDED, mais sans le bit EXTENDED_STARTUPINFO_PRESENT (0x80000). C'est un contournement anti-AV concret et nommé, pas une simple "détection d'outils d'analyste" générique.
  • La structure passée en dernier argument à CreateProcessW est $KNIGHTANALYSTSTREAMTWENTY = DllStructCreate("ptr Process; ptr Thread; dword ProcessId; dword ThreadId"), une réplique champ pour champ de la structure Windows PROCESS_INFORMATION. Juste après l'appel, $PACIFIC_ED et $N_CONFUSIONALTERNATIVEGAUGE (utilisés ensuite dans NtWriteVirtualMemory, NtSetContextThread, NtResumeThread) sont lus directement dans les champs "Process" et "Thread" de cette structure. Ce n'est donc pas juste un nom de variable qui y ressemble : ce sont bien, structurellement, les handles process et thread renvoyés par la création du process cible.
  • Le handle de section utilisé dans NtMapViewOfSection ($GOTTENRULEDDIVERSITYWHETHER) vient de $YARNFUN = DllCall("ntdll.dll", "long", "NtOpenSection", "handle*", 0, ...), puis $GOTTENRULEDDIVERSITYWHETHER = $YARNFUN[1] (en AutoIt, l'index 1 d'un retour DllCall correspond au premier paramètre passé par référence, ici le handle de section ouvert). La chaîne NtOpenSection → NtMapViewOfSection → NtWriteVirtualMemory est donc tracée de bout en bout, pas seulement observée dans l'ordre d'apparition.
  • Point important qui affine la conclusion initiale : dans l'appel NtMapViewOfSection, le handle de process passé est bien +4294967295 (soit -1, le pseudo-handle du process courant), donc cette étape mappe la section dans le process du loader lui-même, pas dans le process enfant. L'écriture vers le process cible ($PACIFIC_ED) se fait ensuite via NtWriteVirtualMemory. Autrement dit : la technique n'est pas un hollowing "à l'ancienne" (VirtualAllocEx + WriteProcessMemory), mais une variante basée sur une section mappée localement puis copiée vers le process distant, une méthode bien documentée pour ce type de loader.

6.2 Anti-sandbox / anti-analyse

  • GetTickCount, QueryPerformanceCounter : détection de ralentissement (timing checks, patchs de sandbox/debugger)
  • GetSystemMetrics : résolution d'écran (les VMs ont souvent des résolutions atypiques)
  • IsProcessorFeaturePresent : cohérence des features CPU (détection de CPU virtualisé)
  • Chaînes anti-VM ciblant Xen, déjà identifiées en §4.2
  • Détection explicite du process avp.exe (Kaspersky), voir §6.1 : le comportement du loader change concrètement selon qu'il est présent ou non

6.3 Ce qu'on n'a pas trouvé

Une fois les 11 698 chaînes déchiffrées, je n'ai trouvé aucune URL de C2 ni aucun domaine en clair. La seule occurrence est microsoft.com, sans doute un leurre ou une référence légitime (un User-Agent, un test de connectivité). Ça va dans le sens de l'hypothèse : ce binaire n'est que le loader. Son rôle est de décompresser et d'injecter le vrai payload StealC (récupéré ou embarqué ailleurs), et c'est ce payload qui porte la config C2. Les crypters de StealC séparent toujours les rôles ainsi : le loader ne contient jamais les IOC réseau, pour limiter la détection.

6.4 Sur l'identité du crypter : Asgard Protector

Le §4 parlait d'un « crypter commercial typique ». On peut être plus précis.

La réponse vient de MalwareBazaar, en regardant les autres échantillons qui partagent le même imphash (646167cce332c1c252cdcb1839e0cf48, voir §10 et le runbook) : plusieurs sont tagués AsgardProtector. Ce tag renvoie à un crypter documenté par SpyCloud Labs (source : spycloud.com, "Asgard Protector - Malware Crypter Analysis"), et la correspondance technique est solide :

Élément décrit par SpyCloud Observé dans cet échantillon
Script AutoIt utilisant "a basic state machine and string hiding" Confirmé : control-flow flattening + BATHROOMREWARDLIVED (§5.1 à 5.3)
Décompilation via autoit-ripper Même outil utilisé dans cette analyse
Décompression via RtlDecompressFragment et LZNT1 Mêmes API observées (§6.1)
Injection dans explorer.exe pour évader la détection Correspond à la cible du spoofing de PPID identifiée en §7.3
Vérifications de processus incluant SophosHealth, AvastUI, AVGUI (via un script batch en amont) Mêmes noms de processus retrouvés dans les checks ProcessExists (§7.3), à ceci près que mes vérifications sont faites directement en AutoIt, pas dans un .bat préalable
Service "AUTOcrypt" avec stub généré automatiquement via un bot Telegram Non vérifiable statiquement sur cet échantillon seul, mais cohérent avec le modèle crypter-as-a-service déjà supposé

Une différence à signaler : SpyCloud décrit une détection de sandbox par ping vers des domaines qui ne devraient jamais répondre. J'ai cherché les API réseau habituelles (Ping, DnsQuery, gethostbyname, getaddrinfo) dans deobfuscated.au3 et je n'ai rien trouvé de tel. Soit cette version ou ce build n'inclut pas cette vérification, soit elle n'est pas dans ce stage. Le reste de la correspondance (RtlDecompressFragment/LZNT1, cible explorer.exe, mêmes AV, mêmes outils d'analyse utilisés par des chercheurs indépendants) est assez spécifique pour considérer l'attribution à Asgard Protector comme solide. Je ne l'affirme pas à 100 % pour autant, faute d'un sample de référence identique au bit près à comparer.


7. Persistance et évasion anti-AV

Cette partie creuse deux points laissés de côté jusqu'ici : le mécanisme de persistance exact, et l'inventaire complet des comportements qui dépendent de la présence d'un antivirus. L'analyse reste 100 % statique.

7.1 Une couche d'obfuscation supplémentaire : ChrW calculé

En cherchant le contenu exact du mécanisme de persistance, je suis tombé sur une deuxième technique d'obfuscation de chaînes, différente de BATHROOMREWARDLIVED. Certains caractères sont produits un par un avec ChrW(<expression arithmétique>), toujours avec l'astuce des grandes constantes à relire comme des négatifs. Un script dédié, decode_chrw.py, repère chaque appel ChrW(...) en appariant les parenthèses, évalue l'expression et la remplace par le caractère obtenu.

Dans deobfuscated.au3, il n'y a que 16 appels de ce type, tous dans la routine qui écrit le script VBS de relance (voir §7.2).

7.2 Chaîne de persistance complète

En traçant les variables qui construisent les chemins et le contenu du VBS, la chaîne complète est la suivante :

Élément Valeur
Dossier de dépôt %LocalAppData%\CodeInnovate Technologies Co\
Copie de l'interpréteur InnoCoder.exe (ou AutoIt3.exe, voir §7.3)
Copie du script compilé q (ou q.a3x, voir §7.3)
Script de relance InnoCoder.vbs
Persistance Raccourci .lnk dans %StartupDir%, nommé InnoCoder.lnk, pointant vers InnoCoder.vbs

Les copies sont déposées aux lignes 2255 et 2280 de deobfuscated.au3. Le script lit son propre fichier (@ScriptFullPath) et le vrai interpréteur (@AutoItExe), puis les réécrit tels quels aux emplacements ci-dessus, seulement s'ils n'y sont pas déjà.

Le contenu du fichier InnoCoder.vbs, une fois les ChrW décodés et les concaténations recomposées à la main :

Set InnoCoder = CreateObject("Wscript."+"Shell")
InnoCoder.CurrentDirectory = "<LocalAppData>\CodeInnovate Technologies Co"
InnoCoder.Exec """InnoCoder.exe"" ""q"""

(le découpage "Wscript."+"Shell" et le triple jeu de guillemets dans Exec servent à éviter que la chaîne littérale Wscript.Shell ou le nom des fichiers déposés n'apparaissent tels quels dans le code source, sans changer le résultat : la ligne s'exécute comme un CreateObject("Wscript.Shell") classique suivi d'un lancement de la copie de l'interpréteur avec la copie du script en argument).

Le raccourci InnoCoder.lnk dans le dossier de démarrage de Windows garantit cette relance à chaque connexion, sans dépendre de la clé de registre Run vue au §4.2. Les deux mécanismes de persistance semblent donc coexister.

7.3 Inventaire complet des vérifications anti-AV

Une recherche de tous les appels ProcessExists dans le fichier déobfusqué (voir le runbook) remonte 4 comportements distincts déclenchés par la présence de logiciels de sécurité, en plus des vérifications anti-VM déjà connues :

Processus détecté Éditeur Effet
avp.exe Kaspersky Change le flag de création de CreateProcessW (retire EXTENDED_STARTUPINFO_PRESENT, voir §6.1)
AvastUI.exe, AVGUI.exe, bdagent.exe, SophosHealth.exe Avast, AVG, Bitdefender, Sophos La copie de l'interpréteur est déposée sous le nom AutoIt3.exe au lieu de InnoCoder.exe
AvastUI.exe, AVGUI.exe, SophosHealth.exe Avast, AVG, Sophos La copie du script compilé est déposée sous le nom q.a3x au lieu de q
bdagent.exe Bitdefender Définit $YEN_SHERIFF = "cscript" au lieu de "wscript" (voir note ci-dessous)
avastui.exe Avast Déclenche une pause de 20 secondes vérifiée par timing (voir §7.4)
bdagent.exe Bitdefender Déclenche une pause de 160 secondes vérifiée par timing (voir §7.4)

Une réserve : $YEN_SHERIFF n'est assigné qu'à ces deux lignes (vérifié dans script.au3 et dans deobfuscated.au3) et n'apparaît nulle part ailleurs dans le fichier. Je n'ai pas pu confirmer en statique à quoi il sert (probablement à choisir entre wscript.exe et cscript.exe pour lancer InnoCoder.vbs sans fenêtre visible). C'est une hypothèse raisonnable, pas une conclusion vérifiée comme le reste de cette partie.

À part cet inventaire, deux vérifications supplémentaires, non liées à un AV précis, ont aussi été localisées :

  • Kill switch anti-VM/sandbox immédiat : If ProcessExists("vmtoolsd.exe") Or ProcessExists("VboxTray.exe") Or ProcessExists("SandboxieRpcSs.exe") Then Exit. Détection explicite de VMware Tools, VirtualBox Guest Additions et Sandboxie, avec sortie immédiate du script si l'un des trois est présent. (La chaîne exacte présente dans l'échantillon est VboxTray.exe ; ProcessExists étant insensible à la casse, cela cible bien le vrai process VBoxTray.exe de VirtualBox.)
  • Préparation d'un spoofing de PPID ciblant explorer.exe : un handle est ouvert sur le process explorer.exe via OpenProcess, cohérent avec l'utilisation observée en §6.1 de InitializeProcThreadAttributeList/UpdateProcThreadAttribute pour faire apparaître le process injecté comme un enfant d'explorer.exe plutôt que du loader réel. Le spoofing de PPID évoqué au §6.1 a donc une cible concrète.

7.4 Détection de sandbox par altération du Sleep

La fonction LOPEZFUNDING($ms), appelée dans plusieurs des branches ci-dessus, implémente une vérification classique de détection de sandbox :

  1. Mesure le temps réel via QueryPerformanceCounter/QueryPerformanceFrequency avant l'appel.
  2. Appelle Sleep($ms).
  3. Mesure à nouveau le temps réel après l'appel.
  4. Si le temps réellement écoulé ne correspond pas (à une tolérance de -1 à +500 ms près) à la durée demandée, le script s'arrête (Exit).

C'est la technique standard pour détecter un Sleep intercepté ou accéléré par un environnement d'analyse automatisé, qui patche souvent cette fonction pour raccourcir les délais. Les durées utilisées ici : 20 secondes si Avast est détecté, 160 secondes si Bitdefender est détecté.

Note technique sur le calcul de ces durées. Les arguments passés à LOPEZFUNDING sont eux aussi obfusqués avec la même astuce de grandes constantes. Pour la durée Bitdefender, l'argument est 794769 + 4294332527. La règle de réinterprétation appliquée par deobfuscate.py/validate_crossref.py re-signe chaque littéral individuellement s'il tombe dans [2^31, 2^32[ (ou [2^63, 2^64[) : 4294332527 est lui-même dans cette plage → 4294332527 - 2^32 = -634769, d'où 794769 - 634769 = 160000 ms, soit 160 s. Le même résultat s'obtient en sommant d'abord puis en re-signant modulo 2^32 ((794769 + 4294332527) mod 2^32 = 160000), c'est d'ailleurs la forme utilisée dans le snippet du runbook. Piège à éviter : appliquer le test de plage à la somme brute (4295127296) échouerait, car elle dépasse 2^32 et sort de l'intervalle ; il faut re-signer soit chaque littéral avant l'addition, soit la somme via un modulo 2^32.

7.5 Gestion explicite des deux architectures (CONTEXT x86/x64)

L'un des blocs Switch (autour de la ligne 3595 de script.au3) est un choix de structure CONTEXT Windows selon l'architecture cible :

Switch $RESPECTIVE_OPERATED_TRICK_CLIMB
Case 1
    $STRINGSFICTIONEVANSCLASSROOM = 65543      ' 0x10007 : CONTEXT_FULL (x86)
Case ( 82 + 4294967216 )                        ' réinterprété : 2
    $STRINGSFICTIONEVANSCLASSROOM = 1048583    ' 0x100007 : CONTEXT_FULL (x64)
EndSwitch

0x10007 et 0x100007 sont les valeurs CONTEXT_FULL respectivement pour la structure CONTEXT x86 et x64 de Windows. Juste avant ce Switch, un If/Else construit la structure $FORMULATEMPLELIVE (utilisée ensuite dans GetThreadContext/NtSetContextThread, voir §6.1) à partir de trois chaînes déchiffrées dans la branche 64-bit, et de deux seulement dans la branche 32-bit. Le loader gère donc explicitement les deux architectures pour l'étape de détournement du contexte de thread, plutôt que de supposer une seule architecture cible.


8. Confirmation externe et infrastructure de campagne

À ce stade, il reste deux trous : le vrai payload StealC, jamais observé, et le stage GCleaner en amont, jamais examiné. Cette partie les comble sans que j'exécute le binaire. J'exploite des champs de l'API MalwareBazaar que je n'avais pas lus (comment, file_information, vendor_intel), puis je pivote sur les indicateurs réseau avec ThreatFox et des vérifications passives.

Précision de méthode : à partir d'ici, la confirmation du payload final et de sa config C2 repose sur l'exécution de cet échantillon par un tiers (la sandbox Triage), pas sur une exécution ou une émulation de ma part. Tout ce qui précède (§1 à §7) reste strictement statique.

8.1 Ce que l'API MalwareBazaar contenait déjà

Une simple requête get_info sur le hash, que j'ai cette fois lue en entier (je n'en avais extrait que quelques champs jusque-là), contient :

  • "comment": "url: http://158.94.209.95/service" : une URL de livraison notée par le reporter.
  • "file_information": [{"context": "dropped_by_malware", "value": "Gcleaner"}, ...] : confirmation structurée du parent GCleaner, au-delà du simple tag communautaire déjà connu.
  • "vendor_intel" → Triage a exécuté cet échantillon exact en sandbox et en a extrait une configuration StealC complète :
family: stealc
botnet: euromix
c2: http://160.20.109.75/d19ca32cb5a444ac8b87.php
lien du rapport : https://tria.ge/reports/260713-hewm3sey3t/

Triage a observé les signatures comportementales Deletes itself, Drops startup file, Executes dropped EXE et Suspicious use of SetThreadContext. Elles recoupent les mécanismes que j'avais tracés en statique aux §6 et §7 (NtSetContextThread, dépôt de persistance, relance par VBS). Elles ajoutent un détail que l'analyse statique n'avait pas isolé : le loader se supprime lui-même après avoir déposé ses copies.

Il existe aussi un rapport CAPE Sandbox pour ce hash (lien dans file_information). Je ne l'ai pas consulté en détail, il recoupe Triage sur l'essentiel.

8.2 Vérification indépendante du C2 (ThreatFox)

J'ai cherché le C2 extrait par Triage sur ThreatFox (la plateforme sœur de MalwareBazaar, consacrée aux indicateurs réseau), indépendamment de la sandbox qui l'a produit :

Champ Valeur
IOC http://160.20.109.75/d19ca32cb5a444ac8b87.php
Famille win.stealc
Confiance 75%
Sightings 431
Première observation 2026-07-07 23:55 UTC
Dernière observation 2026-07-13 11:28 UTC

431 observations, et une dernière vue quelques heures avant cette analyse. Ce n'est pas une infrastructure isolée ou éphémère : c'est un C2 actif, suivi depuis au moins une semaine.

8.3 Un deuxième échantillon de la même campagne

ThreatFox référence un autre hash MalwareBazaar associé au même C2 : 8b7537c6624998423c0dc5e63d133a4380df59ff64a623f35f2f669e63061c52, vu le 07/07/2026 (6 jours avant mon échantillon). Comparaison :

Champ Mon échantillon Second échantillon
SHA256 afbeeeaa... 8b7537c6...
Architecture I386 AMD64
Imphash 646167cce332... (Asgard Protector) e387f9bdbdc8... (différent)
Commentaire de livraison http://158.94.209.95/service http://158.94.209.95/service (identique)
Tag parent dropped-by-GCleaner dropped-by-GCleaner
C2 (extrait par Triage) 160.20.109.75/d19ca32cb5a444ac8b87.php 160.20.109.75/d19ca32cb5a444ac8b87.php (identique)
Botnet ID euromix eu1

Même URL de livraison, même parent GCleaner, même C2, mais deux builds différents (imphash et architecture distincts) à une semaine d'écart, avec des identifiants de botnet différents. On a donc une même infrastructure de campagne qui fait tourner plusieurs builds ou sous-campagnes en parallèle, pas un incident isolé.

8.4 Le C2 est-il toujours actif ?

Je l'ai vérifié sans me connecter au serveur. L'IP appartient à un opérateur malveillant, et s'y connecter exposerait l'adresse de la machine qui fait la requête, sans bénéfice réel. J'ai fait deux vérifications passives à la place :

  • ThreatFox : dernière observation à 11:28 UTC le jour même de cette analyse.
  • Shodan InternetDB (scan passif déjà réalisé par Shodan, aucune requête de ma part vers le C2) : ports 22 (SSH, OpenSSH 9.6p1) et 80 (HTTP, nginx 1.24.0) ouverts sur cet IP, hébergé sur Ubuntu Linux, tag eol-product.

Un port 80 ouvert colle avec un panel C2 HTTP actif. Ça ne garantit pas à 100 % que l'endpoint précis (/d19ca32cb5a444ac8b87.php) répond encore, seulement que le serveur est en ligne et sert du HTTP. Mais avec la dernière observation ThreatFox du jour même, c'est une bonne indication que l'infrastructure tourne toujours.


9. Conclusion générale

GCleaner (delivery: http://158.94.209.95/service)
  → Stub IExpress protégé par Asgard Protector (§4, §6.4)
    → AutoIt3.exe (interpréteur légitime) + Quotes.a3x (script compilé)
      → décompilé (autoit-ripper) → déobfusqué (script maison, §5)
        → anti-VM/sandbox immédiat (vmtoolsd.exe / VboxTray.exe / SandboxieRpcSs.exe)
        → détection Sleep patché (Avast 20 s, Bitdefender 160 s)
        → adaptation du comportement selon l'AV détecté (Kaspersky, Avast, AVG, Bitdefender, Sophos)
        → dépôt persistant dans %LocalAppData%\CodeInnovate Technologies Co\ + relance VBS/Startup
          → process hollowing (section mappée localement puis copiée, NtSetContextThread), gestion x86/x64
            → StealC réel, botnet "euromix"/"eu1"
              → C2 : 160.20.109.75/d19ca32cb5a444ac8b87.php (actif, 431 sightings)

Les deux trous relevés à la fin du §7 sont comblés. Le payload final est identifié (StealC, config C2 extraite en dynamique par un tiers) et le stage amont est confirmé (GCleaner, avec la même URL de livraison retrouvée sur un second échantillon). Je n'ai pas eu à exécuter le binaire : il a suffi de lire en entier des champs d'API déjà accessibles, puis de pivoter passivement sur les indicateurs réseau qui en sortaient.

Ce loader ne se contente pas d'injecter un payload. Il adapte son comportement (noms de fichiers, flags de création, délais d'exécution) à l'antivirus qu'il détecte sur la machine, avant même de tenter l'injection.

10. Limites

  • Validation croisée des Switch. Sur 354 blocs, 351 sont résolus indépendamment par les deux méthodes croisées (arithmétique vs présence d'ExitLoop/Return) avec un accord de 100%. Les 3 blocs restants n'ont qu'un seul des deux signaux disponibles (petites tables de correspondance sans ExitLoop, un cas de figure attendu, pas une anomalie), et ont été relus et confirmés à la main un par un ; l'un d'eux est détaillé en §7.5. deobfuscate.py résout finalement les 354 blocs sans laisser aucune branche ambiguë dans deobfuscated.au3.
  • Payload final non observé. L'analyse reste 100% statique jusqu'à la §8 : le payload réellement injecté (post-hollowing) n'est jamais présent en clair dans ce binaire (cohérent avec le design du loader). Le déroulement de la création de process, la structure PROCESS_INFORMATION et la chaîne NtOpenSection/NtMapViewOfSection/NtWriteVirtualMemory sont en revanche tracés statiquement de bout en bout (§6.1), pas juste déduits de l'ordre d'apparition des appels.
  • Attribution du crypter. L'identification comme Asgard Protector repose sur une correspondance technique solide avec une source publique indépendante (§6.4), pas sur une comparaison bit à bit avec un échantillon de référence identique. La détection anti-sandbox par ping DNS décrite par cette source n'a pas été retrouvée dans mon échantillon.
  • Confirmation finale via un tiers. Le payload et sa config C2 sont confirmés par l'exécution qu'un tiers (Triage) a faite du sample, recoupée par ThreatFox et un scan passif Shodan, jamais par une exécution ou une connexion réalisée par l'auteur.

11. IOCs consolidés

Type Valeur
SHA256 (stub SFX) afbeeeaa7952579bc73b5d220ef1a828ecfdb62b80e339b007f07c82c60ab6da
MD5 (stub SFX) dc8db3908bec45fc19bfb4d2c4514474
Imphash (crypter Asgard Protector) 646167cce332c1c252cdcb1839e0cf48
SHA256 (AutoIt3.exe légitime abusé) 92c6531a09180fae8b2aae7384b4cea9986762f0c271b35da09b4d0e733f9f45
SHA256 (Quotes.a3x, script malveillant) 49ded704632abe3642b76c32c60d46ab99402495624921787e0c57a85f83327d
Second échantillon lié (AMD64) 8b7537c6624998423c0dc5e63d133a4380df59ff64a623f35f2f669e63061c52
URL de livraison (GCleaner) http://158.94.209.95/service
C2 StealC http://160.20.109.75/d19ca32cb5a444ac8b87.php
Botnet ID euromix, eu1
Clé de registre persistance Software\Microsoft\Windows\CurrentVersion\RunOnce
Dossier de dépôt %LocalAppData%\CodeInnovate Technologies Co\
Artefacts déposés InnoCoder.exe / AutoIt3.exe, q / q.a3x, InnoCoder.vbs, InnoCoder.lnk
Chaînes anti-VM XeN:, XeNa, xeNe (détection Xen)
Process kill-switch VM/sandbox vmtoolsd.exe, VboxTray.exe, SandboxieRpcSs.exe
Timestamp PE falsifié 2040-03-15 (réel probable : proche du 12-13/07/2026)

12. Reproduire l'analyse

Le journal détaillé (commande, raison, sortie brute, ce que j'en retiens) de chacune des quatre parties est dans runbook.md, qui couvre :

Prérequis et séquence complète pour rejouer de zéro : voir reproductibilité de bout en bout.