Сообщений 33    Оценка 226        Оценить  
Система Orphus

Как скрыть использование NAT

Автор: Вадим Смирнов
Источник: RSDN Magazine #5-2005
Опубликовано: 04/20/2006
Исправлено: 22.04.2006
Версия текста: 1.0
Введение
Начинаем разбор
Строим защиту

Исходные тексты к статье

Введение

Сначала несколько слов о том, как зародилась идея написать данную статью, они же и послужат нам постановкой задачи. Как-то, просматривая форумы RSDN, я наткнулся на анонимный пост следующего содержания (да простит меня неизвестный автор за цитирование, оригинальная лексика сохранена): “Уже несколько раз у различных провайдеров напарывался на подобные фразы в договоре:"...Не допускается использование на компьютере абонента прокси-серверов (WinGate и т.д) или трансляции адресов...". Тарифы с большим объемом трафика. Собственно. вопросов два — насколько это законно, и существует ли способ определения того, что используется NAT. Дома несколько компов. и подобные ограничения напрягают...” Вопросы законности мы рассматривать не будем (хотя лично я и не могу найти разумного объяснения подобным требованиям провайдера) и ограничимся только технической стороной вопроса. Мы попробуем замаскировать факт использования NAT при подключении к Internet небольшой сети. Чтобы не быть голословными, рассмотрим домашнюю сеть из нескольких компьютеров, где выход в Internet организован через одну из систем с операционной системой Windows XP (использование Unix-систем в качестве NAT мы здесь не рассматриваем), которая непосредственно подключена к Internet. Для простоты будем считать, что в качестве NAT используется встроенный в Windows сервис Internet Connection Sharing (хотя все наши рассуждения будут верны практически для всех реализаций NAT), подключение к Internet осуществляется через довольно распространенный (если не самый распространенный) на данный момент Zyxel USB ADSL-модем. Итак, для начала давайте задумаемся, какую нежелательную информацию несет наш внешний трафик. Что же может позволить провайдеру заподозрить нас в использовании NAT?

Начинаем разбор

При самом первом рассмотрении можно указать две самые заметные сигнатуры, присутствующие в заголовке каждого IP-пакета (см. рисунок 1) и выдающие нас с головой даже без использования какого либо сложного анализа. Достаточно взглянуть на наш внешний трафик в любом сетевом сниффере, и факт использования NAT можно считать установленным.

Версия (4 бита)Длина (4 бита)Тип обслуживания (8 бит)
Длина пакета
Идентификатор
0DFMFСмещение фрагмента
Число переходов (TTL)Протокол
Контрольная сумма заголовка
IP-адрес отправителя (32 бита)
IP-адрес получателя (32 бита)
Параметры (до 320 бит)
Данные (до 655535 байт минус заголовок)
Рисунок 1. Заголовок IP-пакета.

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

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

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

Биты 0-34-910-1516-31
Порт источникаПорт назначения
Номер последовательности
Номер подтверждения
Смещение данныхЗарезервированоФлагиОкно
Контрольная суммаУказатель важности
Опции (необязательное)
Опции (продолжение)Заполнение (до 32)
Данные
Рисунок 2. TCP-заголовок.
Биты 0-1516-31
Порт источникаПорт назначения
ДлинаКонтрольная сумма
Данные
Рисунок 3. UDP-заголовок.

Перечисленные элементы не несут информации об использовании NAT так очевидно, как два рассмотренных выше основных признака. Однако же они несут некоторую дополнительную информацию, которая при более детальном анализе на большом объеме трафика позволит выявить закономерности, указывающие на присутствие NAT. Попробуем расположить перечисленные признаки по степени их значимости. Несомненно, самый значимый признак многих реализаций NAT – это использование определенного фиксированного диапазона портов для трансляции адресов. Накопив некоторый объем статистики внешнего трафика, можно с уверенностью выделить этот диапазон. Само его наличие прямо не указывает на использование NAT, однако найти разумное объяснение тому факту, что сетевые приложения начинают использовать порты источника, скажем, после цифры 10000, и практически игнорируют диапазон 1025-9999 – не так-то просто. В случае с Initial Sequence Number (ISN), опять же накопив определенную статистику по трафику и зная алгоритм генерации ISN c его привязкой ко времени, и при известных слабостях этого алгоритма, можно разделить TCP сессии на группы, принадлежащие к разным системам. Сделать это будет, разумеется, посложнее, чем вычислить диапазон портов в предыдущем случае, однако теоретически подобная возможность существует, и нельзя с уверенностью сказать, что не существует ее практической реализации. Возможность утечки информации с ICMP минимальна, этот протокол не так часто используется, однако если ставить себе задачу закрыть все побочные источники информации, то и ту дополнительную информацию, которую может нести в себе протокол ICMP, лучше скрыть.

С основными угрозами нашей приватности мы разобрались, давайте теперь искать решение.

Строим защиту

Чтобы получить возможность изменять заголовки пакетов на внешнем (Internet) сетевом интерфейсе, нам понадобится драйвер – фильтр пакетов уровня NDIS. Написание подобного драйвера требует навыков работы в kernel mode и выходит за рамки данной статьи, желающих разобраться самостоятельно я отсылаю к документации DDK. Мы же воспользуемся готовым решением, которое позволяет реализовать прозрачную фильтрацию и обработку пакетов в user mode. Это библиотека WinpkFilter от ntkernel.com. Этот продукт бесплатен для некоммерческого использования, скачать run-time библиотеку можно отсюда: WinpkFilter.

За основу нашей разработки возьмем пример PassThru из WinpkFilter SDK и изменим его для модификации описанных выше полей в заголовках протоколов IP, ICMP, TCP и UDP. Исходный код получившегося приложения SafeNat прилагается к данной статье. Здесь же мы рассмотрим его фрагменты и остановимся на некоторых ключевых моментах.

Основной цикл фильтрации пакетов довольно прост и незначительно отличается от оригинального PassThru. Мы читаем пакеты с указанного в параметрах интерфейса (это должен быть внешний интерфейс с работающим NAT), производим необходимые модификации и возвращаем пакеты в стек. Обработка всех признаков реализована независимо, так что при желании можно легко варьировать функциональность SafeNat.

while (TRUE)
{
  WaitForSingleObject ( hEvent, INFINITE ); // Ждем появления пакетов
  ResetEvent(hEvent);
    
  while(api.ReadPacket(&Request)) // Читаем пакеты с интерфейса пока они есть
  {
    
           pEthHeader = (ether_header*)PacketBuffer.m_IBuffer;
           pIpHeader = (iphdr*)(PacketBuffer.m_IBuffer + ETHER_HEADER_LENGTH);
    
    // Для всех исходящих IP-пакетов изменяем идентификатор и TTL
           if((ntohs(pEthHeader->h_proto) == ETH_P_IP) &&
              (PacketBuffer.m_dwDeviceFlags == PACKET_FLAG_ON_SEND))
    {
      ChangeIPID(pIpHeader);
      RecalculateIPChecksum(pIpHeader);
        
    }
    // Для TCP пакетов модифицируем порт и номер последовательности
    if(pIpHeader->ip_p == IPPROTO_TCP)
    {
      if (PacketBuffer.m_dwDeviceFlags == PACKET_FLAG_ON_SEND)
      {
        ChangePorts(&PacketBuffer);
        ChangeSN(&PacketBuffer);
      }
      else
      {
        ChangeSN(&PacketBuffer);
        ChangePorts(&PacketBuffer);
      }
    }
    // Для UDP пакетов модифицируем порт
    if(pIpHeader->ip_p == IPPROTO_UDP)
    {
      ChangeUDPPorts(&PacketBuffer);
    }
    // Для ICMP пакетов модифицируем ICMP ID
    if(pIpHeader->ip_p == IPPROTO_ICMP)
    {
      ChangeICMPID (&PacketBuffer);
    }

    if (PacketBuffer.m_dwDeviceFlags == PACKET_FLAG_ON_SEND)
    {
      // Отправляем пакет в сеть от имени TCP/IP
      api.SendPacketToAdapter(&Request);
    }
    else
    {
      // Отправляем пакет TCP/IP протоколу
      api.SendPacketToMstcp(&Request);
    }
  }
}

Рассмотрим подробнее функции, непосредственно связанные с обработкой заголовков пакетов.

ChangeIPID – данная функция работает с заголовком IP-пакета. Изменению подлежат поля идентификатора и TTL. С TTL все более или менее просто, мы выставляем для всех исходящих IP-пакетов TTL, равный 128. Таким образом, все пакеты имеют один и тот же TTL, что не позволяет делать какие-либо выводы о структуре нашей сети на основе значений TTL в заголовках IP-пакетов. Первую угрозу конфиденциальности можно считать устраненной. Несколько сложнее обстоит дело с идентификаторами IP-пакетов. Эти идентификаторы используются для сборки фрагментированных пакетов, так что мы должны правильно модифицировать данное поле, присвоив одно и то же значение всем фрагментам пакета. Для каждого фрагментированного IP-пакета создается структура следующего вида:

typedef struct ipid
{
  unsigned short old_id; /* оригинальный ID */
  unsigned short new_id; /* сгенерированный ID */
  unsigned short ip_summary; /* текущая обработанная длина пакета */
  unsigned short length; /* полная длинна фрагментированного пакета  */
  DWORD          dwTime; /* метка времени */
} ipid, *ipid_ptr;

Выделение и инициализация этой структуры происходит при обработке первого полученного фрагмента IP-пакета. Освобождение выделенной памяти происходит либо после обработки последнего фрагмента IP пакета, либо по таймауту (если не все фрагменты обработаны в течение определенного времени).

void ChangeIPID(iphdr* pIpHeader)
{
  ipid_ptr pIpId  = NULL;

  pIpHeader->ip_ttl  = DEFAULT_TTL; // Навязываем всем пакетам TTL = 128
  
  if((ntohs(pIpHeader->ip_off) == IP_DF) || 
     (pIpHeader->ip_off == 0)) // или это первый фрагмент пакета
  {
    // Если в пакете установлен флаг DF или это первый фрагмент пакета
    pIpHeader->ip_id = GetID(); // Генерируем новый идентификатор для IP пакета
    dprintf (("Diagnostics: New IP ID generated = 0x%X\n", 
      htons(pIpHeader->ip_id)));
  }
  else
  {
    // Обработка фрагментов
    std::vector<ipid_ptr>::iterator theIterator;
  
    for (theIterator = IdTable.begin(); theIterator != IdTable.end();)
    {
      if((*theIterator)->old_id == pIpHeader->ip_id)
      {
        (*theIterator)->dwTime = GetTickCount();
        pIpHeader->ip_id = (*theIterator)->new_id; // установка ID для 
                                               // фрагмента
        dprintf (("Diagnostics: New IP ID applied for the packet = 0x%X\n",
          htons(pIpHeader->ip_id)));
        
        // Подсчет длины пакета
        (*theIterator)->ip_summary += ntohs(pIpHeader->ip_len) – 
          4 *  pIpHeader->ip_hl;

        // Проверка на последний фрагмент
        if(!(ntohs(pIpHeader->ip_off) & IP_MF))
          (*theIterator)->length = 
            (USHORT)(8 *  ((ntohs(pIpHeader->ip_off)) & 0x3fff) +
            ntohs(pIpHeader->ip_len) - 4 *  pIpHeader->ip_hl);
              
        if((*theIterator)->length == (*theIterator)->ip_summary)
        {
          free(*theIterator);
          IdTable.erase(theIterator);
        }
         
         return;
      }
      else
      {
        // освободить ресурсы для всех фрагментов, для которых 
        // вышел таймаут
        if(GetTickCount() - (*theIterator)->dwTime >= ID_TIME_LIMIT)
        {
          free(*theIterator);
          
          if((theIterator = IdTable.erase(theIterator)) == IdTable.end())
            break;
        }
        else
          theIterator++;
      }
    }

    if(theIterator == IdTable.end())
    {
      // Создать новый элемент фрагментированного пакета
      pIpId = (ipid_ptr)malloc(sizeof(ipid));
      ZeroMemory(pIpId, sizeof(ipid));
      
      pIpId->old_id = pIpHeader->ip_id;
      pIpHeader->ip_id = GetID();
      dprintf (("Diagnostics: New IP ID generated for the packet = 0x%X\n", 
        htons(pIpHeader->ip_id)));
      pIpId->new_id = pIpHeader->ip_id;
      pIpId->ip_summary = ntohs(pIpHeader->ip_len) - 4 *  pIpHeader->ip_hl;
      pIpId->dwTime = GetTickCount();
      
      IdTable.push_back(pIpId);


    }
  }
}

Генерация нового идентификатора для пакета вынесена в отдельную функцию GetID. Изменяя данную функцию, можно задавать произвольный закон генерации идентификаторов IP-пакетов. В простейшем случае это функция простого инкремента глобальной переменной, последовательно выдающая значения от 1 до 65535. Генерация идентификатора по такому закону представляет нашу сеть как одну систему, что в принципе решает поставленную задачу. В приложении SafeNat применен более сложный закон генерации идентификаторов с использование линейной реккурентной последовательности максимального периода (при использовании такой последовательности каждое значение идентификатора повторяется только после использования всех остальных значений). В качестве GetID можно использовать и другие методы генерации идентификаторов, затрудняющие анализ сети.

ChangeICMPID – эта функция решает задачу с идентификаторами и номерами последовательности для ICMP-пакетов следующих типов сообщений: Echo/Echo Reply, Timestamp/Timestamp Reply, Information Request/Information Request Reply. Для каждого исходящего ICMP-сообщения, принадлежащего типам ICMP Echo, Timestamp и Information request, мы выделяем и инициализируем структуру следующего вида:

typedef struct icmpid
{
  u_long      sourceIP;  /* IP адрес источника */
  unsigned short    old_id;    /* Оригинальный ICMP ID */
  unsigned short    new_id;    /* Сгенерированный ICMP ID */
  unsigned short    old_seq;   /* Оригинальный ICMP SEQ */
  DWORD      dwTime;    /* Метка времени */
} icmpid, *icmpid_ptr;

Эта структура необходима для выполнения операции восстановления оригинальных значений идентификатора и номера последовательности при получении ICMP Echo Reply, Timestamp Reply и Information Request Reply. Если не выполнить этого обратного преобразования, эти ICMP-сообщения будут просто отброшены TCP/IP-стеком.

void ChangeICMPID(PINTERMEDIATE_BUFFER pPacket)
{
  icmpid_ptr IcmpId = NULL;
  iphdr_ptr pIpHeader = (iphdr_ptr)&pPacket->m_IBuffer[sizeof(ether_header)];
  icmphdr_ptr pIcmpHeader = (icmphdr_ptr)(((PUCHAR)pIpHeader) 
    + sizeof(DWORD)*pIpHeader->ip_hl);

  // Поле идентификатора имеет смысл только 
  // для определенных типов ICMP-сообщений
  if ((pIcmpHeader->type != 8)&&
    (pIcmpHeader->type != 0)&&
    (pIcmpHeader->type != 13)&&
    (pIcmpHeader->type != 14)&&
    (pIcmpHeader->type != 15)&&
    (pIcmpHeader->type != 16)
    )
    return;

  // Мы не модифицируем идентификатор для исходящих ICMP XXX REPLY сообщений
  if (((pIcmpHeader->type == 0)||(pIcmpHeader->type == 14)||
     (pIcmpHeader->type == 16))&&
    (pPacket->m_dwDeviceFlags == PACKET_FLAG_ON_SEND)
    )
    return;

  std::vector<icmpid_ptr>::iterator theIterator;

  // Найти существующую ICMP-запись для данного пакета
  if (pPacket->m_dwDeviceFlags == PACKET_FLAG_ON_RECEIVE)
    for (theIterator = IcmpIdTable.begin(); 
      theIterator != IcmpIdTable.end();)
    {
      if((pIpHeader->ip_dst.S_un.S_addr == 
           (*theIterator)->sourceIP)&&
        (pIcmpHeader->id == (*theIterator)->new_id)
        )
      
      {
        pIcmpHeader->id = (*theIterator)->old_id;
        pIcmpHeader->seq = (*theIterator)->old_seq;
        (*theIterator)->dwTime = GetTickCount();

        RecalculateICMPChecksum(pPacket);
        
        return;
      }
      else
      {
        // Освобождаем старые ICMP-записи
        if(GetTickCount() - (*theIterator)->dwTime >= 
          ICMP_TIME_LIMIT)
        {
          free(*theIterator);
            
          if((theIterator = IcmpIdTable.erase(theIterator)) ==
              IcmpIdTable.end())
            break;
        }
        else
          theIterator++;
      }
    
    }
  
  if(pPacket->m_dwDeviceFlags == PACKET_FLAG_ON_SEND)
  {
    // Выделяем и инициализируем новую ICMP-запись для исходящего 
    // ICMP пакета
    
    IcmpId = (icmpid_ptr)malloc(sizeof(icmpid));
    IcmpId->sourceIP = pIpHeader->ip_src.S_un.S_addr;
    IcmpId->old_id = pIcmpHeader->id;
    IcmpId->old_seq = pIcmpHeader->seq;
    IcmpId->new_id = GetID();
    IcmpId->dwTime = GetTickCount();

    pIcmpHeader->id = IcmpId->new_id;
    pIcmpHeader->seq = pIcmpHeader->id;

    dprintf (("Diagnostics: ICMP ID generated = %u. Old = %u.\n",
      ntohs(IcmpId->new_id), ntohs(IcmpId->old_id)));

    RecalculateICMPChecksum (pPacket);

    IcmpIdTable.push_back(IcmpId);
    
  }
}

Для генерации новых значений идентификатора и последовательности (в данном случае мы используем одно и то же значение для обоих полей) так же используется функция GetID. Аналогично функции ChangeIPID можно использовать и любой другой подобный генератор.

ChangeSN – эта функция ответственна за изменения поля “номер последовательности” TCP-заголовка. При создании нового TCP-соединения функцией GetSNJump генерируется смещение для ISN, таким образом, “номер последовательности”, видимый во внешней сети, представляет собой оригинальное значение, увеличенное на величину, сгенерированную функцией GetSNJump. При этом (можно привести математическое доказательство, но интуитивно это вполне понятно) неопределенность (случайность, энтропия) нового ISN не меньше неопределенности значений, генерируемых GetSNJump. Можно было бы попросту сгенерировать новый ISN, однако наш механизм генерации может оказаться хуже оригинального, использованного стеком. При использовании же подхода с генерацией смещения, неопределенность результирующего значения по крайней мере не хуже оригинального. Очевидно, чтобы TCP-соединения нормально работали, необходимо обрабатывать пакеты в обоих направлениях. Для этого мы выделяем и инициализируем структуру следующего вида:

typedef struct tcp_state
{
  u_long      sourceIP;  /* IP-адрес источника*/
  u_long      destIP;    /* IP-адрес назначения */
  u_short      sourcePort;  /* порт источника */    
  u_short      destPort;  /* порт назначения */
  u_long      Jump;    /* сдвиг оригинального ISN */
  DWORD      dwTime;    /* метка времени */
} tcp_state, *ptcp_state;

Для исходящих TCP-пакетов мы увеличиваем номер последовательности (Sequence Number) на величину Jump, для входящих – уменьшаем номер подтверждения (Acknowledgement Number) на ту же величину. Память под структуру освобождается по истечении указанного таймаута. Дополнительно, структуру можно освобождать при обнаружении факта закрытия TCP-сессии, однако для простоты реализации в приведенном ниже коде этого не делается.

void ChangeSN (PINTERMEDIATE_BUFFER pPacketBuffer)
{
  iphdr_ptr   pIpHeader  = NULL;
  tcphdr_ptr  pTcpHeader = NULL;
  ptcp_state  pTcpState  = NULL;
  BOOL        bChange    = FALSE;
  
  pIpHeader = (iphdr_ptr)(pPacketBuffer->m_IBuffer + ETHER_HEADER_LENGTH);
  pTcpHeader = (tcphdr_ptr)(((PUCHAR)pIpHeader) + 
      sizeof(DWORD)*pIpHeader->ip_hl);
  
  // Проверка необходимости создания новой записи для TCP-соединения
  if( ((pTcpHeader->th_flags ==  (TH_SYN|TH_ACK))
     || (pTcpHeader->th_flags == TH_SYN))
    && (pPacketBuffer->m_dwDeviceFlags == PACKET_FLAG_ON_SEND))  
     bChange = TRUE;
    
  std::vector<ptcp_state>::iterator theIterator;
  
  // Найти существующую запись для TCP-пакета
  for (theIterator = TcpTable.begin(); theIterator != TcpTable.end(); )
  {
    if(((pPacketBuffer->m_dwDeviceFlags == PACKET_FLAG_ON_SEND)&&
    (pIpHeader->ip_src.S_un.S_addr == (*theIterator)->sourceIP)&&
    (pIpHeader->ip_dst.S_un.S_addr == (*theIterator)->destIP)&&
    (pTcpHeader->th_sport  == (*theIterator)->sourcePort)&&
    (pTcpHeader->th_dport  == (*theIterator)->destPort))
    ||
    ((pPacketBuffer->m_dwDeviceFlags == PACKET_FLAG_ON_RECEIVE)&&
    (pIpHeader->ip_src.S_un.S_addr == (*theIterator)->destIP)&&
    (pIpHeader->ip_dst.S_un.S_addr == (*theIterator)->sourceIP)&&
    (pTcpHeader->th_sport  == (*theIterator)->destPort)&&
    (pTcpHeader->th_dport  == (*theIterator)->sourcePort)))
    {
      
      if(bChange)
      {
        free(*theIterator);
        TcpTable.erase(theIterator);
        theIterator = TcpTable.end();
        break;
      }
      
      // Изменить SEQ или ACK в соответствии со сгенерированным
      // инкрементом
      if(pTcpHeader->th_flags & (TH_SYN|TH_ACK))
      {
        (*theIterator)->dwTime = GetTickCount();

        if(pPacketBuffer->m_dwDeviceFlags == 
          PACKET_FLAG_ON_RECEIVE)
        {
          pTcpHeader->th_ack = 
            htonl(htonl(pTcpHeader->th_ack) –
              (*theIterator)->Jump);
        }
        else
          pTcpHeader->th_seq  =
            htonl(htonl(pTcpHeader->th_seq) +
            (*theIterator)->Jump);

        RecalculateTCPChecksum (pPacketBuffer);
        
        return;
      }
      else
        return;

    }
    else
    {
      // Освобождаем записи старых TCP-соединений
      if(GetTickCount() - (*theIterator)->dwTime >= 
            TCP_TIME_LIMIT)
      {
        free(*theIterator);
        
        if((theIterator = TcpTable.erase(theIterator)) 
            == TcpTable.end())
          break;
      }
      else
        theIterator++;
    }
    
  }
  
  if((theIterator == TcpTable.end()) && (bChange))
  {
    // Создаем запись для нового TCP-соединения
    pTcpState = (ptcp_state)malloc(sizeof(tcp_state));
    pTcpState->Jump = GetSNJump();
    pTcpState->sourceIP = pIpHeader->ip_src.S_un.S_addr;
    pTcpState->destIP = pIpHeader->ip_dst.S_un.S_addr;
    pTcpState->sourcePort = pTcpHeader->th_sport;
    pTcpState->destPort = pTcpHeader->th_dport;
    pTcpState->dwTime = GetTickCount();

    pTcpHeader->th_seq  = htonl(htonl(pTcpHeader->th_seq) +
      pTcpState->Jump);

    dprintf (("Diagnostics: New ISN generated = %u\n",
      htonl(pTcpHeader->th_seq)));

    RecalculateTCPChecksum (pPacketBuffer);

    TcpTable.push_back(pTcpState);
  }
}

ChangePorts/ChangeUDPPorts – эти функции решают последнюю оставшуюся задачу, они маскируют использование для NAT TCP/UDP портов из определенного диапазона. Для каждого нового TCP- или UDP-соединения выделяется и инициализируется структура следующего вида:

typedef struct tcpports
{
  u_long      sourceIP;  /* IP адрес источника */
  unsigned short    old_port;  /* оригинальный порт */
  unsigned short    new_port;  /* новый порт  */
  DWORD      dwTime;    /* метка времени */
} tcpports, *tcpports_ptr;

для TCP и

typedef struct udpports
{
  u_long      sourceIP;  /* IP адрес источника */
  unsigned short    old_port;  /* оригинальный порт  */
  unsigned short    new_port;  /* новый порт  */
  DWORD      dwTime;    /* метка времени */
} udpports, *udpports_ptr;

для UDP. Эти структуры используются для трансляции номеров портов в обоих направлениях. На выходе мы подменяем оригинальный порт источника сгенерированным нами значением new_port, на входе восстанавливаем оригинальный порт назначения, используя сохраненное значение old_port.

void ChangePorts(PINTERMEDIATE_BUFFER pPacketBuffer)
{
  iphdr_ptr      pIpHeader  = NULL;
  tcphdr_ptr      pTcpHeader  = NULL;
  tcpports_ptr      pTcpPorts  = NULL;
  BOOL        bChange    = FALSE;
  
  pIpHeader = (iphdr_ptr)(pPacketBuffer->m_IBuffer + ETHER_HEADER_LENGTH);
  pTcpHeader = (tcphdr_ptr)(((PUCHAR)pIpHeader) + 
    sizeof(DWORD)*pIpHeader->ip_hl);
  
  // Проверка на создание нового TCP-соединения
  if ((pTcpHeader->th_flags ==  TH_SYN) 
    && (pPacketBuffer->m_dwDeviceFlags == PACKET_FLAG_ON_SEND)) 
    bChange   = TRUE;
    
  std::vector<tcpports_ptr>::iterator theIterator;
  
  // Найдем запись TCP-соединения для обрабатываемого пакета
  for (theIterator = PortsTable.begin(); theIterator != PortsTable.end();)
  {
    if(((pPacketBuffer->m_dwDeviceFlags == PACKET_FLAG_ON_SEND)&&
    (pIpHeader->ip_src.S_un.S_addr == (*theIterator)->sourceIP)&&
    (pTcpHeader->th_sport  == (*theIterator)->old_port))
    ||
    ((pPacketBuffer->m_dwDeviceFlags == PACKET_FLAG_ON_RECEIVE)&&
    (pIpHeader->ip_dst.S_un.S_addr == (*theIterator)->sourceIP)&&
    (pTcpHeader->th_dport  == (*theIterator)->new_port)))
    {
      
      if(bChange)
      {
        free(*theIterator);
        PortsTable.erase(theIterator);
        theIterator = PortsTable.end();
        break;
      }

      // Модификация порта в зависимости от направления пакета
      if(pPacketBuffer->m_dwDeviceFlags == PACKET_FLAG_ON_SEND)
        pTcpHeader->th_sport = (*theIterator)->new_port;
      else
        pTcpHeader->th_dport = (*theIterator)->old_port;
    
      (*theIterator)->dwTime = GetTickCount();

      // Пересчитать контрольную сумму
      RecalculateTCPChecksum (pPacketBuffer);
        
      return;
      

    }
    else
    {
      // Освобождение старых записей о TCP-сессиях
      if(GetTickCount() - (*theIterator)->dwTime >= TCP_TIME_LIMIT)
      {
        free(*theIterator);
          
        if((theIterator = PortsTable.erase(theIterator)) 
            == PortsTable.end())
          break;
      }
      else
        theIterator++;
    }
    
  }
  
  if((theIterator == PortsTable.end()) && (bChange))
  {
    // Содать новую запись для TCP-сессии

    pTcpPorts = (tcpports_ptr)malloc(sizeof(tcpports));
    pTcpPorts->sourceIP = pIpHeader->ip_src.S_un.S_addr;
    pTcpPorts->new_port = GetPort();
    pTcpPorts->old_port = pTcpHeader->th_sport;
    pTcpPorts->dwTime =  GetTickCount();

    pTcpHeader->th_sport = pTcpPorts->new_port;

    dprintf (("Diagnostics: New source port generated = %u\n",
        ntohs(pTcpHeader->th_sport)));

    RecalculateTCPChecksum (pPacketBuffer);

    PortsTable.push_back(pTcpPorts);
    
  }
          
}

void ChangeUDPPorts(PINTERMEDIATE_BUFFER pPacketBuffer)
{
  iphdr_ptr      pIpHeader  = NULL;
  udphdr_ptr      pUdpHeader  = NULL;
  udpports_ptr      pUDPPorts  = NULL;
  BOOL        bChange    = FALSE;
  
  pIpHeader = (iphdr_ptr)(pPacketBuffer->m_IBuffer + ETHER_HEADER_LENGTH);
  pUdpHeader = (udphdr_ptr)(((PUCHAR)pIpHeader) + 
    sizeof(DWORD)*pIpHeader->ip_hl);

  // Не меняем значения для портов меньше 1025, так как они могут 
  // быть использованы локальными сервисами
  if ((pPacketBuffer->m_dwDeviceFlags == PACKET_FLAG_ON_SEND)&&
    (ntohs(pUdpHeader->th_sport) < 1025))
    return;

  if ((pPacketBuffer->m_dwDeviceFlags == PACKET_FLAG_ON_RECEIVE)&&
    (ntohs(pUdpHeader->th_dport) < 1025))
    return;

  // Мы не модифицируем UDP-фрагменты, кроме первого
  if (ntohs(pIpHeader->ip_off) & (~IP_DF) & (~IP_MF))
    return;
  
  std::vector<udpports_ptr>::iterator theIterator;
  
  // Ищем UDP-запись, соответствующую обрабатываемому пакету
  for (theIterator = UDPPortsTable.begin(); 
      theIterator != UDPPortsTable.end();)
  {
    if(((pPacketBuffer->m_dwDeviceFlags == PACKET_FLAG_ON_SEND)&&
      (pIpHeader->ip_src.S_un.S_addr == (*theIterator)->sourceIP)&&
      (pUdpHeader->th_sport  == (*theIterator)->old_port))
      ||
      ((pPacketBuffer->m_dwDeviceFlags == PACKET_FLAG_ON_RECEIVE)&&
      (pIpHeader->ip_dst.S_un.S_addr == (*theIterator)->sourceIP)&&
      (pUdpHeader->th_dport  == (*theIterator)->new_port)))
    {
      dprintf (("Checking: (*theIterator)-> old_port = %u \n",
            ntohs((*theIterator)->old_port)));
      dprintf (("Checking: (*theIterator)-> new_port = %u \n",
            ntohs((*theIterator)->new_port)));
      dprintf (("Checking: (*theIterator)-> sourceIP = %u \n",
            (*theIterator)->sourceIP));
      dprintf (("Checking: (*theIterator)-> dwTime = %u \n",
            (*theIterator)->dwTime));
      
      if ((*theIterator)->new_port == (*theIterator)->old_port)
      {
        // Мы работаем на строне сервера, 
        // ничего не делаем с пакетом
        dprintf (("Diagnostics: Server port remains 
            unchanged %u \n",
             ntohs((*theIterator)->old_port)));
        (*theIterator)->dwTime = GetTickCount();
        return;
      }

      if(pPacketBuffer->m_dwDeviceFlags == PACKET_FLAG_ON_SEND)
      {
        dprintf (("Diagnostics: Source port for outgoing 
            packet changed %u -> %u \n",
            ntohs(pUdpHeader->th_sport),
            ntohs((*theIterator)->new_port)));
        pUdpHeader->th_sport = (*theIterator)->new_port;
      }
      else
      {
        dprintf (("Diagnostics: Destination port for 
            incoming packet changed %u -> %u \n",
            ntohs(pUdpHeader->th_dport),
            ntohs((*theIterator)->old_port)));
        pUdpHeader->th_dport = (*theIterator)->old_port;
      }
    
      (*theIterator)->dwTime = GetTickCount();

      RecalculateUDPChecksum (pPacketBuffer);
        
      return;
    }
    else
    {
      // Освобождение записей о старых UDP-сессиях
      if(GetTickCount() - (*theIterator)->dwTime >= UDP_TIME_LIMIT)
      {
        dprintf (("Deallocating: (*theIterator)-> old_port 
            = %u \n",
             ntohs((*theIterator)->old_port)));
        dprintf (("Deallocating: (*theIterator)-> new_port 
            = %u \n",
             ntohs((*theIterator)->new_port)));
        dprintf (("Deallocating: (*theIterator)-> sourceIP
             = %u \n",
             (*theIterator)->sourceIP));
        dprintf (("Deallocating: (*theIterator)-> dwTime
             = %u \n",
             (*theIterator)->dwTime));
        dprintf (("Deallocating: GetTickCount() = %u \n",
            GetTickCount()));
        
        free(*theIterator);
          
        if((theIterator = UDPPortsTable.erase(theIterator)) 
            == UDPPortsTable.end())
          break;
      }
      else
        theIterator++;
    }
    
  }
  
  if(theIterator == UDPPortsTable.end())
  {  
    // UDP-запись не найдена, создаем новую
    if(pPacketBuffer->m_dwDeviceFlags == PACKET_FLAG_ON_SEND)
    {
      pUDPPorts = (udpports_ptr)malloc(sizeof(udpports));
      pUDPPorts->sourceIP = pIpHeader->ip_src.S_un.S_addr;
      pUDPPorts->new_port = GetPort();
      pUDPPorts->old_port = pUdpHeader->th_sport;
      pUDPPorts->dwTime =  GetTickCount();

      pUdpHeader->th_sport = pUDPPorts->new_port;

      dprintf (("Diagnostics: New source port for outgoing UDP 
            connection generated = %u. Old = %u.\n",
            ntohs(pUdpHeader->th_sport),
            ntohs(pUDPPorts->old_port)));

      RecalculateUDPChecksum (pPacketBuffer);

      UDPPortsTable.push_back(pUDPPorts);
    }
    else
    {
      pUDPPorts = (udpports_ptr)malloc(sizeof(udpports));
      pUDPPorts->sourceIP = pIpHeader->ip_dst.S_un.S_addr;
      pUDPPorts->new_port = pUdpHeader->th_dport;
      pUDPPorts->old_port = pUdpHeader->th_dport;
      pUDPPorts->dwTime = GetTickCount();

      dprintf (("Diagnostics: Incoming UDP connection for the 
            local server port %u \n",
            ntohs(pUDPPorts->old_port)));

      UDPPortsTable.push_back(pUDPPorts);
    }
  }
}

Как видно из приведенного кода, ChangePorts и ChangeUDPPorts используют функцию GetPort для генерации нового значения номера порта. В SafeNat функция GetPort реализована на основе линейной рекуррентной последовательности максимального периода, из которой выброшены значения, не превосходящие 1024. Это сделано, чтобы избежать перекрытия с множеством распределенных значений портов, используемых для различных локальных сервисов. Как и в случае с идентификатором IP пакета, можно применять различные алгоритмы для реализации этой функции, в том числе и простую функцию инкремента.

Итак, мы получили небольшое приложение (чуть более 1000 строк кода), которое в значительной степени позволяет скрыть нежелательную информацию, которая в силу особенностей реализации различных сетевых протоколов покидает нашу локальную сеть. Нетрудно заметить, что области применимости SafeNat и других подобных приложений не ограничены задачей сокрытия факта использования NAT. Любая утечка информации из внутренней сети может оказаться нежелательной, а то и потенциально опасной. SafeNat и подобные ему программы могут использоваться при маскировке факта подмены одной компьютерной системы другой (например, некая гипотетическая IDS при обнаружении признаков сетевой атаки может перенаправить трафик с реальной системы на Honey Pot).

В заключение хотелось бы добавить, что некоторые протоколы уровня приложений также могут “светить” внутренние адреса во внешней сети (при этом правильно работать они, скорее всего, не будут). Корректная обработка таких протоколов, как правило, возлагается на NAT. Самый известный пример – это протокол FTP, который при работе в активном режиме передает команду PORT, содержащую IP-адрес системы и номер порта для открытия канала передачи данных. Если реализация NAT не поддерживает обработку протокола FTP (это также может произойти, если FTP запущен на нестандартном порту), то во внешнюю сеть выйдет пакет, содержащий адрес из внутренней сети, что в нашем случае нежелательно. Чтобы избежать подобного, стоит ограничить диапазон используемых протоколов уровня приложений (например, используя брандмауэр, открыть выход во внешнюю сеть только по хорошо известным протоколам/портам, поддерживаемым данной реализацией NAT).


Эта статья опубликована в журнале RSDN Magazine #5-2005. Информацию о журнале можно найти здесь
    Сообщений 33    Оценка 226        Оценить