OnionDrop is a multi-stage loader built to deliver infostealers at scale. The Howler Cell Threat Research Team has been tracking it across an active campaign spanning multiple payload variants, and the technical depth under the hood warrants attention well beyond the infostealer category it delivers.
The instinct in threat intelligence is to reserve that level of attention for nation-state actors. Targeted intrusions, purpose-built implants, espionage campaigns tied to geopolitical flashpoints. Those threats pull focus because they carry names, flags, and attribution. Commoditized malware operates in the shadow of that spotlight. That is a prioritization gap worth examining, because OnionDrop's evasion architecture is not a step below nation-state tooling. In several respects, it exceeds it.
The chain starts with a ZIP archive and a legitimate Adobe-signed executable used for DLL sideloading. From there, the malicious DLL walks through four transformation stages: custom byte-pair decoding, Xpress Huffman decompression via RtlDecompressBufferEx, AES-256-CBC decryption with rotating key material, and final shellcode execution through TpPostWork callback abuse inside the Windows Thread Pool.
Stack-string construction, dynamic API resolution via LdrGetProcedureAddress, API hammering, and display device enumeration defeat both static and dynamic analysis at every stage. This is a professionally engineered evasion framework that anyone with access can point at any target.
The most common final payload is LegionLoader (also tracked as CurlyGate), calling back to gainmsg[.]com. Related waves using the same chain delivered CGrabber Infostealer and Vidar Stealer.
The loader is payload-agnostic by design. Howler Cell first documented this operator with the CGrabber Infostealer blog, then tracked it through Direct-sys Loader. OnionDrop is the latest entry in an active and evolving malware family. YARA retro hunting surfaced 645 unique DLL samples across roughly 80 days, with delivery still active at publication.
The Howler Cell Research Team developed a YARA rule for OnionDrop (shared in the Appendix) to hunt for samples. The hunt identified over 645 unique DLL samples. The earliest matching sample first appeared on February 28, 2026. The most recent appeared on May 20, 2026, confirming the OnionDrop loader remains under active delivery.
OnionDrop has delivered different payloads over time. The earliest sample dropped CGrabber Infostealer. The sample covered in this report delivered LegionLoader. More recent samples have delivered Vidar Stealer.
The Vidar chain is worth calling out. After OnionDrop completes its four stages, it hands off to Direct-sys Loader (APC Injector), an intermediate loader Howler Cell first documented during the CGrabber campaign. Direct-sys Loader then delivers the final Vidar payload. That brings the total to five stages before the stealer runs, which illustrates how deep OnionDrop's delivery pipeline can go.
This range of payloads confirms OnionDrop functions as a shared or commoditized loader serving multiple infostealer campaigns simultaneously. The steady volume and frequency of samples within a single 80-day window indicate sustained, high-tempo operations rather than isolated or opportunistic delivery.
OnionDrop kill chain from initial delivery through four unpack stages to final payload execution
The Howler Cell Threat Research Team began its investigation with a suspicious ZIP archive. The archive contains multiple signed modules and executables, as well as two primary malicious DLLs: codecstore384d.dll and sqlite.dll.
Figure 1: Overview of files within the malicious archive
A legitimate Adobe-signed executable, setup.exe (originally named AcroBroker.exe), sideloads the malicious dependency named sqlite.dll. The archive also contains a 100 MB binary file named data.bin filled with random hexadecimal bytes, likely serving as a decoy to artificially inflate the ZIP file size.
Upon loading, sqlite.dll dynamically loads the primary malicious DLL, codecstore384d.dll, via LoadLibraryA. That triggers DllMain and initiates the attack chain.
Figure 2: Dynamically loading a malicious DLL via sqlite.dll
DllMain within codecstore384d.dll dynamically decrypts the Win32 API string NtCreateThreadEx at runtime and resolves it via GetProcAddress. The malware combines stack-based strings with custom decryption logic to reconstruct sensitive strings at runtime, defeating static detection.
The malware constructs essential base strings using stack-string technique, as shown in Figure 3. Strings generated through this mechanism include LdrGetProcedureAddress (for resolving native APIs), BCryptOpenAlgorithmProvider and BCryptGenerateSymmetricKey (for initializing cryptographic algorithms and keys), and BCryptDecrypt (for performing data decryption).
Figure 3: Stack string setup
The address of LdrGetProcedureAddress is first resolved dynamically via GetProcAddress. All subsequent API resolutions run through the resolved LdrGetProcedureAddress. This includes resolving LdrLoadDll, which loads bcrypt.dll while bypassing the more commonly hooked LoadLibrary call. From the loaded bcrypt module, the malware resolves the full BCrypt function chain: BCryptOpenAlgorithmProvider, BCryptSetProperty, BCryptGetProperty, BCryptGenerateSymmetricKey, and BCryptDecrypt.
After resolving the necessary functions, the malware sets up an AES-256-CBC decryption context. It opens an AES algorithm provider, configures the chaining mode to CBC, queries the ObjectLength property to determine the required key object buffer size, and generates a symmetric key using a hardcoded 32-byte secret embedded within the binary (Figure 4).
Figure 4: Setting up AES decryption context
BCryptDecrypt recovers the plaintext string by extracting the first 16 bytes from the input buffer as the initialization vector (IV) and treating the remaining bytes as encrypted ciphertext. The decrypted output is null-terminated and returned to the caller (Figure 5).
Figure 5: AES string decryption using BCryptDecrypt
For a detailed pseudocode breakdown of the string decryption routine, see the Appendix. While the underlying decryption mechanism remains consistent throughout, relying on BCryptDecrypt with AES-CBC, the malware uses distinct hardcoded pbSecret values passed to BCryptGenerateSymmetricKey across different execution stages. Rotating embedded key material between phases adds resistance against static analysis of the encrypted strings.
Throughout all stages of execution, whenever the malware references a string, it obtains that string either through the decryption logic described above or through stack-string construction. Continuing from DllMain, the malware decrypts the string NtCreateThreadEx via the string decryption routine and resolves its address using GetProcAddress.
A new thread executes the remainder of the malicious chain via the resolved NtCreateThreadEx. Running lengthy operations directly from DllMain would risk a deadlock due to the Loader Lock held during DLL initialization. The malware avoids this by spawning a separate thread, waiting briefly via a short sleep interval, and returning from DllMain with a value of 1, allowing the DLL load to complete cleanly.
The malware also makes extensive use of API hammering throughout its execution flow, as shown in Figure 6.
Figure 6: Thread creation via NtCreateThreadEx
API hammering serves two purposes:
Before executing its malicious logic, the malware queries the display device name through EnumDisplayDevicesA. The returned device string is compared against a predefined list of valid display device names (Figure 7), decrypted at runtime using the string decryption routine.
Figure 7: Anti-analysis check
Execution proceeds only if the queried display device string contains at least one matching value from this list, filtering out sandboxes and virtual environments that expose non-standard or emulated display adapters.
|
Table 1: Valid display device strings (used for sandbox detection) |
||
|
INTEL |
GTX |
ARC |
|
AMD |
RTX |
QUADRO |
|
RADEON |
GEFORCE |
|
With the anti-analysis check passed, the malware continues combining the string decryption routine with LdrGetProcedureAddress to resolve all critical NTDLL functions. API hammering calls appear throughout but are separate from this resolution mechanism.
The malware obtains a handle to its own DLL on disk using NtCreateFile, then calls NtQueryInformationFile with the FileStandardInformation class to retrieve file size from the EndOfFile field in the FILE_STANDARD_INFORMATION structure. A hardcoded offset of 0xd9108 is subtracted from this value to calculate the starting position of the encoded data embedded within the DLL. The malware reads the encoded contents from disk into a newly allocated memory region via NtAllocateVirtualMemory (Figure 8).
Figure 8: Reading encoded content from disk
The encoded contents pass through a custom decoding routine (Figure 9; Python reproduction in the Appendix) before being handed to subsequent decryption and decompression stages.
Figure 9: Decoding routine
The decoding logic processes the input buffer by reading byte pairs at a fixed length of two. Each byte maps through a hardcoded 256-byte lookup table to yield a 4-bit nibble value. The first byte produces the high nibble; the second produces the low nibble. These combine to reconstruct the original byte (Figure 10).
Figure 10: Decoded data (right)
Following the custom decoding routine, the malware dynamically resolves RtlGetCompressionWorkSpaceSize and RtlDecompressBufferEx through the same mechanism described above (Figure 11).
Figure 11: Encoded data sent for decompression
RtlGetCompressionWorkSpaceSize determines the required workspace buffer size for the decompression operation. The decoded output from Stage 1 passes to RtlDecompressBufferEx with the compression format parameter set to COMPRESSION_FORMAT_XPRESS_HUFF, indicating the embedded data was compressed using the Xpress Huffman algorithm.
After decompression, the malware calculates an offset of 0x3CDB bytes at runtime and skips past the initial portion of the buffer. The remaining data is then run through the same custom byte-pair decoding routine used in Stage 1, with the stride length set to 3 instead of the initial value of 2 (Figures 12 and 13).
Figure 12: Next stage decoding
Figure 13: Decompressed buffer
The decoded output from Stage 2 passes through the final decryption layer using the same BCryptDecrypt routine with AES-CBC. Unlike the string decryption phase, where pbSecret is referenced from a single hardcoded location, the 32-byte key material passed to BCryptGenerateSymmetricKey in this stage is assembled incrementally in chunks at runtime (Figure 14).
Figure 14: Decryption of payload
AES key: 27 2C 2A E0 E2 2F 8F 29 DC 43 F7 68 75 35 4D 83 37 7D 12 7A 67 0A 75 DA EF EF B3 A5 95 87 29 FE
The decrypted output reveals a series of Win32 API names resolved dynamically, alongside an embedded shellcode payload mapped into memory and executed through callback-based execution mechanisms (Figure 15).
Figure 15: Decrypted buffer revealing Stage 4 shellcode
In the final layer, the malware resolves the next set of Win32 APIs required for staging and executing the embedded shellcode through LdrGetProcedureAddress, consistent with all prior layers.
NtAllocateVirtualMemory allocates a heap region within the malware's own process space. The shellcode copies into that region via NtWriteVirtualMemory. Memory permissions then transition from RW to RX via NtProtectVirtualMemory, preparing the region for execution (Figure 16).
Figure 16: Setting up shellcode for execution
The loader resolves an additional set of Win32 APIs to execute the shellcode through a less common mechanism. As shown in Figure 17, the loader invokes TpAllocWork with the callback pointer set to the start address of the mapped shellcode, registering it as a work item within the Windows Thread Pool. TpPostWork queues and triggers shellcode execution through the thread pool's callback mechanism. TpReleaseWork releases the work object.
Figure 17: Abusing TpAllocWork callbacks
The shellcode embedded within the final stage is a Donut-generated payload. The shellcode dynamically resolves all required API functions at runtime before unpacking the final payload. The packed payload decompresses in memory using RtlDecompressBuffer with the compression format set to COMPRESSION_FORMAT_LZNT1. The resulting unpacked PE file represents the final malicious payload delivered by the loader chain (Figure 18).
Figure 18: Final payload unpacked in-memory
The final payload, identified by SHA256 hash f6e5f7445b9ea717513a04d04acfa343025ca35302d025de33935e176a83f6ae, continues the pattern of dynamic API resolution at runtime, computing and comparing CRC32 hashes of target module and function names.
Once the necessary functions are resolved, the payload decrypts its embedded C2 configuration using RC4 and establishes communication with the command-and-control server via WinHttpCrackUrl (Figure 19).
Figure 19: WinHttp APIs resolved at runtime
Decrypted C2 endpoint and User-Agent string (Figure 20):
C2: hxxps[://]gainmsg[.]com/nfront[.]php
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Vivaldi/5.3.2679.68
Figure 20: LegionLoader C2 connection
Based on infrastructure analysis of the C2 and code-level examination of the final payload, this is attributed as LegionLoader, also tracked as CurlyGate.
Howler Cell also investigated related campaigns using a similar OnionDrop loader chain and confirmed CGrabber Infostealer (originally uncovered by Howler Cell) and Vidar Stealer as additional final payloads.
OnionDrop is not a threat that earned attention because of a named sponsor or a geopolitical motive. It earned it through operational discipline and technical depth. The evasion architecture across four stages, from Loader Lock avoidance and rotating AES key material to API hammering and Thread Pool callback abuse, reflects sustained engineering investment in staying undetected. That investment is running at scale, serving multiple infostealer campaigns simultaneously, with no constraints on who gets targeted.
Howler Cell has tracked this operator from CGrabber through Direct-sys Loader to OnionDrop. The family is evolving. The volume is not slowing down. Indicators of compromise and detection artifacts from this analysis are provided below to support defensive operations across the security community.
|
C2 |
hxxps[://]gainmsg[.]com/nfront[.]php |
|
ZIP (1) |
8559e535128805f1e31fa7a15b33d25ae498915c7b88ea5142cf38858d551a53 |
|
ZIP (2) |
f09be48aab38dc85b7ad46efb98897617af66014ded44a7cf1bddaab59d9dad2 |
|
DLL (1) |
18bb95789e8727be0d98d9a5fce027f0f514e74192c7736b3afa297d2ee4a8fb |
|
DLL (2) |
070a97bf5bcba13c41266a79357e2a5b8d6f4e353db7427bd8ccabceee5c96e3 |
|
Loader (1) |
892f1bd9663c7e14855a0238e0fbb5b2396000b3396ceda79947374a3da78912 |
|
Loader (2) |
c9b96846c9a49ddbed9e143b098972e1d7880654f763bb504d2f7b5d2ab1dafb |
|
CGrabber |
fb31df58549031f0ea24b250b214cbab9eafa39adaa715c675f328f7370904c7 |
|
LegionLoader |
f6e5f7445b9ea717513a04d04acfa343025ca35302d025de33935e176a83f6ae |
|
Vidar |
0a8914b4f794ebc8ea1ce08dd4b5da918cd9697443007622100b0ba0731d428c |
◦ T1566.001 Phishing: Spearphishing Attachment
◦ T1204.002 User Execution: Malicious File
◦ T1106 Native API
◦ T1620 Reflective Code Loading
◦ T1574.002 Hijack Execution Flow: DLL Side-Loading
◦ T1027.002 Obfuscated Files or Information: Software Packing
◦ T1027.007 Obfuscated Files or Information: Dynamic API Resolution
◦ T1027.010 Obfuscated Files or Information: Command Obfuscation
◦ T1140 Deobfuscate/Decode Files or Information
◦ T1497.001 Virtualization/Sandbox Evasion: System Checks
◦ T1497.003 Virtualization/Sandbox Evasion: Time Based Evasion
◦ T1036.005 Masquerading: Match Legitimate Name or Location
◦ T1218 System Binary Proxy Execution
◦ T1082 System Information Discovery
◦ T1071.001 Application Layer Protocol: Web Protocols
◦ T1573 Encrypted Channel
◦ T1132 Data Encoding