Этот протокол дает возможность синхронизировать информацию о системном времени компьютера с такой же информацией на выбранном сервере с точностью до десятков миллисекунд. Эта синхронизация крайне необходима для стабильной работы компьютерных сетей. Например, сегодня при помощи 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 2IP и 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)