Этот протокол дает возможность синхронизировать информацию о системном времени компьютера с такой же информацией на выбранном сервере с точностью до десятков миллисекунд. Эта синхронизация крайне необходима для стабильной работы компьютерных сетей. Например, сегодня при помощи NTP наши компьютеры через сеть получают точные временные метки финансовых транзакций, а также обеспечивается синхронизация времени для регистрации и мониторинга сетевой активности.
Атаки на протокол NTP сегодня перестали быть чем-то необычным. Очень многие системы получают данные от общего NTP сервера. Манипуляция этими данными (предоставление ему ложных данных о текущем времени) может нарушить стабильную работу компьютера, а именно -- внести изменения в его реакцию на ответы от других компьютеров в сети.
До сих пор широко используется NTPv3 запросы. Это как правило анонимные запросы к общедоступным северам, где не используется никакие средства безопасности. Сначала рассмотрим как выглядит NTPv3 header и попытаемся написать клиент для этой версии протокола. Затем расширим нашу реализацию, добавив дополнительные поля в NTP header, и получим новый, совместимый с четвертой версией, NTP клиент.
typedef struct
{
unsigned leap : 2; // 2 bits
unsigned ver : 3; // 3 bits
unsigned mode : 3; // 3 bits
uint8_t stratum; // 8 bits
uint8_t poll; // 8 bits
uint8_t precision; // 8 bits
uint32_t root_delay; // 32 bits
uint32_t root_dis; // 32 bits
uint32_t ref_id; // 32 bits
uint32_t ref_Tm_s; // 32 bits
uint32_t ref_Tm_f; // 32 bits
uint32_t orig_Tm_s; // 32 bits
uint32_t orig_Tm_f; // 32 bits
uint32_t rcv_Tm_s; // 32 bits
uint32_t rcv_Tm_f; // 32 bits
uint32_t tsm_Tm_s; // 32 bits
uint32_t tsm_xTm_f; // 32 bits
} ntp_packet;
Почти все поля этой структуры можно оставить пустыми, т.е. заполненными нулями.
Инициализировать нужно только первые восемь бит. Это первые три поля:
leap (индикатор перехода, 2 бита), ver(номер версии, 3 бита) и mode(режим, 3 бита):
ntp_packet packet = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
// Set the first bits to 00,011,011: leap = 0, ver = 3, mode = 3
*((char *) &packet + 0) = 0x1b; // Represents 00011011 in base 2
IP и UPD заголовки создадим при помощи функции
socket().
Параметр SOCK_DGRAM означает, что мы создаем UDP сокет.
Информацю об удаленном хосте (сервере) можно получить при помощи gethostbyname(). Это устаревшая obsolete функция. Ее использование я оставлю в коде просто как демонстацию. Она до сих пор используется во многих приложениях, но более правильная реализация будет продемонстрированна в следующем NTPv4 примере.
Заполняем структуру serv_addr нулями при помощи функции bzero().
Два единственно необходимых поля в структуре serv_addr, которые мы должны указать явно, это порт и протокол, используемый на третьем OSI уровне (сетевой протокол). Стандартный NTP порт это 123, а AF_INET означает, что мы работаем с IPv4.
Последние две используемые функции это sendto() и recvfrom(); для отправки и получения обратных сообщений от сервера. Последняя перезаписывает значения полей структуры packet, т.е. там будет находиться ответ от сервера.
struct sockaddr_in serv_addr;
struct hostent *server;
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
oops("can't create socket", 2);
if ((server = gethostbyname(*++argv)) == NULL)
oops("can't find this host", 2);
bzero((void *)&serv_addr, sizeof(serv_addr));
bcopy((void *)server->h_addr, (void *)&serv_addr.sin_addr, server->h_length);
serv_addr.sin_port = htons(123);
serv_addr.sin_family = AF_INET;
if (sendto(sockfd, &packet, sizeof(packet), 0, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) == -1)
oops("sendto failed", 3);
socklen_t saddrlen = sizeof(serv_addr);
if (recvfrom(sockfd, (char*) &packet, sizeof(packet), 0, (struct sockaddr *) &serv_addr, &saddrlen) == -1)
oops("recvfrom failed", 3);
Пожалуй еще одна важная вещь, о которой надо сказать, связана со стандартом NTP протокола:
информация от сервера приходит не в виде строки, которую мы привыкли видеть например в
выводе date(1).
Вместо этого сервер просто шлет число. Это
количество секунд,
прошедьшее с
начала эпохи.
Для того, чтобы увидеть привычный нам вывод, мы должны сконвертировать его при помощи
такого кода:
packet.tsm_Tm_s = ntohl(packet.tsm_Tm_s);
time_t tTm = (time_t) (packet.tsm_Tm_s - NTP_TIMESTAMP_DELTA);
printf("Time: %s", ctime((const time_t*) &tTm));
Полный код клиента можно найти тут.
Собираем, запускаем:
% gcc -Wall ntp_client.c -o ntp_client % ./ntp_client time.google.com Time: Thu May 16 17:22:49 2024
Давайте посмотрим, что в действительности мы отправили и получили:
# tcpdump -vvv -nni any port 123
tcpdump: data link type LINUX_SLL2
tcpdump: listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
17:22:49.307966 enp0s31f6 Out IP (tos 0x0, ttl 64, id 11147, offset 0, flags [DF], proto UDP (17), length 76)
192.168.2.213.44130 > 216.239.35.4.123: [bad udp cksum 0xbfba -> 0x782f!] NTPv3, Client, length 48
Leap indicator: (0), Stratum 0 (unspecified), poll 0 (1s), precision 0
Root Delay: 0.000000, Root dispersion: 0.000000, Reference-ID: (unspec)
Reference Timestamp: 0.000000000
Originator Timestamp: 0.000000000
Receive Timestamp: 0.000000000
Transmit Timestamp: 0.000000000
Originator - Receive Timestamp: 0.000000000
Originator - Transmit Timestamp: 0.000000000
17:22:49.325978 enp0s31f6 In IP (tos 0x0, ttl 60, id 10415, offset 0, flags [none], proto UDP (17), length 76)
216.239.35.4.123 > 192.168.2.213.44130: [udp sum ok] NTPv3, Server, length 48
Leap indicator: (0), Stratum 1 (primary reference), poll 0 (1s), precision -20
Root Delay: 0.000000, Root dispersion: 0.000076, Reference-ID: GOOG
Reference Timestamp: 3924861769.317839118 (2024-05-16T15:22:49Z)
Originator Timestamp: 0.000000000
Receive Timestamp: 3924861769.317839118 (2024-05-16T15:22:49Z)
Transmit Timestamp: 3924861769.317839119 (2024-05-16T15:22:49Z)
Originator - Receive Timestamp: 3924861769.317839118 (2024-05-16T15:22:49Z)
Originator - Transmit Timestamp: 3924861769.317839119 (2024-05-16T15:22:49Z)