Module 3: RC4 Encryption in Evasion
An undocumented Windows API that gives you free RC4 — no custom crypto needed.
Module Objective
Understand how the RC4 stream cipher works at a conceptual level, why it is the preferred cipher for sleep obfuscation, how Ekko uses the undocumented SystemFunction032 from advapi32.dll, and the USTRING structure that serves as the interface for both data and key buffers.
1. Why RC4 for Sleep Obfuscation?
Several encryption algorithms could theoretically be used to encrypt an implant's memory during sleep. Ekko specifically uses RC4 (Rivest Cipher 4), and the choice is deliberate for several reasons:
| Property | RC4 | AES-CBC | XOR (single byte) |
|---|---|---|---|
| Symmetric decrypt = encrypt | Yes (XOR-based stream) | No (separate decrypt routine needed) | Yes |
| Block padding required | No (stream cipher) | Yes (16-byte blocks) | No |
| Available via Windows API | Yes (SystemFunction032) | Yes (BCrypt/CNG, but complex) | No built-in API |
| API simplicity | 2 parameters (data + key) | Multiple calls (init, update, final) | N/A |
| Output size | Same as input | Rounded up to block boundary | Same as input |
| Cryptographic strength | Weak by modern standards | Strong | Trivial to break |
The Key Advantage: Symmetry
RC4 is a stream cipher that generates a pseudo-random keystream from the key and XORs it with the data. Because XOR is its own inverse (A ^ K ^ K = A), encrypting with RC4 and then encrypting again with the same key produces the original data. This means Ekko needs only one function for both encryption and decryption — it calls SystemFunction032 to encrypt before sleep and calls it again with the same key to decrypt after sleep. No separate decryption routine is needed.
Cryptographic strength is not the primary goal here. Sleep obfuscation only needs the encrypted data to be unrecognizable to signature scanners and memory analysis tools. RC4 with a random 16-byte key more than satisfies this requirement, even though RC4 is considered broken for protocols like TLS.
2. How RC4 Works
RC4 operates in two phases: Key Scheduling Algorithm (KSA) and Pseudo-Random Generation Algorithm (PRGA).
Phase 1: Key Scheduling Algorithm (KSA)
KSA initializes a 256-byte permutation array (S-box) using the key:
C// RC4 Key Scheduling Algorithm (conceptual)
unsigned char S[256];
int j = 0;
// Initialize S-box to identity permutation
for (int i = 0; i < 256; i++)
S[i] = i;
// Permute S-box using the key
for (int i = 0; i < 256; i++) {
j = (j + S[i] + key[i % key_length]) % 256;
// Swap S[i] and S[j]
unsigned char tmp = S[i];
S[i] = S[j];
S[j] = tmp;
}
Phase 2: Pseudo-Random Generation Algorithm (PRGA)
PRGA generates a keystream byte-by-byte and XORs each with a plaintext byte:
C// RC4 PRGA - generates keystream and XORs with data
int i = 0, j = 0;
for (int n = 0; n < data_length; n++) {
i = (i + 1) % 256;
j = (j + S[i]) % 256;
// Swap S[i] and S[j]
unsigned char tmp = S[i];
S[i] = S[j];
S[j] = tmp;
// Generate keystream byte and XOR with data
unsigned char K = S[(S[i] + S[j]) % 256];
data[n] ^= K; // XOR: encrypt or decrypt
}
The XOR Property
Since the PRGA generates the same keystream given the same key (the S-box is initialized identically each time), and the only operation on the data is XOR, running RC4 twice with the same key first XORs with the keystream (encrypt) then XORs with the identical keystream again (decrypt), recovering the original data. This is the property Ekko exploits.
3. SystemFunction032 — The Undocumented RC4
SystemFunction032 is an undocumented function exported by advapi32.dll (internally implemented in cryptsp.dll on modern Windows). It provides a complete RC4 encryption/decryption operation in a single function call.
C// SystemFunction032 - undocumented RC4 implementation
// Exported by advapi32.dll
//
// NTSTATUS SystemFunction032(
// PUSTRING Data, // Buffer to encrypt/decrypt in place
// PUSTRING Key // RC4 key
// );
//
// Returns STATUS_SUCCESS (0) on success
// Ekko resolves it at runtime:
PVOID SysFunc032 = GetProcAddress(
LoadLibraryA("Advapi32"),
"SystemFunction032"
);
The function takes two parameters, both pointers to USTRING structures. It performs RC4 encryption in-place — the data buffer is modified directly, with no separate output buffer needed.
4. The USTRING Structure
Both the data and key are passed to SystemFunction032 using a structure called USTRING (sometimes called UNICODE_STRING in some implementations, though it is used here as a generic buffer descriptor):
C// USTRING structure - Ekko's definition from Ekko.h
typedef struct {
DWORD Length; // Current length of data in buffer
DWORD MaximumLength; // Maximum capacity of buffer
PVOID Buffer; // Pointer to the actual data
} USTRING;
USTRING vs UNICODE_STRING
This structure is nearly identical to the Windows UNICODE_STRING type used throughout the NT kernel, except the Length/MaximumLength fields are DWORD (32-bit) instead of USHORT (16-bit). This is important because Ekko needs to describe buffers larger than 65,535 bytes — the entire process image can be many megabytes. The DWORD-sized length fields allow describing buffers up to 4GB.
Ekko sets up two USTRING structures — one for the image data to encrypt and one for the RC4 key:
C// From Ekko.c - Setting up the encryption parameters
// The RC4 key: 16 bytes of 0x55 (hardcoded in the PoC)
CHAR KeyBuf[16] = { 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 };
USTRING Key = { 0 };
USTRING Img = { 0 };
// Key setup
Key.Buffer = KeyBuf;
Key.Length = 16;
Key.MaximumLength = 16;
// Image data setup - the entire process image
ImageBase = GetModuleHandleA( NULL );
ImageSize = ((PIMAGE_NT_HEADERS)(ImageBase +
((PIMAGE_DOS_HEADER)ImageBase)->e_lfanew))->
OptionalHeader.SizeOfImage;
Img.Buffer = ImageBase;
Img.Length = ImageSize;
Img.MaximumLength = ImageSize;
Hardcoded Key in the PoC
Ekko's proof-of-concept uses a hardcoded key of 16 bytes of 0x55. In a production implant, this key should be randomly generated for each sleep cycle using a CSPRNG like RtlGenRandom / BCryptGenRandom. A hardcoded key means an analyst who knows the key can decrypt the sleeping beacon's memory. The PoC prioritizes clarity over operational security.
5. How Ekko Passes Arguments to SystemFunction032
Ekko does not call SystemFunction032 directly. Instead, it sets up a CONTEXT structure with the registers configured as if calling the function, then uses NtContinue to "jump" into it. On x64 Windows, the first four integer/pointer arguments are passed in RCX, RDX, R8, R9:
C// Setting up the CONTEXT to call SystemFunction032(&Img, &Key)
// (encrypt the image with the RC4 key)
RopMemEnc.Rsp -= 8; // Stack alignment (explained in Module 6)
RopMemEnc.Rip = SysFunc032; // RIP = SystemFunction032 address
RopMemEnc.Rcx = &Img; // Arg 1: pointer to data USTRING
RopMemEnc.Rdx = &Key; // Arg 2: pointer to key USTRING
// When NtContinue loads this context:
// RIP jumps to SystemFunction032
// RCX = &Img (first argument)
// RDX = &Key (second argument)
// Result: SystemFunction032 encrypts the image in-place with RC4
The decrypt step is identical — same function, same arguments. Because RC4 is symmetric, calling SystemFunction032 again with the same key on the already-encrypted data produces the original plaintext:
C// Decrypt context - identical to encrypt
RopMemDec.Rsp -= 8;
RopMemDec.Rip = SysFunc032; // Same function
RopMemDec.Rcx = &Img; // Same data buffer (now encrypted)
RopMemDec.Rdx = &Key; // Same key
// Result: encrypted data XOR keystream = original plaintext
6. The Image Region Being Encrypted
Ekko encrypts the entire process image — from the base address to base + SizeOfImage. This covers:
| Section | Contents | Why Encrypt It |
|---|---|---|
.text | Executable code | Contains the implant's machine code and signatures |
.rdata | Read-only data, strings, imports | Contains string constants, C2 URLs, pipe names |
.data | Initialized global variables | Contains configuration data, state variables |
.bss | Uninitialized globals | May contain runtime state after initialization |
| PE headers | DOS/NT/section headers | MZ/PE signatures are easily detectable |
C// Getting the image base and size
ImageBase = GetModuleHandleA( NULL ); // Base of current process
// Parse PE headers to get SizeOfImage
ImageSize = ((PIMAGE_NT_HEADERS)(
(PBYTE)ImageBase +
((PIMAGE_DOS_HEADER)ImageBase)->e_lfanew
))->OptionalHeader.SizeOfImage;
Complete Image Coverage
By encrypting the entire SizeOfImage range, Ekko ensures that no recognizable artifacts remain in the process image during sleep — not code, not strings, not PE headers, not configuration blocks. A memory scanner examining this region sees only pseudo-random bytes that match no known signatures.
7. RC4 Limitations & Operational Considerations
While RC4 is excellent for sleep masking, there are limitations to be aware of:
Known Weaknesses
- Key reuse — Using the same RC4 key across multiple sleep cycles leaks information. If an attacker captures memory snapshots from two different sleep cycles encrypted with the same key, XORing them together cancels the keystream and reveals the XOR of the two plaintexts. Production implants should generate a fresh random key for each cycle.
- Biased output — RC4's second output byte has a known bias toward zero. While irrelevant for sleep masking (scanners do not exploit RC4 biases), it illustrates why RC4 is not suitable for cryptographic protocols.
- No integrity protection — RC4 provides confidentiality only. If an attacker modifies the encrypted memory, the decrypted result will be silently corrupted. For sleep masking, this is acceptable since memory corruption during sleep is not a typical attack vector.
8. Standalone SystemFunction032 Example
Here is a complete standalone example demonstrating SystemFunction032 usage outside of Ekko's timer chain:
C#include <windows.h>
#include <stdio.h>
typedef struct {
DWORD Length;
DWORD MaximumLength;
PVOID Buffer;
} USTRING;
typedef NTSTATUS (WINAPI* fnSystemFunction032)(
PUSTRING Data,
PUSTRING Key
);
int main() {
// Resolve SystemFunction032
fnSystemFunction032 SystemFunction032 =
(fnSystemFunction032)GetProcAddress(
LoadLibraryA("advapi32.dll"),
"SystemFunction032");
// Sample data to encrypt
char data[] = "Hello from Ekko course!";
char key[] = "MySecretKey12345"; // 16-byte key
USTRING Data = { sizeof(data)-1, sizeof(data)-1, data };
USTRING Key = { sizeof(key)-1, sizeof(key)-1, key };
printf("Original: %s\n", data);
// Encrypt
SystemFunction032(&Data, &Key);
printf("Encrypted: ");
for (int i = 0; i < Data.Length; i++)
printf("%02x ", (unsigned char)data[i]);
printf("\n");
// Decrypt (same call, same key - RC4 symmetry)
SystemFunction032(&Data, &Key);
printf("Decrypted: %s\n", data);
return 0;
}
Expected Behavior
The first call to SystemFunction032 encrypts the string in-place, producing seemingly random bytes. The second call with the identical key decrypts it back to the original string. This is exactly what happens inside Ekko's timer chain — Timer 2 encrypts and Timer 4 decrypts.
Knowledge Check
Q1: Why can Ekko use the same SystemFunction032 call for both encryption and decryption?
Q2: What does the USTRING structure contain?
Q3: What is the main weakness of Ekko's hardcoded 0x55 key in the proof-of-concept?