Vidar Stealer

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 calls c000066.m00007b with the string fInckSommmenn.
  • The method c000066.m00007b retrieves resource content from the binary resources:

 

  • The extracted resource content, the string fInckSommmenn, and the c000009 instance are passed to c000066.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 is JohnDoe:

 

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 NameDescription
freebl3.dllNetwork Security Services (NSS) from Mozilla Foundation
mozglue.dllMemory management for Mozilla applications
msvcp140.dllMicrosoft Visual C++ library for C++ programming
nss3.dllNetwork security services for SSL/TLS encryption
softokn3.dllCryptographic library for key management and encryption/decryption
sqlite3.dllAccessing and managing SQLite databases
vcruntime140.dllMicrosoft 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
}
Posted in Write-Ups
Write a comment