Skip to content
λmaldev wiki/
pagesString Encryption
T1027WindowsLinuxmacOSC / C++PythonASM

String Encryption

updated 2026-08-047 min readthehackersbrain
Authorized use only. This material is published for detection engineering, malware analysis and authorized red-team engagements. Running these techniques against systems you do not own or have written permission to test is illegal.

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.

Note.

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

  1. 1
    encrypt strings at build time
    Run a pre-build script that encrypts all string literals and replaces them with encrypted byte arrays plus a decryption call.
  2. 2
    store encrypted bytes in .data / .rdata
    Encrypted arrays are indistinguishable from random data; no plaintext strings appear in the binary.
  3. 3
    decrypt on first use
    Inline decrypt function XORs or RC4-decrypts the array into a stack buffer at runtime.
  4. 4
    zero the decrypted buffer after use
    SecureZeroMemory wipes the plaintext from the stack after the string has served its purpose — limits memory scan exposure.

Reference implementation

Single-byte XOR (simplest)

xor_str.cC
#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)

rc4_str.cC
#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

encrypt_strings.pyPython
#!/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 + '').encode())

def emit_c_array(name, data):
  hex_bytes = ', '.join(f'0x{b:02x}' for b in data)
  return f"static const BYTE {name}[] = {{{hex_bytes}}};"

STRINGS = {
  "KERNEL32":   "kernel32.dll",
  "VIRTUALALLOC": "VirtualAlloc",
  "NTDLL":      "ntdll.dll",
  "LSASS":      "lsass.exe",
  "C2_URL":     "https://update.example.com/check",
}

print(f"#define XOR_KEY 0x{XOR_KEY:02x}")
print()
for name, plaintext in STRINGS.items():
  enc = xor_encrypt(plaintext, XOR_KEY)
  print(emit_c_array(f"ENC_{name}", enc))
  print(f"// plaintext: {plaintext}")
  print()

Encryption scheme comparison

Scheme Complexity Detectable pattern Notes
Single-byte XOR Trivial Yes — known-plaintext breaks it Use as baseline only
Rolling XOR Low Partial Key changes per byte; harder to break with known plaintext
RC4 Moderate No (random-looking output) Good balance of simplicity and strength
AES-128 (CTR) Higher No Use when binary size is not a constraint
Per-string key from hash Any No (unique key each string) Best practice; prevents one decrypt from breaking all strings

Common mistakes

Mistake Why it matters
Global decrypted string buffers Memory scan finds all plaintext at once
Reusing the same XOR key for all strings Known-plaintext attack (e.g. null terminator) recovers the key
No SecureZeroMemory after use Decrypted strings persist until garbage-collected or overwritten
Storing the key in plaintext in .rdata The key is as valuable as the strings — encrypt or derive it
Only encrypting some strings Unencrypted strings become higher-value IoCs

Detection

YARA
Presence of a decryption loop pattern (XOR with counter) adjacent to usage of WinAPI strings.
SANDBOX
Memory snapshot between API calls — decrypted strings appear in stack or heap briefly before being zeroed.
BEHAVIOURAL
Process that loads no suspicious imports but calls sensitive APIs resolved via GetProcAddress or API hashing.
MEMORY SCAN
Live memory scan catches strings between decryption and zero — timing-sensitive but effective in sandboxes.
Was this page useful?edit this page ↗