Сниффер на RAW-сокетах в Windows 2000/XP

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

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

    1. Выбор интерфейса для захвата пакетов.

    Для выбора сетевого адаптера прежде всего необходимо получить перечень всех сетевых устройств, установленных на компьютере. Для этого можно воспользоваться функцией GetAdaptersInfo , входящей в стандартную библиотеку Windows IPHlpAPI . dll . Ее прототип описан в одноименном заголовочном файле IPHlpAPI . h и выглядит следующим образом:

    
         DWORD GetAdaptersInfo ( 
    
                               PIP_ADAPTER_INFO pAdapterInfo, 
    
    		           PULONG pOutBufLen ); 

    где pAdapterInfo – указатель на структуру _IP_ADAPTER_INFO ( модуль IPHlpAPI.h); pOutBufLen – указатель на тип unsigned long.

    Структура _IP_ADAPTER_INFO определена в модуле IPHlpAPI.h следующим образом :

    typedef struct _IP_ADAPTER_INFO { 
    
                   struct _IP_ADAPTER_INFO *Next; 
    
                   DWORD ComboIndex; 
    
                   char AdapterName[MAX_ADAPTER_NAME_LENGTH + 4]; 
    
                   char Description[MAX_ADAPTER_DESCRIPTION_LENGTH + 4];  
    
                   UINT AddressLength;  
    
                   BYTE Address[MAX_ADAPTER_ADDRESS_LENGTH];  
    
                   DWORD Index;  
    
                   UINT Type;  
    
                   UINT DhcpEnabled;  
    
                   PIP_ADDR_STRING CurrentIpAddress;  
    
                   IP_ADDR_STRING IpAddressList;  
    
                   IP_ADDR_STRING GatewayList;  
    
                   IP_ADDR_STRING DhcpServer;  
    
                   BOOL HaveWins;  
    
                   IP_ADDR_STRING PrimaryWinsServer; 
    
                   IP_ADDR_STRING SecondaryWinsServer;  
    
                   time_t LeaseObtained;  
    
                   time_t LeaseExpires; 
    
                   } 
    
    			   IP_ADAPTER_INFO, *PIP_ADAPTER_INFO; 

    В ней нам потребуются следующие поля: Description – описание адаптера в виде, привычном для пользователя, и представляет собой указатель на тип char ; IpAddressList – список IP -адресов, закрепленных за интерфейсом, и соответствующих им сетевых масок. Представляет собой тип IP _ ADDR _ STRING ; Next – указатель на следующий элемент списка адаптеров.

    Требуется описание еще одной структуры - IP_ADDR_STRING . Оно также приведено в модуле IPHlpAPI . h выглядит следующим образом:

    typedef struct _IP_ADDR_STRING { 
    
                   struct _IP_ADDR_STRING *Next; 
    
                   IP_ADDRESS_STRING IpAddress; 
    
                   IP_MASK_STRING IpMask; 
    
                   DWORD Context ; 
    
                   } 
    
    			     IP_ADDR_STRING , * PIP_ADDR_STRING ; 

    В этой структуре нам также потребуются не все поля, а лишь два из них: IPAddress – содержит текущий IP -адрес интерфейса. Текущий потому, что интерфейсу может быть поставлено в соответствие несколько IP -адресов, которые могут меняться при динамическом назначении адреса. IpMask – содержит сетевую маску, соответствующую текущему адресу.

    Доступ к значениям сетевой маски и адреса осуществляется через последовательное обращение ко вложенной структуре _IP_ADDR_STRING и затем к ее полю String типа char [16].

    Замечание: при динамической загрузке библиотеки IPHlpAPI.dll необходимо подключать не модуль IPHlpAPI , а модули ipifcons.h и iptypes.h. Это обусловлено тем, что при объявлении в теле программы указателя на функцию GetAdaptersInfo возникает ошибка переопределения функции, объявленной в модуле IPHlpAPI . h .

    Рассмотрим пример работы с данной функцией:

    
    #include "ipifcons.h"
    
    #include "iptypes.h" 
    
    u_long LocalAddrs[10]; 
    
    u_long LocalMasks[10]; 
    
    HINSTANCE iphlpapi_dll; 
    
    DWORD (__stdcall * GetAdaptersInfo)(PIP_ADAPTER_INFO pAdapterInfo, PULONG pOutBufLen); 
    
    	   
    
    int main () 
    
      { 
    
      iphlpapi_dll = LoadLibrary ("iphlpapi.dll"); 
    
      GetAdaptersInfo = (DWORD (__stdcall *) (PIP_ADAPTER_INFO pAdapterInfo, PULONG pOutBufLen)) 
    
    		             GetProcAddress (iphlpapi_dll,"GetAdaptersInfo"); 
    
      PIP_ADAPTER_INFO pAdapterInfo, pAdapt; 
    
      DWORD AdapterInfoSize; 
    
      DWORD Err; 
    
      int cnt; 
    
      sockaddr_in saddr; 
    
      AdapterInfoSize = 0; 
    
      GetAdaptersInfo(NULL, &AdapterInfoSize); 
    
      pAdapterInfo = (PIP_ADAPTER_INFO) GlobalAlloc(GPTR, AdapterInfoSize); 
    
      if (pAdapterInfo == NULL) 
    
        { 
    
        printf("Error in memory allocation."); 
    
        return -1; 
    
        } 
    
      if ((GetAdaptersInfo(pAdapterInfo, &AdapterInfoSize))!=0) 
    
        { 
    
        printf("Error in function call GetAdaptersInfo()");
    
        return -1; 
    
        } 
    
      int = 0;
    
      pAdapt = pAdapterInfo; 
    
      while (pAdapt)
    
         { 
    
         cnt++; 
    
         printf("Found interfaces:\n\n"); 
    
         printf("%s", pAdapt->Description); 
    
         printf("IP-address: %s", pAdapt->IpAddressList.IpAddress.String);
    
         print("NetMask: %s", pAdapt->IpAddressList.IpMask.String);
    
         LocalAddrs[cnt-1] = inet_addr(pAdapt->IpAddressList.IpAddress.String); 
    
         LocalMasks[cnt-1] = inet_addr(pAdapt->IpAddressList.IpMask.String); 
    
         pAdapt = pAdapt->Next; 
    
         }
    
      }; 

    2. Создание сокета и привязка его к интерфейсу

    Для доступа к полям IP-заголовка необходимо создать RAW -сокет с типом протокола IPPROTO_IP и семейством протоколов PF_INET. Ниже рассмотрен пример создания такого RAW-сокета.

    
    #include "winsock2.h" 
    
    #include "windef.h"
    
    int main()
    
      { 
    
      SOCKET sock; 
    
      WSADATA WSAData; 
    
      if ( WSAStartup (MAKEWORD (2,2),&WSAData) != 0) 
    
        { 
    
        printf("Library Winsock DLL is not found"); 
    
        return -1;
    
        }
    
      sock = socket (PF_INET,SOCK_RAW,IPPROTO_IP); 
    
      } 

    Замечание: Windows позволяет получить доступ и к заголовку протокола канального уровня. В этом случае необходимо использовать тип сокетов SOCK _ PACKET , семейство протоколов PF _ PACKET и протокол сетевого уровня ETH _ P _ ALL , объявленных в модуле WinBase .

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

    
    int main () 
    
      { 
    
      //Выбираем интерфейс и создаем сокет
    
      SOCKADDR _ IN saddr ; // хранит IP -адрес выбранного интерфейса 
    
      ZeroMemory(&saddr,sizeof(saddr));// обнуляем переменную saddr 
    
      bind(sock,(SOCKADDR*)&saddr,sizeof(SOCKADDR));
    
      saddr.sin_family = AF_INET ;// задаем семейство адресов 
    
      saddr.sin _addr.S_un.S_addr = LocalAddrs [0];// адрес выбранного интерфейса 
    
      //вместо 0 может быть другой индекс – в зависимости от того, через какой 
    
      //адаптер хотим принимать трафик 
    
      bind(sock,(SOCKADDR*)&saddr,sizeof(SOCKADDR));// привязываем сокет к сетевой карте
    
      } 

    Замечание: если требуется анализировать трафик на всех сетевых устройствах, то вместо адреса устройства необходимо указать INADDR _ ANY .

    3. Перевод адаптера в неразборчивый режим работы

    Для перевода сетевой карты в неразборчивый режим работы используется функция ioctlsocket () из модуля Winsock 2. h . Функции передаются 3 параметра:

  • дескриптор сокета;
  • команда, применяемая к сокету;
  • указатель на параметр команды.
  • В нашем случае параметром будет константа SIO_RCVALL , а параметром – флаг перевода адаптера в неразборчивый режим (1 – включить режим, 0 – выключить режим). Поясним все сказанное примером.

    
    #define SIO_RCVALL 0x98000001 
    
    int main ()
    
      {
    
      //выбор интерфейса, создание и привязка сокета к выбранному адаптеру 
    
      int flag = 1; 
    
      ioctlsocket(sock,SIO_RCVALL,&flags);
    
      }
    
    

    Замечание: после завершения работы сниффера рекомендуется остановить работу адаптера в неразборчивом режиме, установив значение flags = 0 и вызвав функцию ioctlsocket ();

    4. Определение параметров фильтрации и захват пакетов.

    В рассматриваемом примере пакеты будут фильтроваться программным путем уже после их захвата. Собственно же захват осуществляется вызовом в бесконечном цикле функции recv (). Ее входные параметры:

  • дескриптор сокета;
  • указатель на буфер, в который будут помещаться принимаемые пакеты;
  • размер буфера;
  • флаги.
  • Параметр флаги используется для определения поведения функции независимо от выбранных опций сокета. Нам это не нужно, поэтому ставим 0.

    Рассмотрим пример приема TCP -пакетов с установленными флагами SYN и ACK.

    
    struct TCP { 
    
               WORD SrcPort;
    
               WORD DstPort; 
    
               DWORD SeqNum; 
    
               DWORD AckNum; 
    
               BYTE DataOff; 
    
               BYTE Flags; 
    
               WORD Window; 
    
               WORD Chksum; 
    
               WORD UrgPtr; 
    
               }; 
    
    struct IPHeader { 
    
                    UCHAR iph_verlen ; // версия и длина заголовка 
    
                    UCHAR iph_tos ; // тип сервиса 
    
                    USHORT iph_length ; // длина всего пакета
    
                    USHORT iph_id ; // Идентификация 
    
                    USHORT iph_offset ; // флаги и смещения 
    
                    UCHAR iph_ttl ; // время жизни пакета
    
                    UCHAR iph_protocol; // протокол 
    
                    USHORT iph_xsum ; // контрольная сумма 
    
                    ULONG sender ; // IP -адрес отправителя 
    
                    ULONG receiver ; // IP -адрес назначения 
    
                    }; 
    
    int main ()
    
     { 
    
     //привязка сокета к интерфейсу и перевод сетевой карты 
    
     //в неразборчивый режим работы 
    
     //будем хватать 10 пакетов 
    
     int cnt ; 
    
     char buf[1600]; 
    
     int l = 10; 
    
     IPHeader ip; 
    
     TCP tcp; 
    
     while (l>0)
    
       { 
    
       cnt = recv(sock,buf,1600,0);// принимаем пакет 
    
       if (cnt>=sizeof(ip)+sizeof(tcp))// если принята IP- датаграмма
    
        { 
    
        memcpy(&ip,buf,sizeof(ip)); 
    
        if (ip.iph_protocol!=IPPROTO_TCP) break;// проверяем на TCP 
    
        memcpy(&tcp,&buf[sizeof(ip)],sizeof(tcp)); 
    
        if (tcp.flags!=18) break;// проверяем пакет на SYN|ACK 
    
        saddr.sin_addr.S_un.S_addr = ip.sender;// извлекаем адрес 
    
        // источника 
    
        printf("%s ->", inet_ntoa(saddr.sin_addr)); 
    
        saddr.sin_addr.S_un.S_addr = ip.receiver;// извлекаем адрес 
    
        // получателя
    
        printf(" %s ", inet_ntoa(saddr.sin_addr));
    
        printf (" ports : % d -> ", tcp . SrcPort );//извлекаем порт отправителя 
    
        printf (" % d \ n ", tcp . DstPort );//и получателя 
    
        l --; 
    
        } 
    
     } 
    
     flags = 0; 
    
     ioctlsocket ( sock , SIO_RCVALL ,& flags );//отключаем неразборчивый режим 
    
     //работы адаптера 
    
     return 0; 
    
     } 
    
    

    Вот, в принципе, сниффер готов. Полную версию рассмотренного снифера с исходным кодом можно скачать здесь (zip-архив 13 Kb). Остается только добавить, что использовался компилятор Borland C ++ Builder, поэтому при использовании другой среды разработки, возможно, придется заменить некоторые типы, например, USHORT и подобные (но это не факт).

    О всех замеченных недостатках и ошибках, а также свои вопросы и пожелания пишите автору.