

























On March 1, 2026, ThreatLabz identified an attack chain themed around the ongoing Middle East conflict that delivered its payloads via a ZIP archive. The archive included a Windows shortcut (LNK) file that, when opened, downloaded a malicious Windows Compiled HTML Help (CHM) file from a threat actor-controlled server. The CHM content was then leveraged to deploy a multi-stage payload, progressing from a shellcode loader to heavily obfuscated shellcode, and ultimately to the installation of a PlugX backdoor variant. The attack chain is shown in the figure below.

Figure 1: Attack chain leading to deployment of PlugX.
As part of the lure, the attack dropped a decoy PDF containing images of missile strikes. The Arabic text in the PDF translates to “Iranian missile strikes against US base in Bahrain”. The figure below shows the decoy PDF file used in this attack.

Figure 2: PDF lure referencing Iranian missile strikes against a US base in Bahrain.
The following sections summarize the observed attack flow and the files involved.
The ZIP archive contains an LNK file named photo_2026-03-01_01-20-48.pdf.lnk. The LNK’s target command line uses cURL to download a malicious CHM file from hxxps://www.360printsol[.]com/2026/alfadhalah/thumbnail?img=index.png. The LNK file then uses the legitimate Windows HTML Help executable (hh.exe) with the -decompile option to extract the CHM contents. The below table summarizes the files extracted from the CHM.
Filename | Description |
|---|---|
0.lnk | Stage 2 Windows shortcut. |
3 | Decoy PDF used as a lure. |
4 | TAR archive containing malicious components. |
Table 1: Files extracted from the CHM
The Stage 1 LNK launches the Stage 2 shortcut (0.lnk).
The Stage 2 LNK performs the following actions:
3 to photo_2026-03-01_01-20-48.pdf (in the same directory).4 as a TAR archive and extracts its contents into %AppData%.--path a.The figure below shows the directory structure of the files extracted from the TAR archive.

Figure 3: Directory structure of the TAR archive.
Next, ShellFolder.exe uses DLL sideloading to load a malicious DLL named ShellFolderDepend.dll.
ShellFolderDepend.dll is a 32-bit DLL that establishes persistence, and then decrypts and executes an encrypted shellcode payload stored in Shelter.ex.
The shellcode loader stores its strings in encrypted form and decrypts them at runtime using a custom index-based XOR algorithm that incorporates an additive constant, as shown below.
KEY_BASE = 0x34
decrypted = []
for i, byte in enumerate(encrypted_bytes):
key = (i + KEY_BASE) & 0xFF
decrypted.append(chr(byte ^ key))
return "".join(decrypted)To establish persistence, the DLL enumerates running processes to determine whether bdagent.exe (Bitdefender Agent) is present. Based on the result, the DLL uses one of two persistence methods:
C:\Windows\System32\reg.exe ADD HKCU\Software\Microsoft\Windows\CurrentVersion\Run /reg:64 /v BaiNetdisk /t REG_SZ /d "\"%s\" --path a" /f.Before decrypting and loading the shellcode, the shellcode loader installs two inline API hooks:
push hook_handler; retn) is placed on GetCommandLineW to spoof the return value as the wide-character string ShellFolder.exe 701 0, making the caller believe ShellFolder.exe was launched with the command-line arguments 701 0.push hook_handler; retn) is placed on CreateProcessAsUserW.The hook handler first restores the original bytes at the API entry point, then calls Sleep to pause execution indefinitely, effectively preventing any child process from being created.The DLL calls the Windows Native API SystemFunction033 (RC4) to decrypt shellcode stored in Shelter.ex (located alongside the DLL) using the key 20260301@@@. The DLL then:
This stage is a 32-bit, position-independent shellcode that is heavily obfuscated with control flow flattening (CFF). The next-stage backdoor is stored, encrypted, and compressed inside this shellcode, then decrypted and decompressed at runtime. The backdoor is loaded and executed to continue the next stage of the attack.
The CFF technique used in the shellcode leverages a state machine, where a state variable determines the address of the next execution block. Each basic block updates the state variable after execution and returns control to a dispatcher, which routes execution to the next block. This is a simple yet effective implementation of CFF to make reverse engineering more time consuming.
All API names are stored encrypted in the shellcode and are decrypted at runtime using an index-based XOR decryption algorithm similar to the one used in the shellcode loader. The only change is the additive constant, which is 0x36 instead of 0x34.
In addition, the XOR operations are obfuscated using mixed boolean arithmetic (MBA), typically using the pattern (~x & K) | (x & ~K), which is equivalent to x ^ K.
The embedded payload is decrypted using the following steps:
0xc56dd7ea).The decryption algorithm can be represented as follows:
seed = 0xc56dd7ea
def prng_decrypt(encrypted_data, seed):
state = seed
decrypted_blob = bytearray(len(encrypted_data))
for i in range(len(encrypted_data)):
state = (state + (state >> 3) + 0x13233366) & 0xFFFFFFFF
decrypted_blob[i] = encrypted_data[i] ^ (state & 0xFF)
return decrypted_blobThe decrypted blob begins with a 16-byte header followed by a payload compressed using LZNT1 algorithm. Below is the structure of the decrypted blob.
typedef struct {
uint32_t magic; // 4 bytes: Magic header
uint32_t seed; // 4 bytes: Seed
uint32_t decompressed_size; // 4 bytes: Decompressed size
uint32_t compressed_size; // 4 bytes: Compressed size
uint8_t payload[]; // Variable length: LZNT1 compressed payload
} DecryptedBlob;The loader uses the Windows API RtlDecompressBuffer to decompress the LZNT1 compressed payload.
The decompressed payload contains a corrupted MZ/PE header. The IMAGE_DOS_HEADER, DOS stub, and PE signature are corrupted with randomly generated ASCII data as an anti-forensics mechanism to evade memory forensics solutions. The figure below shows the corrupted MZ/PE headers.

Figure 4: Corrupted MZ/PE headers in the decrypted PlugX backdoor.
The table below summarizes which fields are corrupted in the header and which fields are left intact.
Offset | Expected structure and bytes | Actual bytes present | Description |
|---|---|---|---|
0x00 - 0x3B | IMAGE_DOS_HEADER (4D 5A 90 00...) | ASCII: XseAJbaL... | Overwritten (60 bytes) |
0x3C - 0x3F | e_lfanew pointer | 78 00 00 00 | Intact (4 bytes) |
0x40 - 0x77 | DOS Stub ("... This program cannot be run in DOS mode …") | ASCII: FSlznpPq... | Overwritten (56 bytes) |
0x78 - 0x7B | PE Signature (50 45 00 00) | 19 31 00 00 | Overwritten (4 bytes) |
0x7C - 0x8F | COFF File Header | 4C 01 06 00 A4 5A... | Intact (20 bytes) |
Table 2: Summary of the various fields in the corrupted PlugX MZ/PE headers.
The decrypted and decompressed payload is reflectively loaded by mapping all the sections to memory allocated using VirtualAlloc, performing relocations, resolving imports, and marking the memory region as executable. The first 0x20 bytes of the image base are repurposed and used as a context structure that is passed to DllMain of the reflectively loaded DLL. The PlugX encrypted configuration is present inside the shellcode and a pointer to it is stored at offset 0x14 in the context structure. The structure is defined as follows:
typedef struct {
uint8_t Reserved[0x14];
uint32_t EncryptedConfigPtr; // image_base + 0x14 - Pointer to encrypted config
uint32_t EncryptedConfigSize; // image_base + 0x18 - Size of encrypted config
uint8_t Reserved[0x4];
} _CONTEXTIn this instance, the PlugX image headers serve a dual purpose. They are overwritten with junk data to evade memory forensics, and they are also reused as a context structure passed to the reflectively loaded DLL for PlugX configuration decryption.
The PlugX backdoor reflectively loaded by the shellcode is also similarly obfuscated with CFF and MBA. API strings are also encrypted using an algorithm similar to the one observed in the shellcode loader and the shellcode.
After receiving the encrypted PlugX configuration details via the context structure passed to DllMain by the shellcode, the configuration is decrypted in two stages.
Stage 1
First, the entire encrypted blob is decrypted using a custom algorithm (implemented in Python below):
import struct
from ctypes import c_uint32, c_int32
def decrypt_config(data: bytes) -> bytes:
if len(data) > 3) - 0x69696969
part3.value = (old_part3 >> 5) + old_part3 + 0x66666667
part2.value = -127 * c_int32(old_part2).value + 0x65656565
part1.value = -511 * c_int32(old_part1).value + 0x33333333
key_byte = (old_part1 + 51 + part2.value + part3.value + part4.value) & 0xFF
output[i] = data[i] ^ key_byte
return bytes(output)Stage 2 (PlugX configuration decryption)
The individual fields within the decrypted configuration are further decrypted using RC4 with the key qwedfgx202211. The table below summarizes the decrypted configuration used for this PlugX sample.
Offset | Field | Decrypted Value |
|---|---|---|
+0x10 | Extensions | *.doc*|*.pdf|*.xls*|*.ppt*|*.mp3|*.wav |
+0x810 | Date filter | Last 30 days. |
+0x828 | C2 IP / Port | https://91.193.17[.]117:443 |
+0x0d58 | Persistence Path | %ProgramFiles%\Microsoft\Display Broker |
+0x0f58 | Registry Name | DesktopDialogBroker |
+0x1158 | Service Display Name | Microsoft Desktop Dialog Broker |
+0x1358 | Service Description | Manages the connection and configuration of local and remote displays dialog. |
+0x1558 | C2 Traffic RC4 Key | VD*1^N1OCLtAGM$U |
Table 3: The decrypted configuration used for this PlugX sample.
This PlugX sample supports the following C2 channels:
This PlugX sample supports the following C2 commands. These are largely similar to prior PlugX analysis, and a detailed description is available here.
Command ID | Description |
|---|---|
0 | NOP |
1 | Collect and send system information. |
2 | Request another command. |
3 | Trigger plugins. |
4 | Disconnect |
5 | Exit |
6 | Get configuration. |
7 | Update configuration. |
8 | Information about processes with injections (userinit.exe). |
9 | Get results of LAN scanning. |
10 | Proxy to other PlugX instances. |
Table 4: C2 commands supported by this sample of PlugX.
This sample uses the following plugins:
Magic (hex) | Plugin Name |
|---|---|
0x20120325 | Disk |
0x20120204 | Process |
0x20120117 | Service |
0x20120315 | RegEdit |
0x20120215 | Netstat |
0x20120213 | Nethood |
0x20120128 | Option |
0x20120325 | PortMap |
0x20120160 | Screen |
0x20120305 | Shell |
0x20120225 | Telnet |
0x20120323 | SQL |
0x20120324 | Keylog |
Table 5: Plugins used by this sample of PlugX.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。