JitterDropper
A Rust/MSVC dropper fingerprinted by per-API sleep-jitter budgets
Overview
We have observed a new Rust/MSVC Windows dropper under active development since at least 2026-03-18 with nine builds observed across two variant lines. Currently the name is unknown so we will dubbing it JitterDropper.
Variant I embeds the payload in .rdata and runs a multi-pass decryption algorithm producing a Donut shellcode loader with an embeded PE. Variant II ships a smaller stager that downloads a 122 byte encrypted shellcode blob from pixeldrain[.]com and decrypts it with a single SSE-32 repeating XOR key. Every build is compiled against the same Rust 1.92.0 MSVC toolchain (rustc/ded5c06cf21d2b93bffd5d884aa6e96934ee4234).
Though there are multiple variants the use of a specific sleep-jitter to obfuscate API calls ties all samples to the same developer. The following specific jitter bounds are used for each API across all samples: 60 ms before InternetOpenA, 150 ms before StringFromGUID2, 100 ms before CoGetObject, 120 ms before CoUninitialize. All nine samples contain the same 32 bytes that rustc emits for Duration::from_millis(variable) a fixed Lemire mod 1000 reduction followed by imul edx, eax, 0xF4240 to convert milliseconds to nanoseconds before the sleep call. An example of UnpacMe IDA plugin UnpacMe-IDA-Byte-Search identifying related samples based on this jitter implementation is shown below.

Samples
Variant I — embedded multi-pass decrypt, sRDI/Donut second stage
-
812d2d8436985fc8531315970b2af6bcdd697f9033bc12841ce467e6fda94408— I-8iter+sbox -
cb7264735e38f2575a354762c122a90d8994ea406f4911a42d8fa45d397fc4ff— I-10iter+sbox, 2026-03-23 -
e8082e3c0d63f83ad95af88f50e9ae26512e89113339b66b24068648363d676e— I-9iter+mod24, 2026-03-27
Variant II — SSE-32 XOR, pixeldrain-fetched 122-byte hop-loader
-
9957bf9bc95be77c84f83546d37ec3bd81877872f2e18e54adc014c541da3c6a— II baseline, 2026-04-06 -
eede257690bdc8c0668c3bef1e394a6c1e50980ba237647fb36c26f46557a450— II + AES-256-GCM wrap, 2026-04-09 -
4e57a67bf5a1dfa198bafc192ed47016f1a043c0c8ae0349f32c02ed17d296d4— II +CreateRemoteThreadintoexplorer.exe+%APPDATA%persistence, 2026-04-09 -
9e714ba3e2f8fe053550bb0234fc7e3fa64ce41e601bff83f0f47822245312fc— II + RWX regression, 2026-04-11 2ef28834f30d3fd881f8c3004ab67c01e2ec283c03d57e173e43a7b30d52f97b5605f711e9c256b09dcf06c4f39851545dbfc05c1c5b3e5f3129b16506e7df19
Analysis
JitterDropper performs anti-analysis hecks, decrypts or downloads a second-stage shellcode, and transfers control via VirtualAlloc -> VirtualProtect -> call rbx. All builds except 2026-04-11 use W^X; the regressed build allocates RWX directly. The stager imports Rust's standard library and VCRUNTIME140.dll and is built as a GUI executable whose cover window never pumps messages.
Anti-Analysis
Every build carries the same authored anti-analysis features:
-
Inline anti-debug pair —
CheckRemoteDebuggerPresentimmediately followed byIsDebuggerPresent. A detection causes a silent exit. -
GUI cover window —
RegisterClassExA+CreateWindowExAwith randomised class/title strings (observed:ZiEXzryWwge/QlUTPX,TYiYiIqIRv). No message pump. -
EnumWindowsstall loop — 21–27 iterations ofEnumWindowswhose callback only callsGetClassNameAinto a stack buffer and discards the result. A wall-clock padder with no semantic payload. -
GetTickCount-> randomisedSleep->GetTickCount-> elapsed-check gates at each stager checkpoint. When the elapsed delta exceeds the author's threshold (e.g. 1153 ms ine8082e3c), the stager exits. This defeats sandboxes that time-compress or skipSleepcalls. On Windows the Rustthread::sleepcall lowers toCreateWaitableTimerExW+SetWaitableTimerrather thanSleep.
The jitter-budget-per-API fingerprint
The 32-byte Lemire-x1 000 000 byte window that scales milliseconds to nanoseconds for Duration::from_millis(var) is rustc-emitted and appears in unrelated Rust programs that call thread::sleep on a variable millisecond argument. It is not on its own a developer fingerprint. What is unique to JitterDropper is the author's per-API choice of Lemire divisor (the upper bound of each randomised sleep in milliseconds) which is unchanged across every build where that API appears.
| Gated API | Divisor (ms) | Coverage |
|---|---|---|
InternetOpenA |
60 | 6 of 6 network-bearing builds |
StringFromGUID2 |
150 | 4 of 4 COM-bearing builds |
CoGetObject |
100 | 4 of 4 COM-bearing builds |
CoUninitialize |
120 | 4 of 4 COM-bearing builds |
memcpy / memset (pre-payload copy) |
1000 | 6 of 6 sites |
GetCurrentProcess (pre anti-debug) |
1000 | 5 of 5 sites |
EnumWindows (stall-loop pad) |
1000 | 5 of 5 sites |
VirtualProtect (RW -> RX transition) |
49–99 | 7 of 7 sites (narrow cleanup band) |
QueryPerformanceCounter (elapsed-check gate) |
521–770 | 9 of 9 samples |
The GetTickCount before jitter pairing rate is 80 to 100% per sample across the corpus. Unrelated Rust samples that happen to share the rustc-emitted x1 000 000 byte window show 0% GetTickCount pairing and a single uniform /1000 budget. The per-API divisor table is what distinguishes JitterDropper from compiler-output coincidences.
Decryption
Variant I decrypts an embedded .rdata ciphertext in three passes: a byte-XOR chain, 8–10 iterations of an SSE permutation driven by an XMM constant table (with buffer rotation between iterations — in-place would alias and corrupt), and a final repeating-key XOR using either a 16-byte table (cb7264), a 24-byte key with i mod 24 indexing (e8082e), or a similar variant. The output is a ~675 KB raw shellcode.
Variant II decrypts a 122-byte blob downloaded from pixeldrain[.]com with a single SSE 32-byte repeating XOR key (xorps xmm2, xmm0 / xorps xmm3, xmm1 / movups [r13+rcx], xmm2 / movups [r13+rcx+0x10], xmm3 / add rcx, 0x20 / cmp rax, rcx / jne).
The AES-GCM fork (eede2576) wraps this primitive with a BCryptDecrypt call using the literal UTF-16 string ChainingModeGCM and a key supplied in a separate 122-byte pixeldrain[.]com download.
Second stage
All three retrievable Variant I shellcodes share a identical 222 byte sRDI/Donut reflective loader at the call-target of the call rel32 at offset 0, followed by an encrypted inner PE. Two distinct inner PE families have been observed in the decrypted shellcodes, an unknown .NET stealer, and Vidar.
Variant II second stages are 122-byte shellcodes hosted on pixeldrain[.]com/api/file/<id>; these links were dead at the time of sample collection so no shellcode was retrieved for analysis.
Evolution
Timestamps and feature deltas are consistent with a single developer or small team iterating over five weeks. The Variant I decrypt iteration count drops 10 to 9 to 8 across builds and the s-box substitution pass is added and removed; Variant II is introduced on 2026-04-06 as a simpler network-fetching fork; on 2026-04-09 two parallel forks appear. One adds AES-256-GCM wrapping with dynamically-resolved BCrypt, the other adds CreateRemoteThread injection into explorer.exe and %APPDATA% self-copy persistence. The 2026-04-11 build regresses to RWX, suggesting a branch off the tree before W^X was merged in.