XLoader 4.3 (FormBook) — Layers of Deception
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

The execution unfolds across three distinct payloads:
- Stage 1 — .NET dropper → decrypts embedded assemblies → process hollowing.
- Stage 2 — Native XLoader payload → anti-analysis → decryption → injects into a random SysWOW64 binary.
- 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.

Observed behaviour:
- The process starts another instance of itself.
- Multiple DNS and HTTP requests to unrelated domains.
- The binary deletes itself.
- Network requests are issued through
explorer.exe, not the malware process.
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.

The malicious code load binaries from three embedded resources — Quartz (reversed),
Versa and 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:
pendulum— first run-time assembly; the first member returned byGetExportedTypes()is invoked viaSystem.Activator.cruiser.dll— resolved frompendulum’s resources, gzip-decompressed, then loaded. ContainsCausalitySourceandSearchResult, which decrypt the third resource.Discompard.dll— the final decrypted assembly;ParsingStatecalls into it to continue execution.

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:

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.

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:
- Written in pure native assembly + C
- Very high entropy → embedded/packed code
- 0 libraries, 0 imports, 0 strings — the only readable text is the DOS stub message

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:

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.

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 first function stores a large set of encrypted strings / hash codes.
- Next to the
FFvalues, the base address of the running payload is saved. - Then the address of
LdrLoadDllfromntdll— used to load every further library.
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:

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.

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

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).

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:

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:

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

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:

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 intohashes.txtand they are checked againsthash_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.

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:

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:

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

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 #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.

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:

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.

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.

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:

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:

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:

| 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:

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

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.

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.

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

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.

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:
- Enumerate all running processes
- Inject and set inline hooks in any targeted process found
- Steal clipboard data
- Attempt to create the file in
Program Files - Re-add the Run registry key
- Send
POSTandGETrequests to one of the resolved C2 servers throughexplorer.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:
- Clean
ntdllmapping (Lagos Island). Watch for the sequenceNtCreateFile→NtQueryInformationFile→NtReadFileagainst\System32\ntdll.dllor\SysWOW64\ntdll.dll, followed by an RWX private allocation roughly the size of the image. This is the highest-fidelity signal in the entire chain. - Process hollowing. A process creating a suspended child from
SysWOW64, followed byNtUnmapViewOfSection/ section mapping andNtSetContextThread+NtResumeThread. - Cross-process shared sections.
NtCreateSection+NtMapViewOfSectionmapping the same section into a victim process andexplorer.exe. - Explorer making unexpected outbound HTTP requests to newly registered / low-reputation domains, especially bursts of many domains where only one is live.
- Persistence. Run key creation pointing at an executable under
Program Files (x86)that was written moments earlier. dllhost.exeCOM elevation immediately preceding a file copy intoProgram Files.- Artefact. Creation of
%TEMP%\3r9Pk-75is a strong stage 3 indicator.
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
- Fortinet — Deep Analysis of FormBook New Variant Delivered in Phishing Campaign (Part II)
- Zscaler — Technical Analysis of XLoader’s Code Obfuscation in Version 4.3
- Zscaler — Analysis of XLoader’s C2 Network Encryption
- Botconf — In-Depth FormBook Malware Analysis
- Google Cloud / Mandiant — FormBook Malware Distribution Campaigns
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.