XLoader 4.3 (FormBook) — Layers of Deception

XLoader 4.3 infostealer analysis

XLoader — the evolution of the FormBook malware family — is a Malware-as-a-Service infostealer and form grabber sold cheaply on underground forums. Version 4.3 is one of the most defensively engineered commodity samples in circulation: the payload is written in native assembly with no imports, no strings and no libraries; its core functions are encrypted on disk and decrypted only after a gauntlet of anti-analysis checks passes; every API and string is referenced by a CRC-32/BZIP2 hash; and all of its real work is done through a clean, manually mapped copy of ntdll.dll that renders user-mode EDR hooks useless.

This post is the full write-up of my end-to-end reverse engineering of XLoader 4.3, from the initial .NET dropper through three injected stages to the credential theft and form-grabbing loop. Every stage is walked through with the debugger screenshots taken during analysis.

⚠️ Note: This is a re-write by AI, but have been vetted by Author.

📄 The original report is also available as a PDF. Samples (password infected), recreated TTP code and helper scripts are in the Source repository.


Key findings

# Finding
1 Initial dropper — a .NET executable carrying multiple embedded, encrypted DLLs, resolved and loaded at run-time. The same dropper family is shared with Remcos RAT and Agent Tesla. The real payload is launched via process hollowing.
2 Native assembly payload — stage 2 is pure ASM/C. Zero imports, zero libraries, zero meaningful strings, making both static analysis and signature detection extremely hard.
3 Advanced anti-analysis — checks cannot be defeated by simply patching a jump or a return; the checked values are consumed later as decryption key material.
4 Custom encryption — a modified RC4 routine with additional subtraction operations.
5 API / string / library hashing — everything is referenced by CRC-32/BZIP2 hash instead of by name.
6 Encrypted core functions — the malicious functions are encrypted inside .text and are regenerated at run-time only after the anti-VM sequence is satisfied and the key derived.
7 Unhooked ntdll — a clean copy is manually mapped into memory and its native APIs called directly (“Lagos Island method”), bypassing all user-mode hooks.
8 Persistence — copies itself into Program Files (x86) and registers a Run registry key.
9 Privilege escalation — abuses dllhost.exe and COM objects, purely to enable the copy + persistence step.
10 Heavy process injection — injects into multiple processes and migrates itself between them.
11 Decoy C2s — a large set of decoy servers hides the single real C2.
12 Form grabber — inline hooks are installed in victim processes to capture data before it is encrypted.

Attack chain

XLoader 4.3 attack chain

The execution unfolds across three distinct payloads:

  1. Stage 1 — .NET dropper → decrypts embedded assemblies → process hollowing.
  2. Stage 2 — Native XLoader payload → anti-analysis → decryption → injects into a random SysWOW64 binary.
  3. Stage 3 — Partially decrypted XLoader inside the hollowed process → stealer, form grabber, persistence, C2.

Initial detonation

The sample was detonated in an isolated analysis VM with Procmon, Wireshark and similar tools running. Nothing happened — a strong hint that anti-analysis logic was aborting execution. After simply renaming the analysis tools, execution proceeded normally.

Initial detonation process tree

Observed behaviour:

A subset of the resolved domains (defanged) — note that the overwhelming majority of these are decoys:

hxxp:\\www.twin68s.online              hxxp:\\www.cicreception2023.org
hxxp:\\www.morubixaba.com              hxxp:\\www.gestionamostualquiler.org
hxxp:\\www.superios.info               hxxp:\\www.bocahkota.xyz
hxxp:\\www.lolisex77.top               hxxp:\\www.fhsbfjbsljsdfsd.xyz
hxxp:\\www.mifurgoentuangar.fun        hxxp:\\www.necessarymusthave.shop
hxxp:\\www.abk-importexport.com        hxxp:\\www.adoniadou.com
hxxp:\\www.delret.tech                 hxxp:\\www.humidlandscaping.com
hxxp:\\www.wlkwinn.net                 hxxp:\\www.8ai.ooo
hxxp:\\www.minevisn.com                hxxp:\\www.moheganmart.com
hxxp:\\www.jacksonmoddy.com

Stage 1: the .NET dropper

The dropper is a .NET executable of the same breed used by Agent Tesla and Remcos RAT. Static string analysis alone gives away the design:

# String Meaning
1 System.Reflection Assemblies are loaded at run-time
2 ofnIepyTgnirtS (StringTypeInfo), ofnIdohteM (MethodInfo) Reversed strings → reversed embedded resources
3 .edom SOD ni nur eb tonnac margorp sihT! A reversed PE — the embedded resource is another binary
4 System.Activator Assemblies are activated at run-time

So: the dropper is obfuscated, loads assemblies at run-time, and stores its resources reversed to defeat signature scanning. Heavy junk code is interleaved to frustrate reading.

Stage 1 suspicious strings

The malicious code load binaries from three embedded resources — Quartz (reversed), Versa and Zinc:

Embedded resources Quartz, Versa, Zinc

Because assemblies are resolved at run-time, static analysis dead-ends here and the work moves to the debugger. The run-time chain is:

  1. pendulum — first run-time assembly; the first member returned by GetExportedTypes() is invoked via System.Activator.
  2. cruiser.dll — resolved from pendulum’s resources, gzip-decompressed, then loaded. Contains CausalitySource and SearchResult, which decrypt the third resource.
  3. Discompard.dll — the final decrypted assembly; ParsingState calls into it to continue execution.

Run-time modules — pendulum

Discompard.dll is heavily obfuscated with randomised class and variable names. Normally de4dot would be the answer, but because this resource depends on two other run-time-loaded binaries, patching the whole chain is impractical — so analysis proceeded on the obfuscated version, driven by local variables and return values.

Stepping over the functions eventually returns a buffer whose first bytes are 4D 5A (MZ) — a fresh PE:

MZ bytes in returned buffer

From there the behaviour is textbook process hollowing: the malware opens itself in a suspended state, reserves an RWX region, writes the shellcode in chunks, repoints the base image entry to the injected code, and calls ResumeThread.

Process hollowing

Dumping that shellcode yields stage 2 — the real XLoader payload.


Stage 2: XLoader 4.3 native payload

The dumped payload is immediately hostile to analysis:

Stage 2 entry

Execution starts benignly — a few libraries are loaded, some computation runs (likely decompressing code) — and then reaches a call edx into a region IDA cannot identify:

call edx into unidentified code

That unidentified region is encrypted code, resolved by IDA only at run-time. This is the first of many anti-analysis layers. Following it, the process exited silently — the battle with anti-analysis had begun.

Defeating anti-analysis

Take 1 — FAILED (patching the jump)

The obvious move: flip the zero flag so the exit branch isn’t taken.

Take 1 — patching the conditional jump

Execution continues briefly, then throws an exception. The patch cannot work — the malware consumes the values that this check sets later on, so bypassing the branch leaves the downstream state invalid.

Take 2 — FAILED (patching the flags in memory)

XLoader initialises a configuration object that drives nearly all of its control flow. It starts life filled with FFFFFFFF, and each subsequent function contributes to it:

The configuration object

One function maps a clean copy of ntdll into memory and stores its address in the config object, while also setting anti-VM flags starting at element 45:

Clean ntdll mapped into memory

Other checks follow — for example snapshotting running processes and matching them against an internal list (Procmon was duly detected). A final function compares the accumulated anti-VM flag sequence against the expected one; mismatch → exit.

Anti-VM flag sequence mismatch

Patching that flag sequence directly in memory seems to work — the check passes — but the DLL names being searched for come out as garbage:

Library names decrypt incorrectly

This is the key insight of the whole analysis:

The anti-VM flag bytes are not just a check — they are the seed for the decryption routine. Patching them after they are used means the configuration decrypts to garbage. The correct values must be present before the check runs.

Take 3 — PASSED (fixing the values at the source)

After considerable debugging, the culprit was a function being skipped because of a single unmet flag condition. Correcting the condition and the register value pushed into the config object — rather than patching memory after the fact — produced a clean run.

In this environment three flags were wrong: element 3 was 0 when it should be 1, plus the values at positions 11 and 12 (both caused by Procmon and similar tooling — trivially avoided by renaming the tools).

Take 3 passes — libraries load correctly

With the values corrected at run-time before they hit the stack, the library loader now resolves plaintext library names, and the process exits with status 0 — no exceptions.

A Botconf presentation on earlier FormBook versions documents these flags, and XLoader 4.3 still follows a comparable scheme:

FormBook anti-analysis flag reference

Decryption and deobfuscation routine

XLoader layers encryption on everything: APIs are hashes, strings and libraries are hashes, and even the hashes are encrypted inside the config object. The core functions are encrypted and only decrypted once the anti-analysis gate passes.

Library names

The anti-VM flag bytes act as the decryption seed. Library names are decrypted one by one and loaded via the native LdrLoadDll:

Decrypting library names

API names

The decrypted APIs telegraph the next move — more process injection:

Decrypting API names

LookupPrivilegeValueW
SeDebugPrivilege
AdjustPrivilegeToken

String hashing — CRC-32/BZIP2

Rather than comparing process names as strings, XLoader hashes them. In the screenshot below it compares a running process against the stored constant 23 E0 C7 CD → 0xCDC7E023:

CRC-32/BZIP2 hash comparison against procmon.exe

Brute-forcing common 32-bit hash algorithms against the name procmon identified the algorithm as CRC-32/BZIP2. The full blocklist recovered from the sample:

# Bytes Hash Process
1 86 90 BE 3E 0x3EBE9086 vmwareuser.exe
2 B5 DD 6F 4C 0x4C6FDDB5 vmwareservice.exe
3 3E B1 6D 27 0x276DB13E vboxservice.exe
4 8E 0A 0F E0 0xE00F0A8E vboxtray.exe
5 04 94 CF 85 0x85CF9404 sandboxiedcomlaunch.exe
6 84 87 24 B2 0xB2248784 sandboxierpcss.exe
7 23 E0 C7 CD 0xCDC7E023 procmon.exe
8 50 5F 1F 01 0x011F5F50 filemon.exe
9 1C BC D4 1D 0x1DD4BC1C wireshark.exe
10 E2 FC 35 82 0x8235FCE2 netmon.exe
11 D5 E2 2C C7 0xC72CE2D5 unresolved
12 8B 17 63 02 0x0263178B unresolved
13 56 53 58 57 0x57585356 unresolved
14 40 52 B9 9C 0x9CB95240 sharedintapp.exe
15 EF 9F C3 0C 0x0CC39FEF unresolved
16 57 AC 47 93 0x9347AC57 vmsrvc.exe
17 DC 22 95 9D 0x9D9522DC vmusrvc.exe
18 0E C7 1B 91 0x911BC70E python.exe
19 B9 3D 44 74 0x74443DB9 perl.exe
20 A9 1A 4C F0 0xF04C1AA9 regmon.exe

🛠️ A helper script to match hashes against a wordlist is included in the repo at tools_and_scripts/Check_hashes — drop candidate hashes into hashes.txt and they are checked against hash_sources.json.

API hashing

APIs resolved from the injected clean ntdll are called by hash too. XLoader walks every export, computes its CRC-32/BZIP2 hash, and compares against its decrypted constants. On a match it takes the address and calls it directly — so the debugger never sees an API name, and every user-mode hook is bypassed.

API hash resolves to NtResumeThread

Since the hash algorithm was known, an IDA Python script (set_bp.py) was used to breakpoint the resolver and dump each API as it was resolved:

NtOpenDirectoryObject      NtCreateMutant             RtlSetEnvironmentVariable
NtCreateSection            NtMapViewOfSection         NtOpenProcess
RtlAllocHeap               NtQueryInformationToken    NtProtectVirtualMemory
NtCreateFile               NtDelayExecution           NtReadVirtualMemory
NtOpenThread               NtReadFile                 NtUnmapViewOfSection
NtResumeThread             ExitProcess                NtQuerySystemInformation
NtOpenProcessToken         NtAdjustPrivilegesToken    RtlQueryEnvironmentVariable
RtlDosPathNameToNtPathName_U                          NtSuspendThread
NtGetContextThread         NtSetContextThread

Decrypting the core functions

XLoader sets up a function prologue (push ebp / mov ebp, esp) and leaves everything below it encrypted. In older FormBook builds these functions were identifiable by magic bytes such as 48909090 / 49909090; in 4.3 the leading bytes are randomised.

Once the anti-VM checks pass and the RC4 key is established, the bodies are decrypted in place and execution jumps into the freshly materialised assembly:

Decrypting core functions at run-time

Extracting stage 3

Stepping further, the malware reads itself and decrypts a new buffer in an RWX region — this time plain shellcode with no MZ header. That is the partially decrypted stage 3:

Stage 3 shellcode decrypted in memory

Dumping it and running FLOSS finally yields readable strings — browsers, mail clients, SQL queries, HTTP APIs:

FLOSS strings from stage 3

SELECT name, value FROM autofill
SELECT host_key, path, is_secure, expires_utc, name, value, encrypted_value FROM cookies
\Opera Software\Opera Stable          Internet Explorer\IntelliForms\Storage2
Firefox\   Thunderbird\   Chrome   Opera   Outlook Recovery   Fox Recovery
encrypted_key   Local State   \3r9Pk-75_   sqlite3.dll
InternetOpenA  InternetConnectA  HttpOpenRequestA  HttpSendRequestA  InternetReadFile
hxxp://www.sqlite.org/2014/sqlite-dll-win32-x86-3080300.zip

Process enumeration

XLoader calls NtQuerySystemInformation to snapshot every running process, then walks the list comparing CRC-32/BZIP2 hashes against the values in its config object.

Process injection

Stage 2 performs two injections:

Injection overview

Injection #1 — start the victim process. A small shellcode buffer containing a single static string (the target process name) is injected into a random already-running process, which then launches a randomised 32-bit binary from SysWOW64 (e.g. chkdsk.exe) in a suspended state and returns its process information.

Target process name in injected buffer

This indirection is itself an anti-analysis measure: XLoader never opens the target process itself. In this run the shellcode landed inside a 64-bit IDA instance:

Injected shellcode observed in IDA64

Injection #2 — migrate into the victim. Two buffers are written into the suspended process: one of ~180 KB and one of ~40 KB. The smaller holds the original chkdsk.exe bytes; the larger receives the stage 3 shellcode and is a shared memory region written simultaneously in both processes.

Injected buffers observed with injectionview

Tracking this many injections is painful by hand — the injectionview tool was built to make it tractable. Special thanks to Osama Ellahi for putting it together so quickly.

Finally the thread is resumed and stage 2 terminates via ExitProcess.


Stage 3: partially decrypted XLoader

Attaching x32dbg to the victim process before resuming the thread (the injected entry point address is already in EAX) allows stage 3 to be debugged from its first instruction.

Stage 3 entry point

The lifecycle largely repeats: anti-VM checks → decrypt library names → LdrLoadDll → decrypt API names → resolve by hash from the freshly mapped clean ntdll. Privilege-related APIs resolve again (LookupPrivilegeValue, SeDebugPrivilege, NtAdjustPrivilegeToken).

Indicator removal

Stage 2’s on-disk file is deleted with:

Indicator removal API sequence

NtCreateFile → NtQueryInformationFile → NtReadFile → NtClose → ZwDeleteFile

Further injection

NtCreateSection → NtMapViewOfSection → NtAllocateVirtualMemory →
NtOpenProcessToken → NtQueryInformationToken → ConvertSidToStringW →
NtAllocateVirtualMemory

Additional RWX sections are staged for the next injection target.

System information discovery

NtCreateKey and NtQueryValueKey read OS details from the registry — ProductName, CurrentBuild, and similar:

System information discovery

Process enumeration and migration to explorer.exe

wininet.dll is loaded through LdrLoadDll, then the process list is enumerated again via NtQuerySystemInformation — the next injection target is explorer.exe, which will carry the network traffic. Supporting calls: NtCreateMutant, NtCreateSection, NtMapViewOfSection, NtDelayExecution, NtAllocateVirtualMemory.

Botnet registration

The first outbound request registers the victim:

Botnet registration request

Field Value
Magic word XLNG
Bot ID 202293EF
XLoader version 4.3
OS Windows 10 Enterprise x64
Username base64-encoded

The stealer

XLoader hunts a broad set of browsers, email clients, FTP clients and messaging apps:

Stealer target enumeration

For Chrome it locates the Login Data store and queries it with winsqlite3.dll:

Chrome credential theft via sqlite

SELECT origin_url, username_value, password_value FROM logins

Values are then decrypted with crypt32.CryptUnprotectData using the key recovered from Local State.

Anything harvested is written to a file in %TEMP% with the static name 3r9Pk-75 — if the file already exists it is deleted and rewritten with a fresh timestamp — using NtCreateFile → NtQueryInformationFile → NtReadFile → NtWriteFile.

Decrypted functions and target list

Much of the target data is invisible until the encrypted functions are decrypted at run-time; the strings for targeted applications and paths are pushed onto the stack only then.

Targeted processes

Privilege escalation

Privileges are escalated by abusing dllhost.exe and COM objects, and only for one purpose: copying stage 2 into Program Files. If privileges are insufficient, XLoader writes the payload to %TEMP% via Explorer and then uses the dllhost abuse to move it.

Privilege escalation via dllhost and COM

Persistence

Once the payload sits in Program Files, persistence is established with a Run registry key created through NtCreateKey:

Run key persistence

Form grabbing via inline hooks

XLoader is not only a stealer. It installs inline hooks in targeted processes to lift plaintext data straight from function parameters — before TLS encryption is applied.

Inline hooks in victim processes

Stolen data lives in a memory region shared between three processes: the victim process, the stage 3 malware, and explorer.exe.

The malware then settles into its main loop, repeating indefinitely:

  1. Enumerate all running processes
  2. Inject and set inline hooks in any targeted process found
  3. Steal clipboard data
  4. Attempt to create the file in Program Files
  5. Re-add the Run registry key
  6. Send POST and GET requests to one of the resolved C2 servers through explorer.exe, which hosts the injected exfiltration payload

MITRE ATT&CK mapping

Tactic Technique ID
Defense Evasion Obfuscated Files or Information: Software Packing T1027.002
Defense Evasion Obfuscated Files or Information: Encrypted/Encoded File T1027.013
Defense Evasion Process Injection: Process Hollowing T1055.012
Defense Evasion Reflective Code Loading T1620
Defense Evasion Impair Defenses: Disable or Modify Tools T1562.001
Defense Evasion Indicator Removal: File Deletion T1070.004
Defense Evasion Virtualization/Sandbox Evasion T1497
Discovery Process Discovery T1057
Discovery System Information Discovery T1082
Credential Access Credentials from Password Stores: Web Browsers T1555.003
Credential Access Input Capture T1056
Collection Clipboard Data T1115
Privilege Escalation Abuse Elevation Control Mechanism T1548
Persistence Boot or Logon Autostart Execution: Registry Run Keys T1547.001
Command & Control Application Layer Protocol: Web Protocols T1071.001
Exfiltration Exfiltration Over C2 Channel T1041

See the full project matrix on the ATT&CK coverage page.


Detection opportunities

Because XLoader deliberately avoids named API calls and hooked ntdll, detection is best anchored on behaviour rather than strings:


Recreated TTPs

Two of XLoader’s most interesting techniques have been reimplemented from scratch, with full source and build files:

TTP Description Code
Lagos Island ntdll unhooking Reads a raw ntdll from disk, manually maps it, and resolves exports from the clean copy — defeating user-mode hooks Write-up · Code
Encrypted .text / memory patch Reproduces run-time decryption and execution of encrypted function bodies Write-up · Code

Indicators of compromise

Sample (stage 1 dropper, SHA-256)

f5a8212add9f9bfbc85f6337d2194ca9aa66bd3c12830c8ebe7d7ff60934d5ad

Host artefacts

%TEMP%\3r9Pk-75                              stolen-data staging file
%ProgramFiles(x86)%\<random>\<random>.exe    persistence copy
HKCU\...\CurrentVersion\Run                  persistence value
Randomised SysWOW64 binary (e.g. chkdsk.exe, msiexec.exe) hollowed
explorer.exe                                 injected exfiltration payload

Network — see the decoy domain list above. Only one host in the set is the genuine C2; the rest exist purely to dilute blocklists and confuse analysts.

⚠️ The samples in this repository are live malware, stored in archives with the password infected. Detonate only inside an isolated, network-controlled VM. See SECURITY.md.


References


Disclaimer

The artifacts, code and analysis in this repository are provided strictly for educational and research purposes. I do not condone or support any malicious use of this material.

← Back to all analyses