Vidar Stealer - The End of its' Malware Life Cycle
Introduction
This article delves into a recent phishing campaign targeting content creators by masquerading as a prominent brand and proposing collaboration opportunities. Classic Malvertising with a .zip with a poor choice of .scr file to social engineer the victim into executing the Vidar binary. Please note the .scr was inflated > 650mb to prevent virustotal & any.run analysis.
The Sample
The phishing email sent to the target describes a collaboration opportunity with the company, detailing the required promotional video post’s duration and the promised payment.
At the email’s bottom, a link to the promotional materials and a personal password are provided:
The link directs the recipient to Google Drive, instructing them to download an archive named H&M Corporation Advertising Contract.zip.
Within the archive are several decoy files related to H&M and a >600MB .scr file named H&M Advertising Contract and Payment Information.pdf.scr.
Managed NET Loader
Analysis of the loader using Detect It Easy (DiE) reveals it is a 32-bit .NET assembly protected by Smart Assembly:
Opening the loader in dnSpy confirms the Smart Assembly protection, evident from the static information fields:
The analysis of the entry point indicates that working with the loader in its current obfuscated state is inefficient:
To deobfuscate the code, Simple Assembly Explorer (SAE) was utilized:
Reopening the deobfuscated file in dnSpy now reveals clearer code:
Payload Extraction
The loader executes several critical actions:
- Creates an instance of
c000009
, with an internal field containing the path to the injected process.
- Passes this instance to the method
c000066.m000022
, which callsc000066.m00007b
with the stringfInckSommmenn
. - The method
c000066.m00007b
retrieves resource content from the binary resources:
The extracted resource content, the string
fInckSommmenn
, and thec000009
instance are passed toc000066.m000019
, which decrypts the payload using an XOR routine and returns the decrypted binary:
- The decrypted binary and the full path to the injected process are passed to
c000066.m00002a
for process injection with the decrypted binary content:
- A PowerShell script was created to extract the decrypted binary:
# Load the file.
$assembly = [System.Reflection.Assembly]::LoadFile("C:\Users\igal\Desktop\loader.exe")
#Initialize "NS005.c000009" object.
$ini = [Activator]::CreateInstance($assembly.Modules[0].GetType("NS005.c000009"),@())
#Retrieve the resource fetching method and invoke it.
$classType2 = $assembly.GetType("NS004.c000066")
$array = $classType2.GetMethod("m00007b").Invoke($null,@("fInckSommmenn", "fInckSommmenn"))
#Invoke the decryption method with the necessary arguments.
$fixedArray = $classType2.GetMethod("m000019").Invoke($null,@($array, "fInckSommmenn", $ini))
#Write the output to a file.
[io.file]::WriteAllBytes('C:\Users\igal\Desktop\payload.bin',$fixedArray)
Vidar Core Payload
This section examines the Vidar stealer’s capabilities, evasion techniques, and anti-analysis methods. DiE identifies the payload as a 32-bit C/C++ binary:
Anti-Analysis Measures
Upon opening the payload in IDA, the WinMain
function is not immediately recognized, appearing as an instruction instead:
Converting the data to code reveals some chunks not correctly interpreted:
After converting the data to code, instructions from the beginning of WinMain
to the relevant mov - pop - return
sequence were marked, defining the function’s end (in this case, instructions ranged from 0x4156B0 to 0x415891).
The decompiled view initially shows broken code:
Opaque predicates are employed to confuse the decompiler:
Utilizing a script by @_n1ghtw0lf, the decompiled code becomes clearer:
import idc
ea = 0
while True:
ea = min(idc.find_binary(ea, idc.SEARCH_NEXT | idc.SEARCH_DOWN, "74 ? 75 ?"), # JZ / JNZ
idc.find_binary(ea, idc.SEARCH_NEXT | idc.SEARCH_DOWN, "75 ? 74 ?")) # JNZ / JZ
if ea == idc.BADADDR:
break
idc.patch_byte(ea, 0xEB) # JMP
idc.patch_byte(ea+2, 0x90) # NOP
idc.patch_byte(ea+3, 0x90) # NOP
We see some JUMP OUT instruction that suggests more illusive code.
mov eax, 0FEB912E8h
Undefine EAX:
Scrubbing another EAX move;
mov eax, 0FEB9C8E8h
Clear class;
The author added useless calls to deter reverse engineers or confuse them.
Self Termination Trigger / MELT and ANTI-VM
Vidar incorporates several self-termination triggers:
- VirtualAllocExNuma: Checks for systems with more than one physical CPU:
- Physical Memory Check: Terminates if system memory is below 769MB:
- Computer Name Check: Terminates if the computer name is
HAL9TH
or the user name isJohnDoe
:
String Decryption
Vidar’s strings are encrypted using XOR. The decryption function requires three parameters: Length, XOR key, and Encrypted string.
A modified script by @eln0ty was used to decrypt these strings:
import idc
START = 0x401190
END = 0x40134D
TEMP = 0x0
FLAG = True
'''
[0] = Encrypted String.
[1] = Xor Key.
[2] = Length.
'''
VALUES = []
ea = START
# XOR decryption helper function.
def xorDecrypt(encString, xorKey, keyLen):
decoded = []
for i in range(0,len(encString)):
decoded.append(encString[i] ^ xorKey[i % keyLen])
return bytes(decoded)
while ea <= END:
# get argument values
if idc.get_operand_type(ea, 0) == idc.o_imm:
VALUES.append(idc.get_operand_value(ea, 0))
if len(VALUES) == 2:
if idc.get_operand_type(ea, 0) == idc.o_reg:
VALUES.append(idc.get_operand_value(ea, 1))
if idc.print_insn_mnem(ea) == "call":
length = VALUES[2]
data = idc.get_bytes(VALUES[0], length)
key = idc.get_bytes(VALUES[1], length)
VALUES = []
TEMP = ea
while FLAG:
ea = idc.next_head(ea, END)
if (idc.print_insn_mnem(ea) == "mov") and (idc.get_operand_type(ea, 0) == idc.o_mem) and (idc.get_operand_type(ea, 1) == idc.o_reg):
dec = xorDecrypt(data, key, length).decode('ISO-8859-1')
print(f'current location:{hex(ea)}, value will be: {dec}')
dwordVar = idc.get_operand_value(ea, 0)
idc.set_cmt(ea, dec, 1)
idc.set_name(dwordVar, "STR_" + dec, SN_NOWARN)
FLAG = False
ea = TEMP
break
# move to next instruction
FLAG = True
ea = idc.next_head(ea, END)
e.g; Some string names will not be properly staged. IDA parsing issues do not allow foreign language intricacies, so adding a plain common string rectifies the issue:
The decrypted strings output:
DynAPI Resolution
Vidar uses LoadLibraryA
and GetProcAddress
to resolve necessary APIs along with decrypted strings:
Another script by @eln0ty was employed to replace variable names for easier analysis:
import idc
start = 0x420874
end = 0x420901
ea = start
api_names = []
while ea <= end:
# get GetProcAddress API name
if (idc.print_insn_mnem(ea) == "mov") and (idc.get_operand_type(ea, 0) == idc.o_reg) and (idc.get_operand_type(ea, 1) == idc.o_mem):
addr = idc.get_operand_value(ea, 1)
name = idc.get_name(addr)
if name.startswith("STR_"):
api_names.append(name)
# assign GetProcAddress result to global var
if (idc.print_insn_mnem(ea) == "mov") and (idc.get_operand_type(ea, 0) == idc.o_mem) and (idc.print_operand(ea, 1) == "eax"):
addr = idc.get_operand_value(ea, 0)
name = api_names.pop(0)
idc.set_name(addr, "API_" + name[4:])
# move to next instruction
ea = idc.next_head(ea, end)
Core C2 Communication
A DOWNLOADER is used to obtain crucial .dlls for VIDAR Stealer communication.
DLL Name | Description |
---|---|
freebl3.dll | Network Security Services (NSS) from Mozilla Foundation |
mozglue.dll | Memory management for Mozilla applications |
msvcp140.dll | Microsoft Visual C++ library for C++ programming |
nss3.dll | Network security services for SSL/TLS encryption |
softokn3.dll | Cryptographic library for key management and encryption/decryption |
sqlite3.dll | Accessing and managing SQLite databases |
vcruntime140.dll | Microsoft Visual C++ library for memory management and I/O |
In this case, strangely enough, Vidar used external services as shim/gaskets/DynDNS to point to their host. This is seen as bad practice given that it exposes the core IP in plain text… and puts the operators DynDNS in control of abuse-rejecting entities.
Surprising considering how much effort the development team has placed into obfuscation..
This would suggest heuristic detections over SSL/TLS are heavily detected, thus nin turn suggesting the end of the product (VIDAR) life cycle.
- Telegram:
- Steam:
Trigger check would suggest a plain C2 as a backup.
After retrieving the C2 Vidar will send a POST
request to the URI:
{C2}/{BOT_ID}
In my case the bot id is: 907
which is also assigned a plain string:
After that first request was made the client will receive a response from the server;
1,1,1,1,1,b36abae611984b4404a903d57724b39e,1,1,1,1,0,123;%DOCUMENTS%\;*.txt;50;true;movies:music:mp3:exe;
Each operation is split with ;
delimiter
Conclusion
Though illusive, the VIDAR Stealer is heavily bound to heuristic detections. The Development team is attempting to squeeze the final bit by acquiring reputable services as a C2 for their outputs to evade heuristic analysis. This suggests the end for Vidar stealer and its’ product life cycle.
YARA Rule
rule win_Vidar
{
meta:
author = "dBouLabs"
description = "Vidar Stealer Obfuscation"
Date = "18-02-2023"
strings:
$dll1 = "vcruntime140.dll" ascii wide
$dll2 = "softokn3.dll" ascii wide
$dll3 = "nss3.dll" ascii wide
$dll4 = "msvcp140.dll" ascii wide
$dll5 = "mozglue.dll" ascii wide
$dll6 = "freebl3.dll" ascii wide
$dll7 = "sqlite3.dll" ascii wide
$c2Fetch1 = "t.me" ascii wide
$c2Fetch2 = "steamcommunity.com" ascii wide
$stringDec = {
68 ?? ?? ?? 00
68 ?? ?? ?? 00
B9 ?? ?? 00 00
E8 ?? ?? ?? ??
68 ?? ?? ?? 00
68 ?? ?? ?? 00
B9 ?? ?? 00 00
A3 ?? ?? ?? ??
}
condition:
uint16(0) == 0x5a4d and 3 of ($dll*) and 1 of ($c2Fetch*) and #stringDec >= 15
}