Skip to content

Summary/Title Text

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco.

Key Findings

  • Sustained delivery. 645+ unique OnionDrop DLL samples between February 28 and May 20, 2026. Delivery remains active at publication.
  • Nation-state-grade evasion. Evasion depth rivals purpose-built targeted tooling with no targeting constraints on who it hits.
  • Four-stage unpack chain. Custom byte-pair decoding, Xpress Huffman decompression, AES-256-CBC with rotating key context, ending in Donut shellcode.
  • DLL sideloading entry point. ZIP archive paired with a legitimate Adobe-signed executable initiates execution through sideloading.
  • Anti-analysis by design. Stack-string construction, LdrGetProcedureAddress resolution, API hammering, and display device enumeration defeat static and dynamic review.
  • Thread Pool execution. Final shellcode runs through TpPostWork callback abuse, avoiding standard thread-creation telemetry.
  • Payload-agnostic loader. Confirmed payloads include LegionLoader (CurlyGate), CGrabber Infostealer, and Vidar Stealer.
  • C2 infrastructure. LegionLoader samples communicate with gainmsg[.]com.
  • Active family tracking. Third documented component in a campaign, Howler Cell has tracked across CGrabber and Direct-sys Loader.

Overview

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.

Campaign Timeline

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 Attack Flow

OnionDrop kill chain from initial delivery through four unpack stages to final payload execution

OnionDrop attack chain flowchart from initial access through multi-stage unpacking to payload delivery.

Technical Analysis

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

Windows Explorer listing of OnionDrop sample files, showing data.bin inflated to over 107 MB.

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

Decompiled DllMain pseudocode showing the OnionDrop sample loading codecstore384d.dll on DLL_PROCESS_ATTACH via LoadLibraryA.

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.

String Decryption Routine

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

Decompiled stack-string code building "BCryptDecrypt" one character at a time to evade static detection.

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

Decompiled CNG crypto setup with an arrow highlighting the 32-byte AES key material at data_18001a530.

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

Decompiled AES-CBC decrypt routine extracting a 16-byte IV from the input before calling 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.

Thread Creation via NtCreateThreadEx: Avoiding Loader Lock Deadlock

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

Decompiled code highlighting an NtCreateThreadEx call that spawns the malware's main thread, mw_w_threadMain.

API hammering serves two purposes:

  • It pollutes sandbox traces with a large volume of irrelevant API activity, making it harder for analysts to isolate meaningful behavior.
  • It increases the time and effort required during manual analysis to filter noise and identify actual malicious operations.

Anti-Analysis Check

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

Decompiled nested strstr checks scanning display-device strings against decrypted GPU vendor names to detect sandboxes.

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

 

Stage 1: Peeling the First Layer — Custom Byte-Pair Decoding

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

Decompiled code allocating heap memory via NtAllocateVirtualMemory, then dynamically resolving NtReadFile to read an encrypted payload from data.bin.

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

Decompiled byte-pair decode loop using a lookup table to combine two input bytes into one output byte.

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)

Side-by-side hex dumps comparing the encoded payload, full of repeating bytes, against the high-entropy decoded output.

Stage 2: Peeling the Decompression Layer — RtlDecompressBuffer

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

Decompiled code resolving and calling RtlDecompressBufferEx with Xpress Huffman format to decompress the Stage 2 payload.

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

Decompiled code invoking the custom table decode a second time on an offset into the decompressed payload.

Figure 13: Decompressed buffer

Annotated hex dump showing high-entropy decompressed data, then a highlighted region of encoded content for the next decode pass.

Stage 3: Peeling the Decryption Layer — AES-CBC Decryption

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

Decompiler and cross-reference view showing the 32-byte AES key (pbSecret) being built piecemeal via XOR operations across runtime.

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

Side-by-side debugger memory dumps comparing the high-entropy AES-encrypted buffer against the decrypted shellcode, with readable Win32 API names.

Stage 4: Peeling the Final Layer — Shellcode Execution via TpAllocWork Callback Abuse

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

Decompiled shellcode-loading routine highlighting NtAllocateVirtualMemory, NtWriteVirtualMemory, and NtProtectVirtualMemory calls separated by junk code, with the final memory protection set to PAGE_EXECUTE_READ.

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

Decompiled Stage 4 execution routine highlighting the thread-pool API sequence TpAllocWork, TpPostWork, TpWaitForWork, and TpReleaseWork used to run the shellcode via a work-item callback instead of a created thread.

From OnionDrop to Donut: Shellcode Takes the Stage

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

Decompiled code populating an API table with WinHTTP functions WinHttpOpen, WinHttpConnect, WinHttpSendRequest, WinHttpReadData, and others each resolved via CRC32 hash lookup for the payload's C2 network communication.

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

image041

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

``` Debugger view stopped at the resolved WinHttpCrackUrl function, with a register showing the C2 URL the payload contacts: https://gainmsg[.]com/nfront.php. ```

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.

Conclusion

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. 

Indicators of Compromise

Network Indicators

C2

hxxps[://]gainmsg[.]com/nfront[.]php

SHA256 Hashes

Initial Malicious ZIP

ZIP (1)

8559e535128805f1e31fa7a15b33d25ae498915c7b88ea5142cf38858d551a53

ZIP (2)

f09be48aab38dc85b7ad46efb98897617af66014ded44a7cf1bddaab59d9dad2

Malicious DLL Modules

DLL (1)

18bb95789e8727be0d98d9a5fce027f0f514e74192c7736b3afa297d2ee4a8fb

DLL (2)

070a97bf5bcba13c41266a79357e2a5b8d6f4e353db7427bd8ccabceee5c96e3

OnionDrop Loader

Loader (1)

892f1bd9663c7e14855a0238e0fbb5b2396000b3396ceda79947374a3da78912

Loader (2)

c9b96846c9a49ddbed9e143b098972e1d7880654f763bb504d2f7b5d2ab1dafb

Final Payloads

CGrabber

fb31df58549031f0ea24b250b214cbab9eafa39adaa715c675f328f7370904c7

LegionLoader

f6e5f7445b9ea717513a04d04acfa343025ca35302d025de33935e176a83f6ae

Vidar

0a8914b4f794ebc8ea1ce08dd4b5da918cd9697443007622100b0ba0731d428c

MITRE ATT&CK Coverage

Initial Access

◦    T1566.001  Phishing: Spearphishing Attachment

Execution

   T1204.002  User Execution: Malicious File
◦    T1106  Native API
◦    T1620  Reflective Code Loading

Persistence

◦    T1574.002  Hijack Execution Flow: DLL Side-Loading

Defense Evasion

◦    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

Discovery

◦    T1082  System Information Discovery

Command and Control

◦    T1071.001  Application Layer Protocol: Web Protocols
◦    T1573  Encrypted Channel
◦    T1132  Data Encoding

Be Ready

Stay informed with Howler Cell

Receive the latest Howler Cell news and research directly to your inbox. 

Optional featured resource text

Howler Cell has been tracking and investigating the new variant of MedusaLocker. MedusaLocker is a well-known ransomware family active since late 2019

Ready to close your security gaps?

To stay ahead of today’s relentless threatscape, you’ve got to close the gap between security strategy and execution. Cyderes helps you act fast, stay focused, and move your business forward.