Back Morphisec From Business Emails to XLoader: An Order Lure That Led to In
In recent months, Morphisec has observed increased XLoader activity using a range of business-themed email lures. This investigation focuses on a distinct execution chain: an archive containing a single obfuscated JScript that embeds an x86 LuaJIT interpreter, a Lua loader, and an encoded native payload. The order and payment messages shown here illustrate three of the email lures observed in this activity.
Beyond the chain analyzed here, the campaign also delivered several other payloads, including Agent Tesla, Remcos, and XWorm.
Tracing the embedded components shows where execution remains local and where it crosses into another process. The Lua loader starts a packed PE on a thread within LuaJIT; the packer then decrypts and calls the XLoader core on that same thread. The final handoff uses a shared section to map a clone of the XLoader stage into a selected x86 process, then redirects a thread’s execution context to the mapped code.
The chain begins with a Greek-language email presented as a request to confirm order 001828 . Its subject starts with RE:, suggesting an ongoing exchange, and the short message asks the recipient to review the attached order and as soon as possible. This familiar business request gives the recipient a reason to open the archive without a lengthy or elaborate pretext. The message is addressed to “undisclosed-recipients,” so the capture does not identify a specific intended organization. Its RAR attachment repeats the order number, but contains a single obfuscated JScript rather than an order document.
Figure 1. Greek-language order confirmation email. The attached RAR is presented as an order but contains the JScript that starts the XLoader chain
Figure 2. Slovenian-language order email with a RAR attachment, requesting a price quote and invoice to proceed with payment.
Figure 3. Quotation-themed email with two attachments, including a RAR archive, requesting the recipient’s best price and availability while emphasizing an urgent item.
When Windows Script Host runs the JScript, it extracts an x86 LuaJIT interpreter and an extensionless Lua program from embedded data, writes both to the user’s Documents directory, and starts the interpreter with the Lua file as its input. The Lua program attempts process-local security interference, decodes a packed PE, and manually maps it into LuaJIT. Although the mapper calls CreateRemoteThread, it passes GetCurrentProcess(), so this handoff starts a thread inside LuaJIT. The packed PE then decrypts the XLoader core and calls it on the same thread.
XLoader prepares a clone of its stage and selects an x86 target process. It maps a shared section into both processes, sets the controlled thread’s context to enter the remote view, copies the clone through the local view, and resumes the thread. This is the chain’s cross-process transfer. The archive, LuaJIT executable, and Lua source are disk components; the packed PE, decrypted core, and transferred clone are memory stages.
The JScript Installs Its Own Runtime – (Παραγγελί623768193.JS)
The JScript is the first executable stage inside the delivered archive. Despite its size, its active path has a focused purpose: recover its operational strings, extract two embedded components, and launch the stage. A rotated string table hides the COM objects, methods, and paths used by that sequence. Large encoded literals, unused filler, and anti-analysis checks make the installer harder to inspect without adding another execution stage.
Figure 4. Two JScript obfuscation examples: decorative and filler text (left), and wrapped functions with obfuscated identifiers (right).
When Windows Script Host runs the file, the script first attempts to overwrite its own Zone.Identifier stream with a blank line. It then reverses and cleans two encoded literals and uses Microsoft.XMLDOM to decode their Base64 content. Binary ADODB.Stream writes place an x86 LuaJIT-compatible executable and an extensionless Lua program in the user’s Documents directory. Both components are already present in the JScript; this stage does not retrieve them from a server.
The script checks whether the Lua file exists before writing either component, but its final launch command sits outside that condition. It starts the LuaJIT executable hidden, passing the extensionless file as its input without waiting for the process to finish. LuaJIT is therefore the interpreter that runs the stage, while the Lua program supplies the logic that decodes and enters the native payload.
Figure 5. The JScript attempts to clear Zone.Identifier, drops the embedded LuaJIT and Lua loader, and launches LuaJIT hidden.
The LuaJIT Executable and Its Lua Script
The JScript writes two components to the user’s Documents directory: EHFTUOMQDRXVTJSF.exe, an x86 LuaJIT-compatible interpreter, and FSJTVXRDQMOUTFHE, an extensionless Lua source file. The JScript launches the executable with the Lua file as its argument. This relationship matters because the executable provides the process in which the stage runs; the Lua file contains the logic that advances the chain.
The supplied executable exposes LuaJIT 2.1 and Lua 5.1 interfaces and imports functions used for loading libraries, allocating memory, changing page protections, and managing threads. Those properties are consistent with its interpreter role. They do not establish that the attacker supplied an untouched official LuaJIT build. The executable’s role is to host the Lua program under an attacker-chosen filename in the user’s Documents directory.
The Lua source uses FFI to access Windows APIs from inside that process. Its active path hides the console, attempts to interfere with selected security-related functions, checks a conditional persistence branch, decodes an embedded PE, and maps that PE into LuaJIT’s memory. Much of the source’s size comes from repeated, unreferenced blocks; following the active calls reveals the sequence that leads to native execution.
Inside the Lua Loader (FSJTVXRDQMOUTFHE)
The extensionless FSJTVXRDQMOUTFHE file is Lua source executed by the supplied LuaJIT interpreter. Although it contains large repeated blocks resembling additional logic, the active path does not call them. The loader uses LuaJIT’s FFI to access Windows APIs, modify memory in its own process, decode an embedded PE, and start that PE from memory.
Figure 6. Reconstructed execution sequence of the Lua loader inside the supplied LuaJIT process.
The loader first hides the console window, then attempts two forms of process-local interference. One routine scans loaded CLR memory for AmsiScanBuffer and AmsiInitialize and attempts to zero matching strings. Another resolves 12 exports from the process’s mapped ntdll: EtwEventWrite, EtwEventWriteFull, EtwEventWriteTransfer, EtwEventWriteString, NtTraceEvent, EtwEventEnabled, NtOpenThread, NtSuspendThread, NtTerminateThread, NtTerminateProcess, NtSuspendProcess, and NtCreateSection. For each export it finds, the wrapper attempts to replace its first 16 bytes with a RET instruction followed by 15 NOP bytes. The routines skip missing modules and exports and do not require every modification to succeed. Their code establishes the loader’s intent; successful patching on an affected host was not observed.
A separate persistence branch checks for a .mui marker beside the LuaJIT executable. When that marker exists, the loader can write a VBS launcher, check for a scheduled task named EHFTUOMQDRXVTJSF, and create one that invokes the launcher every ten minutes:
schtasks /query /tn “EHFTUOMQDRXVTJSF”
schtasks /create /sc minute /mo 10 /tn “EHFTUOMQDRXVTJSF” /tr “wscript.exe \” \”” /ru “ ” /f /it
The expected first-run JScript path does not create the marker. These commands therefore document a conditional persistence capability, not a task confirmed to have been created during the analyzed chain.
The loader then decodes a PE embedded in its Lua source and copies its headers and sections into executable memory. Its allocation, write, and thread-creation calls all receive the current LuaJIT process handle. The resulting thread starts the packed PE inside LuaJIT ; the decoded XLoader core performs the later transfer into another process.
Recovering the PE Embedded in Lua
The Lua source contains a 382,982-byte encoded literal beginning with the two-character prefix |K. The K selects row 20 of the decoder’s substitution table (the 21st row when counted from one), which maps @#$%^&*()_ to ABCDEFabcd. The decoder removes the prefix, applies that substitution to the remaining 382,980 bytes, and reverses the result. A custom Base64 routine then decodes the text, ignoring characters outside its alphabet and padding.
Figure 7: The Lua source embeds the encoded PE as a long string beginning with |K. The K selects the substitution-table row used in the first decoding step.
After Base64 decoding, the loader applies a character rotation and removes a leading control byte, revealing a packed PE32 image. It then maps that image into the LuaJIT process.
The Importless Native Packer
The PE recovered from Lua is a 32-bit image with one high-entropy .text section and no declared imports. Its entry point at RVA 0x26F0, named packed_loader_entry during analysis, drives loader_state_dispatcher at RVA 0x10A0. The dispatcher locates a shared stack frame using the sentinel 0x65B6B058. A call/pop sequence at 0x118F recovers the image’s current location, allowing the loader to work from the address where Lua mapped it.
The Importless Native Packer
The PE recovered from Lua is a 32-bit image with one high-entropy .text section and no declared imports. Its entry point at RVA 0x26F0, named packed_loader_entry during analysis, drives loader_state_dispatcher at RVA 0x10A0. The dispatcher locates a shared stack frame using the sentinel 0x65B6B058. A call/pop sequence at 0x118F recovers the image’s current location, allowing the loader to work from the address where Lua mapped it.
Figure 8. The Lua-recovered PE contains a single high-entropy .text section. Its entry point is at RVA 0x26F0, and the section entropy is approximately 7.9937 bits per byte.
The packer constructs encrypted module and API names on the stack. It walks the current process’s PEB to locate ntdll.dll, parses that module’s export directory, and resolves NtAllocateVirtualMemory. This supplies the allocator without an import table or import resolution by Lua’s manual mapper. The call reserves and commits 0x45031 bytes with executable, writable protection inside the LuaJIT process, after which the packer copies its source data into the allocation.
The encrypted portion begins at allocation offset 0x1C31 and spans 0x43400 bytes. The packer applies one RC4 pass across that entire region. It then initializes RC4 separately for each of 15 consecutive 0x47BB-byte blocks using a second key. Those blocks cover all but the final 11 bytes, which retain the result of the first pass. Reinitializing the cipher for every block is essential to reproducing the recovered code.
After decoding, the dispatcher makes a synchronous indirect call to allocation + 0x30181. That address is a short entry stub leading into the XLoader core. The packer performs this transfer on the thread Lua started inside LuaJIT; it does not select or inject a second process.
Figure 9. The entry at 0x30181 before decoding (right) and after decoding (left), where it calls xloader_stage_main.
Inside the XLoader Core
The decoded entry stub calls xloader_stage_main at allocation offset 0x2E781. This routine prepares the XLoader runtime context, sets its protected dispatch selector to 0xF2, and initializes the core. If it detects an existing instance, it exits. Otherwise, it passes the descriptor seed 0xC02DCD50 to decrypt_execute_reencrypt_primary_routine. The main routine therefore leads into the protected injector wrapper; the injection operations are implemented beyond this entry function.
Figure 10. xloader_stage_main initializes the runtime, checks for an existing instance, and invokes the protected routine.
The call to decrypt_execute_reencrypt_primary_routine exposes the core’s on-demand code protection. The wrapper combines the caller’s descriptor seed with its own stored value to derive a 20-byte key, then locates the injector between six-byte markers at offsets 0x1844B and 0x18AA7. It decrypts the 0x656-byte body between those markers, invokes the protected function at 0x18441, and re-encrypts the region after the call. The injector is therefore available as plaintext only during this wrapper’s execution.
Figure 11. The XLoader wrapper decrypts and runs the protected injector, then re-encrypts it before returning.
Shared-Section Injection
The protected injector creates a 0x43400-byte clone of the XLoader stage and transforms its 0x1000-byte prefix before transferring it. It then selects an x86 target. One route searches for an eligible running process, making up to three attempts. The other tries to launch a SysWOW64 host with its primary thread suspended. Its 12 candidates are cleanmgr.exe, clip.exe, TSTheme.exe, getmac.exe, UserAccountControlSettings.exe, proquota.exe, SearchProtocolHost.exe, mobsync.exe, mshta.exe, ftp.exe, fsutil.exe, and find.exe.
The injector maps a shared section into LuaJIT and the selected target. It retrieves the target thread’s x86 context and sets CONTEXT.Eax to remote_view + 0xC9C1. In the existing-process route, it also suspends a selected thread and redirects its EIP through a bootstrap. The injector copies the transformed clone into its local view and resumes the thread. Because both views map the same section, the clone becomes available in the target without copying the entire stage through WriteProcessMemory or creating a remote thread.
The bootstrap at remote_view + 0xC9C1 signals its arrival through the shared section, initializes the XLoader context in the target, and enters the protected core. XLoader then prepares its network worker and endpoint configuration, processes responses and commands, and can invoke collection routines. The recovered code includes an HTTP GET command path, Windows Vault enumeration, and cookie-record preparation.
The target’s original image remains mapped. This mechanism is therefore shared-section injection with x86 thread-context redirection , rather than process hollowing.
Figure 12. XLoader’s suspended-host injection sequence. The injector maps a shared section into LuaJIT and the target process, sets the target thread’s EAX to the mapped bootstrap, copies the XLoader clone through the local view, and resumes the thread.
XLoader Network Configuration and Capabilities
Zscaler ThreatLabz documented the obfuscation and network protocol used by recent XLoader builds. The stage recovered in this investigation closely matches that architecture: protected functions are decrypted when called, the code constructs the PKT2: marker and POST parameters including dat=, &un=, and &br=9, and it implements both WinINet and raw-socket transport paths. The matching implementation supports its classification as modern XLoader in the FormBook lineage.
The sample’s encrypted configuration contains 64 hostname candidates with four-character path tags, plus one separately encrypted host. Its initializer builds 16 endpoint records using fixed selectors from the 64-entry pool, then replaces the host in the final record with www[.]990match[.]com. That record retains the selected ccp8 path tag. Unlike the random initial selection described by Zscaler, this sample starts with a fixed selector array and has separate logic to refresh five slots later. The configured hosts include decoys; the decrypted list alone does not identify which endpoint is the real C2.
Slot, Initial configured endpoint
1, hxxp://iqos-one[.]com/gf2n/
2, hxxp://offon33fv[.]club/6yav/
3, hxxp://eolienzaer[.]com/jrsi/
4, hxxp://tori[.]fi/8yfp/
5, hxxp://okbajegame[.]com/l3iy/
6, hxxp://komplek[.]id/gkws/
7, hxxp://sonhero[.]top/xc0y/
8, hxxp://grandseosite91[.]shop/1ud5/
9, hxxp://moreultras[.]store/2r3a/
10, hxxp://bagolone[.]it/qbqm/
11, hxxp://tomatox[.]com[.]tw/upw6/
12, hxxp://511mma[.]ru/dnir/
13, hxxp://hahashoe[.]com/r0zm/
14, hxxp://snvipvn[.]com/v0rc/
15, hxxp://revelintravel[.]com/zd56/
16, hxxp://www[.]990match[.]com/ccp8/
The recovered stage initializes a network worker and processes responses through a command dispatcher. Its command 0x34 recognizes RMTD and RMTU and reaches an HTTP GET implementation with a Winsock fallback. The code also contains Windows Vault enumeration and cookie-record preparation. These sample-specific findings complement Zscaler’s broader description of XLoader’s command and information-stealing capabilities without repeating the full family analysis.
How Morphisec can help
This chain gives defenders several opportunities to stop XLoader before its network and collection code runs. The Lua loader manually maps a packed PE into the LuaJIT process; the packer decrypts and executes the XLoader core in memory; and the injector transfers a clone into another process through a shared executable section. Morphisec Automated Moving Target Defense (AMTD) is designed to prevent malicious in-memory execution, including shellcode and reflective injection techniques. In this case, investigation should also correlate the order-themed JScript with the dropped LuaJIT executable and Lua file, changes to ntdll exports, and a shared executable mapping followed by x86 thread-context redirection.
Consolidated Indicators of Compromise
Filenames, paths, and task
Email delivery indicators
Initial configured XLoader endpoints