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, 65More 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.