CommieLoader: Leveraging SUMATRA PDF for DLL ForwardSideloading

CommieLoader

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

                                     [email protected]

                                     subject=Resource support

                                   SessionId

                                     header “Cookie”

                                   Output

                                     base64url

                                     prepend “message=”

                                     print

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

FilenameSHA256Description
dbgcore.dlla9121e70c39de2c10e6790da4aa3a22079242a201da2c1aeeb4ed65070e68e93Malicious DLL loaded by the SumatraPDF installer via DLL forward sideloading
Version_Application_2.0_202566_Application_Number_0234521870_Date_0000000200cb1d73323d3d80004ada185844b0d461abd9ded736d5dc690607f935b4f2b58aLegitimate SumatraPDF installer
Settings.txtb9fac5fd68f333b9459fa4b0111da8fba64a20022df8ea8595eae6a2fc4b9d9dText 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

TypeIndicator
C2refugee-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*)

}

References:

hexacorn

github

Share post on:

XING
Twitter
LinkedIn

SECUINFRA Falcon Team • Autor

Digital Forensics & Incident Response experts

In addition to the activities that are the responsibility of customer orders, the Falcon team takes care of the operation, further development and research of various projects and topics in the DF/IR area.

> all articles