Executive summary

The 74 MB sample is a hijacked copy of MultiCommander’s MultiUpdate.exe with a 347 KB encrypted Lumma payload smuggled inside its .reloc section, and 70 MB of 0xCC padding bolted onto the end purely to defeat AV/sandbox file-size limits and the published YARA rule’s filesize < 5000KB condition.

This binary is a textbook 2024-era LummaC2 delivery artifact. Four structural tricks combine to defeat naive defenses:

  1. Legitimate-software disguise. The carrier is a recompiled copy of the open-source MultiCommander auto-updater. PDB path, manifest identity (Microsoft.Windows.AutoUpdate), embedded XML schema, version-string lookups against multicommander.com, and ASUS-related strings all survive in the binary. A defender allowlisting on those signatures would be wide open.
  2. Lifted Authenticode signatures from the leaked NVIDIA certs. The carrier is dual-signed with both NVIDIA Corporation code-signing certificates leaked in the March 2022 Lapsus$ breach (serials 14781BC8…DCC518 and 7BC15AF2…8642DE). Both signatures fail cryptographic verification (INVALID_DIGEST) — this is signature lifting, not signing-with-leaked-key — but EDR and triage tools that check “is signed by NVIDIA” without validating the digest will trust the file. See §3.
  3. Encrypted payload concealed in .reloc. The PE’s relocation section legitimately holds 33 KB of base relocations (entropy 6.65). A further 347 KB of high-entropy data (entropy 7.51) is appended inside the same section, well past the boundary declared in the BaseRelocationTable data directory. Static analysers that trust the directory miss it; the runtime loader needs only the legitimate relocs and ignores the trailer.
  4. Massive 0xCC overlay padding. 73,513,369 bytes of INT3 bytes pad the file to 74 MB. The padding has zero functional purpose. It exists to (a) exceed AV file-size scanning limits, (b) push the file beyond the 32 MB / 50 MB upload caps used by sandboxes and bazaars, and (c) silently break the public YARA rule (win.lumma_w1.yar, Embee Research) which requires filesize < 5000KB.

Family attribution is certain. A companion artifact System.txt recovered from the same engagement is a panel-side exfiltration dump (“LummaC2, Build Oct 9 2023”, victim SINTHUJAN, egress 185.160.247.17 / CH) — confirming the Lumma operator chain directly from infrastructure-side telemetry.

74 MB
Delivered file size
1.6 %
Of file is real PE
347 KB
Encrypted payload

Sample identification

Delivered artifact (74 MB padded)

File typePE32 GUI executable, Intel i386, 5 sections, MFC-based
Size74,715,545 bytes (71.25 MiB)
MD5a3be0b0ebbf9428015cacc27cf5d51a7
SHA-123f0f30e2bc4fb1308c01328e951b1681f439d46
SHA-256eff51f995cd6463cd9b3a2ea4a14cc85e3cc5c1b5b71db6d90765b3df175abba
PE TimeDateStamp2024-12-23 13:44:40 UTC (forgeable)
ImageBase / EP RVA0x00400000 / 0x00046ffc
Digital signatureBroken — security data dir VA=0x473d7d8 Size=0x39c0 points beyond EOF
LinkerMicrosoft VS 2022 Pro, MSVC 14.42 (ATL/MFC, statically linked libxml2)
PDB pathD:\Projects\MultiCommander\BuildOutput\Output\Win32\Release v143\MultiUpdate\MultiUpdate.pdb
Manifestname=“Microsoft.Windows.AutoUpdate” description=“MultiUpdate”

Stripped carrier (overlay removed)

Created byTruncating to first 1,202,176 bytes (sum of section raw sizes)
Size1,202,176 bytes (1.15 MiB)
SHA-256c466795354007a604fa1805b6d97b6f3e43179c85544594fe365f22ede8fe0a6
UseSubstrate for radare2 / static analysis. Now under the 5000 KB threshold of the published YARA rule (still does not match — see §8)
Engagement corroboration

Family attribution: certain

The companion file System.txt recovered from the same engagement is a real LummaC2 panel exfil dump (“LummaC2, Build Oct 9 2023”, victim SINTHUJAN, egress 185.160.247.17 / CH) — confirming the Lumma operator chain directly from infrastructure-side telemetry recovered during the response.

Distribution model: Malware-as-a-Service on Russian-language forums, attributed to threat actor Shamel / Lumma (Eastern European cybercriminal ecosystem).


Carrier anatomy

File-level layout (74 MB delivered artifact)

Bar A — true-to-scale view of the 74 MB file. The PE image (1.2 MB of code + data) is the thin sliver on the left; everything else is 0xCC overlay padding.

PE1.2 MB · 1.61 % of file
0xCC overlay padding70.1 MiB · 98.39 % · 73,513,369 bytes

Bar B — zoomed into the 1.2 MB PE image (overlay removed, this is where every meaningful byte lives). Segment widths are proportional to raw section sizes within the image.

.text50.6 % · 594 KB
.rdata13.8 % · 162 KB
.data1.0 % · 11 KB
.rsrc2.8 % · 33 KB
.reloc legit2.8 % · 33 KB relocs
.reloc trailer28.9 % · 347 KB ENCRYPTED

The encrypted .reloc trailer (red, right side of Bar B) is almost a third of the actual binary — yet it is invisible in Bar A because of the overlay bloat that hides it from file-size-bounded scanners. That asymmetry is the whole point of the technique.

PE section table

NameVAddrRawSizeEntropyNote
.text0x004010000x00094a006.6849MFC application code (carrier)
.rdata0x004960000x000288005.1722Read-only data, RTTI, manifest, vftables
.data0x004bf0000x00002e004.4563Initialised globals
.rsrc0x004c90000x000084004.7417MFC dialog/string resources
.reloc0x004d20000x0005d0007.557933 KB legit relocs + 347 KB encrypted trailer

Overlay characterisation

Start (file offset)0x00125800 (= last raw section end)
End0x04747999 (EOF)
Length73,513,369 bytes (≈ 70.1 MiB)
Content0xCC fill (INT3 instruction byte). First-1 MB and middle-1 MB chunks contain 8 distinct byte values; the last ~14 KB at file offset 0x473d7d8 is a fully-formed Authenticode signature blob lifted from a legitimately NVIDIA-signed binary (see §3 Authenticode signature)
Entropy (first 1 MB)0.0775
Entropy (last 1 MB)4.2181
FunctionDefeat AV/sandbox file-size limits; bypass YARA rules with filesize conditions; inflate uploads beyond bazaar caps

The MultiCommander disguise

The carrier is built from MultiCommander’s open-source MultiUpdate.exe auto-updater. The threat actor took the legitimate source, added the unpacking stub plus the encrypted payload, and recompiled. Multiple inherited artefacts survive in the binary — including the embedded application manifest:

<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity version="1.0.0.0" processorArchitecture="X86"
                    name="Microsoft.Windows.AutoUpdate" type="win32"/>
  <description>MultiUpdate</description>
  <dependency><dependentAssembly>
    <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls"
                      version="6.0.0.0" processorArchitecture="X86"
                      publicKeyToken="6595b64144ccf1df" language="*"/>
  </dependentAssembly></dependency>
  <trustInfo><security><requestedPrivileges>
    <requestedExecutionLevel level="asInvoker" uiAccess="false"/>
  </requestedPrivileges></security></trustInfo>
</assembly>

Selected UTF-16 strings recovered from the carrier (genuine MultiCommander UI text):

"Multi Commander-http://multicommander.com/updates/version.xml"
"Failed to copy existing version to backup. Turn off Backup setting if you want to..."
"Warning! Failed to create directory \"%s\". But the folder might already exists..."
"Warning. A newer MultiUpdate.exe was found on disk. It is recommended that you run that instead."
"%s' is running. It can't be running when it is about to be updated."
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; GTB5; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"
"Or reinstall a new version that you download from ASUS."
"D:\Projects\MultiCommander\BuildOutput\Output\Win32\Release v143\MultiUpdate\MultiUpdate.pdb"

Suspicious imports (carrier surface)

The visible import table is the carrier’s surface and is intentionally benign-looking. The payload’s real imports are resolved dynamically post-decryption via LoadLibraryA/W + GetProcAddress. Selected suspicious-but-explainable entries from the 433-function visible IAT:

DLLFunctionCarrier usePayload abuse
KERNEL32LoadLibraryA / LoadLibraryWUpdater plug-insResolve payload API set at runtime
KERNEL32GetProcAddressDynamic API resolution (T1129)
KERNEL32WinExec / CreateProcessWAuto-relaunchLaunch second-stage payload
KERNEL32OpenProcessUpdater process checkProcess inspection / token theft
KERNEL32IsDebuggerPresent(none)Anti-debug (T1622)
ADVAPI32RegCreateKeyExW / RegSetValueExWUpdater configPersistence via Run key (T1547.001)
SHELL32ShellExecuteW / ShellExecuteExWOpen release notes URLDrop-and-execute child stages
WS2_32WSAStartupUpdate downloadC2 communication primitive
Defensive implication

Allowlisting on file/process name fails.

Allowlisting Lumma carriers by file/process name (MultiUpdate.exe) or by inherited string content is fragile — the actor controls those. Enforce signed-publisher checks (genuine MultiCommander binaries are Authenticode-signed; this one is not), or behavioural detection (an updater that unpacks code from .reloc, beacons to Steam, then to .shop domains).

Authenticode signature: lifted leaked NVIDIA certs

The PE has an Authenticode signature blob at file offset 0x473d7d8 (size 0x39c0, 14,784 bytes — well within the file, not past EOF as one might conclude from the file-size arithmetic alone). The blob is a fully-formed PKCS#7 SignedData structure. It parses cleanly, and what it contains is the interesting part: the carrier is dual-signed with two NVIDIA Corporation code-signing certificates. The SHA-1 leaf cert is one of the two confirmed March 2022 Lapsus$ leaked NVIDIA certs; the SHA-256 partner is the matching dual-sign cert that travelled with it in the lifted PKCS#7 blob.

#DigestCert serialSubject CN/OUValidityIssuer
0 SHA-1 14781BC862E8DC503A559346F5DCC518 NVIDIA Corporation 2015-07-28 → 2018-07-26 VeriSign Class 3 Code Signing 2010 CA
1 SHA-256 7BC15AF21367D0758BEDDCCA118642DE NVIDIA Corporation, OU=IT MIIS 2015-07-13 → 2018-07-13 Symantec Class 3 SHA256 Code Signing CA

Both signatures countersign with a Symantec Time Stamping Services Signer cert at signing time 2017-03-16 23:31:05 UTC — a date that falls comfortably inside both leaf certs’ validity windows, but more than seven years before this binary’s PE TimeDateStamp of 2024-12-23. That gap is the smoking gun: the timestamp didn’t come from a legitimate TSA round-trip on this binary in 2017 because this binary didn’t exist in 2017. The entire signed PKCS#7 blob was lifted from a real NVIDIA-signed driver or utility and grafted onto the carrier.

Cryptographic verification confirms it. Using signify:

$ python3 -c "
from signify.authenticode import AuthenticodeFile
with open('eff51f99...abba.exe', 'rb') as f:
    af = AuthenticodeFile.from_stream(f)
    print(af.explain_verify())"

AuthenticodeVerificationResult.INVALID_DIGEST
The expected hash does not match the digest in the indirect data.

The hash baked into the signed Authenticode structure does not match the actual bytes of this PE. Windows would refuse to validate either signature. So why bother lifting them?

Defensive implication

Metadata-only signature checks see “NVIDIA Corporation” and stop asking questions.

Plenty of EDR pipelines, triage tools, and human analysts read the certificate metadata without performing the cryptographic digest verification. To them, this file is signed by NVIDIA Corporation — full stop. Lifted signatures cost attackers nothing (no need to compromise the private key, just copy a signed blob from any GeForce driver) and produce a trust signal that bypasses metadata-based reputation checks. Cryptographic verification of the Authenticode signature, not just the certificate chain, is the only check that catches this pattern.

The two cert serials and SHA-1 thumbprints are durable IOCs for this campaign cluster — reused across many lifted- signature samples in the wild — and are reproduced in §10 IOCs.

Common misreading: is the 70 MB padding encrypted, packed, or a bound browser?

The file size makes the overlay tempting to read as a hidden second-stage binary — a packed Lumma payload, a bundled browser, or some other concealed cargo. It isn’t. Five independent static checks all point the same way:

  1. Entropy. 4 KB-block entropy across the 67.5 MB middle of the overlay sits at 0.0775 bits/byte. Encrypted data is at ~7.95, compressed (gzip / zip / lzma) at ~7.5, mixed-section PE at 6–7. There is no encryption or compression scheme that produces 0.08.
  2. Whole-file block scan. rahash2 -B -b 4096 -a entropy over the full 75 MB file flags only 77 blocks at entropy ≥ 7.0. Every one of them lives inside the .reloc trailer (the known 347 KB Lumma payload, §4) or the trailing 14 KB lifted NVIDIA Authenticode blob (legitimately high entropy because real signed PKCS#7 contains compressed signature material). The 67.5 MB main body has zero high-entropy blocks.
  3. Magic-byte hunt. Searches across the overlay for MZ, \x7fELF, PK\x03\x04, gzip, 7-Zip, RAR, NSIS, Inno Setup, single-byte-XOR’d MZ, LZMA, PNG / JPEG / WebM — zero hits. A bound browser would leave at least the wrapper or inner-PE signature.
  4. Imports. VirtualAlloc, VirtualProtect, and the Crypt* family are not in the static IAT. Only VirtualQuery is. The carrier as-shipped has no static path to allocate executable memory or flip a page to RX — which is what hosting an embedded binary would require.
  5. Structure. The overlay isn’t random fill, but it also isn’t data. A 15-byte template (f8 64 6e 09 · eight 0xCC · 90 b0 80) repeats identically every 1,035 bytes — ~71,000 cycles across 67.5 MB. Every 1 MB chunk has the same byte-distribution fingerprint: 8 distinct values, entropy 0.0775, exactly 7,091 non-0xCC bytes. Identical statistics across 68 megabytes is the signature of a custom padding tool, not data.
Verdict

The bound-browser hypothesis fails on entropy, magic bytes, imports, and structure.

The actual payload is the 347 KB encrypted blob in the .reloc trailer (§4). The 70 MB overlay is inert padding produced by tooling. The 7-byte beacon (f8 64 6e 0990 b0 80) is plausibly a campaign or build fingerprint and is itself a candidate for a structural YARA hunt rule independent of any payload changes. The padding pattern’s slight non-0xCC content also defeats the most naive compression-ratio detection (pure 0xCC would gzip 70 MB to ~30 bytes, which is itself anomalous).


The .reloc trick

The .reloc section is the most interesting part of the file and is where Lumma is actually hiding. The PE’s BaseRelocationTable data directory declares VA=0xd2000, Size=0x8224 — i.e. the legitimate relocation table is exactly 33,316 bytes. But the section’s raw size on disk is 0x5D000 (380,928 bytes). That leaves 347,612 bytes of “extra” data inside the section that the PE loader will happily map into the process but that the relocation logic will never touch.

Per-4-KB-window entropy across .reloc

Sliding entropy reveals exactly where the encrypted blob lives — clearly bracketed by zero-filled padding on both sides:

section_off  file_off    entropy  visualisation
0x000000   0x00c8800   6.752   ███████████████████████████   ┐
0x002000   0x00ca800   6.761   ███████████████████████████   │ legitimate
0x005000   0x00cd800   5.792   ███████████████████████       │ relocation
0x008000   0x00d0800   7.003   ████████████████████████████  │ table (33 KB)
0x009000   0x00d1800   5.391   █████████████████████   ← drop-off
0x00a000   0x00d2800   4.181   ████████████████        ← zero-filled gap
0x00b000   0x00d3800   7.256   █████████████████████████████ ┐
0x00c000   0x00d4800   7.396   █████████████████████████████ │ ENCRYPTED
0x00d000   0x00d5800   7.284   █████████████████████████████ │ LUMMA
0x00f000   0x00d7800   7.313   █████████████████████████████ │ PAYLOAD
…                                                            │ (~339 KB,
0x051000   0x0119800   6.493   █████████████████████████     │  steady ~7.4
0x054000   0x011c800   6.850   ███████████████████████████   │  entropy)
0x05c000   0x0124800   7.195   ████████████████████████████  ┘
                                                  ← 317-byte zero alignment

Encrypted blob coordinates

Section offset (start)0x8400
File offset (start)0x000d0c00
Virtual address (start)0x004da400
Length346,819 bytes (0x54ac3)
Entropy (just the blob)7.5097
Padding before blob0x8224 → 0x8400 (476 bytes of zeros — alignment)
Padding after blob0x5cec3 → 0x5d000 (317 bytes of zeros — alignment)

First 256 bytes of the encrypted payload

0x004da400 4f 81 f2 17 f5 50 5f 33 f2 87 c3 c1 c8 0c f7 de O....P_3........ 0x004da410 2b f1 c1 ce 0f 81 e8 d0 cb 19 ef f7 d1 46 2b dd +............F+. 0x004da420 33 dc f7 de 33 d9 c1 c1 1d 87 f2 33 c4 33 c4 87 3...3......3.3.. 0x004da430 f2 c1 c9 1d 33 d9 f7 de 33 dc 03 dd 4e f7 d1 81 ....3...3...N... 0x004da440 c0 d0 cb 19 ef c1 c6 0f 03 f1 f7 de c1 c0 0c 87 ................ 0x004da450 c3 33 f2 81 f2 17 f5 50 5f 47 be a4 47 05 00 c1 .3.....P_G..G... 0x004da460 c2 1d f7 d6 c1 c3 0f 81 ea 56 e0 1e da 87 c3 c1 .........V...... 0x004da470 ce 13 81 e9 b6 62 f3 8e 42 f7 d1 c1 c0 02 c1 c8 .....b..B....... 0x004da480 02 f7 d1 4a 81 c1 b6 62 f3 8e c1 c6 13 87 c3 81 ...J...b........ 0x004da490 c2 56 e0 1e da c1 cb 0f f7 d6 c1 ca 1d 87 c1 81 .V..............

No plaintext signatures (no "expand 32-byte k" ChaCha sigma, no AES S-box, no ZIP/PK magic). Byte distribution is near-uniform across all 256 values, consistent with a stream cipher or compressed-then-XOR’d output. Recent Lumma analyses (Q4 2024) document ChaCha20 as the in-payload string cipher, with the loader stage typically using a custom XOR-rotate construction; expect either to apply here.

Why this works against most tooling

Three independent layers of obscurity.

PE viewers that read the Base Relocation Table directory only see 33 KB and conclude .reloc is normal. Section-entropy heuristics that average across the section are diluted by the legitimate relocs and look “OK-ish” (overall .reloc entropy 7.56 is borderline). Static unpackers typically follow Authenticode / overlay / TLS callback / packer-section-name patterns; “data hidden after the legitimate reloc table” is unusual and unlikely to be spotted automatically.


Reverse engineering with radare2

All commands below were run against /tmp/lumma_stripped.exe (overlay-stripped derivative) with radare2 6.1.4. The stripped binary is functionally identical to the carrier — the overlay does not affect any code or data referenced by the PE.

Triage

$ r2 -e bin.cache=true /tmp/lumma_stripped.exe
[0x00446ffc]> aaa             # full analysis pass
[0x00446ffc]> iI               # binary info
[0x00446ffc]> iS               # sections + entropy bars
[0x00446ffc]> ii~LoadLibrary   # filter imports
[0x00446ffc]> aflc             # function count
3017

3,017 functions is a lot for a 596 KB .text; consistent with a fully-built MFC application. The unpacking stub is hidden among thousands of legitimate framework functions — security-through-obscurity layered on top of the section trick.

Entry point: standard MSVC CRT bootstrap

[0x00446ffc]> pd 4 @ entry0
        ;-- entry0:
┌ 337: entry0 ();
│       0x00446ffc      e87a0a0000     call fcn.00447a7b   ; __scrt_common_main_seh / cookie init
│       jmp 0x446e80                                       ; __scrt_common_main → mainCRTStartup → WinMain

The entry point is a normal MSVC C runtime startup. The first call (0x447a7b) is the __security_init_cookie-style cookie initialiser; control then jumps into the CRT pre-main and ultimately reaches the carrier’s WinMain. The unpacking stub is not at the entry point — that would be too obvious. It is reached after normal MFC initialisation, hidden among the genuine update logic.

The PE-section walker (function 0x00447014)

radare2’s analyser identified a function at 0x00447014 that walks the loaded image’s PE header and iterates the section table. This is the classic shape of “find .reloc at runtime, then read the trailer at offset 0x8400”:

[0x00447014]> pdf @ 0x447014
┌ 66: fcn.00447014 (int32_t arg_8h, uint32_t arg_ch);
│   0x00447014   55             push ebp
│   0x00447015   8bec           mov ebp, esp
│   0x00447017   8b4508         mov eax, dword [arg_8h]    ; image base
│   0x0044701b   8b483c         mov ecx, dword [eax + 0x3c] ; e_lfanew
│   0x0044701e   03c8           add ecx, eax                ; → IMAGE_NT_HEADERS
│   0x00447020   0fb74114       movzx eax, word [ecx + 0x14]; SizeOfOptionalHeader
│   0x00447024   8d5118         lea edx, [ecx + 0x18]       ; → first section header
│   0x00447029   0fb74106       movzx eax, word [ecx + 6]   ; NumberOfSections
│   0x0044702d   6bf028         imul esi, eax, 0x28         ; * sizeof(IMAGE_SECTION_HEADER)
│   ...
│   0x00447052   mov eax, edx                                ; return matching SECTION_HEADER*
│     ret                                                    ; or NULL if none

This is functionally an RVA-to-section lookup: given an image base and a target RVA (arg_ch), iterate the section headers and return the one that owns that RVA. It is a building block used both by legitimate code (manifest resource lookup, etc.) and by the unpacking stub when it needs to translate “where did the loader put my .reloc data?” into a usable address.

References from .text into the encrypted region

A scan of all 32-bit immediates in .text for values inside the .reloc VA range yields 80 hits, of which the highest-multiplicity targets are:

.reloc 0x004e00fe   6 refs
.reloc 0x004e00fb   6 refs
.reloc 0x00507883   2 refs
.reloc 0x004ee800   2 refs
.reloc 0x005046c7   2 refs
… 75 single-reference targets …
TOTAL: 80 immediates pointing at ≥ 0x4da400 (the encrypted blob)

Multi-referenced targets like 0x4e00fe (offset 0x2E0FE inside .reloc) are likely small lookup tables or key material that the unpacking stub indexes into multiple times during decryption.


Execution flow

End-to-end kill-chain summary. Each row is a phase; technique IDs map each step to §7 MITRE ATT&CK coverage.

Initial access — delivery
Malicious ad served via doubleclick.net (NZZ)T1189drive-by malvertising vector
Drive-by download drops data_4 to Chrome cacheT1204.002user lands on staged carrier
On-host execution
User executes carrier — the “MultiUpdate.exe” disguiseT1036.005
MSVC CRT bootstrapentry0 → __scrt_main → WinMainunpacker not at entry point; reached after MFC initialisation
Malicious post-exploitation
Unpack .reloc trailer — read 347 KB blob @ 0x4da400, decrypt to second-stage PET1027 / T1027.002
Dynamic API resolutionLoadLibrary + GetProcAddress rebuilds the stealer’s IAT post-decryptionT1129
PersistenceHKCU\…\Run + scheduled taskT1547.001
Host profiling — WMI SELECT * FROM AntiVirusProduct; HWID, OS, GPU, localeT1082 / T1518.001
Steam dead-drop lookupGET steamcommunity.com/profiles/<id>, parse bio → resolved C2 domainT1102.001
C2 beacon (.shop) — HTTPS POST init / config pullT1071.001 / T1573
Credential harvesting — browser SQLite, cookies, wallets, clipboardT1555.003 / T1115 / T1539
Exfiltration
Exfiltration — ZIP archive POSTed back to the active C2 (loops to C2 beacon above)T1041

Delivery On-host execution Malicious post-exploitation Exfiltration


MITRE ATT&CK coverage

Techniques compiled from sandbox detonation in the engagement environment, cross-checked with the static analysis above. ✅ = directly observed in this sample’s behaviour or static structure; 🟡 = expected for LummaC2 generally, would need dynamic confirmation.

Initial Access · TA0001

  • T1189 Drive-by Compromise Malvertising via doubleclick.net
  • 🟡 T1566.002 Spearphishing Link Fake “AI editor” ad campaigns

Execution · TA0002

  • T1204.002 User Execution Trojanized “MultiUpdate”
  • T1129 Shared Modules Dynamic API resolution via LoadLibrary + GetProcAddress
  • 🟡 T1047 WMI SELECT * FROM AntiVirusProduct

Persistence · TA0003

  • 🟡 T1547.001 Run Key RegCreateKeyExW + RegSetValueExW imports present
  • 🟡 T1574.002 DLL Side-Loading Tries to load missing DLLs alongside trusted exe

Privilege Escalation · TA0004

  • 🟡 T1134 Access Token Manipulation
  • 🟡 T1574.002 DLL Side-Loading

Defense Evasion · TA0005

  • T1027 Obfuscated Files Encrypted blob in .reloc trailer
  • T1027.001 Binary Padding 73,513,369 bytes of INT3 fill
  • T1027.002 Software Packing Custom packer + 70 MB overlay
  • T1036.005 Masquerading Disguised as MultiUpdate.exe
  • T1553.002 Code Signing Lifted PKCS#7 with leaked NVIDIA cert (Lapsus$ 2022)
  • 🟡 T1070.006 Timestomp
  • 🟡 T1112 Modify Registry
  • 🟡 T1497 Virtualization / Sandbox Evasion
  • 🟡 T1622 Debugger Evasion IsDebuggerPresent imported

Credential Access · TA0006

  • 🟡 T1555.003 Credentials from Web Browsers Web Data, Login Data SQLite
  • 🟡 T1539 Steal Web Session Cookie
  • 🟡 T1056.001 Keylogging

Discovery · TA0007

  • 🟡 T1012 Query Registry
  • 🟡 T1016 System Network Configuration
  • 🟡 T1018 Remote System Discovery (hosts file)
  • 🟡 T1033 Owner / User Discovery
  • 🟡 T1057 Process Discovery
  • 🟡 T1082 System Information Discovery
  • 🟡 T1083 File / Directory Discovery
  • 🟡 T1518.001 Security Software Discovery
  • 🟡 T1614 System Location Discovery

Command & Control · TA0011

  • 🟡 T1071.001 Web Protocols HTTPS to .shop domains
  • 🟡 T1102.001 Dead Drop Resolver Steam profile bio
  • 🟡 T1573 Encrypted Channel

Collection · TA0009

  • 🟡 T1005 Data from Local System Browser profiles, crypto wallets
  • 🟡 T1115 Clipboard Data Crypto address swap
  • 🟡 T1119 Automated Collection
  • 🟡 T1056.001 Input Capture: Keylogging

Exfiltration · TA0010

  • 🟡 T1041 Exfil over C2 Channel Stolen archive POSTed to active C2

Detection

Existing rule that does fire: Florian Roth’s leaked-NVIDIA-cert YARA

Florian Roth (Nextron Systems) published a rule in his signature-base on 2022-03-03 (announcement tweet), two days after the Lapsus$ disclosure. The rule (SUSP_NVIDIA_LAPSUS_Leak_Compromised_Cert_Mar22_1) fires on any PE compiled after 2022-03-01 whose Authenticode chain includes either of the two confirmed leaked NVIDIA cert serials, issued by VeriSign Class 3 Code Signing 2010 CA:

import "pe"

rule SUSP_NVIDIA_LAPSUS_Leak_Compromised_Cert_Mar22_1 {
   meta:
      description = "Detects a binary signed with the leaked NVIDIA certifcate and compiled after March 1st 2022"
      author = "Florian Roth (Nextron Systems)"
      date = "2022-03-03"
      reference = "https://twitter.com/cyb3rops/status/1499514240008437762"
   condition:
      uint16(0) == 0x5a4d and filesize < 100MB and
      pe.timestamp > 1646092800 and
      for any i in (0 .. pe.number_of_signatures) : (
         pe.signatures[i].issuer contains "VeriSign Class 3 Code Signing 2010 CA" and (
            pe.signatures[i].serial == "43:bb:43:7d:60:98:66:28:6d:d8:39:e1:d0:03:09:f5" or
            pe.signatures[i].serial == "14:78:1b:c8:62:e8:dc:50:3a:55:93:46:f5:dc:c5:18"
         )
      )
}

The carrier matches: PE timestamp 2024-12-23 is well past the 2022-03-01 floor, file is under 100 MB, and signature #0 carries the second of the two listed serials with the expected VeriSign issuer string. This rule already catches the sample — defenders running signature-base in any retro-hunt or file-arrival scan will see it without further work.

Tuning note

Comment out the pe.timestamp > 1646092800 guard for historical hunts.

The author’s comment recommends this for sweeps over older corpora; it widens recall to any binary signed with the leaked cert, regardless of compile timestamp (which is forgeable anyway). Keep the guard in place for live file-arrival scanning to suppress the historical legitimately-NVIDIA-signed pre-2022 fleet.

Why the published Lumma YARA rule misses this artefact

The Embee Research rule win_lumma_w1 targets three obfuscated UTF-16 strings (Chrome’s “Web Data” / “Login Data” SQLite filenames, “Opera Neon” profile dir) and constrains filesize < 5000KB:

rule win_lumma_w1 {
  strings:
    $o1 = { 57 00 ?? 00 ?? 00 ?? 00 ?? 00 ?? 00 ?? 00 65 00 62 00 ... }
    $o2 = { 4f 00 70 00 ?? 00 ?? 00 ?? 00 ?? 00 ?? 00 ?? 00 65 00 ... }
    $o3 = { 4c 00 6f 00 ?? 00 ?? 00 ?? 00 ?? 00 ?? 00 ?? 00 67 00 ... }
  condition:
    uint16(0) == 0x5a4d and filesize < 5000KB and (all of ($o*))
}
Sample variantFilesize gate$o1 / $o2 / $o3Verdict
Original 74 MB carrierFAIL (74 MB > 5 MB)0 / 0 / 0No match
Stripped 1.2 MB carrierPASS0 / 0 / 0No match
Memory-dumped unpacked payloadPASS3 / 3 / 3 expectedMatch

Recommended new YARA rule — carrier structure

This rule targets the artefact’s structural fingerprint rather than the Lumma payload’s strings, so it survives operator string-shuffling and matches the delivery stage (where the existing rule fails):

import "pe"
import "math"

rule LummaC2_MultiUpdate_Carrier_2024
{
    meta:
        author        = "CSIRT Versus Security (B. Schmid)"
        date          = "2025-02-14"
        description   = "Lumma carrier disguised as MultiCommander MultiUpdate.exe"
        sample_sha256 = "eff51f99...abba"
        tlp           = "AMBER"

    strings:
        $pdb = "MultiCommander\\BuildOutput\\Output\\Win32\\Release v143\\MultiUpdate\\MultiUpdate.pdb" ascii
        $manifest_a = "name=\"Microsoft.Windows.AutoUpdate\"" ascii
        $manifest_d = "<description>MultiUpdate</description>" ascii
        $multi_url  = "Multi Commander-http://multicommander.com/updates/version.xml" wide

    condition:
        uint16(0) == 0x5a4d and pe.is_pe and
        $pdb and ($manifest_a or $manifest_d or $multi_url) and
        for any sec in pe.sections : (
            sec.name == ".reloc" and
            sec.raw_data_size > (pe.data_directories[5].size + 0x4000) and
            math.entropy(sec.raw_data_offset, sec.raw_data_size) > 7.0
        )
}

Sigma — process / image-load anomaly

title: Lumma carrier disguised as MultiUpdate.exe
id: c1ae2a8e-93be-4e1c-9bc2-3f73a7fae8b2
status: experimental
description: Detects an MFC executable masquerading as MultiCommander's
  MultiUpdate.exe but unsigned and beaconing to .shop / steamcommunity.com
author: CSIRT Versus Security (B. Schmid)
date: 2025/02/14
logsource:
  product: windows
  category: process_creation
detection:
  selection:
    Image|endswith:
      - '\MultiUpdate.exe'
      - '\AutoUpdate.exe'
    OriginalFileName: 'MultiUpdate.exe'
  filter_signed:
    SignatureStatus: Valid
    Signature|contains: 'Mathias Svensson'
  condition: selection and not filter_signed
falsepositives:
  - Genuine MultiCommander update — must be Authenticode-signed
level: high
tags:
  - attack.defense_evasion
  - attack.t1036.005

Suricata — outbound TLS to Lumma .shop C2

alert tls $HOME_NET any -> $EXTERNAL_NET 443 (msg:"LummaC2 outbound TLS to .shop C2"; \
  tls.sni; pcre:"/^(abruptyopsn|cegu|cloudewahsj|framekgirus|klipgonuh|nearycrepso|noisycuttej|rabidcowse|tirepublicerj|wholersorie)\.shop$/i"; \
  classtype:trojan-activity; sid:1000201; rev:1;)

alert tls $HOME_NET any -> $EXTERNAL_NET 443 (msg:"LummaC2 outbound TLS to .click rotator"; \
  tls.sni; content:"regularlavhis.click"; \
  classtype:trojan-activity; sid:1000202; rev:1;)

alert tls $HOME_NET any -> $EXTERNAL_NET 443 (msg:"LummaC2 dead-drop resolver lookup (Steam)"; \
  tls.sni; content:"steamcommunity.com"; flow:established,to_server; \
  threshold:type both, track by_src, count 1, seconds 60; \
  classtype:policy-violation; sid:1000203; rev:1;)

Splunk SPL — Sysmon process-create with carrier signature

The base hunt: Sysmon Event ID 1, process image or OriginalFileName matches MultiUpdate, but the binary is not Authenticode-signed by the genuine MultiCommander publisher (Mathias Svensson). Surfaces both the Lumma carrier and any other unsigned masquerade.

index=sysmon EventCode=1
| eval is_multiupdate = if(match(Image,"\\MultiUpdate\.exe$")
                            OR OriginalFileName="MultiUpdate.exe", 1, 0)
| where is_multiupdate=1
| eval signed_ok = if(SignatureStatus="Valid"
                       AND match(Signature,"Mathias Svensson"), 1, 0)
| where signed_ok=0
| stats count
        values(Hashes) as hashes
        values(ParentImage) as parent
        min(_time) as first_seen max(_time) as last_seen
    by Computer, User, Image
| convert ctime(first_seen) ctime(last_seen)
| sort -last_seen

Splunk SPL — Steam dead-drop → .shop transaction

The behavioural signature: any host that reaches steamcommunity.com over TLS and then beacons to a .shop or .click domain within a 5-minute window. Catches Lumma victims regardless of which specific rotating .shop domain is in use. Requires Zeek ssl.log ingested into Splunk.

index=zeek sourcetype=ssl
| eval is_steam = if(match(server_name,"steamcommunity\.com$"), 1, 0)
| eval is_shop  = if(match(server_name,"\.shop$")
                      OR match(server_name,"\.click$"), 1, 0)
| where is_steam=1 OR is_shop=1
| transaction src maxspan=5m
| where mvfind(is_steam,"1") > -1 AND mvfind(is_shop,"1") > -1
| eval shop_dest = mvfilter(match(server_name,"\.shop$|\.click$"))
| table _time, src, dst, shop_dest, server_name, uid
| sort -_time

Splunk SPL — high-entropy section anomaly via Sysmon hashes

Lower-confidence hunt: any unsigned PE created on disk whose IMPHASH matches the carrier’s import-table fingerprint. IMPHASH survives section-content changes because the operator recompiled but kept MFC + libxml2 statically linked with the same import set.

index=sysmon EventCode=1
| rex field=Hashes "IMPHASH=(?<imphash>[A-F0-9]{32})"
| where imphash IN ("[redacted-imphash-MFC-libxml2-carrier]")
| eval signed_ok = if(SignatureStatus="Valid", 1, 0)
| where signed_ok=0
| stats values(Image) values(Computer) values(User)
        by imphash
| sort -values(Computer)

KQL (Microsoft Defender / Sentinel) — carrier execution

DeviceProcessEvents
| where FileName =~ "MultiUpdate.exe"
   or ProcessVersionInfoOriginalFileName =~ "MultiUpdate.exe"
| extend signed_by_legit =
    iff(ProcessVersionInfoCompanyName has "MultiCommander"
        and isnotempty(ProcessVersionInfoCompanyName)
        and SignatureType == "Valid",
        true, false)
| where signed_by_legit == false
| project Timestamp, DeviceName, AccountName, FolderPath, FileName,
          SHA256, MD5, ProcessCommandLine, InitiatingProcessFileName,
          SignatureType, ProcessVersionInfoCompanyName
| sort by Timestamp desc

KQL — Steam dead-drop call followed by .shop beacon

let steam_calls =
    DeviceNetworkEvents
    | where RemoteUrl has "steamcommunity.com"
    | project SteamTime = Timestamp, DeviceId, DeviceName,
              steam_url = RemoteUrl;
let shop_calls =
    DeviceNetworkEvents
    | where RemoteUrl matches regex @"\.(shop|click)(/|$)"
    | project ShopTime = Timestamp, DeviceId,
              shop_url = RemoteUrl, shop_dest = RemoteIP;
steam_calls
| join kind=inner shop_calls on DeviceId
| where ShopTime between (SteamTime .. SteamTime + 5m)
| project SteamTime, ShopTime, DeviceName,
          steam_url, shop_url, shop_dest
| sort by ShopTime desc

Static unpacker dead-end

Result

The static unpacker is partially blocked.

The .reloc trailer’s first 1,072 bytes are real x86 code with a polymorphic-MBA structure that emits two length constants (ESI=0x547a4, ECX=0x31f; sum equals the blob size 0x54ac3) — but at instruction 146 it executes sub dword [ecx], 0x1f7eeabb expecting [ecx] to be a writable buffer. No code path in the carrier sets up such a buffer. The carrier is non-self-unpacking; the cipher requires runtime context that does not exist in any reachable code path of .text.

Approach

Stand-alone Python script (lumma_static_unpacker.py) that:

  1. Strips the 0xCC overlay (drops 70 MB of padding).
  2. Locates the encrypted blob by scanning for the first non-zero byte after the legitimate base-relocation table (BaseRelocationTable.Size = 0x8224, blob actually begins at section offset 0x8400 due to 0x200 alignment).
  3. Extracts the 1,072-byte polymorphic prefix and the 345,747-byte encrypted body separately.
  4. Documents the cipher constants harvested from the prefix’s instruction stream.
  5. Records the static-only dead-end and points the analyst at the correct dynamic-analysis breakpoint.

Cipher constants discovered

32-bit immediates that appear in the prefix’s arithmetic instructions, harvested from the x86 disassembly:

ConstantUsed inOperationLikely role
0x5f50f517+0x001 / +0x053xor edx, KEDX whitening (paired)
0xef19cbd0+0x015 / +0x03fsub eax / add eaxEAX offset (paired)
0x8ef362b6+0x072 / +0x084sub ecx / add ecxECX offset (paired)
0xda1ee056+0x067 / +0x08fsub edx / add edxEDX offset (paired)
0x1c1addf8+0x09fadd edx, KEDX increment
0xac8b0af8+0x0bdsub edx, KEDX decrement
0xe5a0b512+0x0c7 / +0x0ebxor eax, KEAX whitening (paired)
0xdc8117ae+0x0acxor eax, KEAX whitening
0x1f7eeabb+0x193sub [ecx], KPer-block decrypt step (memory write — needs valid [ecx])
0x7b5677b6+0x1a9add ebx, KEBX offset
0x547a4+0x05amov esi, KLength: encrypted-body bytes (0x54ac3 − 0x31f)

Standard ciphers tested negative

Before concluding the static path is blocked, we ruled out the obvious primitives by brute force against the encrypted blob:

HypothesisTestResult
Single-byte XORAll 256 keys, look for MZ at decrypted offset 00 keys produce MZ
4-byte rolling XOREach prefix constant as the 4-byte key0 keys produce MZ; 36–55 % printable
RC4Each prefix constant + blob[0:16] as the key0 keys produce MZ; 33–44 % printable
Cyclic XOR with legit reloc tableBody bytes XOR with the 33 KB legitimate reloc data, cyclicallyNo MZ, no PK, no plaintext
zlib / raw-deflate / lzmaDecompress at offsets 0, 0x430, 0x800, 0x1000No valid stream at any offset
Embedded markersSearch for MZ, PE\0\0, PK\x03\x04, gzip magic5 random MZ bytes (offsets 0x13b82, 0x217e6, 0x3749e, 0x47bf7, 0x527be) — all surrounded by high-entropy noise, none has a real DOS header following

Unicorn emulation result

Booting the prefix with all GP registers zero, mapping the full carrier image (PE headers + all five sections), and providing a TIB at fs:0 reaches instruction 146 cleanly, then aborts:

$ python3 emulate_prefix.py
Mapping image at VA 0x400000 size 0x12f000
  .text    VA 0x401000 size 0x94a00
  .reloc   VA 0x4d2000 size 0x5d000

Emulating prefix with full image mapped...
  INVALID READ @ EIP=0x4da593 address=0x31f size=4
Emulation halted: Invalid memory read (UC_ERR_READ_UNMAPPED)

Instructions executed: 146
Final EIP: 0x004da593  (offset into blob: 0x193)
Final registers:
  ECX = 0x0000031f      <-- accumulated length, NOT a pointer
  ESI = 0x000547a4      <-- accumulated length

The failing instruction

+0x018d 0x004da58d: b14e           mov cl, 0x4e
+0x018f 0x004da58f: c1c702         rol edi, 2
+0x0192 0x004da592: 49             dec ecx
+0x0193 0x004da593: 8129bbea7e1f   sub dword ptr [ecx], 0x1f7eeabb   <<< needs valid [ecx]
+0x0199 0x004da599: c1c815         ror eax, 0x15

This is the cipher’s per-block decrypt step. ECX is supposed to point at a destination buffer; the prefix walks it 4 bytes at a time, applying [ecx] -= 0x1f7eeabb mixed in with rotation/XOR of the surrounding registers. In our static run, ECX=0x31f (a small integer), confirming that the carrier’s caller would set ECX to a valid address before transferring control here.

Why the static unpacker is blocked

The carrier never executes the blob from anywhere in .text.

  • No .text reaches the blob. Confirmed via radare2 (axt @ 0x004da400 = empty), value search for 0xd2000 / 0x4d2000 / 0x8224 / 0x547a4 as immediates anywhere in .text = zero hits, and pattern search for push 0xd2000 / mov reg, 0xd2000 = zero hits.
  • The carrier itself never executes the blob. Even with full PE-image emulation, no execution path from entry0 through WinMain ever transfers control to address 0x004da400.
  • The prefix is genuine cipher code, not noise. Length constants 0x547a4 + 0x31f = 0x54ac3 exactly match the blob size; the failing instruction is a legitimate decrypt step. This is a real cipher waiting for a context that does not appear in this binary.
Two viable interpretations

The blob is either cargo or it’s waiting on runtime state.

(1) Cargo for a separate loader stage. The blob is data on disk. A different component in the campaign (a JavaScript dropper, PowerShell stage, or a downloader) reads the carrier file off disk, locates .reloc, sets up a 0x547a4-byte buffer + a 0x31f-byte buffer, jumps into the prefix code, and the cipher fills the buffers with the unpacked Lumma payload. The carrier is just a delivery wrapper.

(2) Cipher requires runtime state from the carrier’s WinMain. A heap allocation, a global initialised by MFC’s CWinApp::InitInstance, or a value passed via TLS — something the static analysis has not enumerated — sets up [ecx] in the live process. The unpacker only runs after a long initialisation sequence and is reached via an indirect mechanism (vtable, callback, exception handler, message map) not via a literal address.

Recommended dynamic-analysis breakpoint

To extract the unpacked payload in REMnux/FlareVM with x32dbg / WinDbg:

  1. Open the sample in the debugger; let MFC WinMain initialise (run until idle).
  2. Set a memory-write breakpoint at the page covering VA 0x004da593, OR a hardware execute breakpoint at 0x004da593 directly.
  3. Continue execution. When the breakpoint fires, capture ECX — that points to the destination buffer the cipher writes to.
  4. Let the loop run to completion (the prefix ends at 0x004da830); dump the destination buffer of size ESI (= 0x547a4 bytes) to disk.
  5. Re-run the public YARA rule (win.lumma_w1.yar) against the dump — it should match the obfuscated UTF-16 strings and confirm the unpacked payload is the Lumma stealer.

Equivalent breakpoint in WinDbg syntax: ba e1 0x004da593

Files emitted by the partial static unpacker

For reproducibility, the stand-alone Python helper (lumma_static_unpacker.py) strips the overlay, locates the encrypted .reloc trailer, and splits the obfuscation prefix from the encrypted body:

$ python3 lumma_static_unpacker.py "Malware/eff51f99...abba.exe"
[*] Reading Malware/eff51f99...abba.exe
    size   = 74,715,545 bytes (71.25 MiB)
    sha256 = eff51f995cd6463cd9b3a2ea4a14cc85e3cc5c1b5b71db6d90765b3df175abba
[*] Stripping overlay padding...
    overlay length = 73,513,369 bytes (70.11 MiB)
    -> /tmp/lumma_stripped.exe       sha256=c466795354007a604fa1805b6d97b6f3e43179c85544594fe365f22ede8fe0a6
[*] Extracting encrypted .reloc trailer...
    blob length    = 346,819 bytes (338.7 KiB)
    -> /tmp/lumma_encrypted_blob.bin sha256=6e59580e512687981236cd42b23f46507834cea6009d1845ebfe177bef6c5062
[*] Obfuscation prefix: 1072 bytes  -> /tmp/lumma_obfuscation_prefix.bin
[*] Encrypted body:     345,747 bytes  (entropy ~7.51)

Indicators of compromise

All IOCs defanged for safe handling.

File hashes

ArtefactTypeValue
Delivered carrier (74 MB padded)SHA-256eff51f995cd6463cd9b3a2ea4a14cc85e3cc5c1b5b71db6d90765b3df175abba
Delivered carrierMD5a3be0b0ebbf9428015cacc27cf5d51a7
Stripped carrierSHA-256c466795354007a604fa1805b6d97b6f3e43179c85544594fe365f22ede8fe0a6

Code-signing — lifted leaked NVIDIA certificates (Lapsus$ March 2022)

Sig digestCert serialSHA-1 thumbprint
SHA-1 14781BC862E8DC503A559346F5DCC518 30:63:2E:A3:10:11:41:05:96:9D:0B:DA:28:FD:CE:26:71:04:75:4F
SHA-256 7BC15AF21367D0758BEDDCCA118642DE SHA-256 dual-sign partner present in the same lifted PKCS#7 blob

The SHA-1 leaf is one of the two confirmed leaked NVIDIA certs from the Lapsus$ disclosure (the other is 43BB437D609866286DD839E1D00309F5, not present in this sample). Both NVIDIA certs in our chain expired in 2018 and were revoked after the leak; Microsoft added them to the disallowed-cert list, but legacy systems and metadata-only checkers may still trust them. Either of the two leaked SHA-1 serials appearing in an Authenticode chain on a non-NVIDIA file is a high-confidence IOC for lifted-signature tradecraft.

Network — suspected C2 / dead-drop infrastructure

DomainRoleConfidence
abruptyopsn[.]shopC2 candidateMedium
cegu[.]shopC2 candidateMedium
cloudewahsj[.]shopC2 candidateMedium
framekgirus[.]shopC2 candidateMedium
klipgonuh[.]shopC2 candidateHigh — observed
nearycrepso[.]shopC2 candidateMedium
noisycuttej[.]shopC2 candidateMedium
rabidcowse[.]shopC2 candidateMedium
regularlavhis[.]clickC2 candidateMedium
tirepublicerj[.]shopC2 candidateMedium
wholersorie[.]shopC2 candidateMedium
yuriy-gagarin[.]comC2 candidateHigh — observed
steamcommunity[.]comDead-drop resolver (legit, abused)High
www[.]gstatic[.]cnC2 candidate (typosquat of gstatic.com)Medium

Network — observed IPs

IPPortAssociatedCategory
104[.]21[.]82[.]94443yuriy-gagarin[.]comSuspicious (Cloudflare-fronted)
172[.]67[.]199[.]224443yuriy-gagarin[.]comSuspicious (Cloudflare)
172[.]67[.]162[.]153443klipgonuh[.]shopSuspicious (Cloudflare)
185[.]160[.]247[.]17Lumma exfil egress observed in panel dump
64[.]233[.]181[.]94443Suspicious

TLS fingerprints (Steam dead-drop call)

SNIsteamcommunity.com
VersionTLS 1.2
JA3a0e9f5d64349fb13191bc781f81f42e1
JA3Sb677083c9768d0548331fca998152a10
Cert thumbprinte4fde2a81727d33dcbe228f20c59a9ee522fc470 (DigiCert SHA2 EV CA → Valve Corp)

Host artefacts

TypeValue
Initial cache hit (Chrome)%LocalAppData%\Google\Chrome\User Data\Default\Code Cache\js\47aa920d2b1e1d49_0
Dropped sample (Chrome cache)%LocalAppData%\Google\Chrome\User Data\Default\Cache\Cache_Data\data_4
Inherited PDB pathD:\Projects\MultiCommander\BuildOutput\Output\Win32\Release v143\MultiUpdate\MultiUpdate.pdb
Manifest identityname="Microsoft.Windows.AutoUpdate" / description="MultiUpdate"
LummaC2 panel banner (companion file)"LummaC2, Build Oct 9 2023" / LID format BhgGkI--IB4

Recommendations

For incident responders working this engagement

  1. Validate the new YARA rule against the existing IOC pack and historical malware-bazaar pulls. Roll into EDR if matches are clean.
  2. Expand the dead-drop hunt: any host with Sysmon-recorded TLS to steamcommunity.com followed by TLS to a *.shop domain within ≤ 5 minutes is likely a Lumma victim regardless of which specific .shop domain rotated in.
  3. Re-issue affected user credentials — session-token revocation for any browser-stored OAuth tokens; the operator has the cookies.
  4. Block the inherited carrier identity: AppLocker or WDAC rule for any MultiUpdate.exe not signed by the genuine MultiCommander publisher (Mathias Svensson), denied at execution.
  5. Browser hardening: users on managed endpoints should not have password-manager-style “save in browser” privileges for high-value applications; promote SSO + hardware key wherever possible.

For continuing the analysis

  1. Static decryption of the .reloc trailer. The encrypted blob is preserved at /tmp/lumma_encrypted_blob.bin. Walk back from the cross-reference to 0x004da400 to identify the unpacking stub, then re-implement the cipher in Python. Once decrypted the blob is expected to be a PE that the public YARA rule will match — closing the detection gap end-to-end.
  2. Dynamic confirmation in REMnux or FlareVM. Capture the actual cipher output at runtime (memory dump of the carrier process after WinMain entry); compare against the static decryption result to validate.
  3. Submit findings upstream: the carrier-structure YARA is genuinely useful — share it with Embee Research as a complementary detection alongside the existing string-obfuscation rule.

References

  • Embee Research — Lumma string-obfuscation YARA — github.com/embee-research/Yara-detection-rules
  • Malpedia — malpedia.caad.fkie.fraunhofer.de/details/win.lumma
  • g0njxa — Approaching stealer devs (LummaC2 interview) — g0njxa.medium.com/approaching-stealers-devs-94111d4b1e11
  • dexpose.io — In-depth technical analysis of Lumma Stealer (2024)
  • Malwarebytes — Free AI editor lures (Nov 2024)
  • Florian Roth (Nextron Systems) — SUSP_NVIDIA_LAPSUS_Leak_Compromised_Cert_Mar22_1 YARA rule — github.com/Neo23x0/signature-base/blob/master/yara/gen_nvidia_leaked_cert.yar
  • Florian Roth — announcement / context for the leaked-NVIDIA-cert YARA — twitter.com/cyb3rops/status/1499514240008437762
  • BleepingComputer — Malware now using NVIDIA’s stolen code-signing certificates (Mar 2022)
  • Threatpost — NVIDIA’s Stolen Code-Signing Certs Used to Sign Malware (Mar 2022)
Distribution

Reproduction and sharing

This report is published under TLP:CLEAR and may be redistributed without restriction. The associated full IOC pack is published separately under TLP:AMBER and is restricted to named subscribers.

For attribution, please cite as: B. Schmid, “Lumma Stealer’s .reloc trick,” CSIRT Versus Security, 14 February 2025.