20 minute read

This is a last part of our analysis report of Stealc, an information stealer promoted by its supposed developer Plymouth on Russian-language underground forums and sold as malware as a service since January 9, 2023.


Chrome Cookies

the first file hit is the Cookies file and to avoid detection and security configuration Stealc copies the Cookies file to a new file which enables it to do what it can do without caring about file handles that may look wired if a solution is found that a normal executable owns a handle for cookies file.


so copying the DB files to the ProgramData path will be done with all files.

then Stealc will start accessing DB and execute queries, I am going to explain it in detail step by step, keep reading.

1- it first calls sqlite_open which opens a database file and returns a handle to the Database connection on the ppb argument

int sqlite3_open(  
  const char *filename,   /* Database filename (UTF-8) */  
  sqlite3 **ppDb          /* OUT: SQLite db handle */  


2-compile SQL Query using Sqlite3_prepare_v2 to be used again to extract data from the DB file

int sqlite3_prepare_v2(  
  sqlite3 *db,            /* Database handle */  
  const char *zSql,       /* SQL statement, UTF-8 encoded */  
  int nByte,              /* Maximum length of zSql in bytes. */  
  sqlite3_stmt **ppStmt,  /* OUT: Statement handle */  
  const char **pzTail     /* OUT: Pointer to unused portion of zSql */  


SELECT HOST_KEY, is_httponly, path, is_secure,  
(expires_uc/1000000)-11644480800,name, encrypted_value from cookies

so let’s break down this query and see the expected output.

  • HOST_KEY: The domain of the website that sets the cookie.
  • is_httponly: Whether or not the cookie can be accessed by JavaScript.
  • path: The path on the website where the cookie is valid.
  • is_secure: Whether or not the cookie is only sent over secure connections.
  • (expires_uc/1000000)-11644480800: The expiration date of the cookie in Unix time.
  • name: The name of the cookie.
  • encrypted_value: The encrypted value of the cookie.

and if we take a look at this Sqlite file in any viewer it will ensure our analysis and decryption phase.


3- Call SQLite3_step which is used to execute a prepared statement obtained from SQLite_perpare_v2 and advance to the next row of results

int sqlite3_step( sqlite3_stmt* stmt );

so it will call Sqlite3_step Repeatedly until it returns 100 -> (SQLITE_ROW), indicating that another row of output is available.


4-calls Sqlite3_column_text and this function will contain the output of the query based on the pushed column number in the iCol argument as a UTF-8 string.

const unsigned char *sqlite3_column_text(  
      sqlite3_stmt *stmt,     // handle to the perpared statemtn  
       int iCol               // the index of the columen to be retrived  


after each API call, it will copy the result to a local or global variable then it will compare the result of the index (1) => is_http_only and it returns a boolean value. It indicates whether the cookie is marked as HttpOnly, HttpOnly cookies are not accessible via JavaScript.

also, it checks the is_secure element which also returns a boolean output, and in the 2 conditions it will append “False” if the output is “0”


Then it collects all these outputs in one buffer using StrCatA which will be sent to C2.

5-then it starts handling index 6 “encrypted_value” and this is done by first calling sqlite3_column_bytes to get the length of this column and then calling sqlite3_column_blob to extract the data of the column but this time in byte format cause it may not be formatted as a string.

then it will pass the 2 outputs to the mw_Decrypt_Using_AES() function which will use the generated Asymmetric key obtained before from the local state file after decrypting the key using DAPI to decrypt the encrypted value.


inside the AES decryption routine it will first compare the first 3 bytes of the encrypted bytes against str “v10” and if it does not match it will exit the function.


and “v10” here refers to the version used by the website when it saved the cookies so why does it exit if the version is different?

we have two versions of cookies used by Chrome when it handles cookie encryption,v10, and v11

  • v10: Uses static private key “peanuts” salted with “saltysalt”
  • v11: Stores private key in Operating System’s key chain

then it will decrypt this data using the key handle of AES private key using BcryptDecrypt API


after decryption, it will append the decrypted cookie value to the buffer which will be sent to the C2 server, and it will iterate over all the db file to retrieve the encrypted_value column values and decrypt it.

and here is how the data is formatted for every record.


and the cookies buffer is base64 encoded and sent to the C2 server, and as the first data is pushed to C2 a packet ID is generated and appended to HTTP headers.


after that, it will close the DataBase handle and delete the dropped file.


Chrome History

the same as cookies is done with the History file.

1-get handle for **_AppData\Local\Google\Chrome\User Data\Default\History_**

2-copy the file to %programdata% folder with a random name

3-open handle to the database file.

4-Execute a query to retrieve the first 1000 history record

SELECT url FROM urls LIMIT 1000

5- base64 data and send it to C2 then close database handle and delete the file from %programdata% folder.

Chrome Login Data

after collecting cookies and history it will get a handle to the login_data file

**_AppData\Local\Google\Chrome\User Data\Default\Login Data_**

which contains the username and password for every website the user logged to.

the query in this case is different


SELECT origin_url, username_value, password_value FROM logins

the password_value is AES Encrypted so it will handle it as same as the cookies file.


The Chrome login data is not sent directly after allocating it , because it allocates all logins related to all browsers in the victim machine then it sends them into one buffer but absolutely the data is formatted to identify the browser these data belong to.

Chrome Web Data: autofill data

This file is so important as login data this file contains information about many settings :

  • autofill data
  • contact info
  • saved credit cards
  • IBANs numbers
  • all payment data

it handles web data via 2 phases, first, it tries to get autofill data and saves it remotely in a txt file, and here is the query used to retrieve auto-fill data.


SELECT name, value FROM autofill

to test all of this I previously created a record in Chrome to be saved in a web data file and our stealer here succeeded in retrieving this data





and then it resolves a string to identify this data cause it is specifically related to Chrome using this string => **“autofill\Google Chrome_Default.txt”**

and then send the data to c2, and then it closes the DB handle.

Chrome Web Data: Credit Card data

after that, it will try to retrieve saved credit cards in Chrome and append this string to be used as an identifier to this data or it will be used to save this data remotely in c2.

**“cc\Google Chrome_Default.txt”**

and it executes this query which will be used to retrieve the card name, exp month and year, and card number.

SELECT name_on_card, expiration_month, expiration_year, card_number_encrypted FROM credit_cards


and then the card number will be decrypted using the AES key handle and then the data will be sent to C2, it closes the DB handle and removes the dropped file in the %programdata% folder.

Chrome Extensions (Crypto Wallets):

as we know some extensions handle cryptocurrency operations that act as crypto wallets and these extensions are targeted by Stealc, so after it steals browser data it will iterate over all extensions that had been downloaded previously to extract its data and send it to TA, How does this happens? that is what I will explain next.

it first gets a pointer to an array of structures, and every structure contains data about a specific extension like name, name length, and ID

struct extension  
  Dword* Name_ptr;  
  Dword Null_bytes;  
  Dword Name_length;  
  Dword* ID_ptr  


then using FindFirstFile and FindNextFile it iterates over all folders in UserData folder just to get the path of the extension and due to time the required file path is :

**_%APPDATA%local\Google\Chrome\UserData\Default\local extension settings\<Extension_ID >_**

and then after getting the path of this file, it will then construct an identifier for this file to be used in c2 communication to identify this data.

I have installed MetaMask Extension to simulate this process and the constructed Identifier contains the browser this extension is installed on and the plugin name and “local” or “Sync” extension to identify if the extension data is saved locally or being synchronized with a remote server.

**_plugins\MetaMask\Google Chrome\Default\Local Extension Settings_**

then it will copy this first file on the extension folder to a program data path as it did before with web data


it then reads this file by allocating a buffer using localalloc and then using Readfile with ReadOnly Handle gotten by CreateFileA it fills the buffer with file data.

and here is the file that will be used in the C2 connection as ID for the posted data.

**_plugins\MetaMask\Google Chrome\Default\Local Extension Settings\000005.ldb_**

then it will encode the data and the file Identifier using base64 and send it to C2, and it does this for all files in the <extension_id> folder, in my case when I installed MetaMask:


and then will iterate over the full array of extensions and try to find its files if it is and send them to the TA server.

Firefox Credentials:

due to that, Stealc handles Opera-based browsers the same as Chrome but the DB files are saved in different destinations, so I will explain the difference between file structure and settings of Chrome and Firefox

it first constructs the path where Firefox saves its data


and then start downloading 6 DLLs by constructing the URL of each DLL and the local folder to be saved in.


it downloads 6 Dlls to be used to retrieve Firefox Data

  • Freebl3.dll
  • mozglue.dll
  • nss3.dll
  • vcruntime140.dll
  • softokn3.dll
  • msvcp140.dll


then it drops these Dlls in the ProgramData path


then it will start resolving some APIs from Nss3.dll by first getting a handle to this DLL and then trying to get addresses for :

  • NSS_Init
  • NSS_Shutdown
  • PK11_GetInternalKeySlot
  • PK11_FreeSlot
  • PK11_Authenticate
  • PK11SDR_Decrypt


and when it comes to Firefox it will search for 4 files that contain user data and browsing settings


  • This file is responsible for storing information about web cookies


  • database file used by Mozilla Firefox to store information related to web forms and user input.


  • database file used by the Mozilla Firefox web browser to store various information related to your browsing history, bookmarks, and other web-related data


  • stores all of the saved logins for the Firefox profile. This includes the website URL, username, and password for each saved login

For the 3 SQLite files it handles them the same as Chrome using Sqlite APIs resolved before but with different queries, and also the data is not encrypted as Chrome

Firefox: Cookies.SQLite

it handles Firefox Cookies as Chrome but with a different query, also firefox cookies are not encrypted or if it is it will decrypt them remotely in C2 so it extracts them and sends them immediately to C2


SELECT host, isHttpOnly, path, isSecure,expiry, name, value FROM moz_cookies

  • host: The domain name of the website that set the cookie.
  • isHttpOnly: A boolean value indicating whether the cookie can only be accessed by HTTP requests, or if it can also be accessed by JavaScript.
  • path: The path on the website where the cookie is valid.
  • isSecure: A boolean value indicating whether the cookie is only sent over secure HTTPS connections.
  • expiry: The date and time when the cookie expires.
  • name: The name of the cookie.
  • value: The value of the cookie.

Firefox: Places.SQLite

as I have said before places file stores history and some settings like bookmarks etc..

but it only retrieves the history record


SELECT url FROM moz_places LIMIT 1000

and here is how this Data was constructed and sent to C2


Firefox: FormHistory.SQLite

As I said before this file contains data related to automated filling and web forms


SELECT fieldname, value FROM moz_formhistory

and here is the result of the query on a Sqlite viewer


note* this a fake account :)

Firefox: Logins.Json

this file stores the most important data in Firefox so Stealc handles it differently, let’s explain it in formatted steps to make it easier to understand.

1- it gets a handle with the (OPEN_EXISTING) flag for the JSON file not the original file but the file that was copied to
Program Data path as it did with Chrome

2- it reads the file in memory, let’s parse the file on an online JSON beautifier


3- it will get a ptr to 2 elements on this file which are the most important “encryptedUserName” and “encryptedPassword” using strstr API

4- after getting these two elements it will go forward to encrypt them via some steps but after decoding them from base646 cipher.


5- it will execute a call to PK11_GetInternalKeySlot that returns a pointer to the internal key slot.

and this is the type of keys

  • keyType: The type of key to return the internal key slot for. The key type can be any of the following:
  • PK11_TYPE_DSA: A DSA key.
  • PK11_TYPE_RSA: An RSA key.
  • PK11_TYPE_ECDSA: An ECDSA key.
  • PK11_TYPE_DH: A Diffie-Hellman key.
  • PK11_TYPE_KEA: A Kerberos key.

6- It then will call PK11_Authenticate,PK11_Authenticate a function typically used to perform authentication or login actions in the context of cryptographic tokens, security modules, or hardware security modules (HSMs). Here’s a general description of what this function does


7- then it will call PK11SDR_Decrypt to decrypt the cipher text using the token offered before from the Authenticate function.

Send Login Data

after allocating all usernames and passwords Stealch will format this data and send it to C2, and in my VM I only had installed Chrome and logged into only one website due to testing purposes (dropbox.com).

it first will encode the allocated login data which is formatted in YAML format.

browser: Google Chrome  
profile: Default  
url: https://www.dropbox.com/login  
login: f73eb0a00c@emailboxa.online  
password: 123456#Lol


then it will encode the string “docia.docx” which will be used as an identifier for the login data



Local Crypto Currency Wallets

the next part involves how Stealc Exfiltrate crypto wallets which data is stored on the device through some steps.

1- it first asks C2 for Wallets configuration.



and here is the decoded data

crypto wallet Name wallet configuration path boolean value
Bitcoin Core    		|\Bitcoin\wallets\|wallet.dat|1|  
Bitcoin Core Old		|\Bitcoin\|*wallet*.dat|0|  
Dogecoin				|\Dogecoin\|*wallet*.dat|0|  
Raven Core				|\Raven\|*wallet*.dat|0|  
Daedalus Mainnet		|\Daedalus Mainnet\wallets\|she*.sqlite|0|  
Blockstream Green		|\Blockstream\Green\wallets\|*.*|1|  
Wasabi Wallet			|\WalletWasabi\Client\Wallets\|*.json|0|  
Ethereum				|\Ethereum\|keystore|0|  
Electrum				|\Electrum\wallets\|*.*|0|  
ElectrumLTC				|\Electrum-LTC\wallets\|*.*|0|  
Exodus					|\Exodus\|exodus.conf.json|0|  
Exodus					|\Exodus\|window-state.json|0|  
Exodus					|\Exodus\exodus.wallet\|passphrase.json|0|  
Exodus					|\Exodus\exodus.wallet\|seed.seco|0|  
Exodus					|\Exodus\exodus.wallet\|info.seco|0|  
Electron Cash			|\ElectronCash\wallets\|*.*|0|  
MultiDoge				|\MultiDoge\|multidoge.wallet|0|  
Jaxx Desktop (old)		|\jaxx\Local Storage\|file__0.localstorage|0|  
Jaxx Desktop			|\com.liberty.jaxx\IndexedDB\file__0.indexeddb.leveldb\|*.*|0|  
Atomic					|\atomic\Local Storage\leveldb\|*.*|0|  
Binance					|\Binance\|app-store.json|0|  
Binance					|\Binance\|simple-storage.json|0|  
Binance					|\Binance\|.finger-print.fp|0|  
Coinomi					|\Coinomi\Coinomi\wallets\|*.wallet|1|  
Coinomi					|\Coinomi\Coinomi\wallets\|*.config|1|

then the data is parsed on the stack by creating an array of structures for each wallet.

struct wallet  
  DWORD* Wallet_Name;  
  DWORD Null_Value;  
  DWORD Wallet_Name_Length;  
  DWORD* Wallet_Path;  
  DWORD Null_Value_1;  
  DWORD Wallet_Path_Length;  
  DWORD* Wallet_Config_File;  
  DWORD Null_Value_2;  
  DWORD Wallet_Config_File_Length;  
  bool flg_value; // 0 or 1  


inside sub_40111E it will start stealth behavior related to wallets by getting the path of (APPDATA\Romaing) using SHGetFolderPathA API using 0x1A as CSIDL

inside (%APPDATA%Romaing) it will iterate over all files until it hits the wallet path that passed previously to sub_40111E which for the first element in the wallets array is (Bitcoin\wallets), so I have created this file to emulate that.


so it will keep iterating on files in this folder "APPDATA\Romaing\\" until it finds the "Bitcoin\wallets" folder then it searches for “wallet.dat” which stores the wallet information


then it will copy this file “wallet.dat” to %ProgramData” folder with a randomly generated name.

then it will read the created file



then this file content is base64 encoded and sent to C2.

Efiltrate Files

then C2 will feed Stealc with some file names to collect and exfiltrate it to TA, these files are related to some cryptocurrency wallet configuration and local password file where the user may save its passwords.




then it will try to get the path of some common file and check if the path exists on the decoded stream received from C2



_ProgramFiles x86_

and then will iterate over these folders and subfolders, if it finds any file that contains any string of those which was sent from C2 like “seed”, “passphrase”,” MetaMask” etc.., it will read its content and send it to C2

Usage of COM

COM => Stealc is using the component object model to handle ShellLinks or shortcuts cause it may find a file that has a name like “seedX.lnk” so if it copies this file it will copy the shortcut itself not the original file pointed by the shortcut so it handle this via COM to get the original file, so lets summary this in some steps.

1: it initializes the com interface via CoCreateInstance

HRESULT CoCreateInstance(  
  [in]  REFCLSID  rclsid,  
  [in]  LPUNKNOWN pUnkOuter,  
  [in]  DWORD     dwClsContext,  
  [in]  REFIID    riid,  
  [out] LPVOID    *ppv  

the rclsid used here is {000214EE-0000–0000-C000–000000000046} => IShellLinkA so to handle this interface we need to convert the type of PPV to be a type of (_IShellLinkA*)** which will contain a handle to the created **_COM


2- it will create another object for another interface by this time using the previously created COM and execute a call for


{0000010b-0000–0000-C000–000000000046} => UCOMIPersistFile

unk_40f040 => refers to interface-id


the object refers to IPersistFile Interface.

3-then it will get a handle to the file using load method from IPersistFile interface with Read permission (STGM_READ) => second argument


4- it executes a call to resolve method to find the target of a Shell link, even if it has been moved or renamed, (SLR_NO_UI) to not display a dialog box if the link cannot be resolved


5- next, it will get the path of the file pointed by the short link using GetPath method with (SLGP_RAWPATH) to retrieve the raw path name

  [out]     LPSTR            pszFile,  
  [in]      int              cch,  
  [in, out] WIN32_FIND_DATAA *pfd,  
  [in]      DWORD            fFlags);


file path I mean

then it will copy this file to program data and copy its content into memory using Readfile API and is sent to C2.

Steal Steam Files

Steal can collect Steam credentials and post them to C2, for Gamers A Steam account is the most valuable resource on the machine, so how does it handle this

1- it gets the path of the Steam folder using the Registry, Steam path is saved on


and it retrieves the path using RegQueryValueExA to get “SteamPath” key value


2- After getting the Steam path it will try to get a handle on some important files used by steam


ssfn files are part of the Steam Guard process, a security feature provided by the Steam gaming platform, The Steam servers use the computer identifier to verify that the user is logging in from an authorized computer

config.vdf is used to store various configuration settings for the Steam client, including user preferences, interface settings, and other configurations.

loginusers.vdf file that typically contains information related to user accounts that have logged in to Steam on a particular computer. This includes data about the user’s Steam account, including the username, Steam ID, and other relevant account information.

DialogConfig.vdf contains configurations related to various dialog boxes and user interface (UI) elements within the Steam client. These dialog boxes may include settings, options, and preferences related to how Steam interacts with users.

3- For each file, it invokes the function sub_40AB8E. Within this function, a handle to the file is obtained, followed by reading its contents. Subsequently, the function appends the Data Identifier that will be utilized in the C2 (Command and Control) connection

the identifier, in this case, is “softsteam\” followed by the file name


Steal Discord Tokens

Stealing Discord Databases is not that easy, It costs more effort so how ?;

1-resolve the path where Discord Saves its files and configuration and then check the if current file exists on Leveldb folder


_%APPDATA%\Romaing\discord\Local Storage\leveldb\CURRENT_
%APPDATA%\Romaing\discord\Local Storage\leveldb\\

2- It invokes sub_40B110E with the path of Leveldb folder as an argument


3- inside sub_40B110E it will pass each file in leveldb folder to sub_40AEE5, these files are database files


4-Within the context of sub_40AEE5, the most pivotal actions revolve around the decryption of a crucial token. This decryption process shares similarities with how Chrome handles sensitive data in its files, as discussed previously. Let’s provide a concise overview of how this decryption typically unfolds

4.1- it gets the path of the LocalState file which contains the AES key


4.2 it will read LocalState File which contains the Encrypted Key,

the key is base64 encoded and encrypted with DPAPI


then it will decrypt it using DPAPI



using CryptUnProtectData will get the AES key which will be used later to Generate the AES key


4.3 After getting the handle it manipulates the DB file to search for the token, it first reads the DB file and then searches for “dQw4w9WgXcQ” which is the start or ID of Discord Tokens.


the value assigned with this token identifier is base64 encoded


4.4 then the data is AES decrypted using the Key handle optioned before

5- the last part of Stealing Discord Tokens is adding a Data Identifier to the allocated data and this time is **“soft\Discord\tokens.txt”**


Steal Telegram Sessions

it will get the path of telegram files and DBs which is located at _“%APPDATA%\Romaing and try to locate some files which is used to to store telegram sessions like_


the map file contains the current telegram session, so it reads it and sends it directly to C2.


The Data Identifier here is **_“soft\Telegram\maps”_**

the source code of this task => click here

Steal qTox Files

qTox provides an easy-to-use application that allows you to connect with friends and family without anyone else listening in, so it’s like Telegram :)

Stealc collects “*.ini” files which are located at


and then send it To C2


Steal OutLook Credentials

In its covert operation, Stealc demonstrates its decryption prowess by unraveling the registry key where Outlook securely stores its configuration and account data. This intricate process involves the meticulous iteration over a total of 24 registry keys to successfully extract valuable Outlook account information, including usernames and passwords.

Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles\Outlook\9375CFF0413111d3B88A00104B2A6676\  
Software\Microsoft\WindowsMessaging Subsystem\Profiles\9375CFF0413111d3B88A00104B2A667

, and for each Key, it tries to open these 4 sub-keys


so for each subkey, it calls sub_40B8D2, this function involves getting the subkey


for each key, it gets the name and if it matches “password” it gets the key value

using CryptUnprotectData, it will decrypt the password value and convert it to a MultiByte string


so as I said 24 times this function is called searching for accounts and their passwords, then it adds “soft\Outlook\accounts.txt” as an Identifier for the transferred data.

Steal Pidgin Credentials

Pidgin is an open-source instant messaging (IM) client that allows users to communicate with friends and colleagues through various IM networks

so Stealc tries to search for its config and DB files and send it To C2, it resolves the path where Pidgin config is located



Act as Downloader

the last stealth behavior is that Stealc asks C2 for another stage to execute it and then drops the file in the Temp directory then executes it.


Removing it Self

the last malware behavior is that Stealc deletes the downloaded Dlls, removes itself from the machine, and exits the process.


"C:\Windows\system32\cmd.exe" /c timeout /t 5 & del /f /q "<Current File Path>" & del "C:\ProgramData\*.dll" & exit

so my analysis ends here I hope this article meets your expectations and if u want to correct anything don’t hesitate to DM me


sha256 : 1E09D04C793205661D88D6993CB3E0EF5E5A37A8660F504C1D36B0D8562E63A2  
C2 : hxxp://fff-ttt[.]com/984dd96064cb23d7.php  
   : hxxp://moneylandry[.]com/2ccaf544c0cf7de7  
   : hxxp://162.0.238[.]10/752e382b4dcf5e3f.php  
   : hxxp://185.5.248[.]95/api.php  
   : hxxp://aa-cj[.]com/6842f013779f3d08.php  
   : hxxp://moneylandry[.]com/bef7fb05c9ef6540.php  
   : hxxp://94.142.138[.]48/f9f76ae4bb7811d9.php  
   : hxxp://185.247.184[.]7/8c3498a763cc5e26.php  
   : hxxps://185.247.184[.]7/8c3498a763cc5e26.php  
   : hxxp://23.88.116[.]117/api.php  
   : hxxp://95.216.112[.]83/413a030d85acf448.php  
   : hxxp://179.43.162[.]2/d8ab11e9f7bc9c13.php  
   : hxxp://185.5.248[.]95/c1377b94d43eacea.php

Yara Rule

rule Detect_Stealc_Stealer{  
        description="Stealc Info Stealer"    
        $s2="Network Info:"  
        $s3="- IP: IP?"  
        $s4="- Country: ISO?"  
        $hex_value = {74 03 75 01 b8 e8}  
        $hex_value2= {8B 48 F8 83 C0 F0 C7 00 01 00 00 00 85 C9 74 0A 83 39 00}  
        uint16(0)==0x5A4D and all of($s*) and all of ($hex_value*)   

We hope this post spreads awareness to the blue teamers of this interesting malware techniques, and adds a weapon to the red teamers arsenal.

Big thanks to @farghlymal for this detailed report.

By Cyber Threat Hunters from MSSPLab:


Stealc config decryptor
Malware Analysis Stealc - part 1

Thanks for your time happy hacking and good bye!
All drawings and screenshots are from farghlymal blog