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.relocsection, and 70 MB of0xCCpadding bolted onto the end purely to defeat AV/sandbox file-size limits and the published YARA rule’sfilesize < 5000KBcondition.
This binary is a textbook 2024-era LummaC2 delivery artifact. Four structural tricks combine to defeat naive defenses:
-
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 againstmulticommander.com, and ASUS-related strings all survive in the binary. A defender allowlisting on those signatures would be wide open. -
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…DCC518and7BC15AF2…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 §4. -
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 theBaseRelocationTabledata directory. Static analysers that trust the directory miss it; the runtime loader needs only the legitimate relocs and ignores the trailer. -
Massive
0xCCoverlay padding. 73,513,369 bytes ofINT3bytes 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 requiresfilesize < 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.
Engagement timeline
Q1 2025. An analyst at the customer’s site loads a major Swiss news publisher in their browser. A malvertising creative served through DoubleClick’s ad rotation drops the carrier into Chrome’s on-disk cache. Four hours later, the SOC’s Splunk surfaces the activity. Eleven days after that, forensics confirms LummaC2.
This section reconstructs the engagement narrative end-to-end — from drive-by drop, through SOC detection, to forensic confirmation and IOC deployment. The customer is a financial-services organisation; their identifying details, hostnames, internal IPs, and usernames are redacted. Sample family attribution and IOCs are unchanged from the original engagement record.
Customer & environment
| Customer | Financial-services organisation (redacted) |
| Affected host | Single managed Windows endpoint (hostname redacted) |
| Affected user | One user account (redacted) |
| SOC stack | Splunk SIEM, Sysmon-grade endpoint telemetry, EDR with content-control on user-writable execution paths |
| Date of compromise | 10 January 2025, 09:44:45 (T+0) |
| Date of confirmation | 21 January 2025, 22:35 (T+11d) |
| Outcome | Contained on a single host. No customer or transactional data exfiltration. User-account credentials presumed compromised; reset across the user’s browser-stored estate. Host preserved offline; not returned to service. |
| Regulatory notification | Not required — no reportable data category impacted. |
Timeline
Times are local (CET). Drop time is taken from Chrome cache file-creation timestamps; SOC times are from Splunk event ingestion. T+ deltas are measured from drop.
| Time | Event |
|---|---|
| 10.01.2025 09:44:45 T+0 |
Carrier dropped to Chrome cache. data_4 file created at ...\AppData\Local\Google\Chrome\User Data\Default\Cache\Cache_Data\. An adjacent JS cache entry from ad.doubleclick.net (...\Code Cache\js\47aa920d2b1e1d49_0) is written in the same second — the malvertising redirector. T1583.006 / T1189 |
| 10.01.2025 09:45:04 | First suspicious outbound connection from the host. EDR flags and partially blocks the activity. The carrier itself remains dormant on disk. |
| 10.01.2025 10:56:47 10.01.2025 11:36:48 |
Two further bursts of suspicious connections over the next ~110 minutes. Network signature consistent with browser-context implant attempting outbound resolution. |
| 10.01.2025 14:00:17 T+4h 16m |
First suspicious event surfaces in SOC Splunk monitoring. The correlation that finally fires keys on the outbound-connection pattern, not the file-creation event. Detection lag: 4 hours, 16 minutes from drop. |
| 10.01.2025 14:02:07 | Initial analyst review begins. Splunk → analyst handoff: ~2 minutes. |
| 10.01.2025 14:11:45 | Formal IR process activated. Analyst → IR activation: ~10 minutes. |
| 10.01.2025 15:18:10 | Extended threat-hunting initiated across the customer estate. |
| 13.01.2025 11:00 T+3d |
Affected host received by the forensics team for offline analysis. |
| 13.01.2025 16:17:30 13.01.2025 16:17:48 |
Two suspicious WMI queries surface during forensic timeline reconstruction. Pattern consistent with Lumma’s host-profiling step (cf. §8 Anti-analysis for the in-implant equivalent). |
| 14.01.2025 15:30 | Forensic image acquired. |
| 15.01.2025 11:30 | Formal forensic investigation opens. |
| 21.01.2025 22:35 T+11d |
Carrier confirmed as malicious binary. YARA hit on Florian Roth’s SUSP_NVIDIA_LAPSUS_Leak_Compromised_Cert_Mar22_1 — the leaked-certificate signature (cf. §4 Carrier anatomy). |
| 22.01.2025 | Detailed binary analysis (the substrate of this report) and proactive estate-wide threat hunt with extracted IOCs begin. |
Initial access — malvertising via DoubleClick
The infection vector was a malvertising creative served through
DoubleClick’s ad rotation on a major Swiss news
publisher’s site. The publisher itself was not compromised
— the site was clean — but the creative inside the
ad-network rotation carried a malicious script. The cached
request itself, surfaced from the forensic image, shows the
full ad.doubleclick.net URL with its query
parameters — and a https://nzz.ch/ Referer
header at the bottom, corroborating the publisher path:
https://nzz.ch/ Referer at the bottom
— the trail back to the publisher whose ad rotation
served the malicious creative.
At the time of the incident, only 1 of 96 vendors on
VirusTotal flagged the associated URL as malicious.
Content-type was text/javascript;charset=UTF-8: a
redirect or loader, not the carrier itself.
ad.doubleclick.net URL flagged malicious by
exactly one of 96 vendors (Quttera). Status 200, content
type text/javascript; charset=UTF-8. The
ad-network URL is the redirector, not the carrier; the
payload itself lands further downstream in Chrome cache.
The lone detection was Quttera. Their reasoning is a category-by-category dump of why the URL fell out of trust — poor reputation, “phishing target name” overlap, blacklisted-country hosting, unusual domain shape, hosting-service signals, low popularity rank. None are smoking guns individually; together they were enough for one engine, and only one, to call it.
Two artefacts were dropped to the user’s Chrome profile within the same second:
| Path | What it is |
|---|---|
...\AppData\Local\Google\Chrome\User Data\Default\Code Cache\js\47aa920d2b1e1d49_0 |
Cached JavaScript fetched from ad.doubleclick.net. The loader/redirector that initiated the drive-by. |
...\AppData\Local\Google\Chrome\User Data\Default\Cache\Cache_Data\data_4 |
Lumma carrier (the binary this report dissects; SHA-256 eff51f99…abba). Stored dormant in Chrome cache awaiting later execution. Detected later via YARA. |
%TEMP%, Downloads, or
%APPDATA%\Roaming. The carrier sat in
Cache_Data\ silently, with no execution, until a
downstream trigger. A defender hunting on user-writable
execution paths must include browser cache directories
in scope — the path
...\Chrome\User Data\Default\Cache\ is normal for
fetched bytes but abnormal for a PE32 GUI executable.
Detection & containment
The 4-hour gap between drop and first SOC surfacing is the central defensive lesson of this engagement. Not because the SOC was slow — once the alert surfaced, analyst pickup was 2 minutes and formal IR activation another 10 — but because the initial drive-by activity was partially blocked at the EDR layer, which suppressed the loudest network indicators while the carrier itself remained dormant on disk. The Splunk correlation that ultimately fired keyed on outbound connection patterns, not the file-creation event.
Once the IR process activated, the host was network-isolated, pulled offline for forensic preservation, and never returned to production. The user account was migrated to a clean replacement endpoint. Browser-stored credentials were treated as compromised: passwords rotated, sessions revoked, MFA re-enrolled across affected services. The estate-wide threat-hunt cleared all other endpoints of indicators.
Outcome
| Customer-data exfiltration | None confirmed. The sample never reached its post-execution profile (it was contained before stealer behaviour completed against the user’s logged-in sessions). |
| Credentials | Treated as compromised across the user’s browser-stored estate (autofill, cookies, password manager exports). Reset, revoke, re-enrol. No fraudulent activity observed in the post-incident monitoring window. |
| Lateral movement | None. Estate-wide hunt against extracted IOCs returned clean. |
| Other infections | None on the customer estate. The sample was confined to a single endpoint. |
| Regulator notification | Not required. No reportable data category impacted; no PCI / GDPR trigger. |
| Threat-intel uplift | 2,000+ Lumma-related IOCs (file hashes, C2 domains, infrastructure IPs) extracted from the binary analysis and threat-hunt fed back into customer detection content. |
| Public-disclosure lead time | This variant was identified and characterised in the engagement approximately 1.5 months before Microsoft published its public advisory on the same Lumma Stealer iteration. |
| Affected host | Preserved offline as forensic reference. Not returned to service. |
Sample identification
Delivered artifact (74 MB padded)
| File type | PE32 GUI executable, Intel i386, 5 sections, MFC-based |
| Size | 74,715,545 bytes (71.25 MiB) |
| MD5 | a3be0b0ebbf9428015cacc27cf5d51a7 |
| SHA-1 | 23f0f30e2bc4fb1308c01328e951b1681f439d46 |
| SHA-256 | eff51f995cd6463cd9b3a2ea4a14cc85e3cc5c1b5b71db6d90765b3df175abba |
| PE TimeDateStamp | 2024-12-23 13:44:40 UTC (forgeable) |
| ImageBase / EP RVA | 0x00400000 / 0x00046ffc |
| Digital signature | Broken — security data dir VA=0x473d7d8 Size=0x39c0 points beyond EOF |
| Linker | Microsoft VS 2022 Pro, MSVC 14.42 (ATL/MFC, statically linked libxml2) |
| PDB path | D:\Projects\MultiCommander\BuildOutput\Output\Win32\Release v143\MultiUpdate\MultiUpdate.pdb |
| Manifest | name=“Microsoft.Windows.AutoUpdate” description=“MultiUpdate” |
Stripped carrier (overlay removed)
| Created by | Truncating to first 1,202,176 bytes (sum of section raw sizes) |
| Size | 1,202,176 bytes (1.15 MiB) |
| SHA-256 | c466795354007a604fa1805b6d97b6f3e43179c85544594fe365f22ede8fe0a6 |
| Use | Substrate for radare2 / static analysis. Now under the 5000 KB threshold of the published YARA rule (still does not match — see §10) |
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.
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.
PE section table
| Name | VAddr | RawSize | Entropy | Note |
|---|---|---|---|---|
| .text | 0x00401000 | 0x00094a00 | 6.6849 | MFC application code (carrier) |
| .rdata | 0x00496000 | 0x00028800 | 5.1722 | Read-only data, RTTI, manifest, vftables |
| .data | 0x004bf000 | 0x00002e00 | 4.4563 | Initialised globals |
| .rsrc | 0x004c9000 | 0x00008400 | 4.7417 | MFC dialog/string resources |
| .reloc | 0x004d2000 | 0x0005d000 | 7.5579 | 33 KB legit relocs + 347 KB encrypted trailer |
Overlay characterisation
| Start (file offset) | 0x00125800 (= last raw section end) |
| End | 0x04747999 (EOF) |
| Length | 73,513,369 bytes (≈ 70.1 MiB) |
| Content | 0xCC 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 §4 Authenticode signature) |
| Entropy (first 1 MB) | 0.0775 |
| Entropy (last 1 MB) | 4.2181 |
| Function | Defeat 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:
| DLL | Function | Carrier use | Payload abuse |
|---|---|---|---|
| KERNEL32 | LoadLibraryA / LoadLibraryW | Updater plug-ins | Resolve payload API set at runtime |
| KERNEL32 | GetProcAddress | — | Dynamic API resolution (T1129) |
| KERNEL32 | WinExec / CreateProcessW | Auto-relaunch | Launch second-stage payload |
| KERNEL32 | OpenProcess | Updater process check | Process inspection / token theft |
| KERNEL32 | IsDebuggerPresent | (none) | Anti-debug (T1622) |
| ADVAPI32 | RegCreateKeyExW / RegSetValueExW | Updater config | Persistence via Run key (T1547.001) |
| SHELL32 | ShellExecuteW / ShellExecuteExW | Open release notes URL | Drop-and-execute child stages |
| WS2_32 | WSAStartup | Update download | C2 communication primitive |
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.
| # | Digest | Cert serial | Subject CN/OU | Validity | Issuer |
|---|---|---|---|---|---|
| 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?
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 §12 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:
- Entropy. 4 KB-block entropy across the 67.5 MB middle of the overlay sits at
0.0775bits/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 produces0.08. - Whole-file block scan.
rahash2 -B -b 4096 -a entropyover the full 75 MB file flags only 77 blocks at entropy ≥ 7.0. Every one of them lives inside the.reloctrailer (the known 347 KB Lumma payload, §5) 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. - Magic-byte hunt. Searches across the overlay for
MZ,\x7fELF,PK\x03\x04, gzip, 7-Zip, RAR, NSIS, Inno Setup, single-byte-XOR’dMZ, LZMA, PNG / JPEG / WebM — zero hits. A bound browser would leave at least the wrapper or inner-PE signature. - Imports.
VirtualAlloc,VirtualProtect, and theCrypt*family are not in the static IAT. OnlyVirtualQueryis. 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. - Structure. The overlay isn’t random fill, but it also isn’t data. A 15-byte template (
f8 64 6e 09· eight0xCC·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-0xCCbytes. Identical statistics across 68 megabytes is the signature of a custom padding tool, not data.
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 (§5). The 70 MB overlay
is inert padding produced by tooling. The 7-byte beacon
(f8 64 6e 09…90 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 |
| Length | 346,819 bytes (0x54ac3) |
| Entropy (just the blob) | 7.5097 |
| Padding before blob | 0x8224 → 0x8400 (476 bytes of zeros — alignment) |
| Padding after blob | 0x5cec3 → 0x5d000 (317 bytes of zeros — alignment) |
First 256 bytes of the encrypted payload
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.
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. 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 §9 MITRE ATT&CK coverage.
data_4 to Chrome cacheT1204.002user lands on staged carrierentry0 → __scrt_main → WinMainunpacker not at entry point; reached after MFC initialisation.reloc trailer — read 347 KB blob @ 0x4da400, decrypt to second-stage PET1027 / T1027.002LoadLibrary + GetProcAddress rebuilds the stealer’s IAT post-decryptionT1129HKCU\…\Run + scheduled taskT1547.001SELECT * FROM AntiVirusProduct; HWID, OS, GPU, localeT1082 / T1518.001GET steamcommunity.com/profiles/<id>, parse bio → resolved C2 domainT1102.001Delivery On-host execution Malicious post-exploitation Exfiltration
Anti-analysis & sandbox evasion
Lumma’s anti-analysis stack is layered, and none of
it is in the visible PE. The lifted MultiCommander
.text section runs only the genuine MultiCommander
startup sequence (MSVC CRT bootstrap, MFC CWinApp
init, message-loop entry); we confirmed this end-to-end with
radare2 plus an API-monitor trace. Every check below executes
after the .reloc trailer cipher unpacks the
second stage, which means none of these checks can be located or
patched statically — they live inside the encrypted blob.
The behaviours are reconstructed from dynamic execution traces
and from public sandbox detections of the unpacked stage matched
by Kevin O’Reilly’s
Lumma.yar
(kevoreilly/CAPEv2).
The cpuid block at 0x447dae is benign MSVC CRT, not anti-VM.
Five cpuid instructions cluster between
0x447dae and 0x447ec8 — classic
anti-VM real estate. They’re not. Look at the operations
that follow each cpuid: xor edi,
0x756e6547 (“Genu”), xor eax,
0x49656e69 (“ineI”), xor eax,
0x6c65746e (“ntel”) — that’s
the GenuineIntel vendor-string check, followed by
cmp’s against specific 0x106c0 /
0x20660 / etc. CPU stepping IDs that have known
errata-driven CRT codepaths. This is
__check_cpu_features — MSVC’s CRT
generates this on every binary compiled with VS 2017+ to pick
the AVX-vs-SSE2 codepath for memcpy/
strcmp intrinsics. No anti-VM in the visible PE.
The six checks observed in dynamic execution
-
Self-validation hash check.
T1480
The carrier opens its own file via
GetModuleFileNameW → CreateFileW → ReadFile, hashes a region of the on-disk binary, and compares the digest against a constant baked into the encrypted blob. Mismatch triggersExitProcesswith no error dialog and no telemetry. Why it matters: any modification of the on-disk file — Defender quarantine + restore, FSFD content rewrite, manual byte-patch — trips this check and the sample silently exits. A defender who sees “sample exited cleanly with code 0” and assumes the binary is dead has been deceived. -
WMI
Win32_VideoControlleradapter check. T1497.001 IssuesSELECT * FROM Win32_VideoControllerand inspects theDescription/VideoProcessorfields. Bails onQXL,Standard VGA,VirtualBox Graphics Adapter,VMware SVGA, and similar hypervisor display strings. Default analysis images (QEMU+QXL, plain VirtualBox or VMware) trip this check immediately — the carrier opens its own file, runs the cipher, sees the hypervisor adapter string, and exits silently. Only well-seasoned sandbox images (GPU passthrough or a WMI provider hook faking an NVIDIA- / Intel-branded adapter, plus populated browser history and recent-file telemetry) pass this check and capture the full kill chain. -
WMI
AntiVirusProductenumeration. T1518.001 IssuesSELECT * FROM AntiVirusProductagainst theSecurityCenter2namespace, builds a list of installed AV products, and (on the panel side) tags the bot with the result. Likely informs which credential-stealing modules run; on managed-AV hosts, some Lumma builds defer certain stealer plugins. -
SetUnhandledExceptionFilteranti-debug. T1622 Installs a top-level structured exception handler, then deliberately raises an exception (typically a sentinelRaiseExceptioncode) that only fires under a debugger. The custom handler redirects flow into the next unpacking stage. Without that handler installed (or under a naive debugger that swallows the exception), the unwind crashes the process before unpacking completes, and the sample dies pre-payload. Defeats casual x32dbg/x64dbg attach without ScyllaHide’s SEH-bypass profile active. -
Cuckoo / sandbox harness thread suppression.
T1497
Enumerates threads in the running process via
CreateToolhelp32Snapshot → Thread32First/Next(orNtQuerySystemInformation) and looks at thread entry-point addresses. Threads whose entry points fall in DLLs known to belong to Cuckoo’s API monitor (and, transitively, to forks like CAPE) are passed toSuspendThread— effectively silencing the harness while letting the real malware code keep running. CAPE’s own signature engine flags this behaviour literally as “Tries to suspend Cuckoo threads to prevent logging”. -
Keyboard layout / locale check.
T1614
Calls
GetKeyboardLayout+GetUserDefaultLangID+GetSystemDefaultUILanguage. Lumma operator builds explicitly refuse to run in CIS-region locales (Russian, Belarusian, Ukrainian, Kazakh) — the “bulletproof clause” that keeps the developers safe at home. As a side-effect, defenders running CIS-locale workstations are structurally immune to this strain.
Behavioural profile
When all six checks pass, the unpacked Lumma core executes its
collection routine and exits. The dynamic profile is unusually
flat for a stealer: 441 registry reads, zero registry
writes, zero scheduled tasks, zero services, single process,
single dropped artefact in %TEMP%, exit code 0,
run duration under 90 seconds. This is a
smash-and-grab design — read browser stores, exfil to C2,
die. Persistence (when it exists) is operator-driven via a
follow-on payload pushed back from C2, not a self-installing
Run-key. Defenders looking for “malware that drops a
persistence artefact on disk” will miss the carrier
entirely.
Build a VM that answers WMI plausibly — or detonation will exit silently.
Off-the-shelf analysis images fail check #2 above instantly. To capture this kill chain in your own lab, the VM has to look like a real workstation across four dimensions:
- Video adapter: avoid
QXL, “Standard VGA”, and generic VirtualBox / VMware strings. Use VFIO passthrough of a consumer GPU, or hook the WMI provider so theWin32_VideoControllername returns an NVIDIA- or Intel-branded device. - Browser history + recent files: seed the profile with several weeks of plausible browsing, downloads, and Recent Items. Lumma reads these to gauge sandbox vs. live host.
- Username + locale: avoid
analyst,sandbox,maltestand similar fingerprints; use a plausible local username. Note that CIS keyboard locales (Russian, Belarusian, Ukrainian, Kazakh) are self-protecting — the malware exits there by design. - CPU + memory: ≥ 4 vCPUs and ≥ 4 GB RAM, matching a typical user laptop. Single-core, 1 GB images get flagged immediately.
Patching the checks out of the binary is not a viable
shortcut: the anti-analysis routines live inside the
encrypted .reloc trailer and only materialise
in memory at runtime, so static patching of the on-disk
file can’t reach them (see
§11). A well-seasoned VM is
the most reliable path; a debugger session armed with
ScyllaHide’s VMProtect profile is the second.
MITRE ATT&CK coverage
Techniques compiled from dynamic execution traces of the unpacked stage cross-checked with the static analysis above. ✅ = directly observed in this sample’s behaviour or static structure; 🟡 = expected for LummaC2 generally, would need additional environment seasoning to confirm in a local lab.
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, SELECT * FROM Win32_VideoController
Persistence · TA0003
- 🟡 T1547.001 Run Key Imports present, not observed in dynamic run — this carrier is smash-and-grab; persistence (if any) is operator-driven post-exfil
- 🟡 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
- ✅ T1140 Deobfuscate / Decode .reloc cipher unpacks ~317 KB Lumma core at runtime
- ✅ T1480 Execution Guardrails Self-validation hash — reads own file, hashes, exits silently on tamper
- ✅ T1497 Virtualization / Sandbox Evasion Cuckoo / harness thread enumeration + suspension
- ✅ T1497.001 System Checks WMI Win32_VideoController hypervisor-adapter detect
- ✅ T1553.002 Code Signing Lifted PKCS#7 with leaked NVIDIA cert (Lapsus$ 2022)
- ✅ T1622 Debugger Evasion SetUnhandledExceptionFilter SEH redirection
- 🟡 T1070.006 Timestomp
- 🟡 T1112 Modify Registry
Credential Access · TA0006
- ✅ T1555.003 Credentials from Web Browsers Web Data, Login Data SQLite enumerated
- ✅ T1539 Steal Web Session Cookie Cookies SQLite enumerated
- ✅ T1056.001 Keylogging GetKeyState polling
- 🟡 T1555 Credentials from Password Stores DPAPI module loaded; vault reads expected for the unpacked stage
Discovery · TA0007
- ✅ T1012 Query Registry 441 registry reads in dynamic run, no writes
- ✅ T1057 Process Discovery Thread enumeration for harness suppression
- ✅ T1082 System Information Discovery WMI Win32_OperatingSystem, GetSystemInfo, GlobalMemoryStatus
- ✅ T1083 File / Directory Discovery Browser profile dir enumeration (Firefox, Chrome User Data)
- ✅ T1518.001 Security Software Discovery WMI AntiVirusProduct query confirmed
- ✅ T1614 System Location Discovery GetKeyboardLayout / GetUserDefaultLangID — CIS-locale guard
- 🟡 T1016 System Network Configuration
- 🟡 T1018 Remote System Discovery (hosts file)
- 🟡 T1033 Owner / User Discovery
Command & Control · TA0011
- ✅ T1071.001 Web Protocols HTTPS to nine .shop / .click C2 domains; matched by Suricata ET MALWARE Win32/Lumma Stealer Related CnC Domain
- ✅ T1573 Encrypted Channel TLS 443 to all observed C2
- 🟡 T1102.001 Dead Drop Resolver Steam profile bio — observed in companion engagement traffic, not in this single dynamic run
Collection · TA0009
- ✅ T1005 Data from Local System Browser profiles, certificate stores
- ✅ T1115 Clipboard Data OpenClipboard / GetClipboardData polling
- ✅ T1056.001 Input Capture: Keylogging GetKeyState polling
- 🟡 T1119 Automated Collection
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.
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.
Existing rule that fires on the unpacked stage: Kevin O’Reilly’s CAPE Lumma.yar
Kevin O’Reilly maintains the canonical
Lumma YARA rule inside the
kevoreilly/CAPEv2
repo. The rule targets a sequence of bytes that appears in the
unpacked Lumma core — which means it does
not match the on-disk carrier
(.reloc blob is still encrypted) but does
match the in-memory Lumma image after the
.reloc-trailer cipher runs. We confirmed the rule
matches the unpacked stage of this artefact — ~317 KB
of decrypted Lumma core captured at virtual address
0x03330000 in the carrier process (see
§11) — which anchors family
attribution against an external pre-existing detection,
independent of any submission we made to public databases.
Run Lumma.yar against process memory dumps, not against on-disk PEs.
For a host suspected of running Lumma: pull a full process
dump (e.g. via procdump -ma <pid>),
then YARA-scan the dump with the
kevoreilly/CAPEv2 rule. A hit is high-confidence
Lumma. Static disk scans of the carrier never match this rule
because the body is still under the
.reloc-trailer cipher; pair it with
SUSP_NVIDIA_LAPSUS_Leak_Compromised_Cert_Mar22_1
(above) for the on-disk path and
LummaC2_MultiUpdate_Carrier_2024 (below) for the
structural fingerprint of the carrier itself.
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 variant | Filesize gate | $o1 / $o2 / $o3 | Verdict |
|---|---|---|---|
| Original 74 MB carrier | FAIL (74 MB > 5 MB) | 0 / 0 / 0 | No match |
| Stripped 1.2 MB carrier | PASS | 0 / 0 / 0 | No match |
| Memory-dumped unpacked payload | PASS | 3 / 3 / 3 expected | Match |
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
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:
- Strips the 0xCC overlay (drops 70 MB of padding).
- 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). - Extracts the 1,072-byte polymorphic prefix and the 345,747-byte encrypted body separately.
- Documents the cipher constants harvested from the prefix’s instruction stream.
- 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:
| Constant | Used in | Operation | Likely role |
|---|---|---|---|
| 0x5f50f517 | +0x001 / +0x053 | xor edx, K | EDX whitening (paired) |
| 0xef19cbd0 | +0x015 / +0x03f | sub eax / add eax | EAX offset (paired) |
| 0x8ef362b6 | +0x072 / +0x084 | sub ecx / add ecx | ECX offset (paired) |
| 0xda1ee056 | +0x067 / +0x08f | sub edx / add edx | EDX offset (paired) |
| 0x1c1addf8 | +0x09f | add edx, K | EDX increment |
| 0xac8b0af8 | +0x0bd | sub edx, K | EDX decrement |
| 0xe5a0b512 | +0x0c7 / +0x0eb | xor eax, K | EAX whitening (paired) |
| 0xdc8117ae | +0x0ac | xor eax, K | EAX whitening |
| 0x1f7eeabb | +0x193 | sub [ecx], K | Per-block decrypt step (memory write — needs valid [ecx]) |
| 0x7b5677b6 | +0x1a9 | add ebx, K | EBX offset |
| 0x547a4 | +0x05a | mov esi, K | Length: 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:
| Hypothesis | Test | Result |
|---|---|---|
| Single-byte XOR | All 256 keys, look for MZ at decrypted offset 0 | 0 keys produce MZ |
| 4-byte rolling XOR | Each prefix constant as the 4-byte key | 0 keys produce MZ; 36–55 % printable |
| RC4 | Each prefix constant + blob[0:16] as the key | 0 keys produce MZ; 33–44 % printable |
| Cyclic XOR with legit reloc table | Body bytes XOR with the 33 KB legitimate reloc data, cyclically | No MZ, no PK, no plaintext |
| zlib / raw-deflate / lzma | Decompress at offsets 0, 0x430, 0x800, 0x1000 | No valid stream at any offset |
| Embedded markers | Search for MZ, PE\0\0, PK\x03\x04, gzip magic | 5 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.
The carrier never executes the blob from anywhere in .text.
- No
.textreaches the blob. Confirmed via radare2 (axt @ 0x004da400= empty), value search for0xd2000/0x4d2000/0x8224/0x547a4as immediates anywhere in.text= zero hits, and pattern search forpush 0xd2000/mov reg, 0xd2000= zero hits. - The carrier itself never executes the blob. Even with full PE-image emulation, no execution path from
entry0throughWinMainever transfers control to address0x004da400. - The prefix is genuine cipher code, not noise. Length constants
0x547a4 + 0x31f = 0x54ac3exactly 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.
The cipher requires runtime state from the carrier’s WinMain, then writes to a freshly VirtualAlloc’d region.
The unpacker is reached only after a long MFC
initialisation sequence, via an indirect mechanism (vtable,
callback, exception handler, or message map) rather than a
literal address in .text. A heap allocation, a
global initialised by CWinApp::InitInstance, or
a value passed via TLS sets up [ecx] in the
live process — the missing context the static path
cannot reconstruct.
Dynamic execution in a seasoned sandbox passes the
anti-analysis checks (§8) and
reaches the unpacking routine. The unpacked Lumma core
lands at virtual address
0x03330000 in the carrier
process — a VirtualAlloc’d region
at the ≈ 51 MB mark of address space, not an
address resolvable from the static image. Final size:
324,608 bytes (close to but smaller than
the encrypted blob — the cipher output is then
padded up to a page boundary). Two intermediate decryption
stages are also captured (~347 KB and ~368 KB
blobs; see §12 for hashes).
The unpacked stage matches Kevin O’Reilly’s
Lumma.yar rule
(kevoreilly/CAPEv2)
with high confidence — that’s how the family
attribution is anchored in dynamic-analysis pipelines. This
doesn’t unblock the static-only path: the
anti-analysis stack inside the encrypted blob (self-hash,
WMI video adapter, Cuckoo thread suppression, SEH redirect)
still gates the cipher, so static patching of the carrier
remains blocked.
Recommended dynamic-analysis breakpoint
To extract the unpacked payload in REMnux/FlareVM with x32dbg / WinDbg:
- Open the sample in the debugger; let MFC
WinMaininitialise (run until idle). - Set a memory-write breakpoint at the page covering VA
0x004da593, OR a hardware execute breakpoint at0x004da593directly. - Continue execution. When the breakpoint fires, capture
ECX— that points to the destination buffer the cipher writes to. - Let the loop run to completion (the prefix ends at
0x004da830); dump the destination buffer of sizeESI(= 0x547a4 bytes) to disk. - 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
| Artefact | Type | Value |
|---|---|---|
| Delivered carrier (74 MB padded) | SHA-256 | eff51f995cd6463cd9b3a2ea4a14cc85e3cc5c1b5b71db6d90765b3df175abba |
| Delivered carrier | MD5 | a3be0b0ebbf9428015cacc27cf5d51a7 |
| Stripped carrier (overlay removed) | SHA-256 | c466795354007a604fa1805b6d97b6f3e43179c85544594fe365f22ede8fe0a6 |
Encrypted .reloc trailer (extracted) | SHA-256 | 6e59580e512687981236cd42b23f46507834cea6009d1845ebfe177bef6c5062 |
Unpacked Lumma core — in-memory at VA 0x03330000, 324,608 bytes (matches kevoreilly Lumma.yar) | SHA-256 | 7d62169297c2236d94cf343174472ee5ad0c27352f7fc4431a425d91cfb60c2b |
| Intermediate decryption stage 1 (346,797 bytes) | SHA-256 | 4cfe4e9d0e5b65eb8fca0d6953aba7c671e21a3f753e1c352433d1f3ec35c147 |
| Intermediate decryption stage 2 (367,882 bytes) | SHA-256 | fdd23f242d3b73030791645400880a2ea27977b101094225d9b4255d4afc8ff6 |
Dropped artefact in %TEMP% — HTML mistyped as .exe (sinkholed-C2 response) | SHA-256 | b73df91c83960a7dcce8f112b1f7e4db8ec6b659d4ac706f79a1a703297533dd |
| Same dropped artefact | MD5 | 2e59df53309dbd234f876bad5c73f5b4 |
Code-signing — lifted leaked NVIDIA certificates (Lapsus$ March 2022)
| Sig digest | Cert serial | SHA-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
| Domain | Role | Confidence |
|---|---|---|
| abruptyopsn.shop | C2 (Lumma) | High — ET MALWARE sig hit |
| cegu.shop | C2 candidate | Medium |
| cloudewahsj.shop | C2 (Lumma) | High — ET MALWARE sig hit |
| framekgirus.shop | C2 (Lumma) | High — ET MALWARE sig hit |
| klipgonuh.shop | C2 candidate | High — observed |
| nearycrepso.shop | C2 (Lumma) | High — ET MALWARE sig hit |
| noisycuttej.shop | C2 (Lumma) | High — ET MALWARE sig hit |
| rabidcowse.shop | C2 (Lumma) | High — ET MALWARE sig hit |
| regularlavhis.click | C2 (Lumma rotator) | High — ET MALWARE sig hit |
| tirepublicerj.shop | C2 (Lumma) | High — ET MALWARE sig hit |
| wholersorie.shop | C2 (Lumma) | High — ET MALWARE sig hit |
| yuriy-gagarin.com | C2 candidate | High — observed |
| steamcommunity.com | Dead-drop resolver (legit, abused) | High |
| www.gstatic.cn | C2 candidate (typosquat of gstatic.com) | Medium |
Network — observed IPs
| IP | Port | Associated | Category |
|---|---|---|---|
| 104.21.82.94 | 443 | yuriy-gagarin.com | Suspicious (Cloudflare-fronted) |
| 172.67.199.224 | 443 | yuriy-gagarin.com | Suspicious (Cloudflare) |
| 172.67.162.153 | 443 | klipgonuh.shop | Suspicious (Cloudflare) |
| 185.160.247.17 | — | — | Lumma exfil egress observed in panel dump |
| 64.233.181.94 | 443 | — | Suspicious |
TLS fingerprints (Steam dead-drop call)
| SNI | steamcommunity.com |
| Version | TLS 1.2 |
| JA3 | a0e9f5d64349fb13191bc781f81f42e1 |
| JA3S | b677083c9768d0548331fca998152a10 |
| Cert thumbprint | e4fde2a81727d33dcbe228f20c59a9ee522fc470 (DigiCert SHA2 EV CA → Valve Corp) |
Host artefacts
| Type | Value |
|---|---|
| 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 |
| Single dropped artefact (dynamic run) | %TEMP%\5F1EMLRXIIJGDQ03YLNXFTUK54.exe — HTML content despite .exe extension; sinkholed-C2 response saved verbatim |
| Inherited PDB path | D:\Projects\MultiCommander\BuildOutput\Output\Win32\Release v143\MultiUpdate\MultiUpdate.pdb |
| Manifest identity | name="Microsoft.Windows.AutoUpdate" / description="MultiUpdate" |
| Process mutex | Unnamed CRT mutex via CreateMutexA(null) at startup; Lumma family-specific named mutex created post-unpack (LUMMA prefix; full name not exposed by sandbox engines) |
| Persistence footprint | None observed. 441 registry reads, 0 writes; 0 scheduled tasks; 0 services; single process exits with code 0 in < 90 s. Smash-and-grab profile. |
| Unpacked stage memory location | VA 0x03330000 in carrier process — VirtualAlloc’d region holding the 324,608-byte Lumma core (matches kevoreilly Lumma.yar) |
| LummaC2 panel banner (companion file) | "LummaC2, Build Oct 9 2023" / LID format BhgGkI--IB4 |
Recommendations
For incident responders working this engagement
- Validate the new YARA rule against the existing IOC pack and historical malware-bazaar pulls. Roll into EDR if matches are clean.
-
Expand the dead-drop hunt: any host with
Sysmon-recorded TLS to
steamcommunity.comfollowed by TLS to a*.shopdomain within ≤ 5 minutes is likely a Lumma victim regardless of which specific.shopdomain rotated in. - Re-issue affected user credentials — session-token revocation for any browser-stored OAuth tokens; the operator has the cookies.
-
Block the inherited carrier identity:
AppLocker or WDAC rule for any
MultiUpdate.exenot signed by the genuine MultiCommander publisher (Mathias Svensson), denied at execution. - 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
-
Static decryption of the
.reloctrailer. The encrypted blob is preserved at/tmp/lumma_encrypted_blob.bin. Walk back from the cross-reference to0x004da400to 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. - 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.
- 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 - Kevin O’Reilly (CAPE Sandbox) — Lumma YARA rule for the unpacked stealer core —
github.com/kevoreilly/CAPEv2/blob/master/data/yara/CAPE/Lumma.yar - Emerging Threats Open ruleset — ET MALWARE Win32/Lumma Stealer Related CnC Domain Suricata signatures —
rules.emergingthreats.net - 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)
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.