IP/TCP заголовки: WinAPI

Аутлог за 31.12.2021, 00:17

В представленном материале будет поднят вопрос изменения пакетов: подмена IP адресов и портов в пакетах уровня IP/TCP.

В статье преподносится как теоретический, так и практический материал. Весь код (C++, WinAPI) и информация представлены <контактом>.

Весь приведенный материал приводится только для ознакомления и общего просвещения. Не используйте материал во вред любому объекту.

IP/TCP: теория

IP и TCP – два протокола, использующиеся для передачи данных. Я не буду заострять внимание на том, как они устроены в деталях – информации об этом более чем достаточно, а при желании можно ознакомиться со стандартами.

IP/TCP заголовки: WinAPI, image #1

Важно отметить, что TCP/UDP относятся к транспортному уровню, работающему на уровне сети (IP/ICMP), соответственно и подход к работе с таким уровнем отличается от работы с уровнем сети.

О TCP поговорим позже. На данный момент больший интерес для нас представляет именно уровень сети, посему считаю важным выделить три составляющих IP протокола:

  1. Адрес отправителя (Source Address), 32 бита.

2. Протокол (Protocol), 8 бит

3. Длина данных (Total Length), 16 бит

В частности, адресом отправителя зачастую является ваш собственный IP адрес.

IP/TCP заголовки: WinAPI, image #2

При работе с сетью очень маленькое количество программ используют общение через IP: это громоздко и неудобно, ведь всё придумано за нас: TCP протокол, гарантирующий (в большинстве случаев) передачу данных и IP протокол, передающий вышестоящие уровни модели OSI.

Теперь поговорим о каждом поле подробнее.

  1. Адрес отправителя (Source Address) – 32 битное значение. Зачастую адресом отправителя является ваш IP адрес, хотя и не всегда так происходит (IP адрес может быть любым).

2. Протокол (Protocol) – 8 битное значение. Его значение выставляется в соответствии с выбором протокола TCP/UDP, чьи значения – 6 и 17 соответственно (0x06, 0x11), однако WinAPI предоставляет макросы IPPROTO_TCP и IPPROTO_UDP. Со списком протоколов и их значениями можно ознакомиться здесь: https://ru.wikipedia.org/wiki/%D0%A1%D0%BF%D0%B8%D1%81%D0%BE%D0%BA_%D0%BF%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%D0%BE%D0%B2,_%D0%B8%D0%BD%D0%BA%D0%B0%D0%BF%D1%81%D1%83%D0%BB%D0%B8%D1%80%D1%83%D0%B5%D0%BC%D1%8B%D1%85_%D0%B2_IP

3. Длина данных (Total Length) – 16 бит. Длина данных – это вся длина, которая будет отправлена в пакете: длина IP заголовка + длина заголовка протокола транспортного уровня + длина сообщения.

4. Затрону и IHL (IP Header Length) и будем считать, что его значение всегда будет равно 5 (0x05).

О других полях тоже нужно знать, но статья на это не нацелена. Если будет время – опубликую материал, в которой расскажу о полях и о том, за что они отвечают.

Об IP заголовке мы поговорили достаточно. Теперь обратимся к TCP.

IP/TCP заголовки: WinAPI, image #3

Как и с IP заголовком, выделим лишь те поля, что нас интересуют:

  1. Порт источника (Source Port Number) – 2 байта (как и Source Address из IP заголовка – может быть любым).

2. Флаги (Control Flags) – 9 бит.

3. Размер окна (Window Size) – 2 байта.

Из приведенных выше полей рассмотрим флаги и размер окна.

  1. Флаги.
    Дело в том, что поля Data Offset, Reserved и Control Flags – два байта в пакете (чтобы не было вопросов о том, почему флаги занимают девять бит). Соответственно и выставлять флаги нужно только в диапазоне от 9 до 16 бита (8…15).

Наибольший интерес представляют флаги SYN и ACK (Synchronization и Acknowledgment) – флаги подключения/знания. Более подробно со смыслом этих флагов можно ознакомиться, почитав о 3-way-TCP-handshake, суть которого заключается в следующем: клиент высылает пакет с флагом SYN, сервер получает пакет и отсылает пакет с флагом ACK, клиент получает SYN+ACK и высылает ACK.

Для управления соединением существуют флаги FIN (прекратить соединение) и RST (оборвать соединения, сбросить буффер).

IP/TCP заголовки: WinAPI, image #4

2. Размер окна.
Размер окна – это такое количество байт, которое готов принять клиент-собеседник. Как правило этот размер получает такой клиент, который первый отсылает пакет другому клиенту. Размер окна говорит о том, сколько один клиент может послать другому без остановки, но при этом важно отметить, что длина сегмента – 1460 байт, а поэтому для отправки особо большого объема информации будет использовано несколько фреймов, т.е. такой пакет будет дробиться и отправляться частями.

IP/TCP: progtask

Получив достаточно информации о IP/TCP, можно приступить к написанию кода, однако перед этим…

Нельзя просто так обмениваться самодельными IP/TCP пакетами через обычные функции отправки/чтения сокета: обычно функции помещают сообщение в выбранный протокол (TCP) и автоматически конструируют TCP и IP заголовки, а самодельные IP/TCP пакеты, высылаемые через обычный сокет будут именно сообщениями, размещающимися после TCP заголовка. В случае надобности изменить пакет (в ручную) при вызове таких функций потребуется хукинг функций библиотек, что зачастую будет огромной проблемой.

Существует понятие “сырой сокет” (Raw Socket). Такой сокет позволяет обмениваться самодельными IP/TCP пакетами, а работать с ними можно через приложение. Помимо этого, нужно понимать, что не все операционные системы поддерживают работу с сырыми сокетами (https://ru.wikipedia.org/wiki/%D0%A1%D1%8B%D1%80%D0%BE%D0%B9_%D1%81%D0%BE%D0%BA%D0%B5%D1%82), а отправка данных идет последовательно: IP заголовок, TCP заголовок, сообщение.

Тесты будут проводиться на Windows 7.

Приступим к написанию кода.

Для начала опишем структуру IP заголовка:

...

using uchar = unsigned char; // 1 байт (0…255)

using ushort = unsigned short; // 2 байта (0…65535)

using uint = unsigned int; // 4 байта (0…(2^31)-1)

…

typedef struct IPHeader {
uchar version :4;

uchar ihl :4;

uchar tos;

ushort total_length;

ushort id;

ushort flags_and_fragment; // not used by this article

uchar TTL;

uchar protocol;

ushort checksum;

uint src_ip;

uint dest_ip;

} IP_HDR;

Эта структура – заголовок IP протокола. Здесь описано всё что нужно. Опишем структуру TCP заголовка:

typedef struct TCPHeader {
ushort src_port;

ushort dest_port;

uint seq_num;

uint ack_num;

uchar data_offset :4;

uchar reserved :3; // still not used, 0x00

// flags:

uchar ns :1; // experimental flag Nonce Sum Flag, RFC3540

uchar cwr :1; // congestion window reduced

uchar ece :1; // ECN Echo

uchar urg :1; // urgent pointer

uchar ack :1; // acknowledgment

uchar psh :1; // push

uchar rst :1; // reset

uchar syn :1; // synchronization

uchar fin :1; // final
} TCP_HDR;

Так описаны заголовки IP и TCP протоколов. Вспомним о сырых сокетах.

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

Посмотрим на реализацию сырого сокета. Его отличие состоит лишь в том, что мы имеем “вольность” выбрать тот уровень (из предоставляемых API), на котором мы хотим работать. Также потребуется последовательная отправка данных: IP, TCP, сообщение.

В данном примере будем рассматривать работу сырого сокета на уровне IP/TCP.

…

#include <ws2tcpip.h>

#include <string>

…

const char* source_ip = "192.168.1.9"; // ANY ip

const char* destination_ip = "127.0.0.1"; // valid listener ip

int src_port = 20; // ANY port

int dest_port = 8080; // valid listener port

SOCKET sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);

if(sock == SOCKET_ERROR) {/*error, WSAGetLastError()*/ return; }

BOOL optval = TRUE;

if(setsockopt(sock, IPPROTO_IP, IP_HDRINCL, &optval, sizeof(optval)) == SOCKET_ERROR) {/*socket isn’t raw, aborting…*/ return; }

…

Теперь требуется создать IP/TCP заголовки, которые будут отправлены по сырому сокету связи. В качестве итогового пакета, который будет высылаться, будет использован массив packet (char), чей размер либо перекрывает размер заголовков IP/TCP и размер сообщения, либо имеет размер заголовков + сообщения.

В коде ниже пакет имеет размер заголовков и размер сообщения.

…

const char* message = "hello, IP";

const int size = sizeof(IP_HDR) + sizeof(TCP_HDR);

const int message_size = strlen(message);

char* packet = new char[size + message_size];

IP_HDR* ip_header = (IP_HDR*)packet;

ip_header->version = 4;

ip_header->ihl = 5;

ip_header->tos = 0;

ip_header->total_length = htons(size + message_size);

ip_header->id = htons(2);

ip_header->flags_and_fragment = 0;

ip_header->TTL = 8;

ip_header->protocol = IPPROTO_TCP;

ip_header->checksum = 0;

ip_header->src_ip = inet_addr(source_ip); // custom IP

ip_header->dest_ip = inet_addr(destination_ip);

TCP_HDR* tcp_header = (TCP_HDR*)packet[sizeof(IP_HDR)];

tcp_header->src_port = htons(src_port);

tcp_header->dest_port = htons(dest_port);

tcp_header->seq_num = 0;

tcp_header->ack_num = 0;

tcp_header->syn = 0x01; // WE WANT TO HANDSHAKE WITH SERVER flag

tcp_header->checksum = 0;

std::strcpy(packet + size, message);

…

Теперь в коде имеются IP и TCP заголовки, хранящиеся в массиве packet. Содержимое сообщения, которое мы хотим отправить также было помещено в packet, начиная с последнего незанятого байта (т.е. с размера IP_HDR + TCP_HDR). Объяснять такую тонкость я не буду, так как это не относится к теме статьи.

Теперь поговорим о том, как отправить такой пакет через сокет.

Будем считать, что мы имеем объявленный addr, являющийся SOCKADDR_IN. Он проинициализирован так:

…

SOCKADDR_IN addr;

addr.sin_addr.S_un.S_addr = INADDR_ANY;

addr.sin_port = htons(dest_port);

addr.sin_family = AF_INET;

…

Выше мы сделали сырой сокет, но для работы с ним требуется специальная функция. Обратившись к WinAPI, воспользуемся функцией sendto.

…

int result = sendto(sock, packet, size + message_size, 0, (SOCKADDR*)&addr, sizeof(addr));

if(result == SOCKET_ERROR) {/*failed to send data*/ return; }

else {/*data was send*/ return; }

…

Подводя итог практической части следует отметить, что были описаны структуры, в которых можно поменять IP адрес и порт источника, отправляя пакет от чужого “имени”. Следует отметить, что принятие данных через сырой сокет тоже требует внимания, но цель статьи – осветить именно отправку измененного пакета.

Аутлог закончен.

637 views·16 shares