Malware analysis report: Stealc stealer - part 1
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 header → IMAGE_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.
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