Table of contents
Contexte
Ce rapport fait suite au précédent article de blog sur Stealc. Stealc est un infostealer annoncé sur les forums clandestins XSS, Exploit et BHF par l’acteur de la menace Plymouth. Dans cet article de blog, nous nous concentrons sur l’analyse technique d’un échantillon autonome. Des similitudes ont été observées avec les infostealers de Vidar, de raccoon et de Mars pendant la phase de reverse engineering.
Les fonctionnalités implémentées dans Stealc, notamment :
- La détection de l’environnement,
- L’anti-analyse, l’obscurcissement des chaînes, la résolution dynamique de l’API,
- Une liste importante de navigateurs ciblés, d’extensions, de portefeuilles et de logiciels installés en font une menace de premier plan au sein de l’écosystème infostealer.
Analyse du malware
Les sections suivantes listent les différentes techniques observées au cours de l’opération de reverse engineering de Stealc. PLus précisement, elles fournissent des informations et des explications détaillées sur les opérations et les comportements de Stealc.
Toutes les informations sur la chaîne d’infection, la distribution et le tracking de cette menace ont été fournis dans la première partie.
L’échantillon Stealc SHA-256 utilisé pour cette analyse technique est : 77d6f1914af6caf909fa2a246fcec05f500f79dd56e5d0d466d55924695c702d
L’échantillon Stealc SHA-256 avec une étape suivante configurée est : 1587857ad744c322a2b32731cdd48d98eac13f8aa8ff2f2afb01ebba88d15359
Anti analysis
Le malware implémente des techniques anti-analyse en ajoutant un saut inconditionnel à un décalage proche, confondant le décompilateur qui ne peut pas saisir la fonction.
Comme le montre la figure 1, le décompilateur a analysé une fonction avec plusieurs instructions de saut (jz, jnz opcodes), avec une adresse de destination définie à l’adresse suivante plus un décalage de 1 ou 2 (selon le cas). Il en résulte que le décompilateur ne fait pas une hypothèse correcte et évite de décompiler la fonction.
En reconstruisant la fonction qui définit l’emplacement sur undefined et en corrigeant l’emplacement de l’octet avec une instruction NOP, decompilers can work properly. les décompilateurs peuvent fonctionner correctement. Lors de l’application de cette technique, les opcodes décompilés sont les suivants :
Ici, les instructions mov eax, 9DE9h (B8 E8 9D 00 00) sont mal montées à cause de location+1. L’annulation de la définition de l’emplacement, le remplacement du B8 de l’instructionmov par un NOP(0x90) et la redéfinition du début de l’instruction suivante vers E8 entraînent le désassemblage correct de cette section de code.
Vue d’ensemble de la fonction principale
Suite au patching de l’échantillon, la fonction principale de Stealc montre des similitudes avec celle analysée dans Raccoon and Mars stealers, notamment en termes d’ordre de fonctionnement et de techniques utilisées.
Le flux d’exécution de Stealc est simple, il désobfusque d’abord les chaînes utilisées pour une résolution d’API dynamique ultérieure.
Ensuite, il effectue diverses vérifications sur l’hôte infecté pour sortir dans des conditions particulières. Il vérifie également la quantité de RAM et si elle est exécutée par une solution antivirus. Enfin, il vérifie que la date actuelle précède la date codée en dur.
Après cette configuration et détection initiale, le malware passe à la fonction « responsible for the C2 », dans laquelle la configuration du stealer est téléchargée et les données sont exfiltrées.
Déjouer le chiffrement des chaînes
Le malware stocke ses chaînes et une partie de sa configuration est obscurcie. Les données de Stealc sont cryptées RC4 et codées en base64. La clé de déchiffrement est stockée dans le PE en clair, comme le montre la première affectation de variable sur la figure 5.
Pour une analyse plus approfondie, un script IDA permettant de déchiffrer les chaînes et d’affecter leur valeur au DWORD correct est fourni à l’annexe 2.
Résolution dynamique de l’API
Pour réduire son taux de détection par les solutions antivirus, Stealc utilise la technique de la résolution dynamique de l’API (T1027.007). Pour ce faire, le malware recherche l’adresse de base kernel32 à l’aide de la structure d’en-tête et passe par LDR_DATA_TABLE_ENTRY. Ensuite, il itére sur la tableau jusqu’à ce qu’il corresponde à la fonction GetProcAddress et retourne l’adresse de l’entrée dédiée.
Dans la figure 6, le registre EAX est utilisé pour stocker l’adresse de base kernel32 :
- Le répertoire fs:000030h est l’adresse de ProcessEnvironmentBlock (PEB) membre de la structure ThreadEnvironmentBlock (TEB) ;
- L’offset 0xC de la structure PEB est le membre de la structure LDR_DATA qui contient un pointeur vers le membre InMemoryOrderModuleList;
- InMemoryOrderModuleList est une structure de type LIST_ENTRY dont le membreDllBase pointe vers Kernel32 (voir Figure 6)
Une fois que le malware obtient l’adresse de GetProcAddress, il charge la fonction LoadLibrary et d’autres fonctions de kernel32 y compris OpenEventA, CreateEventA, Sleep, VirtualAlloc, etc.
LoadLibrary est utilisé pour charger advapi32, gdi32, user32, crypt32 et ntdll DLLs, seules les fonctions spécifiques de ces bibliothèques sont chargées par la suite.
Détection et contrôle de l’environnement
Stealc tente de détecter son environnement dans deux buts :
- Quitter dans des conditions particulières (environnement sandbox, emplacement indésirable, etc.)
- Empreinte digitale de l’hôte
Le malware implémente les conditions de sortie suivantes :
- • Le nom d’utilisateur est JohnDoe (nom d’utilisateur par défaut de Windows Defender emulator);
- • Le nom d’hôte est HAL9TH (Windows Defender emulator default hostname);
- • La langue configurée est le russe;
- • La date d’expiration est en retard, une date est codée en dur dans le binaire, si cette date est dépassée, le malware se ferme. Il s’agit presque certainement d’une fonctionnalité ajoutée à la construction par le(s) développeur(s) dans le cadre de leur business model;
- La section .text doit être accessible en écriture (par défaut, Stealc configure son fichier .text avec une autorisation d’écriture).
- La capacité de la RAM est inférieure à 1GB;
- Aucun affichage n’est configuré.
Diverses fonctionnalités
Stealc implémente également des fonctionnalités communes aux autres malwares de la famille des stealers. Il a la capacité de prendre une capture d’écran de l’hôte infecté et de prendre les empreintes digitales de la machine infectée. Pour récupérer ces informations, l’échantillon interroge les clés de Registre appropriées et interagit avec l’API Windows.
Les données récupérées concernant :
- L’adresse IP publique;
- La géolocalisation;
- L’ID du matériel;
- La version du système d’exploitation;
- L’architecture;
- Le nom de l’utilisateur;
- Le nom de l’ordinateur;
- L’heure locale;
- La Langue;
- Le clavier;
- Ressources physiques: CPU (core, name), RAM, nombre de threads, résolution d’affichage et pilote GPU;
- Liste des processus en cours d’exécution;
- Liste des applications installées.
Communication Command and Control
Le malware communique via HTTP. Les données sont envoyées dans des requêtes POST qui utilisent une structure multi-forme dont les formulaires sont les données volées encodées en base64.
Lors de la première interaction, l’hôte infecté envoie son HWID (hardware ID) et son nom de build (la valeur est « default« ).
Le serveur répond avec la chaîne base64 suivante :
MWZjZTYzMTFhZDg1NmUzYTVjNTQ5OTQ0NDU0NWJmOGJjNjc2MDc0YTY3ZWIwZDJiMmZiNTQwMWE4OTMxODM3Y2NiZDlhMTllfGlzZG9uZXxkb2NpYS5kb2N4fDF8MXwwfDF8MXwxfDF8MXw=
Le contenu décodé à partir du format base64 est :
1fce6311ad856e3a5c5499444545bf8bc676074a67eb0d2b2fb5401a8931837ccbd9a19e|isdone|docia.docx|1|1|0|1|1|1|1|1|
Le premier hachage est en fait un identifiant utilisé comme jeton pour toutes les communications, et envoyé dans un format dédié pour chaque message.
Les premières communications du malware visent à télécharger la configuration du stealer, par exemple, le chemin et les modèles de fichiers à rechercher sur l’hôte infecté, les portefeuilles ou les extensions à rechercher, etc.
Le stealer obtient sa configuration du C2, avec une requête POST dont les deux formulaires sont envoyés. Le nom du formulaire « message » indique quel type de données sera envoyé. Il peut s’agir de « navigateur », « plugin », « portefeuilles » ou « fichiers ». La structure de la réponse C2 (pour la configuration) est toujours la même. Les données sont concaténées avec le caractère | (Voir figure 9).
Il répète cette même opération pour chaque navigateur, leurs extensions, pour les portefeuilles et les applications installées.
La liste des actifs ciblés est fournie dans la partie 1 de cet article; et plus précisement dans l’annexe 1 dédié aux capacités de Stealc.
Après avoir téléchargé la configuration du bot, le stealer envoie les informations d’empreintes digitales (Pour voir la liste des données exfiltrées, consulter la section de cet article dédiée à la présentation des diverses fonctionnalités de Stealc).
Le tableau ci-dessous affiche les communications entre l’hôte infecté et Stealc C2.
Request | Request forms | Response | Functionality |
Register infected host and download configuration | |||
POST main URL | hwid, build name | token | |
POST main URL | token, message=”browsers” | browsers configuration | configure the browsers stealing operation |
POST main URL | token, message=”plugins” | plugins configuration | configure the plugins stealing operation |
POST main URL | token, host fingerprint (RAM, OS, apps, etc) | ||
Target Chromium-based browsers (e.g. Chrome, Chromium, Edge) | |||
GET DLLs URL sqlite3.dll | download sqlite3.dll | ||
POST main URL | token, file_name, file | Chrome cookies | |
POST main URL | token, file_name, file | Chrome history | |
POST main URL | token, file_name, file | Chrome extensions (exfiltrated each file separately) | |
GET DLLs URL freebl3.dll | download freebl3.dll | ||
GET DLLs URL mozglue.dll | download mozglue.dll | ||
GET DLLs URL msvcp140.dll | download msvcp140.dll | ||
GET DLLs URL nss3.dll | download nss3.dll | ||
GET DLLs URL softoknn3.dll | download softokn3.dll | ||
GET DLLs URL vcrunctime140.dll | download vcrunctime140.dll | ||
Target Firefox-based browsers, repeat the actions executed for Chromium-based | |||
Target Opera-based browsers, same actions executed for Chromium-based | |||
POST main URL | token, message=”wallets” | list of targeted wallets | configure the wallets stealing operation |
POST main URL | token, message=”files” | file grabber configuration | configure the file grabber |
POST main URL | token, file_name, file | exfiltrate each file matching the grabber configuration | |
Target desktop applications: Outlook, Steam, Tox, Pidgin, Discord, Telegram | |||
POST main URL | token, file_name, file | send the screenshot | |
POST main URL | token, message=isdone | Next stage URL | Get the URL of the next stage to execute |
GET unrelated URL to Stealc infrastructure | Executable | Download the next stage |
Tableau 1. Tableau des communications HTTP de Stealc avec le C2
- main url: 752e382b4dcf5e3f.php
- DLLs url: /dbe4ef521ee4cc21/
Pour chaque navigateur, portefeuille, plugins, les mêmes actions sont répétées et les formulaires sont les mêmes. La dernière communication est facultative. Cette demande n’est envoyée que si Stealc a une prochaine étape configurée sur son panneau.
Capture de fichiers
Après avoir dérobé les données des navigateurs ciblés et leurs extensions, le stealer utilise sa fonctionnalité de capture de fichiers. La configuration du module d’acquisition est reçue du C2 et est formatée comme suit :
standart|%DESKTOP%\\|*.txt,*.doc,*.docx,*.xls|7000|1|0|
La structure de la configuration est la suivante. D’abord un nom, puis un répertoire ou un raccourci vers un répertoire (ici le bureau), troisièmement une liste d’extensions de fichiers que le malware veut exfiltrer et enfin la taille maximale. Nous avons également identifié deux paramètres supplémentaires qui n’étaient pas utiles pour l’analyse.
Dans le cas où le nom de fichier et le chemin correspondent aux filtres de la carte d’acquisition, il est exfiltré dans une requête POST au C2 avec trois formulaires.
Form ID | Form name | Form value |
1 | token | The token value provided by the C2 in the earlier communication |
2 | file_name | The full file path to the stolen file encoded in base64 |
3 | file | The file content encoded in base64 |
Chargement des DLLs
Pour accéder à des fichiers ou des données particuliers, Stealc nécessite des DLL externes qui ne sont pas intégrées dans le PE mais plutôt téléchargées à partir d’une URL spécifique hébergée par le C2. Les DLL téléchargées sont les suivantes :
- sqlite3.dll
- freebl3.dll
- mozglue.dll
- msvcp40.dll
- nss3.dll
- softokn3.dll
- vcruntime140.dll
Les DLL sont toutes écrites dans le répertoire C:\ProgramData\ et sont ensuite chargées (TTP: Shared Module: T1129). Il est à noter que seules des fonctions spécifiques sont chargées par le malware.
Après avoir chargé les fonctionnalités requises à partir des DLL, Stealc les exploite pour accéder aux données d’intérêt. De même, lorsqu’une donnée ciblée est trouvée sur l’hôte infecté, elle est envoyée au C2 en utilisant une requête POST et en encodant les données en base64.
Comme décrit dans cette section, Stealc peut être bruyant dans le cas où de nombreux fichiers sont exfiltrés vers le C2.
Étape suivante
Comme d’autres stealers analysés où l’on observe la mise à niveau de leurs fonctionnalités, Stealc est également capable de télécharger et d’exécuter une nouvelle étape de charge utile. L’étape suivante est configurée par la requête contenant le format « isdone » or “done”, selon le type d’échantillon. Le C2 répond avec une base de données 64 contenant l’URL de l’étape suivante à télécharger.
L’échantillon (Stealc SHA-256: 1587857ad744c322a2b32731cdd48d98eac13f8aa8ff2f2afb01ebba88d15359) est configuré pour exécuter une étape suivante qui est un Laplas Clipper. Voici la réponse de Stealc C2 pour configurer l’étape suivante. La prochaine charge utile est configurée par une URL que Stealc télécharge et exécute (Voir figure 15).
Suppression de ses traces
Stealc tente de réduire ses traces d’infection en les supprimant lui-même , y compris ses DLL téléchargées (T1070.004) avec la commande d’une ligne suivante :
cmd.exe /c timeout /t 5 & del /f /q "$STEALERPATH" & del "C:\ProgramData\*.dll" & exit
La commande est exécutée avec une fonction ShellExecuteA de Shell32.dll.
Conclusion
Stealc affiche toutes les fonctionnalités et tous les comportements pour être un outil viable dans le catalogue des infostealers. Il sera certainement incorporé dans des ensembles d’intrusions, soit en tant qu’outil de remplacement, soit en tant qu’une extension de leurs capacités.
D’après les similitudes observées entre Stealc et d’autres logiciels malveillants de la famille infostealer, notamment Raccoon et Mars stealer, les analystes de Sekoia.io estiment qu’il s’agit probablement : d’une confirmation de transmission et de circulation de connaissances. Celles-ci concernent le code source mais aussi les ressources humaines de l’écosystème cybercriminel russophone.
Les analystes de Sekoia.io s’attendent à ce que le développeur de Stealc continue à mettre à jour son stealer avec des fonctionnalités nouvelles et/ou améliorées à court terme. Ceci afin de répondre aux attentes des clients et élargir sa clientèle.
Pour fournir à nos clients des renseignements exploitables, les analystes de Sekoia.io continueront de surveiller les infostealers émergents et déjà reconnus, y compris Stealc.
Annexe 1 – Extraction de la configuration
Comme présenté dans la section Chaînes d’obscurcissement, Stealc intègre l’adresse du C2 et ses différentes URLs dans la section rdata du PE.
D’après nos observations, le script devrait répondre aux exigences suivantes :
- Récupérer la clé RC4 dans rdata;
- Désobfusquez les chaînes jusqu’à ce que tous les motifs liés au C2 soient repérés.
La clé RC4 est codée en dur dans le PE en clair. Et par définition, les clés RC4 ont une longueur de 20 octets. Les informations Stealc C2 sont stockées avec la structure suivante :
- C2 base URL: http://<ip or domain> or https://<ip or domain>;
- C2 URL resource which is a random string ending by .php extension;
- C2 directory name where the DLLs are hosted (nss3.dll, sqlite3.dll, etc…).
L’extracteur de configuration fourni effectue simplement une boucle sur cette section pour trouver les modèles décrits précédemment.
from base64 import b64decode
from pefile import PE, SectionStructure
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
class Stealc:
"""Stealc configuration"""
rc4_key: bytes = b""
base_url: str = ""
endpoint_url: str = ""
dlls_directory: str = ""
def __str__(self):
out = f"Stealc RC4 key: {self.rc4_key}\n"
out += f"SteaC Command and Control:\n"
out += f"\t- {''.join([self.base_url, self.endpoint_url])}\n"
out += f"\t- {''.join([self.base_url, self.dlls_directory])}\n"
return out
def rc4_decrypt(self, data: bytes) -> bytes:
"""decrypt RC4 data with the provided key."""
algorithm = algorithms.ARC4(self.rc4_key)
cipher = Cipher(algorithm, mode=None)
decryptor = cipher.decryptor()
return decryptor.update(data)
def get_section(pe: PE, section_name: str) -> SectionStructure:
"""return section by name, if not found raise KeyError exception."""
for section in filter(
lambda x: x.Name.startswith(section_name.encode()), pe.sections
):
return section
available_sections = ", ".join(
[_sec.Name.replace(b"\x00", b"").decode() for _sec in pe.sections]
)
raise KeyError(
f"{section_name} not found in the PE, available sections: {available_sections}"
)
def get_rdata(pe_path: str) -> SectionStructure:
"""Extract Stealc radata section"""
pe = PE(pe_path)
section_rdata = get_section(pe, ".rdata")
return section_rdata
def is_valid_string(data: bytes) -> bool:
return True if all(map(lambda x: x >= 43 and x <= 122, data)) else False
def search_Command_and_Control(stealc: Stealc, rdata_section: SectionStructure):
"""
Search two types of strings in rdata section of Stealc:
1. The RC4 key which is 20 bytes long;
2. Strings matching the way Stealc stores its C2 configuration (these strings are decoded (base64 decode + RC4 decryption),
This works for the Stealc version at least until 15 Feb 2023 but could change in new versions...
2.1 base url (`http://something...` or `https://something...`)
2.2 endpoint which ends with `.php`
2.3 DLLs directory starts and ends with `/` (eg: `/something_random/`)
"""
for string in filter(
lambda x: x and is_valid_string(x), rdata_section.get_data().split(b"\x00" * 2)
):
if len(string) == 20 and not stealc.rc4_key:
# Hopefully the RC4 key is stored as the beginning of the rdata section
stealc.rc4_key = string
print(f"[+] RC4 key found: {stealc.rc4_key}")
if stealc.rc4_key and string != stealc.rc4_key:
try:
cleartext = stealc.rc4_decrypt(b64decode(string))
# print(f"{string.decode():<40} {cleartext}")
if cleartext.startswith(b"http://") or cleartext.startswith(
b"https://"
):
print(f"[+] Found StealC Command and Control")
stealc.base_url = cleartext.decode()
elif cleartext.startswith(b"/") and cleartext.endswith(b"/"):
print(f"[+] Found DLLs URL directory name")
stealc.dlls_directory = cleartext.decode()
elif cleartext.endswith(b".php"):
print(f"[+] Found StealC endpoint")
stealc.endpoint_url = cleartext.decode()
except Exception:
pass
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print(f"not enough parameter, please provide as argument the path to stealc sample.")
stealc = Stealc()
rdata = get_rdata(sys.argv[1])
search_Command_and_Control(stealc, rdata)
print(stealc)
Annexe 2 – Script IDA pour la désobfuscation des chaînes
from idaapi import *
from ida_bytes import *
from ida_name import *
from base64 import b64decode
from string import ascii_letters, digits
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
def read_rdata(name: str) -> str:
print(f"read_rdata: {name}")
addr = get_name_ea_simple(name)
size = get_max_strlit_length(addr, ida_nalt.STRENC_DEFAULT)
return get_bytes(addr, size - 1)
def rc4_decrypt(key: bytes, data: bytes) -> bytes:
algorithm = algorithms.ARC4(key)
cipher = Cipher(algorithm, mode=None)
decryptor = cipher.decryptor()
return decryptor.update(data)
def deobfuscate_string(base: int, end: int , KEY: bytes):
ea = base
size = 0
clear = []
addr = []
while ea <= end:
flags = ida_bytes.get_flags(ea)
if ida_bytes.is_code(flags):
instr_str = idc.generate_disasm_line(ea, 1)
instr_str = " ".join(instr_str.split())
if instr_str.startswith("push offset a") or instr_str.startswith("mov dword ptr [esp], offset a"):
value = instr_str.split("offset")[-1].split(';')[0].strip()
value = read_rdata(value)
clear.append(rc4_decrypt(KEY, b64decode(value)))
elif instr_str.startswith("mov dword_"):
temp = instr_str.replace("mov dword_", "")
temp = temp.split()[0].replace(",","")
addr = int(temp, 16)
string = get_bytes(addr, size)
cleartext = clear.pop(-1)
cleartext = cleartext.decode()
idc.set_cmt(ea, cleartext, 0)
text = ""
for c in cleartext:
if c in f"{ascii_letters}{digits}":
text += c
else:
text += "_"
cleartext = f"str_{text}"
print(f"replace dword_{addr:x} by `{cleartext}`")
set_name(addr, cleartext)
ea += 1
Merci d’avoir lu cet article de blog. Vous pouvez également consulter d’autres résultats d’enquêtes réalisées par nos analystes sur l’écosystème des infostealers :