Difficulty: Beginner

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:

PropertyRC4AES-CBCXOR (single byte)
Symmetric decrypt = encryptYes (XOR-based stream)No (separate decrypt routine needed)Yes
Block padding requiredNo (stream cipher)Yes (16-byte blocks)No
Available via Windows APIYes (SystemFunction032)Yes (BCrypt/CNG, but complex)No built-in API
API simplicity2 parameters (data + key)Multiple calls (init, update, final)N/A
Output sizeSame as inputRounded up to block boundarySame as input
Cryptographic strengthWeak by modern standardsStrongTrivial 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:

SectionContentsWhy Encrypt It
.textExecutable codeContains the implant's machine code and signatures
.rdataRead-only data, strings, importsContains string constants, C2 URLs, pipe names
.dataInitialized global variablesContains configuration data, state variables
.bssUninitialized globalsMay contain runtime state after initialization
PE headersDOS/NT/section headersMZ/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

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?

A) SystemFunction032 detects whether the data is already encrypted
B) The function takes a mode parameter that selects encrypt or decrypt
C) RC4 is a XOR-based stream cipher, so applying it twice with the same key restores the original data
D) Windows automatically tracks encryption state in the USTRING structure

Q2: What does the USTRING structure contain?

A) A null-terminated string pointer and character encoding flag
B) Length, MaximumLength (both DWORD), and a Buffer pointer
C) A hash value and the original data size
D) An encryption algorithm identifier and IV

Q3: What is the main weakness of Ekko's hardcoded 0x55 key in the proof-of-concept?

A) An analyst who knows the key can decrypt the sleeping beacon's memory
B) RC4 cannot use repeated byte patterns as keys
C) The key is too long for SystemFunction032 to handle
D) Windows Defender specifically detects the 0x55 key pattern