Content
Key Findings
- During an Incident Response engagement, the SECUINFRA Falcon Team identified an interesting malware sample codenamed “CommieLoader”, which was masquerading as a job application.
- CommieLoader delivered a Cobalt Strike Beacon, which was used for Command&Control communication by the threat actor
Overview
Our client received a ZIP archive via email containing three files. This archive contained previously unknown malware, which we have named “CommieLoader” based on certain artifacts, and which ultimately led to data exfiltration. In the following, we examine the complete attack chain of this malware.
Timeline
The initial infection occurred in the user’s download directory. By executing the legitimate Sumatra Installer, which was contained in the ZIP archive, the malicious dbgcore.dll in the same directory was loaded using DLL forward sideloading, and malicious routines were executed.
Below, we will take a closer look at the malicious file dbgcore.dll.
Analysis of the malware
Dbgcore.dll : a9121e70c39de2c10e6790da4aa3a22079242a201da2c1aeeb4ed65070e68e93
SumatraPDF installer (“Version_Application_2.0_202566_Application_Number_0234521870_Date_0000000200.exe”):
cb1d73323d3d80004ada185844b0d461abd9ded736d5dc690607f935b4f2b58a
Settings.txt :
b9fac5fd68f333b9459fa4b0111da8fba64a20022df8ea8595eae6a2fc4b9d9d
During our investigations, we found a ZIP archive on one of our customers’ systems claiming to contain job application documents, but instead contained an installer for the SumatraPDF Viewer, a text file named “Settings.txt,” and a dynamic-link library (DLL) named “dbgcore.dll.” The installer turned out to be a legitimate PE file that contains a signature from Krzysztof Kowalczyk (the developer of SumatraPDF) and whose hash matches that of the official SumatraPDF Version 3.3.3 installer, which is why our focus initially fell on the dbgcore.dll.
dbgcore.dll would be, if it were legitimate, a Microsoft system file providing functions for memory dumping and debugging as a part of the Debugging Tools For Windows package. However, as a system file, it usually resides in the system32 directory, not in the users document folder. Our DLL was also signed with two invalid certificates, which are shown below:


Figures 1 & 2: Invalid certificates from “ESET, spol. s.r.o.”
Looking at the import table of the Sumatra Installer which lists the DLLs and the specific functions required by the executable, it is noticeable that dbgcore.dll isn’t listed as one of the needed DLLs. As people don’t usually add DLLs and send them around for no reasons, the next step is to look into how the execution of the Sumatra Installer (the so called „application“) could lead to the execution of the potentially malicious dbgcore.dll.

Figure 3: Imported DLLs of the Sumatra Installer
Opening the installer in a dissassembler reveals an attempt to load the debug help library „dbghelp.dll“.

Figure 4: Importing the legitimate dbghelp.dll
At first glance, the installer appears to load the dbghelp.dll as expected: the absolute path of the DLL is constructed using PathAppendW() from the return value of GetSystemDirectoryW() and “dbghelp.dll”, and only then is the DLL imported using LoadLibraryW().
However, if we look at what the handle of the imported DLL is used for, we see a call to GetProcAddress() with MiniDumpWriteDump as the second parameter:

Figure 5: Resolving the address to MiniDumpWriteDump
If we now take a look at the export table of dbghelp.dll, we see that the MiniDumpWriteDump function is forwarded to “dbgcore.dll”:

Figure 6: MiniDumpWriteDump as a forwarded function implemented in dbgcore.dll
To determine how the entry point of dbgcore.dll is called, we put a legitimate dbgcore.dll in a folder with the Sumatra Installer and opened the installer in a debugger. We set a breakpoint on GetProcAddress() and, after single-stepping for a while, we observed that LdrpCallInitRoutine() is called with the entry point of the DLL to be invoked.

Figure 7: Call stack from GetProcAddress to LdrpCallInitRoutine
As can be seen in the screenshot, LdrpCallInitRoutine() is called here with the address of the entry point in the RCX register (first parameter).

Figure 8: Call to LdrpCallInitRoutine with the entry point of dbgcore.dll in the RCX register (first parameter in __fastcall)
This confirms our suspicion that the malicious code is located at the entry point of the malicious dbgcore.dll. Unlike classical DLL search order hijacking, the loading of the malicious DLL is handled by another, legitimate DLL, in our case dbghelp.dll.
Exports
If we take a look at the export table of our DLL, we see that the dbgcore.dll file in question exports a total of 38 functions. The legitimate dbgcore.dll from Microsoft exports only two functions (MiniDumpReadDumpStream and MiniDumpWriteDump). Additionally, current versions of the DLL provided by Microsoft are usually signed, which is not the case with our dbgcore.dll sample. The following exported functions from our dbgcore.dll sample stand out in particular:
ladeComm
antifa
kommEncoding
helloFriendssscommIsCom
stalin
SW3_HashSyscall
SC_Address
GetStalinNumber
GetStalin
AAAWriteaaaaaVirtualComm
ResumeComm
ProtectComm
We note that the two original dbgcore.dll functions, MiniDumpWriteDump and MiniDumpReadDumpStream, are not included in our sample. Instead, there are references to “stalin,” “getStalinNumber,” and “SW3_HashSyscall,” which are rather unusual references for a DLL. We therefore conclude that this is a malicious DLL with no connections to the legitimate dbgcore.dll from Microsoft.
Deep Dive
When we open dbgcore.dll in a disassembler and jump to the main function (DllMain), we see the following:

Figure 9: DllMain
The malicious payload in the mw_Entrypoint() function is therefore only executed if the DLL is loaded using LoadLibrary() or a similar API, which sets fdwReason to DLL_PROCESS_ATTACH.
In the mw_Entrypoint() function, we can get a rough overview of the payload.

Figure 10: Entry point of the DLL (malicious code)
Here, we can see three function calls that check whether the program is running within a sandbox or analysis environment, followed by two additional function calls that determine whether the malicious code is to be executed or not.
Anti-Sandbox Mechanisms
The first function, mw_ramCheck(), checks the memory size. Only if more than ~4.19 GB of RAM is installed on the system the execution is returned to the caller. If this is not the case, tooLittleRam() is called, which stops the program. As such, one possible scenario that would lead to termination of the payload would be, for example, an execution in a sandbox or virtual machine with less than 4 GB of RAM.

Figure 11: Sandbox check via query of physical RAM
The next function, mw_detectResolution(), checks whether the position of the lower-right pixel of the desktop window is greater than 1023. If this is not the case, the program assumes that it is running in a controlled environment, as they often use resolutions below 1280×1024 pixels, and terminates. Otherwise, control is returned to the caller.

Figure 12: Sandbox check via a query of the desktop’s screen resolution
The third sandbox check, mw_cpuCount(), terminates the program if the number of processor cores on the system is less than 2.

Figure 13: Sandbox check via query of the number of processor cores
The fourth sandbox check, mw_timeDetect(), verifies whether the Sleep API is being hooked. Some sandboxes, analysis tools, or Endpoint Detection and Response solutions use this trick to shorten wait times. First, GetTickCount() is called, followed by a call to Sleep() to suspend execution for one second. Then GetTickCount() is called again, and the difference between the second tick count and the initial tick count is compared to 0.9 seconds. If the result is less than 0.9 seconds, the programm assumes the Sleep-function is hooked and the value 1 is returned, causing the program to terminate.

Figure 14: Time-Check to see if Sleep is hooked
The final sandbox check, mw_hostnameCheck(), is more straightforward as it solely compares the hostname against the names of popular sandboxes. Only if no match is found is 0 returned and the execution of the actual malicious code begins – provided no flags where raised during the previous checks.

Figure 15: Sandbox check via hostname query
The remaining code is executed if and only if all five sandbox checks pass. First, the global variable CmdLine is populated with the string “WmiPrvSE.exe”, which refers the Windows Management Instrumentation (WMI) Provider Service. Then the function mw_read_settings() is called.

Figure 16: CommieLoader’s core functionalities
mw_read_settings() sets the working directory to the directory where the installer is located and reads the contents of the “Settings.txt” file into the global variable my_payload.

Figure 17: Function that reads the contents of Settings.txt into the my_payload buffer
When we open the “Settings.txt” file, we find, among other things, various names of communists, a variety of MAC addresses, and excerpts from the Communist Manifesto (many of which contain spelling errors). With this observation, the name “CommieLoader” is born. Upon closer inspection, a pattern seems to emerge in which the clustering of “trotzki” strings could each represent a null byte (0x00). Therefore, we hypothesize that Settings.txt contains an encoded payload, possibly the next stage of execution for the malware. The encoding method would thus be a dictionary substitution, in which a corresponding word is used as a translation for each hexadecimal value from 0x00 to 0xFF.

Figure 18: Excerpt from the contents of Settings.txt
Upon closer examination of the dbgcore.dll file, it becomes apparent that the file contains a data structure with 256 entries suitable for a dictionary and, as suspected, begins with “trotzki” as the value for 0x00.

Figure 18: Excerpt from the dictionary in dbgcore.dll
To export the dictionary from dbgcore.dll, we developed a Python script that can decode the Settings.txt file.
“`
#!/usr/bin/env python3
“””
Decoder for commieLoader Settings.txt payload.
dbgcore.dll contains a 256-entry wordlist stored as fixed-width (5000-byte) slots
starting at offset 0x8B40. Each word in Settings.txt maps to one byte of the
original binary payload.
“””
import sys
import argparse
DLL_PATH = “dbgcore.dll”
SETTINGS_PATH = “Settings.txt”
OUTPUT_PATH = “decoded_payload.bin”
WORDLIST_OFFSET = 0x8B40 # offset of first entry (“trotzki” = 0x00) in dbgcore.dll
WORDLIST_STRIDE = 5000 # each entry occupies a fixed 5000-byte slot
def load_wordlist(dll_path: str) -> dict[str, int]:
with open(dll_path, “rb”) as f:
data = f.read()
word_to_byte: dict[str, int] = {}
for i in range(256):
offset = WORDLIST_OFFSET + i * WORDLIST_STRIDE
chunk = data[offset:offset + WORDLIST_STRIDE]
end = chunk.find(b”\x00″)
word_bytes = chunk[:end] if end != -1 else chunk
word = word_bytes.decode(“utf-8”)
word_to_byte[word] = i
return word_to_byte
def decode(settings_path: str, word_to_byte: dict[str, int]) -> bytearray:
with open(settings_path, “r”, encoding=”utf-8″) as f:
content = f.read()
# Tokens are separated by “, “; remove only leading spaces to preserve
# trailing spaces that are part of some token names (e.g., “Ricardo “).
tokens = [t.lstrip(” “) for t in content.split(“,”)]
unknown = {t for t in tokens if t and t not in word_to_byte}
if unknown:
print(f”[!] Warning: {len(unknown)} unknown token(s): {unknown}”, file=sys.stderr)
out = bytearray()
for t in tokens:
if t:
out.append(word_to_byte.get(t, 0))
return out
def main() -> None:
parser = argparse.ArgumentParser(description=”Decode commieLoader Settings.txt payload”)
parser.add_argument(“–dll”, default=DLL_PATH, help=f”Path to dbgcore.dll (default: {DLL_PATH})”)
parser.add_argument(“–input”, default=SETTINGS_PATH, help=f”Path to Settings.txt (default: {SETTINGS_PATH})”)
parser.add_argument(“–output”, default=OUTPUT_PATH, help=f”Output path for decoded binary (default: {OUTPUT_PATH})”)
args = parser.parse_args()
print(f”[*] Loading wordlist from {args.dll} …”)
word_to_byte = load_wordlist(args.dll)
print(f”[+] Loaded {len(word_to_byte)} wordlist entries”)
print(f”[*] Decoding {args.input} …”)
payload = decode(args.input, word_to_byte)
print(f”[+] Decoded {len(payload)} bytes”)
with open(args.output, “wb”) as f:
f.write(payload)
print(f”[+] Written to {args.output}”)
if __name__ == “__main__”:
main()
“`
We were able to identify the decoded file as CobaltStrike Beacon shellcode through YARA rule matches and manual analysis. CobaltStrike is an adversary emulation framework designed for red teaming, which is used both for legitimate security testing and for command-and-control purposes by attackers with malicious intent. To classify it and learn more about the functionality of the CobaltStrike beacon, we extracted its configuration.
The Cobalt Strike payload contains the following configuration, which defines the functionality, behavior, and camouflage of the malware. Communication occurs via the HTTPS protocol on port 443/tcp. The team server (the attacker’s Command & Control infrastructure) is accessible via the domain refugee-help[.]com and is disguised as a contact form so that the web traffic does not stand out too much in an analysis. The beacon disguises itself as the Windows system program wmiprvse.exe to avoid drawing attention in the process tree. The final piece of information, which is particularly relevant to us, is the watermark value “987654321,” which normally pseudonymously identifies the software’s licensee. However, the descending sequence of numbers is an indication that this must be an unlicensed copy of CobaltStrike, which is often offered for exchange or sale by cybercriminals in online forums.
BeaconType – HTTPS
Port – 443
SleepTime – 30000
MaxGetSize – 16798776
Jitter – 50
MaxDNS – Not Found
PublicKey_MD5 – 1089d58afc804cfab88e6e2aca60e3f3
C2Server – refugee-help.com,/dpixel
UserAgent – Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.3600 (KHTML, like Gecko) Chrome/135.50.90.0 Safari/537.3600
HttpPostUri – /contact.php
Malleable_C2_Instructions – Base64 decode
HttpGet_Metadata – Metadata
base64
header “Cookie”
HttpPost_Metadata – ConstHeaders
Content-Type: application/x-www-form-urlencoded
ConstParams
name=OSF
subject=Resource support
SessionId
header “Cookie”
Output
base64url
prepend “message=”
PipeName – Not Found
DNS_Idle – Not Found
DNS_Sleep – Not Found
SSH_Host – Not Found
SSH_Port – Not Found
SSH_Username – Not Found
SSH_Password_Plaintext – Not Found
SSH_Password_Pubkey – Not Found
SSH_Banner –
HttpGet_Verb – GET
HttpPost_Verb – POST
HttpPostChunk – 0
Spawnto_x86 – %windir%\syswow64\wbem\wmiprvse.exe -Embedding
Spawnto_x64 – %windir%\sysnative\wbem\wmiprvse.exe -Embedding
CryptoScheme – 0
Proxy_Config – Not Found
Proxy_User – Not Found
Proxy_Password – Not Found
Proxy_Behavior – Use IE settings
Watermark_Hash – NtZOV6JzDr9QkEnX6bobPg==
Watermark – 987654321
bStageCleanup – True
bCFGCaution – True
KillDate – 0
bProcInject_StartRWX – False
bProcInject_UseRWX – False
bProcInject_MinAllocSize – 24576
ProcInject_PrependAppend_x86 – b’D@KCLH\x90f\x90\x0f\x1f\x00f\x0f\x1f\x04\x00\x0f\x1f\x04\x00\x0f\x1f\x00\x0f\x1f\x00′
Empty
ProcInject_PrependAppend_x64 – b’D@KCLH\x90f\x90\x0f\x1f\x00f\x0f\x1f\x04\x00\x0f\x1f\x04\x00\x0f\x1f\x00\x0f\x1f\x00′
Empty
ProcInject_Execute – kernel32.dll:BaseThreadInitThunk
NtQueueApcThread-s
kernel32.dll:LoadLibraryA
CreateRemoteThread
RtlCreateUserThread
SetThreadContext
ProcInject_AllocationMethod – NtMapViewOfSection
bUsesCookies – True
HostHeader –
headersToRemove – Not Found
DNS_Beaconing – Not Found
DNS_get_TypeA – Not Found
DNS_get_TypeAAAA – Not Found
DNS_get_TypeTXT – Not Found
DNS_put_metadata – Not Found
DNS_put_output – Not Found
DNS_resolver – Not Found
DNS_strategy – round-robin
DNS_strategy_rotate_seconds – -1
DNS_strategy_fail_x – -1
DNS_strategy_fail_seconds – -1
Retry_Max_Attempts – 0
Retry_Increase_Attempts – 0
Retry_Duration – 0
Once the shellcode is decoded, a new process is created. As mentioned above, the string “WmiPrvSE.exe” was previously copied into the global variable CmdLine. And with the call to CreateProcessA(), a process is created in a suspended state.

Figure 19: Creation of the suspended WmiPrvSE process
Once the process is created, we observe several syscall invocations that indicate classic process injection. The call to ZwAllocateVirtualMemory() allocates a buffer in the newly created process, which serves as storage for the shellcode. This shellcode is then written to the newly allocated buffer using ZwWriteVirtualMemory(), and the process is made executable using ZwProtectVirtualMemory().
Afterward, ZwQueueApcThread() is used to append the shellcode to the process’s APC queue, and ZwResumeThread() is then used to finally execute the shellcode on the main thread.

Figure 20: Transfer of the shellcode into the memory space of the WmiPrvSE process, followed by the execution of the shellcode
After the shellcode has been written to the process and executed, the program begins to implement persistence mechanisms. These are found in the copyfiles() function.
First, the current user’s username is queried to locate their Documents directory. After the path string to the Documents directory has been constructed, all three files – “Settings.txt,” dbgcore.dll, and the Sumatra Installer – are copied into the directory.

Figure 21: Copying the Sumatra installer, dbgcore.dll, and Settings.txt to the user’s Documents folder
Finally, the write_auto function is called, which creates an autorun registry key disguised as “Firefox_Updater_Version_2.3.1000” for the installer. This ensures that the installer runs every time the system starts up, so that the attacker does not lose access to the system after a reboot.

Figure 22: Entry of the autorun key in the registry

Figure 23: Registry entry after execution of the malware
A small bonus: We were able to find artifacts of a Vectored Exception Handler in the DLL that sets a hook on the EtwEventWrite function. However, we could find no evidence that this handler is registered.

Figure 24: A Vectored Exception Handler that overwrites EtwEventWrite with a ret instruction (however, it is never registered)
Noteworthy:
It is perhaps worth noting that our sample only took limited anti-AV measures. Microsoft Defender was running on our client’s system, and as far as we could tell, it was active throughout the entire infection period and scanned the sample multiple times. In the end, it found nothing to complain about, even though it spent the longest time scanning dbgcore.dll on average.
In two instances, however, it identified malicious activity (“VirTool:MSIL/Deimos.A!MTB” and “Trojan:Win32/Sabsik.EN.B!ml”) associated with WmiPrvSE.exe:

Figure 25: Windows Defender detections
In both cases, Windows Defender quarantined WmiPrvSE.exe.
We were unable to associate “CommieLoader” with any known malware family. When searching for it on platforms for analyzing and exchanging malware samples, we were able to identify another sample (SHA256: 127c525b0107045c39d4c956d51a16aba6b28e8a08cb1687e3fe7fc1f16e0de5) using a YARA rule, which was also located in a ZIP archive (“bewerbung_gesamt.zip”) containing a Settings.txt file and the SumatraPDF installer. The payload of this sample is nearly identical to our sample, except that an msedge.exe process is used for the APC injection instead of a WmiPrvSE.exe process. This suggests that this sample could be a test sample designed to test detection by AVs and EDRs. According to the alleged attacker, Elastic EDR would likely have detected the sample as well. Only time will tell whether this attack was a targeted attack against our customer or whether the sample will appear more frequently in the future.
Appendix
Host-based Indicators
| Filename | SHA256 | Description |
| dbgcore.dll | a9121e70c39de2c10e6790da4aa3a22079242a201da2c1aeeb4ed65070e68e93 | Malicious DLL loaded by the SumatraPDF installer via DLL forward sideloading |
| Version_Application_2.0_202566_Application_Number_0234521870_Date_0000000200 | cb1d73323d3d80004ada185844b0d461abd9ded736d5dc690607f935b4f2b58a | Legitimate SumatraPDF installer |
| Settings.txt | b9fac5fd68f333b9459fa4b0111da8fba64a20022df8ea8595eae6a2fc4b9d9d | Text file containing an encoded Cobalt Strike beacon |
Additional host-based IoCs:
Software\Microsoft\Windows\CurrentVersion\Run\Firefox_Updater_Version_2.3.1000:
“C:\Users\[Username]\Documents\Version_Application_2.0_202566_Application_Number_0234521870_Date_0000000200.exe
Network-based Indicators
| Type | Indicator |
| C2 | refugee-help[.]com |
| URIs | /dpixel, /contact.php |
Detection Rule
rule SI_MAL_LDR_CommieLoader_Apr13 {
meta:
version = “1.0”
date = “2026-04-13”
modified = “2026-04-14”
status = “RELEASED”
sharing = “TLP:CLEAR”
source = “SECUINFRA Falcon Team”
description = “Detects the dbgcore.dll used in the CommieLoader campaign”
category = “malware”
mitre_att = “T1129, T1055, T1112”
// SHA-256 hashes of our observed samples
hash1 = “a9121e70c39de2c10e6790da4aa3a22079242a201da2c1aeeb4ed65070e68e93”
hash2 = “127c525b0107045c39d4c956d51a16aba6b28e8a08cb1687e3fe7fc1f16e0de5”
strings:
$mz = { 4d 5a }
// Below are code snippets used in CommieLoaders Sandbox/VM check.
$a1 =
{
// Stub for retrieving the computer name.
48 81 ec 30 01 00 00 // sub rsp, 0x130
48 8d ac 24 80 00 00 00 // lea rbp, [rsp+0x80]
c7 45 ac 00 01 00 00 // mov dword [rbp-0x54], 0x100
48 8d 55 ac // lea rdx, [rbp-0x54]
48 8d 45 b0 // lea rax, [rbp-0x50]
48 89 c1 // mov rcx, rax
48 ?? ?? ?? ?? ?? ?? // mov rax, ?
ff d0 85 c0 // call rax
}
$a2 =
{
// Stub for comparing the computer name against common sandbox hostnames.
48 8d 45 b0 // lea rax, [rbp-0x50]
48 ?? ?? ?? ?? ?? ?? // lea rdx, ?
48 89 c1 // mov rcx, rax
e8 ?? ?? ?? ?? // call ?
85 c0 // test eax, eax
}
// SysWhispers3 function names observed in the samples.
$b1 = “SW3_GetSyscallAddress” ascii
$b2 = “SW3_HashSyscall” ascii
$b3 = “SW3_PopulateSyscallList” ascii
// Strings related to the Autorun key that CommieLoader sets for persistence.
$c1 = “Software\\Microsoft\\Windows\\CurrentVersion\\Run” ascii
$c2 = “Firefox_Updater_Version_2.3.1000” ascii
condition:
$mz at 0 and
filesize < 3MB and
all of ($a*) and
all of ($b*) and
all of ($c*)
}

