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 danstools/.
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 PEcapa9.4.0 + règles officiellescapa-rules, pour la détection de capacitésfloss3.1.1, pour l'extraction de chaînes cachées/décodéescabextract, pour l'extraction d'archive CAB embarquéeautoit-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 au15/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'importCabinet.dllet l'entropie de.rsrc.persist via Run registry key: persistance viaSoftware\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) vaut134742020, soit0x08080004en hexadécimal. Le bit0x4correspond 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.exeest le nom de process de Kaspersky Anti-Virus. Si Kaspersky tourne, la valeur passe à0x08000004: toujoursCREATE_SUSPENDED, mais sans le bitEXTENDED_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 à
CreateProcessWest$KNIGHTANALYSTSTREAMTWENTY = DllStructCreate("ptr Process; ptr Thread; dword ProcessId; dword ThreadId"), une réplique champ pour champ de la structure WindowsPROCESS_INFORMATION. Juste après l'appel,$PACIFIC_EDet$N_CONFUSIONALTERNATIVEGAUGE(utilisés ensuite dansNtWriteVirtualMemory,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 retourDllCallcorrespond au premier paramètre passé par référence, ici le handle de section ouvert). La chaîneNtOpenSection→NtMapViewOfSection→NtWriteVirtualMemoryest 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 viaNtWriteVirtualMemory. 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 estVboxTray.exe;ProcessExistsétant insensible à la casse, cela cible bien le vrai processVBoxTray.exede VirtualBox.) - Préparation d'un spoofing de PPID ciblant
explorer.exe: un handle est ouvert sur le processexplorer.exeviaOpenProcess, cohérent avec l'utilisation observée en §6.1 deInitializeProcThreadAttributeList/UpdateProcThreadAttributepour faire apparaître le process injecté comme un enfant d'explorer.exeplutô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 :
- Mesure le temps réel via
QueryPerformanceCounter/QueryPerformanceFrequencyavant l'appel. - Appelle
Sleep($ms). - Mesure à nouveau le temps réel après l'appel.
- 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 sansExitLoop, 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.pyrésout finalement les 354 blocs sans laisser aucune branche ambiguë dansdeobfuscated.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_INFORMATIONet la chaîneNtOpenSection/NtMapViewOfSection/NtWriteVirtualMemorysont 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 :
- Partie 1 : extraction du stub IExpress et du loader AutoIt (§1 à 4 ci-dessus)
- Partie 2 : déobfuscation (§5 à 6), dont la validation croisée des
Switchet l'identification du crypter par imphash - Partie 3 : persistance et évasion anti-AV (§7)
- Partie 4 : confirmation externe (§8)
Prérequis et séquence complète pour rejouer de zéro : voir reproductibilité de bout en bout.