Stealc Dropper Deep-Dive: A Custom ARX Cipher, Hardware-Bound Key Derivation, and Process Hollowing
Published: 2026-03-08 | Author: Breakglass Intelligence | Tags: stealc, amadey, infostealer, process-hollowing, cryptanalysis, AMSI-bypass
TL;DR
We performed a complete static analysis of a fresh Stealc stealer dropper delivered via the Amadey botnet, compiled on March 5, 2026. The dropper hides a 342 KB encrypted payload inside an oversized .reloc section (81% of the file), protects it with a previously undocumented custom ARX stream cipher, and derives its decryption key through a hardware-bound pipeline that chains AES-NI instructions with CRC32C -- making static-only decryption impossible without native x86-64 execution. Before injecting the stealer via process hollowing, it neutralizes AMSI by zeroing the AmsiScanBuffer prologue with embedded XOR-encoded shellcode.
The Dropper at a Glance
| Field | Value |
|---|---|
| SHA256 | ac30bd536ff63648634c9304c0c7b560eeac0d140788d7c99d575174a5caf602 |
| Size | 848,384 bytes (828 KB) |
| Type | PE32+ (x86-64) Windows GUI |
| Compiler | MSVC (C++ with RTTI, SEH, CRT) |
| Compiled | 2026-03-05 00:31:30 UTC |
| Tags | Stealc, Amadey (MalwareBazaar) |
| Sophistication | Moderate-High |
The binary masquerades as "Desktop Utility Service" by a fictitious vendor called "Peridot Software." The version resource, manifest, and 16 decoy log strings in .rsrc are crafted to make the file look benign in string dumps and quick triage. None of the decoy strings are referenced by actual code paths -- they exist purely to pollute static analysis output.
Execution Flow
Entry (0x140009910)
ββ WinMain (0x14000979C)
ββ CreateMutexW β single-instance guard
ββ Payload Function (0x140007D32)
ββ Locate .reloc section in own PE headers
ββ Hex-decode 685,624 ASCII chars β 342,812 bytes
ββ Split: 342,764 byte ciphertext + 48 byte key header
ββ STAGE 1: AES-NI seed generation β hardware-bound
ββ STAGE 2: CRC32C keystream β decrypt key + nonce
ββ STAGE 3: Custom ARX cipher β decrypt payload
ββ Cabinet API decompression β PE executable
ββ AMSI bypass shellcode β patch AmsiScanBuffer
ββ Process hollowing β inject Stealc into suspended process
The .reloc Anomaly: 686 KB of Hidden Payload
The first indicator that this binary is not what it claims is the section table:
| Section | VirtAddr | VirtSize | RawSize | Entropy | Notes |
|---|---|---|---|---|---|
| .text | 0x1000 | 0x157CD | 0x15800 | 6.45 | 88 KB -- actual code |
| .rdata | 0x17000 | 0xF4D4 | 0xF600 | 5.17 | Read-only data |
| .data | 0x27000 | 0x1C60 | 0xC00 | 2.28 | Globals |
| .rsrc | 0x29000 | 0x1250 | 0x1400 | 4.41 | Decoy strings + manifest |
| .reloc | 0x2B000 | 0xA8000 | 0xA8000 | 4.04 | 688 KB -- 81% of file |
Legitimate PE relocations occupy the first 0x680 bytes (1,664 bytes). The remaining ~686 KB, starting at offset 0x800 within the section, contains 685,624 hex-encoded ASCII characters. The hex encoding is a deliberate obfuscation choice: it doubles the payload size but keeps the section entropy at 4.04 (hex ASCII is restricted to bytes 0x30-0x66), evading entropy-based packed binary heuristics. Once decoded, the 342,812 binary bytes hit an entropy of 7.9994 -- effectively random, confirming strong encryption rather than simple encoding.
The last 48 bytes of the decoded payload serve as a key header:
50adc3872f79de4f9790facad81e86e24d2a0fed2876f8f347f5dd4381f90ce7 β encrypted key (32 bytes)
e03e3927fffcb5d29430866cebba303a β encrypted nonce (12B) + integrity (4B)
Cryptographic Pipeline: Three Stages to Decryption
This is where the dropper gets interesting. The key derivation is intentionally designed to be non-reproducible outside of native x86-64 hardware with AES-NI and SSE4.2 support. This is not theoretical anti-analysis -- it is a hard requirement that breaks software emulation, most sandboxes, and all purely static approaches.
Stage 1: AES-NI Seed Generation (RVA 0x7F22)
The dropper constructs a 128-bit XMM register from binary metadata and performs 8 rounds of the AESENC instruction against a hardcoded round key:
; Construct seed vector
pinsrd xmm0, ecx, 0 ; binary metadata value 1
pinsrd xmm0, eax, 1 ; binary metadata value 2
pinsrd xmm0, r14d, 2 ; binary metadata value 3
mov eax, 0x9E3779B9 ; golden ratio constant (TEA/XTEA)
pinsrd xmm0, eax, 3
; Load static round key from .rdata
movdqu xmm1, [rip + 0x1E9A0] ; 447370032e8a1913d308a385886a3f24
; 8 rounds of AES encryption
.loop:
aesenc xmm0, xmm1
dec ecx
jnz .loop
; Extract 32-bit seed
movd eax, xmm0
The AESENC instruction performs a full AES encryption round: SubBytes, ShiftRows, MixColumns, and XOR with the round key. Software emulation of this instruction produces different results than the Intel hardware implementation due to the specific transformation ordering. Without native execution, the seed value in EAX is unrecoverable.
Stage 2: CRC32C Keystream (RVA 0x7F5C)
Using the hardware CRC32C instruction (opcode F2 0F 38 F1, Castagnoli polynomial 0x1EDC6F41), the dropper expands the seed into a keystream that decrypts the 48-byte key header:
# Phase A: Expand seed (64 rounds)
for i in range(64):
eax = CRC32C(eax, eax ^ 0x9E3779B9)
# Phase B: Decrypt AES-256 key (bytes 0-31)
for i in range(0x20):
eax = CRC32C(eax, i)
header[i] ^= (eax & 0xFF)
# Phase C: Decrypt nonce (bytes 32-43)
for i in range(0x20, 0x2C):
eax = CRC32C(eax, i)
header[i] ^= (eax & 0xFF)
Three CRC32C instruction instances were located at .text+0x6F69, .text+0x6F78, and .text+0x6F99, corresponding to the seed expansion, key decryption, and nonce decryption loops respectively.
Stage 3: Custom ARX Stream Cipher (RVA 0x76D8)
The decrypted 32-byte key and 12-byte nonce feed into a custom Add-Rotate-XOR stream cipher that operates on 256-bit (32-byte) blocks with 12 rounds per block:
Initialization:
v0 = key[0:8] XOR nonce[0:8]
v1 = key[16:24] XOR nonce[8:16] β then bitwise NOT
v2 = key[8:16] XOR (counter << 32)
v3 = key[24:32] XOR nonce[0:8] β then bitwise NOT
Per-block round function (x12):
v0 += v1; v1 = ROL(v1, 31); v1 ^= v0;
v2 += v3; v3 = ROL(v3, 29); v3 ^= v2;
v0 += v3; v3 = ROL(v3, 23); v3 ^= v0;
v2 += v1; v1 = ROL(v1, 17); v1 ^= v2;
Output = [v0+iv0, v1+iv1, v2+iv2, v3+iv3] XOR plaintext
Counter constant: 0x2545F4914F6CDD1D
The rotation schedule (31, 29, 23, 17) and 4-word state structure resemble a Speck-like construction, but the specific combination of rotation constants, initialization procedure, and counter mixing does not match any documented cipher -- Speck, Simon, Chaskey, LEA, or otherwise. This appears to be a custom construction specific to this malware family.
Brute-force validation confirmed this is not a trivially breakable scheme: single-byte XOR (0x00-0xFF), multi-byte XOR (2/4/8/16-byte keys), and RC4 key scheduling all failed to produce recognizable output (MZ headers, URLs, DLL names) from the ciphertext.
Post-Decryption: Cabinet Decompression
After ARX decryption, the plaintext is decompressed through the Windows Cabinet API. The dropper resolves these functions dynamically via stack-string obfuscation:
| Function | Decoder Key | Type |
|---|---|---|
cabinet.dll | 0x21D | Wide string (Decoder 1) |
CreateDecompressor | 0x2A0 | Byte string (Decoder 2) |
Decompress | 0x323 | Byte string (Decoder 2) |
CloseDecompressor | 0x3A6 | Byte string (Decoder 2) |
Cabinet decompression (likely LZMS or LZX) is a documented Stealc technique. The decompressed output is a PE executable -- the Stealc stealer itself.
AMSI Bypass: Embedded Shellcode
Before injection, the dropper neutralizes Windows Antimalware Scan Interface to prevent endpoint security from scanning the decompressed payload in memory.
Located at .rdata+0x2A8, XOR-encoded with a single-byte key of 0xD6, approximately 300 bytes of position-independent x64 shellcode perform the following:
1. Receive LoadLibraryA + GetProcAddress addresses via stack
2. Resolve VirtualProtect via GetProcAddress
3. LoadLibraryA("amsi.dll")
4. GetProcAddress(amsi_handle, "AmsiScanBuffer")
5. VirtualProtect(AmsiScanBuffer, 6, PAGE_EXECUTE_READWRITE, &old)
6. Patch: mov dword ptr [rax], 0x00000000 β zero bytes 1-4
mov word ptr [rax+4], 0x0000 β zero bytes 5-6
7. VirtualProtect(AmsiScanBuffer, 6, old_protect, &old) β restore
8. Return
Overwriting the AmsiScanBuffer prologue with 6 NUL bytes causes the function to return S_OK (AMSI_RESULT_CLEAN) for all subsequent scan requests. This is a well-known bypass technique, but implementing it as embedded XOR-encoded shellcode rather than inline code adds a layer of indirection that complicates signature development.
MITRE ATT&CK: T1562.001 (Impair Defenses: Disable or Modify Tools)
Process Hollowing: Injecting Stealc
The dropper uses classic process hollowing (T1055.012) to execute the decompressed Stealc binary inside a legitimate Windows process:
CreateProcessA(target, CREATE_SUSPENDED) β IAT 0x140021BC8
β
GetThreadContext / Wow64GetThreadContext β IAT 0x140021D00, 0x140021E10
β
NtWriteVirtualMemory (ntdll, dynamic) β stack-string resolved
β
SetThreadContext / Wow64SetThreadContext β IAT 0x140021DD8, 0x140021E18
β
ResumeThread β IAT 0x140021D90
β
[Stealc executing in hollowed process]
The presence of both native x64 (GetThreadContext/SetThreadContext) and WoW64 variants (Wow64GetThreadContext/Wow64SetThreadContext) indicates the dropper handles injection into both 64-bit and 32-bit target processes. Given that Stealc is commonly compiled as x86, the WoW64 path is likely the primary injection route.
NtWriteVirtualMemory is resolved dynamically from ntdll.dll using the third string decoder variant (Decoder 3, 18 operations per character at 0x140004E38), avoiding a direct IAT entry for the most suspicious API call in the chain.
String Obfuscation: Triple Decoder Architecture
The dropper employs three distinct string decoders, each tailored to a specific use case:
| Decoder | Location | Type | Operations/Char | Used For |
|---|---|---|---|---|
| 1 | 0x140008AD2 | Wide strings | 12 | DLL names (cabinet.dll, ntdll.dll) |
| 2 | 0x140008B20 | Byte strings | 11 | Cabinet API function names |
| 3 | 0x140004E38 | Byte strings | 18 | ntdll function names (NtWriteVirtualMemory) |
All decoders operate on stack/register values with no table lookups. Encoded string data lives in .rdata. The absence of any lookup table means there is no single memory region to dump for all decoded strings -- each must be traced through its respective decoder.
Decoder 2 inner loop:
shift = (counter & 0x18)
shr_val = (key >> shift) & 0xFF
decoded = (data[i] - shr_val + running_sub) & 0xFF
running_sub = (running_sub - 1) & 0xFF
counter += 8
Anti-Analysis Summary
| Technique | Implementation | MITRE |
|---|---|---|
| AMSI bypass | XOR 0xD6 shellcode patches AmsiScanBuffer | T1562.001 |
| String obfuscation | 3 custom decoder variants, no plaintext ops strings | T1027 |
| Hardware-bound crypto | AES-NI + CRC32C key derivation breaks software emulation | T1027 |
| Hex-encoded payload | Keeps .reloc entropy at 4.04, evades entropy heuristics | T1027 |
| Decoy metadata | Fake "Peridot Software" vendor, 16 benign log strings | T1036.005 |
| Processor feature check | IsProcessorFeaturePresent -- verifies AES-NI/SSE4.2, may detect VMs | T1497.001 |
| Process hollowing | Runs Stealc under a legitimate process identity | T1055.012 |
| Dynamic API resolution | GetProcAddress with obfuscated stack strings for sensitive APIs | T1106 |
Detection Opportunities
YARA Signatures
rule Stealc_Amadey_ARX_Dropper {
meta:
description = "Stealc dropper with custom ARX cipher and AES-NI key derivation"
author = "Breakglass Intelligence"
date = "2026-03-08"
hash = "ac30bd536ff63648634c9304c0c7b560eeac0d140788d7c99d575174a5caf602"
strings:
// AES round key (16 bytes)
$aes_key = { 44 73 70 03 2e 8a 19 13 d3 08 a3 85 88 6a 3f 24 }
// AESENC + CRC32C instruction proximity
$aesenc_crc32 = { 66 0F 38 DC C1 [0-20] F2 0F 38 F1 }
// Golden ratio constant via PINSRD
$golden_ratio = { B8 B9 79 37 9E }
// Peridot Software version resource
$decoy_vendor = "Peridot Software" wide
// ARX rotation constants in sequence (ROL 31, 29, 23, 17)
$rol_31 = { C1 C? 1F } // ROL reg, 0x1F
$rol_29 = { C1 C? 1D } // ROL reg, 0x1D
$rol_23 = { C1 C? 17 } // ROL reg, 0x17
$rol_17 = { C1 C? 11 } // ROL reg, 0x11
condition:
uint16(0) == 0x5A4D and
filesize < 1MB and
(
$aes_key or
$aesenc_crc32 or
($golden_ratio and 2 of ($rol_*)) or
($decoy_vendor and any of ($rol_*))
)
}
Behavioral Detection
- Process hollowing:
CreateProcessAwithCREATE_SUSPENDED(flag0x4) followed byNtWriteVirtualMemoryandResumeThread - AMSI tampering:
VirtualProtectcalled onAmsiScanBufferaddress range - Cabinet API abuse:
cabinet.dllloaded by a process that is not a Windows installer or archive utility - Mutex creation: Encoded mutex name at runtime (name varies per build)
Debugger Breakpoints for Dynamic Analysis
| Breakpoint | RVA | What You Get |
|---|---|---|
| After AESENC loop | 0x7F55 | 32-bit seed in EAX |
| ARX cipher entry | 0x76D8 | Decrypted key (RDX) + nonce (R8) |
CreateDecompressor call | -- | Compressed payload buffer |
CreateProcessA call | -- | Decompressed Stealc PE in memory |
Indicators of Compromise
File Indicators
| Type | Value |
|---|---|
| SHA256 | ac30bd536ff63648634c9304c0c7b560eeac0d140788d7c99d575174a5caf602 |
| File Size | 848,384 bytes |
| Compile Time | 2026-03-05 00:31:30 UTC |
| CompanyName | Peridot Software |
| ProductName | Desktop Utility |
| InternalName | dskutil |
| OriginalFilename | dskutil.exe |
Cryptographic Constants
| Constant | Value | Context |
|---|---|---|
| AES round key | 447370032e8a1913d308a385886a3f24 | Stage 1 seed generation |
| Golden ratio | 0x9E3779B9 | AESENC seed vector + CRC32C expansion |
| ARX counter | 0x2545F4914F6CDD1D | Stream cipher counter mixing |
| ARX rotations | 31, 29, 23, 17 | Per-block round function |
| AMSI XOR key | 0xD6 | Shellcode decoding |
| Key header | 50adc3872f79de4f9790facad81e86e2... | Encrypted key material (48 bytes) |
Behavioral Indicators
- Mutex creation (encoded name, varies per build)
cabinet.dllloaded dynamically for decompressionamsi.dllloaded andAmsiScanBufferpatched with 6 NUL bytes- Process hollowing via
CREATE_SUSPENDED+NtWriteVirtualMemory .relocsection >80% of total file size (hex-encoded payload)
What We Could Not Extract
C2 URLs, browser/wallet targeting paths, and stealer configuration are encrypted inside the .reloc payload. The hardware-bound AES-NI key derivation prevents static extraction. These indicators require dynamic analysis in an instrumented sandbox with AES-NI and SSE4.2 support, or targeted debugging at the breakpoints listed above.
Analysis performed via static methods using pefile, Capstone, and Python 3 on Linux x86-64. Sample sourced from MalwareBazaar. Dynamic analysis pending.