Intro

This blog will be a bit different from my ususal blogs, it will mainly contain scripts and some research I’ve spent on finding some of the things you’ll read through the blog. I’ve tried to cover things that weren’t covered in previous blogs that can be found on Lumma Stealer Malpedia entry

The Phish

The phishing email pretends to be from Walmart and targets sellers on the Walmart Marketplace.

image-2.png

The email claims that the recipient needs to confirm their contact and related information in order to continue selling on the platform.
The email instructs the recipient to download a file called “Walmart Brand Portal.rar” to update their information and suggests disabling antivirus protection if the download doesn’t work.
Clearly it’s not a mail sent from Walmart and the archive will contain a malicious executable in it.

Dynamic Triage Procedure

In many cases when I’m analyzing malwares I want to reach to the final payload rather than dealing with the inital loader binary.
Every analyst has it’s own tricks of how would he find and dump the actual piece of malware that he wants to analyze; And I will share what is my favourite tool when I want to get my hands quickly on the final payload.

PE-Sieve

PE-Sieve is a great tool created by hasherezade which:
“Scans a given process. Recognizes and dumps a variety of potentially malicious implants (replaced/injected PEs, shellcodes, hooks, in-memory patches).”

Dump Lumma Binary

So in order to dump the Lumma binary I will execute the executable which is stored inside of the archive delivered by the phishing email, I will monitor the executable activity in Process hacker and wait for some internal process to be created as part of the injection process. (in this case the injected process will be AddInProcess32.exe)

Step 1 - Execute the payload:

image.png

Step 2 - Use PE-Sieve on the Injected Process:

image-2.png

Into The Lumma

Now that we have the dumped payload, I will be going through some of the functionalities and capabilities that the Lumma Stealer has.

Control Flow Flattening

Lumma’s developer added some CFF to the stealer code in order to make some hard time on reversers to find their way through the right execution flow of the malware.
There are a lot of blogs talks about this obfuscation technique and how threat actors and malware developers leverages this technique to slow down malware reversers (you can find several by the end of the blog under the References section)

image-2.png

I’ve used SophosLabs emotet_unflatten_poc plugin in order to try and clean the decompiler code abit , it helped by classifying each section as a Label and now it’s abit more accessible and not requiring to scroll over the function alot:

image-3.png

Strings Obfuscation

I took a brief look at the strings presented in the extracted payload and witnessed that alot of them are obfuscated:

image.png

After doing a small research, I found out the Lumma obfuscates the strings by inserting the string 576xed inside of them.
Those strings are being deobfuscated by dedicated function which requires the obfuscated string as an input and afterward it returns the clean string.

image-4.png

I’ve created python script that will get all the xrefs for the strings deobfuscating function, extract the argument being passed to the function and then will deobfuscate it and write the strings to a file:

import idc
import idautils

DECRYPTION_FUNCTION = 0x45DF86 # Change to the relevant function call
STRINGS_FILE_PATH = '' # Output file for the strings
OBFUSCATOR_STRING = '576xed' # Might be changed in future builds

def getArg(ref_addr):
    ref_addr = idc.prev_head(ref_addr)
    if idc.print_insn_mnem(ref_addr) == 'push':
        if idc.get_operand_type(ref_addr, 0) == idc.o_imm:
            return(idc.get_operand_value(ref_addr, 0))
        else:
            return None

stringsList = []

for xref in idautils.XrefsTo(DECRYPTION_FUNCTION):
    argPtr = getArg(xref.frm)
    if not argPtr:
        continue
    data = idc.get_bytes(argPtr, 100)
    obfuscatedData = data.split(b'\x00\x00')[0].replace(b'\x00',b'').decode()
    stringsList.append(obfuscatedData.replace(OBFUSCATOR_STRING,""))

print(f'[+] {len(stringsList)} Strings were extracted')

out = open(STRINGS_FILE_PATH, 'w')
for string in stringsList:
    out.write(f'{string}\n')
out.close()
[+] 135 Strings were extracted

################
# OUTPUT FILE: #
################

\Local Extension Settings\
/Extensions/
*
nkddgncdjgjfcddamfgcmfnlhccnimig
NeoLine
cphhlgmgameodnhkjdmkpanlelnlohao
Clover
nhnkbkgjikgcigadomkphalanndcapjk
Liquality
kpfopkelmapcoipemfendmdcghnegimn
Terra Station
fhmfendgdocmcbmfikdcogofphimnkno
Auro
cnmamaachppnkjgnildpdmkaakejnhae
aeachknmefphepccionboohckonoeemg
Authenticator
bhghoamapcdpbohphigoooaddinpkbai
Cyano
dkdedlpgdmmkkfjabffeganieamfklkm
Byone
Login Data For Account
OneKey
Nifty
jbdaocneiiinmjbjlgalhcelgbejmnid
Math
iWlt
kncchdigobghenbbaddojjnnaogfppfj
EnKrypt
kkpllkodjeloidieedojogacfhpaihoh
Wombat
amkmjjmmflddogmhpjloimipbofnfjih
MEW CX
nlbmnnijcnlegkjjpcfjclmcfggfefdm
Guild
nanjmdknhkinifnkgdcggcfnhdaammmj
Coin98
infeboajgfhgbjpjbeppbkgnabfdkdaf
Leaf
cihmoadaighcejopammfbmddcmdekcje
Authy
ejbalbakoplchlghecdalmeeeajnimhm
nkbihfbeogaeaoehlefnkodbefgpgknn
TronLink
ibnejdfjmmkpcnlpebklmnkoeoihofec
Ronin Wallet
fnjhmkhhmkbjkkabndcnnogagogbneec
Binance Chain Wallet
fhbohimaelbohpjbbldcngcnapndodjp
Yoroi
afbcbjpbpfadlkmhmclhkeeodmamcflc
gaedmjdfmmahhbjefcbgaolhhanlaolb
Saturn
bcopgchhojmggmffilplmbdicgaihlkp
ZilPay
klnaejjgbibmhlephnhpmaofohgkpgkd
Phantom
bfnaelmomeimhlpmgjnjophhpkkoljpa
hcflpincpppdclinealmandijcmnkbgn
Temple
ookjlbkiijinhpmnjffcofjonbfbgaoc
TezBox
mnfifefkajgofkcjkemidiaecocnkjeh
DAppPlay
lodccjjbdhfakaekdiahmedfbieldgik
BitClip
ijmpgkjfkbfhoebgogflfebnmejmfbml
Steem Keychain
History
Jaxx Liberty
cjelfplplebdjjenllpjcblmjkfcffne
BitApp
fihkakfobkmkjojpchpfgcmhfjnmnfpi
Network\Cookies
History
Polymesh
jojhfeoedkpkglbfimdfabpdfjaoolaf
ICONex
flpiciilemghbmfalicajoolhkkenfel
Nabox
ffnbelfdoeiohenkjibnmadjiehjhajb
Web Data
Login Data
aiifbnbfobpmeekipheeijimdpnlpgpp
Keplr
dmkamcknogkgcdfhhbddcghachkejeap
Sollet
nlgbhdfgdhgbiamfdfmbikcdghidoadd
Coinbase
hnfanknocfeofbddgcijnmhnfnkdnaad
Guarda
hpglfhgfnhbgpjdenjgmdgoeiappafln
EQUAL
blnieiiffboillknjnepogjhkgnoapac
lkcjlnjfpbikmcmbachjpdbijejflpcm
Nash Extension
onofpnbbkehpmmoabgpcpmigafmmnjhl
Hycon Lite Client
Trezor Password Manager
imloifkgjagghnncjkhggdhalmcnfklk
EOS Authenticator
oeljdldpnmdbchonielidgobddffflal
GAuth Authenticator
ilgcnhelpchnceeipipijaljkblbcobl
nknhiehlklippafakaeklbeglecifhad
KHC
\Local State
*.txt
%userprofile%
Wallets/Ethereum
keystore
%appdata%\Ethereum
%localappdata%\Kometa\User
Chromium
%localappdata%\Chromium\User Data
Edge
%localappdata%\Microsoft\Edge\Us
%appdata%\Opera Software\Op576xe
Chrome
%localappdata%\Google\Chro
Mozilla Firefox
Wallets/Binance
app-store.json
%appdata%\Binance
Kometa
Important Files/Profile
Opera GX Stable
%appdata%\Opera Software\Op576xe
Opera Neon
Opera Stable
%appdata%\Opera Software\Op576xe
Wallets/Electrum
*
%appdata%\Electrum\wallets
%appdata%\Mozilla\Firefox\Prof57
System.txt

Chrome Extensions (CRX)

Looking at the strings there is a lot of extensions names that Lumma targets, but the thing that I was curious about were the 32 length lower case strings (for example: ilgcnhelpchnceeipipijaljkblbcobl) those strings are actually CRX IDs (Chrome Extension ID) which by navigating to C:\Users\User\AppData\Local\Google\Chrome\User Data\Default\Extensions and looking for the CRX ID the stealer will know whether or not the extension exists on the victims computer and if it does it will continue to exfiltrate the extension sensitive data.

image.png

I’ve created a python script that will lookup for those unique ID’s in the previously extracted strings file and fetch the name of the extension from google chrome webstore:

import re, requests

LUMMA_STRINGS = '/Users/igal/malwares/Lumma/29.03.2023/LummaStrings.txt'
REGEX_PATTERN = '^[a-z]{32}$'
strings = open(LUMMA_STRINGS,'r').read()
crxExtensionList = re.findall(REGEX_PATTERN,strings,re.MULTILINE)


for crxId in crxExtensionList:
    url = f'https://chrome.google.com/webstore/detail/{crxId}'
    responseText = requests.get(url).text
    try:
        startIndex = responseText.index('itemprop="name" content="') + len('itemprop="name" content="')
        endIndex = responseText.index('"', startIndex)
        extensionName = responseText[startIndex:endIndex]
        print(f'[+] Extension name:{extensionName} , CRX ID:{crxId}')
    except ValueError:
        continue
[+] Extension name:NeoLine , CRX ID:cphhlgmgameodnhkjdmkpanlelnlohao
[+] Extension name:CLV Wallet , CRX ID:nhnkbkgjikgcigadomkphalanndcapjk
[+] Extension name:Liquality Wallet , CRX ID:kpfopkelmapcoipemfendmdcghnegimn
[+] Extension name:Auro Wallet , CRX ID:cnmamaachppnkjgnildpdmkaakejnhae
[+] Extension name:Coin98 Wallet , CRX ID:aeachknmefphepccionboohckonoeemg
[+] Extension name:Authenticator , CRX ID:bhghoamapcdpbohphigoooaddinpkbai
[+] Extension name:Cyano Wallet , CRX ID:dkdedlpgdmmkkfjabffeganieamfklkm
[+] Extension name:iWallet , CRX ID:kncchdigobghenbbaddojjnnaogfppfj
[+] Extension name:Enkrypt: Ethereum, Polkadot & Canto Wallet , CRX ID:kkpllkodjeloidieedojogacfhpaihoh
[+] Extension name:Wombat - Gaming Wallet for Ethereum & EOS , CRX ID:amkmjjmmflddogmhpjloimipbofnfjih
[+] Extension name:MEW CX - is now Enkrypt , CRX ID:nlbmnnijcnlegkjjpcfjclmcfggfefdm
[+] Extension name:LeafWallet - Easy to use EOS wallet , CRX ID:cihmoadaighcejopammfbmddcmdekcje
[+] Extension name:MetaMask , CRX ID:nkbihfbeogaeaoehlefnkodbefgpgknn
[+] Extension name:TronLink , CRX ID:ibnejdfjmmkpcnlpebklmnkoeoihofec
[+] Extension name:Ronin Wallet , CRX ID:fnjhmkhhmkbjkkabndcnnogagogbneec
[+] Extension name:Binance Wallet , CRX ID:fhbohimaelbohpjbbldcngcnapndodjp
[+] Extension name:Math Wallet , CRX ID:afbcbjpbpfadlkmhmclhkeeodmamcflc
[+] Extension name:Authy , CRX ID:gaedmjdfmmahhbjefcbgaolhhanlaolb
[+] Extension name:Hycon Lite Client , CRX ID:bcopgchhojmggmffilplmbdicgaihlkp
[+] Extension name:ZilPay , CRX ID:klnaejjgbibmhlephnhpmaofohgkpgkd
[+] Extension name:Phantom , CRX ID:bfnaelmomeimhlpmgjnjophhpkkoljpa
[+] Extension name:KHC , CRX ID:hcflpincpppdclinealmandijcmnkbgn
[+] Extension name:Temple - Tezos Wallet , CRX ID:ookjlbkiijinhpmnjffcofjonbfbgaoc
[+] Extension name:TezBox - Tezos Wallet , CRX ID:mnfifefkajgofkcjkemidiaecocnkjeh
[+] Extension name:DAppPlay , CRX ID:lodccjjbdhfakaekdiahmedfbieldgik
[+] Extension name:Polymesh Wallet , CRX ID:jojhfeoedkpkglbfimdfabpdfjaoolaf
[+] Extension name:ICONex , CRX ID:flpiciilemghbmfalicajoolhkkenfel
[+] Extension name:Yoroi , CRX ID:ffnbelfdoeiohenkjibnmadjiehjhajb
[+] Extension name:Station Wallet , CRX ID:aiifbnbfobpmeekipheeijimdpnlpgpp
[+] Extension name:Keplr , CRX ID:dmkamcknogkgcdfhhbddcghachkejeap
[+] Extension name:Byone , CRX ID:nlgbhdfgdhgbiamfdfmbikcdghidoadd
[+] Extension name:Coinbase Wallet extension , CRX ID:hnfanknocfeofbddgcijnmhnfnkdnaad
[+] Extension name:Guarda , CRX ID:hpglfhgfnhbgpjdenjgmdgoeiappafln
[+] Extension name:Trezor Password Manager , CRX ID:imloifkgjagghnncjkhggdhalmcnfklk
[+] Extension name:EOS Authenticator , CRX ID:oeljdldpnmdbchonielidgobddffflal
[+] Extension name:GAuth Authenticator , CRX ID:ilgcnhelpchnceeipipijaljkblbcobl
[+] Extension name:Nabox Wallet , CRX ID:nknhiehlklippafakaeklbeglecifhad

Dynamic API Resolve

Lumma hides some of its APIs by hashing then using the MurmurHash2 hashing algorithim , it can be identifed by the const: 0x5bd1e995:

image.png

Lumma will pass two arguments to the API resolving function:

  1. The hash of the wanted API
  2. The DLL which contains the API

image-2.png

I have two scripts that I wrote for this part, the first script is IDA script that will get all the xrefs to the API resolving function, extract the hash and the API hash, and will save the output to a text file:

import idc
import idautils

API_FUNCTION = 0x471958 # Change to the relevant function call
APIS_FILE_PATH = '' # Output file for the strings

apiDict = {}

def getDLLRef(hash_addr):
    ref_addr = idc.prev_head(hash_addr)
    if idc.print_insn_mnem(ref_addr) == 'push':
        if idc.get_operand_type(ref_addr, 0) == idc.o_imm:
            dll_addr = idc.get_operand_value(ref_addr, 0)
            return idc.get_bytes(dll_addr, 50).split(b'\x00\x00')[0].replace(b'\x00',b'').decode()
    return None

def getHashDict(ref_addr):
    ref_addr = idc.prev_head(ref_addr)
    if idc.print_insn_mnem(ref_addr) == 'push':
        if idc.get_operand_type(ref_addr, 0) == idc.o_imm:
            hashVal = hex(idc.get_operand_value(ref_addr, 0))
            if hashVal not in apiDict or apiDict[hashVal] == None:
                dllVal = getDLLRef(ref_addr)
                apiDict[hashVal] = dllVal

for xref in idautils.XrefsTo(API_FUNCTION):
    getHashDict(xref.frm)

print(f'[+] {len(apiDict)} API hashes were extracted')

out = open(APIS_FILE_PATH, 'w')
for k, v in apiDict.items():
    out.write(f'{k} - {v}\n')
out.close()

[+] 18 API hashes were extracted

################
# OUTPUT FILE: #
################

0xe8ff1073 - crypt32.dll
0x864087d1 - crypt32.dll
0x7328f505 - kernel32.dll
0xc40f97d4 - advapi32.dll
0x507048c2 - winhttp.dll
0x406457c2 - winhttp.dll
0x7aa0edcc - winhttp.dll
0xb72f0de - winhttp.dll
0x59886bc0 - winhttp.dll
0x76b029a - winhttp.dll
0xf9f57cf0 - winhttp.dll
0xe268a0c1 - winhttp.dll
0xab3372e8 - winhttp.dll
0x5658bf2e - KernelBase.dll
0x23fef64a - advapi32.dll
0x5f086d32 - kernel32.dll
0xa2f80070 - kernel32.dll
0x2f9959e0 - kernel32.dll

The second script will extract the hashes and the DLLs names from the text file, and based on the DLL name it will be loaded using PEfile, and iterate through the DLL exports , hash them using murmurhash2 and compare it to the given hash: (NOTE: The seed in this case was 0x20 but it might be possible changed in future builds)

import pefile
from murmurhash2 import murmurhash2

DLLS_PATH = '/Users/igal/malwares/Lumma/29.03.2023/dlls/' # can be replaced with system32 folder
LUMMA_API_HASHES = '/Users/igal/malwares/Lumma/29.03.2023/LummaApiHashes.txt'

SEED = 0x20 # might be changed in upcoming builds

def hashDllAPI(dllName, apiHash):
    pe = pefile.PE(dllName)
    for export in pe.DIRECTORY_ENTRY_EXPORT.symbols:
        try:
            expName = export.name
            hashValue = murmurhash2(expName, SEED)
            if hex(hashValue) == apiHash:
                return expName.decode()
        except AttributeError:
            continue

apiHashesFile = open(LUMMA_API_HASHES,'r').read()
lines = apiHashesFile.split('\n')


for line in lines:
    args = line.split(' - ')
    dllName = hashDllAPI(f'{DLLS_PATH}{args[1]}', args[0])
    print(f'[+] {args[0]} - {dllName}')
[+] 0xe8ff1073 - CryptStringToBinaryA
[+] 0x864087d1 - CryptUnprotectData
[+] 0x7328f505 - ExpandEnvironmentStringsW
[+] 0xc40f97d4 - GetCurrentHwProfileA
[+] 0x507048c2 - WinHttpOpenRequest
[+] 0x406457c2 - WinHttpConnect
[+] 0x7aa0edcc - WinHttpCloseHandle
[+] 0xb72f0de - WinHttpSendRequest
[+] 0x59886bc0 - WinHttpWriteData
[+] 0x76b029a - WinHttpReceiveResponse
[+] 0xf9f57cf0 - WinHttpOpen
[+] 0xe268a0c1 - WinHttpSetTimeouts
[+] 0xab3372e8 - WinHttpAddRequestHeaders
[+] 0x5658bf2e - IsWow64Process2
[+] 0x23fef64a - GetUserNameA
[+] 0x5f086d32 - GetPhysicallyInstalledSystemMemory
[+] 0xa2f80070 - GetComputerNameA
[+] 0x2f9959e0 - GetSystemDefaultLocaleName

Yara Rule

rule Win_LummaC2 {
    meta:
        author = "0xToxin"
        description = "LummaC2 Strings"
    strings:
		$obfuscatorString = "576xed" ascii wide
		$s1 = "dp.txt" ascii wide
		$s2 = "c2sock" ascii wide
		$s3 = "TeslaBrowser" ascii wide
		$s4 = "Software.txt" ascii wide
    condition:
        uint16(0) == 0x5a4d and all of ($s*) and #obfuscatorString > 10 and filesize < 1500KB
}

Yara Hunt of the rule

Summary

In this blog I went over some techniques used by the Lumma stealer including the hashing algorithim used in the API hashing procedure, the strings obfuscation and some Google Chrome extensions research.
If you want to learn more about how Lumma exfiltrates it data, check the blogs in the references below.

IOCs

References