Программирование на C++ глазами хакера

         

Протокол ICMP


Я уже говорил о том, что протокол IP не обеспечивает надежность передачи данных, поэтому нельзя узнать о целостности принимаемых данных. Но с помощью протокола ICMP (Internet Control Message Protocol, интернет-протокол управляющих сообщений) можно узнать, достиг ли пакет адресата. Пакеты ICMP отправляются в тех случаях, когда адресат недоступен, буфер шлюза переполнен или недостаточен для отправки сообщения, или адресат требует передать данные по более короткому маршруту.

Протокол IСМР был разработан для того, чтобы информировать о возникающих проблемах во время приема/передачи и повысить надежность передачи информации по IP -протоколу, который изначально ненадежен. Но и на ICMP надеяться нельзя, потому что данные могут не дойти до адресата (заблудиться в сети), а вы не получите никаких сообщений. Именно поэтому используют протоколы более высокого уровня (например, TCP), имеющие свои методы обеспечения надежности.

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

Из-за малой надежности протокола ICMP программисты его редко используют в тех целях, для которых он создавался (для контроля доставки данных). Но у него есть другое предназначение, которое получило широкое распространение. Если отправить компьютеру ICMP-пакет, и он дойдет до адресата, то тот должен ответить. Таким способом можно легко проверить связь с удаленным компьютером. Именно таким образом реализованы программы Ping.

Для теста связи нужно знать открытый порт на удаленном компьютере, чтобы попытаться соединиться с ним. Если связь прошла успешно, то компьютер доступен. Не зная порта, можно просканировать весь диапазон, но это займет слишком много времени. Протокол ICMP позволяет избежать этой процедуры.


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

В теории все прекрасно, но на практике есть одна сложность — ICMP-протокол использует пакеты, отличные от TCP или UDP, поддержка которых есть в WinSock. Как же тогда отправить пакет с управляющим сообщением? Нужно самостоятельно сформировать пакет необходимого формата.

В WinSock1 не было возможности доступа напрямую к данным пакета. Функция select в качестве второго параметра (тип спецификации) могла принимать только значения SOCK_STREAM (для TCP-протокола) или SOCK_DGRAM (для UDP-протокола), и я об этом говорил. В WinSock2 появилась поддержка RAW-сокетов, которые позволяют получить низкоуровневый доступ к пакетам. Чтобы создать такой сокет (сырой), при вызове функции socket в качестве второго параметра нужно указать SOCK_RAW.

Рассмотрю, как программно реализована программа типа Ping. Это поможет вам понять, как работать с RAW-сокетами и как проверять связь с помощью ICMP-протокола. Создайте новое MFC-приложение. Главное диалоговое окно будущей программы можно увидеть на 6.7.



6.7. Диалоговое окно будущей программы Pinger

В строку Host будет вводиться имя или IP-адрес компьютера, с которым надо проверить связь. По нажатии кнопки будут отправляться и приниматься ICMP-пакеты и выводиться результат в List Box, который растянут по нижней части окна.

По нажатии кнопки Ping выполняется код из листинга 6.7.



Листинг 6.7. Использование пакетов ICMP
void CPingerDlg::OnBnClickedButton1() { SOCKET rawSocket; LPHOSTENT lpHost; struct sockaddr_in sDest; struct sockaddr_in sSrc; DWORD dwElapsed; int iRet; CString str;



WSADATA wsd; if (WSAStartup(MAKEWORD(2,2), wsd) != 0) { AfxMessageBox("Can't load WinSock"); return; }

// Create socket (Создание сокета) rawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (rawSocket == SOCKET_ERROR) { AfxMessageBox("Socket error"); return; }

// Lookup host (Поиск хоста) char strHost[255]; edHost.GetWindowText(strHost, 255); lpHost = gethostbyname(strHost); if (lpHost == NULL) { AfxMessageBox("Host not found"); return; }

// Socket address (Адрес сокета) sDest.sin_addr.s_addr = *((u_long FAR *) (lpHost-h_addr)); sDest.sin_family = AF_INET; sDest.sin_port = 0;

str.Format("Pinging %s [%s]", strHost, inet_ntoa(sDest.sin_addr));

lMessages.AddString(str);

// Send ICMP echo request (Посылка эхо-запроса ICMP) static ECHOREQUEST echoReq;

echoReq.icmpHdr.Type = ICMP_ECHOREQ; echoReq.icmpHdr.Code = 0; echoReq.icmpHdr.ID = 0; echoReq.icmpHdr.Seq = 0; echoReq.dwTime = GetTickCount(); FillMemory(echoReq.cData, 64, 80); echoReq.icmpHdr.Checksum = CheckSum((u_short *)echoReq, sizeof(ECHOREQUEST));

// Send the echo request (Отправка эхо-запроса) sendto(rawSocket, (LPSTR)echoReq, sizeof(ECHOREQUEST), 0, (LPSOCKADDR)sDest, sizeof(SOCKADDR_IN));

struct timeval tVal; fd_set readfds; readfds.fd_count = 1; readfds.fd_array[0] = rawSocket; tVal.tv_sec = 1; tVal.tv_usec = 0;

iRet=select(1, readfds, NULL, NULL, tVal);

if (!iRet) { lMessages.AddString("Request Timed Out"); } else { // Receive reply (Получение ответа) ECHOREPLY echoReply; int nRet; int nAddrLen = sizeof(struct sockaddr_in);

// Receive the echo reply iRet = recvfrom(rawSocket, (LPSTR)echoReply, sizeof(ECHOREPLY), 0, (LPSOCKADDR)sSrc, nAddrLen);

if (iRet == SOCKET_ERROR) AfxMessageBox("Recvfrom Error");

// Calculate time (Расчет времени) dwElapsed = GetTickCount() - echoReply.echoRequest.dwTime; str.Format("Reply from: %s: bytes=%d time=%ldms TTL=%d", inet_ntoa(sSrc.sin_addr), 64, dwElapsed, echoReply.ipHdr.TTL); lMessages.AddString(str); }



iRet = closesocket(rawSocket); if (iRet == SOCKET_ERROR) AfxMessageBox("Closesocket error");

WSACleanup(); }

Прежде чем работать с сетью, необходимо загрузить библиотеку WinSock с помощью функции WSAStartup . Работа с RAW-сокетами не исключение, но загружать надо библиотеку WinSocket2, потому что в первой версии нет необходимых возможностей.

После этого можно создавать сокет с помощью функции socket со следующими параметрами:

семейство протокола — AF_INET (как всегда);

спецификация — SOCK_RAW для использования RAW-сокетов;

протокол — IPPROTO_ICMP.

Для отправки пакета с данными компьютеру необходимо знать его адрес. Если пользователь ввел символьное имя, то надо определить IP-адрес по имени с помощью функции gethostbyname.

После этого, как и в случае с другими протоколами, заполняется структура типа sockaddr_in, содержащая адрес компьютера, с которым нужно соединяться. ICMP-запрос не будет использовать портов, поэтому параметр Port установлен в 0.

Затем заполняется структура типа ECHOREQUEST. Эта структура является пакетом, который будет отправляться в сеть. Если при использовании протоколов TCP или UDP необходимо только указать данные, которые подлежат отправке, то в случае с ICMP нужно формировать полный пакет, который будет отправлен через IP-протокол. Структура echorequest имеет вид пакета и выглядит следующим образом:

typedef struct tagECHOREQUEST { ICMPHDR icmpHdr; DWORD dwTime; char cData[64]; }ECHOREQUEST, *PECHOREQUEST;

Параметр icmpHdr — это заголовок пакета, который необходимо самостоятельно заполнить, а параметр cData содержит отправляемые данные. В нашем случае будут отправляться пакеты по 64 байта, поэтому объявлен массив из 64 символов. В программе весь массив заполняется символом с кодом 80 с помощью функции FillChar. Для программы Ping не имеет значения, какие отправлять данные, потому что главное — проверить возможность связи с удаленным компьютером.

Параметр dwTime — это время, которое можно использовать на свое усмотрение. По нему чаще всего определяют время прохождения пакета.



Заголовок формируется в зависимости от принимаемого или отправляемого сообщения. Так как я для примера выбрал программу типа Ping, то буду рассматривать необходимые для нее данные. Более подробное описание протокола ICMP вы можете найти в документе RFC 792 по адресу http://info. internet.isi.edu/in-notes/rfc/files/rfc792.txt. Заголовок (параметр icmpHdr) — это структура следующего вида:

typedef struct tagICMPHDR { u_char Type; u_char Code; u_short Checksum; u_short ID; u_short Seq; char Data; }ICMPHDR, *PICMPHDR;

Рассмотрим эти параметры:

Tуре — тип пакета. В нашем случае это ICMP_ECHOREQ, который означает эхо-запрос ответа (сервер должен вернуть те же данные, которые принял). При ответе этот параметр должен быть нулевым;

Code — не используется в эхо-запросах и должен равняться нулю;

Checksum — контрольная сумма. RFC не накладывает жестких требований на алгоритм, и он может быть изменен. В данной программе я использовал упрощенный алгоритм, который вы можете увидеть в листинге 6.8;

ID — идентификатор. Для эхо-запроса должен быть обнулен, но может содержать и другие значения;

Seq — номер очереди, который должен быть обнулен, если код равен нулю.

Листинг 6.8. Функция подсчета контрольной суммы
u_short CheckSum(u_short *addr, int len) { register int nleft = len; register u_short answer; register int sum = 0;

while( nleft 1 ) { sum += *addr++; nleft -= 1; }

sum += (sum 16); answer = ~sum; return (answer); }

После формирования пакета он отправляется с помощью функции sendto, потому что в качестве транспорта используется IP-протокол, который не поддерживает соединение как TCP, и по своей работе схож с UDP-протоколом.

Для ожидания ответа используется функция select, с помощью которой ждем в течение 1-й секунды возможности чтения с сокета. Если за это время ответ не получен, считается, что удаленный компьютер недоступен. Иначе читается пакет данных. В принципе, ответ уже известен, что связь между компьютерами работает, и читать пакет не обязательно, но я сделаю это, чтобы вы увидели весь цикл приема/передачи сообщений через RAW-сокеты. В реальном приложении чтение пакета необходимо, чтобы удостовериться в том, что получен пакет от того компьютера, которому отправлены данные (возможно, что совершенно другой компьютер посылал данные). Чтение пакета необходимо и в том случае, когда пингуется асинхронно сразу несколько компьютеров.



Для чтения пакета используется функция recvfrom, как и при работе с UDP-протоколом. Если при отправке посылается пакет данных в виде структуры ECHOREQUEST, то при чтении принимается пакет типа ECHOREPLY, который выглядит следующим образом:

typedef struct tagECHOREPLY { IPHDR ipHdr; ECHOREQUEST echoRequest; char cFiller[256]; }ECHOREPLY, *PECHOREPLY;

Первый параметр — это заголовок пришедшего пакета. Второй параметр — это заголовок пакета с данными, который был послан.

Заголовок принятого пакета отличается от отправленного и выглядит следующим образом:

typedef struct tagIPHDR { u_char VIHL; u_char TOS; short TotLen; short ID; short FlagOff; u_char TTL; u_char Protocol; u_short Checksum; struct in_addr iaSrc; struct in_addr iaDst; }IPHDR, *PIPHDR;

Это ничто иное, как заголовок протокола IP.

Все необходимые структуры должны быть описаны в заголовочном файле. Для приведенного примера я описал все в файле PingerDlg.h.

Примечание
Исходный код примера, описанного в этом разделе, вы можете найти на компакт - диске в каталоге \Demo\Chapter6\Pinger.

Содержание раздела