< Back to blog
highπŸ”‘Stealer
investigatedMarch 5, 2026publishedMarch 5, 2026

Stealc Dropper Deep-Dive: A Custom ARX Cipher, Hardware-Bound Key Derivation, and Process Hollowing

#stealer#stealc#amadey#c2#botnet

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

FieldValue
SHA256ac30bd536ff63648634c9304c0c7b560eeac0d140788d7c99d575174a5caf602
Size848,384 bytes (828 KB)
TypePE32+ (x86-64) Windows GUI
CompilerMSVC (C++ with RTTI, SEH, CRT)
Compiled2026-03-05 00:31:30 UTC
TagsStealc, Amadey (MalwareBazaar)
SophisticationModerate-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:

SectionVirtAddrVirtSizeRawSizeEntropyNotes
.text0x10000x157CD0x158006.4588 KB -- actual code
.rdata0x170000xF4D40xF6005.17Read-only data
.data0x270000x1C600xC002.28Globals
.rsrc0x290000x12500x14004.41Decoy strings + manifest
.reloc0x2B0000xA80000xA80004.04688 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:

FunctionDecoder KeyType
cabinet.dll0x21DWide string (Decoder 1)
CreateDecompressor0x2A0Byte string (Decoder 2)
Decompress0x323Byte string (Decoder 2)
CloseDecompressor0x3A6Byte 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:

DecoderLocationTypeOperations/CharUsed For
10x140008AD2Wide strings12DLL names (cabinet.dll, ntdll.dll)
20x140008B20Byte strings11Cabinet API function names
30x140004E38Byte strings18ntdll 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

TechniqueImplementationMITRE
AMSI bypassXOR 0xD6 shellcode patches AmsiScanBufferT1562.001
String obfuscation3 custom decoder variants, no plaintext ops stringsT1027
Hardware-bound cryptoAES-NI + CRC32C key derivation breaks software emulationT1027
Hex-encoded payloadKeeps .reloc entropy at 4.04, evades entropy heuristicsT1027
Decoy metadataFake "Peridot Software" vendor, 16 benign log stringsT1036.005
Processor feature checkIsProcessorFeaturePresent -- verifies AES-NI/SSE4.2, may detect VMsT1497.001
Process hollowingRuns Stealc under a legitimate process identityT1055.012
Dynamic API resolutionGetProcAddress with obfuscated stack strings for sensitive APIsT1106

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: CreateProcessA with CREATE_SUSPENDED (flag 0x4) followed by NtWriteVirtualMemory and ResumeThread
  • AMSI tampering: VirtualProtect called on AmsiScanBuffer address range
  • Cabinet API abuse: cabinet.dll loaded 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

BreakpointRVAWhat You Get
After AESENC loop0x7F5532-bit seed in EAX
ARX cipher entry0x76D8Decrypted key (RDX) + nonce (R8)
CreateDecompressor call--Compressed payload buffer
CreateProcessA call--Decompressed Stealc PE in memory

Indicators of Compromise

File Indicators

TypeValue
SHA256ac30bd536ff63648634c9304c0c7b560eeac0d140788d7c99d575174a5caf602
File Size848,384 bytes
Compile Time2026-03-05 00:31:30 UTC
CompanyNamePeridot Software
ProductNameDesktop Utility
InternalNamedskutil
OriginalFilenamedskutil.exe

Cryptographic Constants

ConstantValueContext
AES round key447370032e8a1913d308a385886a3f24Stage 1 seed generation
Golden ratio0x9E3779B9AESENC seed vector + CRC32C expansion
ARX counter0x2545F4914F6CDD1DStream cipher counter mixing
ARX rotations31, 29, 23, 17Per-block round function
AMSI XOR key0xD6Shellcode decoding
Key header50adc3872f79de4f9790facad81e86e2...Encrypted key material (48 bytes)

Behavioral Indicators

  • Mutex creation (encoded name, varies per build)
  • cabinet.dll loaded dynamically for decompression
  • amsi.dll loaded and AmsiScanBuffer patched with 6 NUL bytes
  • Process hollowing via CREATE_SUSPENDED + NtWriteVirtualMemory
  • .reloc section >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.

Share: