В этом посте мы поговорим о структуре сетевых пакетов, в частности о пакете TCP, и о том, как и где мы можем его настроить. В дополнение к этому мы рассмотрим, как пользовательские данные затем могут быть декодированы на принимающей стороне и почему это может быть потенциально полезно для реального приложения. Мы начнем с базовой теории и перейдем к примерам кода, и, надеюсь, к концу этого поста вы вдохновитесь узнать еще больше.

Сначала немного об основах TCP, но не о протоколе, а о наименьшей единице, с которой он работает, — о пакете.

Структура пакета

Проще говоря, пакет TCP состоит из заголовка и полезной нагрузки. Заголовок содержит важную информацию о пакете, а полезная нагрузка содержит фактически передаваемые данные. Давайте сначала подробно рассмотрим заголовок, а затем поговорим о том, куда потенциально можно вставить произвольные данные, которые могут быть распознаны и декодированы принимающей стороной.

TCP-заголовок

Заголовок TCP включает в себя несколько полей, каждое из которых предназначено для определенной цели:

  • Исходный и целевой порт: эти 16-битные числа определяют исходное и целевое приложения, которые взаимодействуют друг с другом.
  • Порядковый номер: этот 32-битный номер используется для определения положения данных в потоке. Начальный порядковый номер выбирается случайным образом, и каждое следующее число является приращением от предыдущего.
  • Номер подтверждения: этот 32-битный номер используется для подтверждения того, что данные были успешно получены получателем. Он сообщает отправителю, какой порядковый номер ожидает получатель следующим.
  • Смещение данных: это 32-битное число указывает размер заголовка TCP в 32-битных словах, другими словами — смещение от начала сегмента TCP до фактических данных.
  • Зарезервировано: эти 4 бита зарезервированы для использования в будущем.
  • Флаги. Они используются для управления поведением TCP-подключения. Среди прочего, флаг SYN используется для инициации соединения, а флаг FIN используется для завершения соединения.
  • Размер окна: этот параметр используется для управления потоком, указывающий объем данных, который получатель готов принять от отправителя перед отправкой обратно пакета подтверждения.
  • Контрольная сумма: используется для обеспечения целостности пакета. Он рассчитывается для всего пакета, включая заголовок и полезную нагрузку, и проверяется получателем, чтобы убедиться, что пакет не был поврежден во время передачи.
  • Срочный указатель: это поле в заголовке TCP, которое редко используется, но все еще присутствует для обратной совместимости. Он используется для указания местоположения срочных данных в TCP-пакете, когда установлен флаг URG.
  • Параметры: длина этого поля определяется полем Смещение данных. Один из возможных параметров, который можно здесь установить, — это MSS (определяет максимальный размер «данной» части пакета) или MTU (определяет максимальный размер всего пакета).
  • Заполнение. Заполнение заголовка TCP (последовательность нулей) используется для обеспечения того, чтобы заголовок TCP заканчивался и данные начинались на 32-битной границе.

За заголовком находится полезная нагрузка, содержащая фактически передаваемые данные. Размер полезной нагрузки может варьироваться от 0 до максимального значения, указанного в заголовке TCP, в частности, в поле Параметры.

Полезная нагрузка TCP

Хотя HTTP является распространенным протоколом, передаваемым через TCP, TCP служит протоколом транспортного уровня для многих других приложений, таких как FTP, Telnet и SMTP. Каждый из этих протоколов имеет собственный формат данных, инкапсулированный в полезной нагрузке TCP.

С точки зрения разработчика, сетевые пакеты можно рассматривать как последовательность байтов, и важнее всего их порядок, а также то, как их можно идентифицировать и собрать на принимающей стороне. Эта точка зрения обеспечивает гибкость, особенно при работе с низкоуровневыми сетевыми приложениями, где становится возможным включение пользовательских данных.

Однако при проверке TCP-потока наши возможности ограничены. Полезная нагрузка TCP обычно занята данными приложения, и изменение зарезервированных битов или битов параметров в заголовке может привести к перезаписи будущих функций или повреждению поля параметров. Тем не менее, пакеты TCP Handshake, включая SYN, SYN-ACK и ACK, представляют собой интересную возможность. Эти пакеты имеют нерелевантную полезную нагрузку, позволяющую вводить произвольные данные.

Чтобы дать вам пример, добавить произвольные данные в пакет TCP SYN так же просто, как сделать что-то вроде этого:

from scapy.all import *

# Define the IPv6 address and port to send the packet to
target = "::1"

# Define destination port on the receiving side
dport = 7777

# Define the TCP flags to use - SYN Packet flag 
flags = "S"

# Define some random payload
payload = "hello"

# Construct the TCP SYN packet
ip = IPv6(dst=target)
tcp = TCP(sport=5555, dport=dport, flags=flags)
raw = Raw(load=payload)
packet = ip/tcp/raw
print(packet.payload)

# Send the SYN packet and wait for a response
start_time = time.time()
syn_ack = send(packet)
end_time = time.time()

# If we received a response, print the response time
if syn_ack:
    print(f"TCP ping response time: {end_time - start_time:.3f} seconds")
else:
    print("TCP ping failed: no response (SYN-ACK) received")

После того, как мы создали пакет с настраиваемой полезной нагрузкой, мы можем обеспечить его распознавание принимающей стороной путем включения дополнительного кода в драйвер сетевой карты, такого как eBPF. Не волнуйтесь, вскоре мы подробно рассмотрим этот практический аспект. Однако, прежде чем мы углубимся в это, есть еще один интригующий аспект, который нам нужно рассмотреть.

Большой против младшего

Когда дело доходит до хранения многобайтовых типов данных, таких как целые числа и числа с плавающей запятой, в памяти компьютера, существуют два разных подхода:

  • с обратным порядком байтов и
  • обратный порядок байтов.

При порядке следования байтов старший байт (содержащий биты старшего порядка) хранится по наименьшему адресу памяти, в то время как младший байт (содержащий младшие биты)хранится по наибольшему адресу памяти. Этот порядок соответствует тому, как мы обычно записываем числа, размещая старшую цифру слева, а наименее значащую цифру — слева. право.

С другой стороны, порядок байтов с прямым порядком байтов использует другой подход. Здесь младший байт хранится по наименьшему адресу памяти, а старший байт хранится по наибольшему адресу памяти. Этот порядок противоположен тому, как мы записываем числа: младшая значащая цифра находится справа. и самая значащая цифра слева.

Чтобы проиллюстрировать различие между прямым порядком байтов и прямым порядком байтов, давайте рассмотрим пример 32-битного целочисленного значения 0x12345678.

В порядке байтов с прямым порядком байтов это значение будет храниться в памяти следующим образом:

Здесь старший байт (0x12) хранится по наименьшему адресу памяти (0x1000), а младший значащий байт (0x78) хранится по наибольшему адресу памяти (0x1003).

При прямом порядке байтов одно и то же значение будет храниться в памяти следующим образом:

Здесь младший значащий байт (0x78) хранится по наименьшему адресу памяти (0x1000), а старший значащий байт (0x12) хранится по наибольшему адресу памяти (0x1003).

Порядок байтов с прямым порядком байтов обычно используется в процессорах на базе x86, а также во многих встроенных системах, таких как микроконтроллеры. Это связано с тем, что порядок байтов с прямым порядком байтов обеспечивает более простые и быстрые операции с памятью, поскольку младший байт хранится по наименьшему адресу памяти, что упрощает доступ и управление отдельными байтами в многобайтовом типе данных.

Зачем я рассказываю вам всю эту чушь?

Что ж, порядок байтов с обратным порядком байтов обычно используется в сетевых протоколах, таких как Интернет-протокол (IP) и протокол управления передачей (TCP), где он известен как "сетевой порядок байтов". Это связано с тем, что порядок байтов с обратным порядком байтов гарантирует, что старший байт передается первым, что упрощает интерпретацию данных принимающими системами, которые могут использовать другой порядок байтов.

Когда хост отправляет данные, он преобразует многобайтовые типы данных из локального порядка байтов системы отправителя в сетевой порядок байтов (обратный порядок байтов) перед их передачей по сети. Когда получатель TCP получает данные, он преобразует многобайтовые типы данных из сетевого порядка байтов обратно в локальный порядок байтов системы получателя перед их обработкой. Поскольку вы, вероятно, имели дело только с приложениями пользовательского пространства, вы никогда не сталкивались с ситуацией, когда это могло бы пригодиться, но в случае, если вы возитесь с кодом в сетевом драйвере, знание этого может помочь вам решить некоторые головные боли. .

Преобразование между порядками байтов обычно выполняется с использованием стандартных библиотечных функций, таких как htonl() (читается как «длинный путь от хоста к сети») и ntohl() (читается как «длинный путь от сети к хосту»). em>»), который преобразует 32-битные целые числа между сетевым порядком байтов и локальным порядком байтов и наоборот, соответственно.

Получение заявки — БФП

Вот мы и подошли к самой на мой взгляд важной части — eBPF (или просто BPF). BPF – относительно новая технология, но она быстро привлекает все больше внимания, поскольку позволяет проверять и изменять сетевые пакеты перед их пересылкой в ​​системный сетевой стек. Это полезно не только в в случае смягчения DoS-атак, но также позволяет использовать множество высокопроизводительных приложений, которые ранее запускались в пользовательском пространстве и были ограничены скоростью сетевого стека. Теперь это можно запустить в самом сетевом драйвере, сократив накладные расходы и повысив производительность с большим запасом. Я настоятельно рекомендую вам взглянуть на это, по крайней мере, чтобы понять, что может предложить эта технология, но в целом это очень низкоуровневая сеть, поэтому, если вы разработчик приложений, вам, вероятно, никогда не придется иметь дело с этим.

Вот пример, который анализирует только входящие сетевые пакеты, но это должно дать вам хорошее представление о том, как вы можете включить свой собственный код:

SEC("xdp_sock") int xdp_sock_prog(struct xdp_md *ctx)
{
 __u32 action = XDP_PASS; // default action
 int eth_type;
 int ip_type;
 void *data = (void*)(long)ctx->data;
 void *data_end = (void*)(long)ctx->data_end;
 struct ethhdr *eth = data;
 struct iphdr *ip;
 struct ipv6hdr *ipv6;
 struct tcphdr *tcp;
 struct hdr_cursor nh;
 nh.pos = data;

 eth_type = parse_ethhdr(&nh, data_end, &eth);
 if (eth_type < 0) {
   action = XDP_ABORTED;
   goto out;
 }

 if (eth_type == bpf_htons(ETH_P_IP)) {
   ip_type = parse_iphdr(&nh, data_end, &ip);
 }
 else if (eth_type == bpf_htons(ETH_P_IPV6)) {
   ip_type = parse_ip6hdr(&nh, data_end, &ipv6);
 }
 else {
   // Default action, pass it up the GNU/Linux network stack to be handled
   goto out;
 }

// If non-TCP traffic, pass it up the GNU/Linux network stack to be handled
 if (ip_type != IPPROTO_TCP) {
   goto out;
 }

 if (parse_tcphdr(&nh, data_end, &tcp) < 0) {
   action = XDP_ABORTED;
   goto out;
 }

 // NOTE: Above the network packet received is parsed, but 
 // you could add some application code here that determines
 // what happens on the packet arrival

out:
  return action;
}

Обратите внимание, что это не полный код, а просто код или так называемый хук BPF, поэтому необходимо выполнить дополнительную работу.

Заключение

В этом сообщении блога мы погрузились в загадочный мир TCP-пакетов и исследовали их структуру и возможности настройки. Изучив заголовок TCP и полезную нагрузку, мы получили полное представление о том, как данные организуются и передаются по сетям. Мы обнаружили возможность вставки произвольных данных в пакеты TCP Handshake без ущерба для зарезервированных или дополнительных битов и предоставили пример кода, иллюстрирующий эту концепцию. Кроме того, мы подчеркнули важность таких технологий, как eBPF, которые позволяют проверять и изменять сетевые пакеты на уровне драйвера сетевой карты, что приводит к повышению производительности и функциональности. Понимание важности прямого и обратного порядка байтов в сетевых протоколах еще больше расширяет наши знания в этой области. Хотя eBPF может быть более уместным для низкоуровневых сетей, изучение его возможностей может расширить наше понимание оптимизации и безопасности сети. Погружаясь в хитросплетения TCP-пакетов, мы надеемся вдохновить на дальнейшие исследования и эксперименты в увлекательной сфере сетевого программирования.

Спасибо за прочтение! 😎 Если вам понравилась эта статья, нажмите кнопку хлопка ниже 👏

Это много значит для меня, и это помогает другим людям увидеть историю. Скажи привет на Linkedin | Твиттер

Хотите начать читать эксклюзивные истории на Medium? Используйте эту реферальную ссылку 🔗

Если вам понравился мой пост, вы можете купить мне хот-дог 🌭

Вы инженер-энтузиаст, которому не хватает возможности составить убедительный и вдохновляющий технический контент? Наймите меня на Upwork 🛠️

Ознакомьтесь с остальными моими материалами на Teodor J. Podobnik, @dorkamotorka и подпишитесь на меня, чтобы не пропустить новые, ура!