10 minute read

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

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

stealc

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

stealc

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

stealc

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.

stealc

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]

stealc

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.

stealc

  • 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  
);

stealc

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.

stealc

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.

stealc

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.

stealc

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]

stealc

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.

stealc

then the malware will try to initialize url using InternetCrackUrlA

stealc

-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

stealc

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

stealc

stealc

stealc

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

stealc

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

stealc

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

stealc

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

stealc

and here is the result of the decode

stealc

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.

stealc

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.

That’s all for today. In the next part, we will write about exfiltration system information and downloader logic.

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:

References

https://malpedia.caad.fkie.fraunhofer.de/details/win.stealc
https://farghlymal.github.io/Stealc-Stealer-Analysis/
https://twitter.com/farghlymal
Stealc config decryptor

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