How malware encrypts its configuration, C2 addresses, and string constants — multi-key XOR, encrypted config blocks in Cobalt Strike and Emotet, per-string encryption with zeroing, and FLOSS for runtime string recovery.
Sign in to mark this article as read and track your progress.
Static analysis tools (FLOSS, strings) scan the binary on disk and recover embedded strings. Malware protects against this by encrypting configuration data and decrypting only at runtime, in memory, immediately before use.
If simple XOR is writing your diary in pig latin, advanced data protection is writing it in a cipher that changes every page, burning each page after reading, and keeping the decoder ring in a different safe. The analyst must find the ring, crack the safe, and read fast before the pages turn to ash.
Instead of a single static byte, a multi-byte key is applied cyclically. The key length divides evenly into the buffer length:
lea esi, [encrypted_buf]
lea edi, [key_buf] ; e.g., key = DE AD BE EF (4 bytes)
xor ecx, ecx ; counter = 0
decrypt_loop:
mov al, [esi+ecx]
mov ebx, ecx
and ebx, 3 ; ebx = ecx mod 4 — cycles through key
xor al, [edi+ebx]
mov [esi+ecx], al
inc ecx
cmp ecx, edx
jl decrypt_loop
Python decryption:
key = bytes([0xDE, 0xAD, 0xBE, 0xEF])
decrypted = bytes([b ^ key[i % len(key)] for i, b in enumerate(encrypted)])
The key value changes with each byte, making frequency analysis ineffective:
mov al, initial_key ; e.g., 0x41
rolling_loop:
xor [esi+ecx], al
inc al ; key increments each byte
inc ecx
loop rolling_loop
Rolling XOR defeats the assumption of a fixed key — every byte is encrypted with a different value.
Identify by: tight XOR loop where the key register is incremented inside the loop body.
Professional malware families store their full configuration — C2 addresses, port numbers, campaign IDs, encryption keys — in an encrypted blob within the binary.
Cobalt Strike beacon configuration is stored in a PE resource and protected using XOR encoding with key 0x69 in older versions, followed by a custom transform in newer versions:
PE Resource (.rsrc section)
→ Resource type 0 or type 10
→ Contains: 0x69-XOR-encoded configuration blob
→ Config structure: C2 host, C2 port, sleep timer, jitter, user-agent, campaign ID
Extraction tools:
# CobaltStrikeParser — automated config extraction
python3 parse_beacon_config.py beacon.exe
# Output:
{
"BeaconType": "HTTPS",
"C2Server": "evil.com,/updates",
"Port": 443,
"SleepTime": 60000,
"Jitter": 10,
"UserAgent": "Mozilla/5.0 (compatible; MSIE 9.0)"
}
Emotet stores its C2 list and RSA public key in the .data section, AES-CBC encrypted with a hardcoded key:
# EmotetConfigExtractor
python3 emotet_extractor.py emotet_sample.exe
# Outputs: list of C2 IPs and ports
When no dedicated extractor exists:
1. Identify encrypted region:
→ High entropy in .data or .rsrc
→ Referenced by a function that loops over it with XOR/AES operations
2. Set breakpoint AFTER decryption function returns
3. Dump the decrypted buffer from memory:
x64dbg → right-click decrypted address → Dump to file
4. Inspect the dump for structured config data:
→ IP:port pairs
→ Domain names
→ Base64 strings (further encoded layers)
Individual strings are encrypted separately and decrypted one at a time immediately before use. After the API call returns, the decrypted string is zeroed:
; Decrypt string immediately before use
call decrypt_string ; decrypts to [ebp-100h]
push [ebp-100h] ; pass decrypted string to API
call RegOpenKeyExA
; After API returns — zero the decrypted string
lea edi, [ebp-100h]
xor eax, eax
mov ecx, string_length
rep stosd ; fill with zeros
The decrypted string exists in memory for microseconds — only during the API call. FLOSS cannot find it because it never persists.
Bypass: Set a breakpoint on the API call (RegOpenKeyExA). When it fires, the string is on the stack. Read it before the function returns.
FLOSS emulates short function sequences and captures decoded string values — recovering strings that the zeroing technique tries to hide.
floss --no-static suspicious.exe # skip plain strings, show decoded only
floss -o decoded_strings.txt suspicious.exe
FLOSS works by:
Limitation: FLOSS emulates in isolation — it cannot follow complex multi-function decryption chains. For those, breakpoint at the final decryption output.
Cobalt Strike payload strings are XOR-encoded with key 0x69:
# Decode Cobalt Strike 0x69-XOR encoded string
key = 0x69
encoded = bytes([0x21, 0x15, 0x1A, 0x00, 0x1B, 0x09, ...])
decoded = bytes([b ^ key for b in encoded])
print(decoded.decode('utf-8', errors='ignore'))
# Output: "http://evil.com/updates"
Searching for 0x69 XOR loops in the disassembly of a suspected Cobalt Strike beacon confirms the family and reveals the decryption key location.
1. Run FLOSS → recover statically accessible decoded strings
2. For encrypted config blocks:
a. Identify high-entropy region in .data or .rsrc
b. Find the decryption function (references the encrypted region)
c. Set breakpoint after decryption completes
d. Dump decrypted buffer from memory
3. For per-string encryption:
a. Set breakpoints on sensitive APIs (CreateFile, RegOpenKey, URLDownload)
b. When breakpoint fires, read string argument from stack/register
4. For known families: use dedicated extractors (CobaltStrikeParser, EmotetExtractor)
5. Record all extracted strings as IOCs