Stealc Stealer

40 minute read

Stealc Stealer Analysis

Stealc is an information stealer advertised by its presumed developer Plymouth on Russian-speaking underground forums and sold as a Malware-as-a-Service since January 9, 2023. According to Plymouth’s statement, Stealc is a non-resident stealer with flexible data collection settings, and its development relies on other prominent stealers: Vidar, Raccoon, Mars, and Redline

Stealc exhibits the ability to exfiltrate a wide range of data from the victim machine. What sets it apart is its efficient approach: with each data allocation, Stealc directly transmits the information to the C2 server, bypassing the need to write it to a raw file. This streamlined process enhances both its data exfiltration capabilities and its ability to maintain a low profile, making it a potent tool for covert operations

it can steal :

  • (Chrome/Firefox/Opera) logins, credit cards, cookies, and History
  • Wallet Extensions installed on the above browsers
  • local Crypto wallets file
  • some files that may contain passwords or important secret data
  • outlook accounts
  • Discord Tokens
  • Telegram Tokens
  • Steam ssfn files and configuration data
  • qtox config files
  • Pidgin config files
  • Take screenshots of the victim’s machine

Technical analysis :

Opaque Predicates

the first time I looked into this malware I found something wrong with the provided code from IDA and X64 Dbg, Stealc uses Opaque to add complexity to the control flow

This obfuscation simply takes an absolute jump (JMP) and transforms it into two conditional jumps (JZ/JNZ). Depending on the value of the, the execution will follow the first or second branch

To fix this I have used Python script to go through the raw file and search for the pattern “\x74\x03\x75\x01\xB8” and replace it with 5 “0x90” nop instruction

  
search_pattern = b'\x74\x03\x75\x01\xb8'  
replacement = b'\x90\x90\x90\x90\x90'  
  
input_file = r'<Stealc File Path >'  
output_file = r'<Clean Stealc>'  
  
with open(input_file, 'rb') as infile:  
    # Read the entire contents of the file  
    file_data = infile.read()  
  
start_pos = 0  
  
while True:  
    found_pos = file_data.find(search_pattern, start_pos)  
    if found_pos == -1:  
        break  
  
    file_data = file_data[:found_pos] + replacement + file_data[found_pos + len(search_pattern):]  
    start_pos = found_pos + len(replacement)  
  
with open(output_file, 'wb') as outfile:  
    outfile.write(file_data)  

the result I got was impressive and fixed all these junk bytes

Malware Configuration :

Malware configuration is base64 encoded and then RC4 decrypted and the decryption key is the first dword in the function that wraps decryption

to make the analysis easier and clear I have written a script to decrypt this configuration and comment and rename global variables

import pefile  
import idautils  
import idc  
import ida_idaapi, ida_kernwin, ida_bytes, ida_name  
file = r"file path"  
  
def rc4_decrypt(ciphertext, key):  
    # Initialization  
    S = list(range(256))  
    j = 0  
    key_length = len(key)  
    plaintext = bytearray(len(ciphertext))  
  
    for i in range(256):  
        j = (j + S[i] + key[i % key_length]) % 256  
        S[i], S[j] = S[j], S[i]  
  
  
    i = j = 0  
    for idx, byte in enumerate(ciphertext):  
        i = (i + 1) % 256  
        j = (j + S[i]) % 256  
        S[i], S[j] = S[j], S[i]  
        keystream_byte = S[(S[i] + S[j]) % 256]  
        if byte == 0x00 :                       #this the modified part of RC4 to ignore null bytes form decryption   
             continue  
        else :  
            plaintext[idx] = byte ^ keystream_byte  
  
    return bytes(plaintext)  
  
def get_PE_Data(file_name):  
    pe=pefile.PE(file_name)  
    for section in pe.sections:  
         if b'.rdata' in section.Name:  
            Key = section.get_data()[3056:3076]  
            encryption_block = section.get_data()[2056:3032]  
    return Key,encryption_block  
          
def map_base64_to_enc(b_data,len_of_base):  # this function is base 64 decoder so u can replace is with built-in module   
    RC4_Key,data = get_PE_Data(file)  
    count = 0  
    mapped_data=[]  
    for i in range(0,len(b_data),3):  
        mapped_data.append(((data[b_data[count + 1]] >> 4 )&0xFF) | ((data[b_data[count]] * 4)&0xFF))  
  
        mapped_data.append(((data[b_data[count + 1]] * 16)&0xFF) | ((data[b_data[count + 2 ]] >> 2)&0xFF))  
  
        mapped_data.append((data[b_data[count+3]]) | ((data[b_data[count + 2]] << 6) & 0xFF))  
  
        count+=4   
          
        if count >= len(b_data):  
            break  
    if (b_data[-1]==0x3d):  
   
        mapped_data[-1] = 0  
    if (b_data[-2]==0x3d):  
  
        mapped_data[-2] = 0  
  
    byte_array=bytes(mapped_data)  
  
    return (rc4_decrypt(byte_array,RC4_Key).decode('utf-8',errors='ignore'))  
      
def Modify_Xrefs(Decryption_routin):  
    Xrefs = idautils.CodeRefsTo(Decryption_routin,0)  
  
    count=0  
    for x in Xrefs:  
        ea = idc.prev_head(x)  
        inst_type = ida_ua.ua_mnem(ea)  
        type = idc.get_operand_type(ea,1)  
        operand_address = idc.get_operand_value(ea,1)  
        size = 200  
        data__ = idaapi.get_bytes(operand_address,size)  
        if operand_address != -1 :  
            index=data__.index(b'\x00\x00')  
            count +=1  
            data__=data__[:index]  
            decrypted_str = map_base64_to_enc(data__,len(data__))  
            idc.set_cmt(x,decrypted_str,0)  
            print(decrypted_str)  
            dword_address = idc.next_head(x)  
            dword_value = idc.get_operand_value(dword_address,0)  
            rename_operand(dword_value,decrypted_str)  
        else:  
            continue  
def rename_operand(address,string):  
    ida_name.set_name(address, string, ida_name.SN_CHECK)  
Decryption_fun_address = 0x00403047  
Modify_Xrefs(Decryption_fun_address)

u can check my repo for Stealc

Dynamic API loading :

Stealc has no static imports so it dynamically resolves the required APIs using GetProcAddr() API, but first it needs to get the address of GetProcAddr to be able to use its import APIs, this is done by involving 6 structures, it first gets PEB address then from, PEB it accesses Ldr structure, and from this structure, it gets the address of InloadOrderModuleList, this is a LinkedList of Modules loaded into memory and every structure contains data about its module, and due to sorting on memory loading, the first module loaded into memory is ntdll.dll and after that kerenl32.dll is loaded, so it accesses the structure of kernel32.dll and then accesses the element at 0x18 which is a pointer to kerenl32.dll base address in memory.

get elfanew offset by adding 0x3C to the DLL base address which points to PE headerIMAGE_NT_HEADERS then adding 0x78 to get a pointer to the Export address table which handles exported APIs by Dll, then it gets 3 addresses from the Export table which points to 3 arrays

  • Address of Names [0x20]
  • Address of functions [0x1C]
  • Address of NameOrdinals [0x24]

AV Evasion :

after loading APIs and Initializing the configuration, Stealc will start to check if it’s running under Emulation Environment By doing some checks for Emulators specifically Windows Defender so that I will go through the techs used in the behavior.

  • check API Emulation
  • Check memory status
  • Check computer and user names
  • check Compilation time

1- Check the Existence of physical memory:

In the function that I have called mw_play_with_mem(), Stealc checks the Emulation by calling this VirtualAllocExNuma API, and this API is specifically because some APIs are not Emulated by AVs yet so it will return zero which will force malware to exit

LPVOID VirtualAllocExNuma(  
  [in]           HANDLE hProcess,  
  [in, optional] LPVOID lpAddress,  
  [in]           SIZE_T dwSize,  
  [in]           DWORD  flAllocationType,  
  [in]           DWORD  flProtect,  
  [in]           DWORD  nndPreferred  
);

2- Check System Memory :

inside mw_Check_system_memory(), Stealc will call GlobalMemoryStatusEx API, which will return information about virtual and physical memory, This API takes a structure as the only argument called LPMEMORYSTATUSEX

BOOL GlobalMemoryStatusEx(  
  [in, out] LPMEMORYSTATUSEX lpBuffer  
);  
  
typedef struct _MEMORYSTATUSEX {  
  DWORD     dwLength;  
  DWORD     dwMemoryLoad;  
  DWORDLONG ullTotalPhys; //contains The amount of actual physical memory  
  DWORDLONG ullAvailPhys;  
  DWORDLONG ullTotalPageFile;  
  DWORDLONG ullAvailPageFile;  
  DWORDLONG ullTotalVirtual;  
  DWORDLONG ullAvailVirtual;  
  DWORDLONG ullAvailExtendedVirtual;  
} MEMORYSTATUSEX, *LPMEMORYSTATUSEX;

ullTotalPhys member contains The amount of actual physical memory, in bytes which should be more than 2 Giga bytes, Stealc will do some shift operation, and if the shift operation is less than 0x457 it will exit.

3- Check Windows Defender Emulation :

After the above 2 checks, Stealc will try to check if it is running under Windows Defender by retrieving the Computer name and User name using GetComputerName and GetUserNameA, then it will compare them against those used by Windows Defender in its Emulator

HAL9TH → Computer Name in Win Defender

JohnDeo → User Name in Win Defender

and if the result matches it will exit.

4- Expiration check:

there is a fixed time and if you try to run it after this time it will exit and do nothing This is achieved by calling GetSystemTime API and then constructing the fixed time which was decrypted before (08/03/2023), after that it will convert these two times from SystemTime to TimeFile format

typedef struct _SYSTEMTIME {  
  WORD wYear;  
  WORD wMonth;  
  WORD wDayOfWeek;  
  WORD wDay;  
  WORD wHour;  
  WORD wMinute;  
  WORD wSecond;  
  WORD wMilliseconds;  
} SYSTEMTIME, *PSYSTEMTIME, *LPSYSTEMTIME;  
  
typedef struct _FILETIME {  
  DWORD dwLowDateTime;  
  DWORD dwHighDateTime;  
} FILETIME, *PFILETIME, *LPFILETIME;

and then it will do 2 checks and if one of these checks met it will exit.

Skip infection

Stealc skips infecting some countries related to political issues like, It’s done by getting Language ID using GetUserDefaultLangID API and then comparing these IDs to some IDs that it wants to skip, and if there is any matching it will exit.

 v0 = GetUserDefaultLangID_() - 0x419;  
  if ( !v0 || (v1 = v0 - 9) == 0 || (v2 = v1 - 1) == 0 || (v3 = v2 - 0x1C) == 0  
   || (result = v3 - 4) == 0 )  
    ExitProcess_(0);    // 0x419   = 1049 -> Russian Language  
                        // v0 - 9    --> 1058 -> Ukrainian  
  return result;        // v2 - 1    --> 1059 -> Belarusian  
}                       // v2 - 0x1c --> 1087 -> Kazakh  
                        // v3 - 4    --> 1091 -> Uzbek 

Event Creation

After all of these checks, Stealc will try to check if it’s running already or not by trying to open an event using OpenEventA and if it is, we will be inside an infinite loop of sleeping, but if it’s the first time it will create a new event using CreateEventA with a structured name to be used as a unique name for the event

HAL9TH[ComputerName][UserName]

Establish C2 Communication

After the above phase of checking AVs, Loading APIs, and Config Decryption, Stealc starts its normal behavior so we will trace it step by step to extract all of its stealth behavior.

As we know our C2 is www[.]fff-ttt[.]com so Stealc will try to reach this server more than once time and every time it sends or receives data or Ethier download need modules, I will trace all C2 calls, and check what will be done, keep reading

-Generate Victim ID :

before any communication, it will get the ‘C’ Drive Serial number and then do some operation on it which results in an ID that will be used to identify the victim machine in all network connections.

then the malware will try to initialize url using InternetCrackUrlA

-Generate Packet ID :

before sending a request Stealc used a standard for its communication, it generates a unique ID for every packet sent to C2, using some mathematical Equations

then it will prepare the full packet content to be used in the connection and will initiate the socket using InternetOpenA, It will connect with the C2 and prepare the request

then the malware will send the packet to C2 using HttpSendRequest

BOOL HttpSendRequestA(  
  [in] HINTERNET hRequest,  
  [in] LPCSTR    lpszHeaders,  
  [in] DWORD     dwHeadersLength,  
  [in] LPVOID    lpOptional,  
  [in] DWORD     dwOptionalLength  
);

, I will take a look at the full packet inside the debugger and Wireshark,

Here are the headers and optional header content of the packet

the malware sends the victim ID in the first packet which was obtained by some operation “c” drive serial number as I have explained before, so the packet format confirmed our code analysis in the above part.

but faced an issue here that the page 984dd96064cb23d7.php was not found and the server resulted in us with a 404 error

So here others will say that the C2 is down and doesn’t complete the analysis. Still, I have an idea to complete the analysis without any problems, I have got an analysis for another file from the same Stealc variant on Any.run sandbox, and we can use the pcap file to emulate the connection without any problem, just we need to copy server response to our debugger and let Stealc do its job under our control.

here is the reply that should be received for the request above, If you take a look at the response you will find that it’s a Base64

so I have copied the response to the buffer of InternetReadFile API

BOOL InternetReadFile(  
  [in]  HINTERNET hFile,  
  [out] LPVOID    lpBuffer,  
  [in]  DWORD     dwNumberOfBytesToRead,  
  [out] LPDWORD   lpdwNumberOfBytesRead  
);

After that, Stealc will Decode the response using Win API CryptStringToBinaryA, and it calls it twice cause the first time it retrieves the required byte length for the buffer that will hold the decoded data

and here is the result of the decode

aa36b6d1c34621ab9876080e89e62c526f27572fa74ad766587fc1e832822fbc85b96f8f|isdone|docia.docx|0|1|1|1|1|1|1|1|

If keep your eyes on the result you will observe that there is a delimiter ‘ | ’ between every string and this may be used next, so stealc will probably strip this output based on the delimiter

inside sub_0040912D() which I have renamed to mw_Strip_C2_reply(), Stealc does what we already have predicted before but first it checks if the first word of the replay = “block’’ and if it is met, it will exit the process.

then it will save the string tokens ‘ 1 1 1 1 1 1 1’ in memory, but until now I don’t know how it will be used but we will, I think these ‘ones’ are used as a boolean value which indicates a stealth option like

1 → grap cookies →

0 → grap search history → don’t allocate search history

It may based on the builder used and the Preferences of the buyer

Download Browsers Configurations:

inside sub_0x403D5F() → renamed to mw_Download_1(), Stealc again will ask C2 to feed it with some configuration to be used in stealth behavior, it will do the same steps done before in the first connection but this time will ask for a different data, and if we look at the TCP stream.

but before that, if you remember the last decoded data in the first stream was like this

“aa36b6d1c34621ab9876080e89e62c526f27572fa74ad766587fc1e832822fbc85b96f8f”

This stream of hexa values will be used in all communication tunnels and acts like a reference for the victim ID obtained before, so C2 receives the Victim ID which was the calculated “C” drive serial number, and then does some equation on this calculated serial and then send the new ID in the first packet received by the victim which will be used repeatedly in all the connection.

If we manually decode the above stream will result in another configuration data related to the browser’s paths where Application User Data is saved

Google Chrome|\Google\Chrome\User Data |chrome|  
Google Chrome Canary|\Google\Chrome SxS\User Data|chrome|  
Chromium|\Chromium\User Data|chrome|  
Amigo|\Amigo\User Data|chrome|  
Torch|\Torch\User Data|chrome|  
Vivaldi|\Vivaldi\User Data|chrome|  
Comodo Dragon|\Comodo\Dragon\User Data|chrome|  
EpicPrivacyBrowser|\Epic Privacy Browser\User Data|chrome|  
CocCoc|\CocCoc\Browser\User Data|chrome|  
Brave|\BraveSoftware\Brave-Browser\User Data|chrome|  
Cent Browser|\CentBrowser\User Data|chrome|  
7Star|\7Star\7Star\User Data|chrome|  
Chedot Browser|\Chedot\User Data|chrome|  
Microsoft Edge|\Microsoft\Edge\UserData|  
chrome|360 Browser|\360Browser\Browser\User Data|chrome|  
QQBrowser|\Tencent\QQBrowser\User Data|chrome|  
CryptoTab|\CryptoTab Browser\User Data|chrome|  
Opera Stable|\Opera Software|opera|  
Opera GX Stable|\Opera Software|opera|  
Mozilla Firefox|\Mozilla\Firefox\Profiles|firefox|  
Pale Moon|\Moonchild Productions\Pale Moon\Profiles|firefox|  
Opera Crypto Stable|\Opera Software|opera|  
Thunderbird|\Thunderbird\Profiles|firefox|

Just like you see this config will be used to steal the browser’s databases and it will try for all Chromium-based browsers that share the same structure of databases and also will explore Mozilla-based web engines and “Thunderbird” mail client which is based on Mozilla also, finally, Opera web engine is on its consideration, also if you observed that for every web-engine at the end of the path it appends |chrome| or |firefox| as I have said that every engine will be treated differently in exfiltration process, so it calls mw_parse_configuration after decoding the stream to enable Stealc to separate.

and here is how this configuration is parsed in a format that enables it to be used later.

00000000 00000000 00000000  
    |        |       |--> BrowserName length  
    |        |-->4 null bytes  
    |-->  pointer to BroswerName or path 

Download Browsers Extensions

then the agent will ask C2 to feed it with plugins that will be used and I observed that it appends a string in the communication request that specifies which content will be retrieved from C2.

so again if we take a look at how our request and response look in our sniffer, it will confirm our previous analysis that the agent will ask for a plugin as a configuration request type.

the response is also base64 stream, I will decode it as past to reveal its secrets and also give us an indication about what will be done next.

Extenstion Name 					| Extenstion ID |   flags or something   
  |                 					    |                 	  |  
  |                  					    |                 	  |  
  
MetaMask						|djclckkglechooblngghdinmeemkbgci|1|0|0|  
MetaMask						|ejbalbakoplchlghecdalmeeeajnimhm|1|0|0|  
MetaMask						|nkbihfbeogaeaoehlefnkodbefgpgknn|1|0|0|  
TronLink						|ibnejdfjmmkpcnlpebklmnkoeoihofec|1|0|0|  
Binance Wallet			|fhbohimaelbohpjbbldcngcnapndodjp|1|0|0|  
Yoroi							  |ffnbelfdoeiohenkjibnmadjiehjhajb|1|0|0|  
Coinbase Wallet extension		|hnfanknocfeofbddgcijnmhnfnkdnaad|1|0|1|  
Guarda							|hpglfhgfnhbgpjdenjgmdgoeiappafln|1|0|0|  
Jaxx Liberty					|cjelfplplebdjjenllpjcblmjkfcffne|1|0|0|  
iWallet							|kncchdigobghenbbaddojjnnaogfppfj|1|0|0|  
MEW CX							|nlbmnnijcnlegkjjpcfjclmcfggfefdm|1|0|0|  
GuildWallet						|nanjmdknhkinifnkgdcggcfnhdaammmj|1|0|0|  
Ronin Wallet					|fnjhmkhhmkbjkkabndcnnogagogbneec|1|0|0|  
NeoLine							|cphhlgmgameodnhkjdmkpanlelnlohao|1|0|0|  
CLV Wallet						|nhnkbkgjikgcigadomkphalanndcapjk|1|0|0|  
Liquality Wallet				|kpfopkelmapcoipemfendmdcghnegimn|1|0|0|  
Terra Station Wallet			|aiifbnbfobpmeekipheeijimdpnlpgpp|1|0|0|  
Keplr							|dmkamcknogkgcdfhhbddcghachkejeap|1|0|0|  
Sollet							|fhmfendgdocmcbmfikdcogofphimnkno|1|0|0|  
Auro Wallet(Mina Protocol)		|cnmamaachppnkjgnildpdmkaakejnhae|1|0|0|  
Polymesh Wallet					|jojhfeoedkpkglbfimdfabpdfjaoolaf|1|0|0|  
ICONex							|flpiciilemghbmfalicajoolhkkenfel|1|0|0|  
Coin98 Wallet					|aeachknmefphepccionboohckonoeemg|1|0|0|  
EVER Wallet						|cgeeodpfagjceefieflmdfphplkenlfk|1|0|0|  
KardiaChain Wallet				|pdadjkfkgcafgbceimcpbkalnfnepbnk|1|0|0|  
Rabby							|acmacodkjbdgmoleebolmdjonilkdbch|1|0|0|  
Phantom							|bfnaelmomeimhlpmgjnjophhpkkoljpa|1|0|0|  
Brave Wallet					|odbfpeeihdkbihmopkbjmoonfanlbfcl|1|0|0|  
Oxygen							|fhilaheimglignddkjgofkcbgekhenbh|1|0|0|  
Pali Wallet						|mgffkfbidihjpoaomajlbgchddlicgpn|1|0|0|  
BOLT X							|aodkkagnadcbobfpggfnjeongemjbjca|1|0|0|  
XDEFI Wallet					|hmeobnfnfcmdkdcmlblgagmfpfboieaf|1|0|0|  
Nami							|lpfcbjknijpeeillifnkikgncikgfhdo|1|0|0  
|Maiar DeFi Wallet				|dngmlblcodfobpdpecaadgfbcggfjfnm|1|0|0|  
Keeper Wallet					|lpilbniiabackdjcionkobglmddfbcjo|1|0|0|  
Solflare Wallet					|bhhhlbepdkbapadjdnnojkbgioiodbic|1|0|0|  
Cyano Wallet					|dkdedlpgdmmkkfjabffeganieamfklkm|1|0|0|  
KHC								|hcflpincpppdclinealmandijcmnkbgn|1|0|0|  
TezBox							|mnfifefkajgofkcjkemidiaecocnkjeh|1|0|0|  
Temple							|ookjlbkiijinhpmnjffcofjonbfbgaoc|1|0|0|  
Goby							|jnkelfanjkeadonecabehalmbgpfodjm|1|0|0|  
Ronin Wallet					|kjmoohlgokccodicjjfebfomlbljgfhk|1|0|0|  
Byone							|nlgbhdfgdhgbiamfdfmbikcdghidoadd|1|0|0|  
OneKey							|jnmbobjmhlngoefaiojfljckilhhlhcj|1|0|0|  
DAppPlay						|lodccjjbdhfakaekdiahmedfbieldgik|1|0|0|  
SteemKeychain					|jhgnbkkipaallpehbohjmkbjofjdmeid|1|0|0|  
Braavos Wallet					|jnlgamecbpmbajjfhmmmlhejkemejdma|1|0|0|  
Enkrypt							|kkpllkodjeloidieedojogacfhpaihoh|1|1|1|  
OKX Wallet						|mcohilncbfahbmgdjkbpemcciiolgcge|1|0|0|  
Sender Wallet					|epapihdplajcdnnkdeiahlgigofloibg|1|0|0|  
Hashpack						|gjagmgiddbbciopjhllkdnddhcglnemk|1|0|0|  
Eternl							|kmhcihpebfmpgmihbkipmjlmmioameka|1|0|0|  
Pontem Aptos Wallet				|phkbamefinggmakgklpkljjmgibohnba|1|0|0|  
Petra Aptos Wallet				|ejjladinnckdgjemekebdpeokbikhfci|1|0|0|  
Martian Aptos Wallet			|efbglgofoippbgcjepnhiblaibcnclgk|1|0|0|  
Finnie							|cjmkndjhnagcfbpiemnkdpomccnjblmj|1|0|0|  
Leap Terra Wallet				|aijcbedoijmgnlmjeegjaglmepbmpkpi|1|0|0|  
Trezor Password Manager			|imloifkgjagghnncjkhggdhalmcnfklk|1|0|0|  
Authenticator					|bhghoamapcdpbohphigoooaddinpkbai|1|0|0|  
Authy							|gaedmjdfmmahhbjefcbgaolhhanlaolb|1|0|0|  
EOS Authenticator				|oeljdldpnmdbchonielidgobddffflal|1|0|0|  
GAuth Authenticator 			|ilgcnhelpchnceeipipijaljkblbcobl|1|0|0|  
Bitwarden					 	|nngceckbapebfimnlniiiahkandclblb|1|0|0|  
KeePassXC						|oboonakemofpalcgghocfoadofidjkkk|1|0|0|  
Dashlane						|fdjamakpfbbddfjaooikfcpapjohcfmg|1|0|0|  
NordPass						|fooolghllnmhmmndgjiamiiodkpenpbb|1|0|0|  
Keeper							|bfogiafebfohielmmehodmfbbebbbpei|1|0|0|  
RoboForm						|pnlccmojcmeohlpggmfnbbiapkmbliob|1|0|0|  
LastPass						|hdokiejnpimakedhajhdlcegeplioahd|1|0|0|  
BrowserPass						|naepdomgkenhinolocfifgehidddafch|1|0|0|  
MYKI							|bmikpgodpkclnkgmnpphehdgcimmided|1|0|0|  
Splikity  						|jhfjfclepacoldmjmkmdlmganfaalklb|1|0|0|  
CommonKey 						|chgfefjpcobfbnpmiokfjjaglahmnded|1|0|0|  
Zoho Vault    					|igkpcodhieompeloncfnbekccinhapdb|1|0|0|  
Opera Wallet  					|gojhcdgcpbpfigcaejpfhfegekdgiblk|0|0|1|

it’s a collection of browser extensions that Stealc will search for in the browser’s DB using the ID provided which adds more stealthy capabilities, the same operation of storing configuration is done with these plugins as browsers did.

Exfiltrate System Information

after that Stealc starts gathering system and hardware information like,

ip address  
country   
processor name  
operating system  
arch 32 or 64  
pc or laptob  
UserName  
computerName  
Screenshot  
installed apps  
running process  
etc.....

and after allocating this data it saves it in a format that is understandable by C2.

then the Collected Data is base64 encoded before transferring to C2

and here is the stream of the fully allocated data

System Summary:  
 - HWID: 8658E8B4266B114684123  
 - OS: Windows 10 Enterprise  
 - Architecture: x64  
 - UserName:   
 - Computer Name: DESKTOP-2C3IQHO  
 - Local Time: 2023/9/14 18:47:27  
 - UTC: -5  
 - Language: en-US  
 - Keyboards: English (United States)  
 - Laptop: FALSE  
 - CPU: Intel(R) Core(TM) i7-4600M CPU @ 2.90GHz  
 - Cores: 1  
 - Threads: 1  
 - RAM: 4095 MB  
 - Display Resolution: 1536x864  
 - GPU:  
  -VMware SVGA 3D  
  -VMware SVGA 3D  
User Agents:  
Installed Apps:  
All Users:  
 HxD Hex Editor version 1.7.7.0 - 1.7.7.0  
 Npcap - 1.55  
 VB Decompiler Lite  
 WinSCP 5.13 - 5.13  
 Wireshark 3.6.0 64-bit - 3.6.0  
 Microsoft Visual C++ 2010 x86 Redistributable - 10.0.30319 - 10.0.30319  
 Microsoft Visual C++ 2008 Redistributable - x86 9.0.30729.4148 - 9.0.30729.4148  
 Microsoft Visual C++ 2015-2022 Redistributable (x64) - 14.32.31326 - 14.32.31326.0  
Current User:  
 Progress Telerik Fiddler - 5.0.20173.50948  
 Microsoft OneDrive - 18.025.0204.0009  
 Opera Stable 91.0.4516.77 - 91.0.4516.77  
 Python 3.9.9 (64-bit) - 3.9.9150.0  
  
Process List:  
 System  
 smss.exe  
 csrss.exe  
 wininit.exe  
 csrss.ex  
 SearchIndexer.exe  
 SearchUI.exe  
 RuntimeBroker.exe  
 RuntimeBroker.exe  
 svchost.exe  
 SettingSyncHost.exe  
 svchost.exe  
 svchost.exe  
 vmtoolsd.exe  
 msdsrv.exe  
 svchost.exe  
 svchost.exe  
 svchost.exe  
 ApplicationFrameHost.exe  
 svchost.exe  
 svchost.exe 

Downloader

after exfiltrating system info, Stealc will download Sqlite3 Dll which will be used to execute some queries to retrieve data from Ghrome Application data, so I will not skip this and try to explain it in detail.

first, it asks for Sqlite3.dll

after downloading the file it starts checking if the file is correct by checking some magic byte related to the dos header and PE header, and after that, it will not copy the whole file just from the start of section headers till the end of the file.

Stealc does all of that just to get addresses of some APIs that will assist in retrieving data from Chrome databases which Chrome itself uses

sqlite3_open

sqlite3_prepare_v2

sqlite3_step

sqlite3_column_text

sqlite3_finalize

sqlite3_close

sqlite3_column_bytes

sqlite3_column_blob

after that, it will start to check the browser structure built before and check the web engine then it will start to iterate over all browsers and if it hits any browser that exists on the victim machine it will then get a handle to db files which I will explain next…….

first, it will resolve

%USER%AppData\Local\Google\Chrome\User Data\LocalState

that exists in Chrome folders, but why this file exactly because it is used to store some more technical information about Chrome

The user’s preferred language

The user’s theme and font settings

The user’s startup settings (e.g., whether to open Chrome maximized)

The user’s privacy settings (e.g., whether to enable cookies)

The user’s extensions and their settings

The user’s bookmarks and history

after getting a handle on the file it will read file data and save a pointer to it into the first passed argument.

after that, it will search for the “encrypted_key “ string on the buffer, then it will try to retrieve the key from the file buffer because the key is saved in a format that I will show in the next figure.

until that, it will iterate over the buffer until it hits the end of the key which is marked by the } symbol

after retrieving the key it will base64 decode it, at the first 5 bytes of the decoded stream the word “DPAPI” indicates a DPAPI decrypted stream, after that, it will use the decoded key to decrypt the AES key using CryptUnprotectData, and the result is an AES key which will be used to decrypt cookies and credentials that because chrome(v80+) is encrypting data using AES and the AES key is encrypted with DPAPI,

then it will AES decrypted key to generate a symmetric key which will be used for the decryption operation, this is done in 3 steps

1- call BCryptOpenAlgorithmProvider to handle a cryptographic algorithm provider which in this case is AES.

2-call BCryptSetProperty to set the mode to ChainingModeGCM, It specifies that the Galois/Counter Mode (GCM) chaining mode should be used. GCM is a mode of operation for block ciphers that provides both encryption and authentication (with the help of Bard chat)

3- call BCryptGenerateSymmetricKey, used to generate a symmetric key for cryptographic operations and save a handle for it in Phkey var.

after that, it will retrieve the browser path that was received from C2, and in our case the first folder path is

%USER%AppData\Local\Google\Chrome\User Data

so it will iterate over all folders on this path looking for some Browser DB files, these files are

AppData\Local\Google\Chrome\User Data\Default\Network\Cookies

AppData\Local\Google\Chrome\User Data\Default\Login Data

AppData\Local\Google\Chrome\User Data\Default\Web Data

AppData\Local\Google\Chrome\User Data\Default\History

which enables it to steal history and web sessions also autofill data will be exfiltrated.

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

fname

lname

email

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

C:\Users\REM\AppData\Roaming\Mozilla\Firefox\Profiles

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

p:\Users\<Username>\AppData\Roaming\Mozilla\Firefox\Profiles\<ProfileName>\cookies.sqlite_**

  • This file is responsible for storing information about web cookies

C:\Users\<Username>\AppData\Roaming\Mozilla\Firefox\Profiles\<ProfileName>\formhistory.sqlite_**

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

C:\Users\<Username>\AppData\Roaming\Mozilla\Firefox\Profiles\<ProfileName>\places.sqlite_**

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

C:\Users\<Username>\AppData\Roaming\Mozilla\Firefox\Profiles\<ProfileName>\logins.json_**

  • 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.

|%DESKTOP%\|*seed*.*,*passphrase*.*,*erc20*.*,*trc20*.*,*exodus*.*,*metamask*.*,*binance*.*,*wallet*.*|0|1|0|  
|%DOCUMENTS%\|*seed*.*,*passphrase*.*,*erc20*.*,*trc20*.*,*exodus*.*,*metamask*.*,*binance*.*,*wallet*.*|0|1|0|

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

Desktop

APPDATA\Romaing

APPDATA\Local

**__**

Document

ProgramFiles

ProgramFiles x86

Recent

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

IShellLinkA->lpVtbl->QueryInterface()

{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

HRESULT GetPath(
  [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

HKEY_CURRENT_USER\Software\Valve\Steam

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

%APPDATA%\Romaing\key_datas\D877F783D5D3EF8C\map

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

Appdata\romaing\tox*.ini

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\Office\13.0\Outlook\Profiles\Outlook\9375CFF0413111d3B88A00104B2A6676\  
Software\Microsoft\Office\14.0\Outlook\Profiles\Outlook\9375CFF0413111d3B88A00104B2A6676\  
Software\Microsoft\Office\15.0\Outlook\Profiles\Outlook\9375CFF0413111d3B88A00104B2A6676\  
Software\Microsoft\Office\16.0\Outlook\Profiles\Outlook\9375CFF0413111d3B88A00104B2A6676\  
Software\Microsoft\WindowsMessaging Subsystem\Profiles\9375CFF0413111d3B88A00104B2A667

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

00000001  
00000002  
00000003  
00000004

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

%APPDATA%\Romaing\purple\accounts.xml

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 "<Curren 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

IOCs

sha256 : 1E09D04C793205661D88D6993CB3E0EF5E5A37A8660F504C1D36B0D8562E63A2  
         77d6f1914af6caf909fa2a246fcec05f500f79dd56e5d0d466d55924695c702d  
         87f18bd70353e44aa74d3c2fda27a2ae5dd6e7d238c3d875f6240283bc909ba6  
  
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{  
  
    meta:  
        description="Stealc Info Stealer"    
        author="@FarghlyMal"  
        hash="sha256,1E09D04C793205661D88D6993CB3E0EF5E5A37A8660F504C1D36B0D8562E63A2"  
        Date="8/11/2023"     
 strings:  
        $s1="block"  
        $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}  
      
    condition:  
        uint16(0)==0x5A4D and all of($s*) and all of ($hex_value*)   
   
}

Updated: