TTL Covert Channels — Encoding Data in DNS Time-to-Live Fields

A TTL covert channel hides binary data inside a field that nobody watches — the Time-to-Live value in DNS responses. This post explains how it works, why it bypasses most detection, and how to find it.

What is TTL in DNS?

Every DNS response includes a Time-to-Live field — an integer between 0 and 2,147,483,647 that tells resolvers how many seconds to cache the answer before querying again. A record with TTL 3600 can be cached for one hour. TTL 0 means do not cache.

In practice, most records have TTLs that are human-chosen round numbers: 60, 300, 900, 3600, 86400. They are set once by a zone administrator and change rarely. Security tools monitor query names, response IPs, NXDOMAIN rates — but almost nothing monitors TTL values closely.

This makes TTL a low-noise covert channel.

How TTL encoding works

An attacker who controls a DNS server can return any TTL they choose. Instead of serving 3600, they encode data in the TTL field itself — one value per DNS response. The implant on the victim machine reads the TTL from the DNS response and interprets it as data, not as a cache instruction.

A simple scheme uses individual bits:

# Encoding scheme (example)
# TTL 64 (0b01000000) = bit 0
# TTL 65 (0b01000001) = bit 1
# The victim queries a rotating set of subdomains
# The attacker responds with TTL encoding the next bit of the payload

# To send the byte 0b10110101 (181):
# attacker responds to 8 queries with TTLs:
# 65, 64, 65, 65, 64, 65, 64, 65

More efficient schemes encode multiple bits per TTL. With a TTL range of 0–255 (one byte), each response carries one full byte of payload — no encoding overhead. The implant queries the C2 domain, reads the TTL, appends the byte to a buffer, and repeats.

Why this bypasses standard defences

  • The queries look normal: short, human-readable subdomain labels, resolving to real IPs. No high-entropy strings.
  • The payload is in the response, not the request: most DNS exfiltration detection focuses on query names. The TTL field in the response is not inspected.
  • No NXDOMAIN: the attacker's server returns valid responses. No anomalous response codes.
  • Low query rate: at 1 byte per query, a 1 KB command takes 1,024 queries. At one query every 2 seconds that is 34 minutes — very low volume, easy to miss.

Detection

TTL covert channels are hard to detect from network metadata alone. The signals to look for:

  • Non-standard TTL values: legitimate records use round numbers. A response with TTL 173 or 241 is suspicious — those are not values a human would configure.
  • TTL inconsistency: the same record returning different TTL values across queries is the strongest signal. A real record has a fixed TTL (decremented by resolvers, but consistent at the authoritative server).
  • High TTL variance for a single domain: calculate the standard deviation of TTL values observed for one domain over time. Legitimate domains are near-zero. A covert channel has high variance.
  • Query frequency without caching: if a host keeps re-querying a domain despite receiving non-zero TTLs, it is deliberately ignoring the cache — a sign the application is reading TTLs, not caching DNS results.

Extracting the payload

In a pcap, filter for DNS responses from the suspicious server. Export the TTL fields in order and interpret them as a byte stream:

import dpkt, socket

with open('capture.pcap', 'rb') as f:
    pcap = dpkt.pcap.Reader(f)
    ttls = []
    for ts, buf in pcap:
        eth = dpkt.ethernet.Ethernet(buf)
        if not isinstance(eth.data, dpkt.ip.IP): continue
        ip = eth.data
        if not isinstance(ip.data, dpkt.udp.UDP): continue
        udp = ip.data
        try:
            dns = dpkt.dns.DNS(udp.data)
            if dns.qr != dpkt.dns.DNS_R: continue  # responses only
            for rr in dns.an:
                if rr.name.endswith('c2domain.com'):
                    ttls.append(rr.ttl)
        except: pass

payload = bytes(ttls)
print(payload.decode(errors='replace'))

Practice it

The Ghost Protocol challenge in FoilLab is rated hard. You are given a packet capture containing a live TTL covert channel session. The encoding scheme and domain are for you to identify. The flag is in the decoded payload.