Стеганография в IP-пакетах

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

    Под катом вас ждёт изучение заголовков IP-пакетов, собственная утилита ping на Python и несколько способов передать данные, не привлекая внимания.

    Содержание


    1. Структура IP-пакета
    2. Настройка окружения
    3. Ping: Лёгкий вариант
    4. Ping: Сложный вариант
    5. Доработки?

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




    Выделим поля, изменение которых не сильно повлияет на пакет:

    IHL может изменяться от 5 до 15.
    Поле ToS используется для приоритизации трафика и уведомлениях о заторах без отбрасывания пакетов. Чаще всего это поле 0. Теоретически можно использовать для передачи целого байта информации.
    Длина пакета прекрасное поле для передачи чисел от 20 до 65535.
    TTL может передавать до 7 бит информации. Необходимо знать количество хопов до принимающего и учитывать это.

    Настройка окружения


    Для повторения эксперимента потребуются две машины с Python и фреймворком scapy.

    Установить оный можно следуя инструкции из документации. В моём случае это были два дроплета на DO со включенной локальной сетью. Для проверки работоспособности стегано были выбраны два маршрута: через локальную сеть за 1 хоп и через интернет за 2 хопа.

    Ping: Легкий вариант


    Сначала реализуем sender.py, который будет отправлять ICMP пакеты без скрытых сообщений.

    from scapy.all import *
    
    # Создаём пакет для 10.0.0.2 с icmp-type 8 (echo-request)
    pkt = IP(src="10.0.0.1", dst="10.0.0.2") / ICMP(type = 8)
    # Отправляем пакет и ждём ответа
    sr1(pkt)
    

    Scapy перед отправкой заполнит остальные поля значениями по умолчанию и подсчитает контрольную сумму.

    На стороне принимающего напишем listener.py, который будет прослушивать и выводить на экран все приходящие ICMP-пакеты.

    from scapy.all import *
    
    # Настраиваем прослушивание пакетов
    #   filter -- только icmp
    #   timeout -- слушаем только 10 секунд
    #   count -- ждём не больше 100 пакетов 
    #   iface -- только на интерфейсе eth1
    packets = sniff(filter = "icmp", timeout = 10, count = 100, iface = "eth1")
    
    # Итерируемся по всем полученным пакетам
    for pkt in packets:
        # Нас интересуют только пришедшие echo-request
        if pkt[ICMP].type != 8:
            continue
        # Просим красиво напечатать
        pkt.show()
    
    

    Примерный вывод слушателя
    ###[ Ethernet ]###
         dst       = hh:hh:hh:hh:hh:hh
         src       = gg:gg:gg:gg:gg:gg
         type      = 0x800
    ###[ IP ]###
         version   = 4
         ihl       = 5
         tos       = 0x0
         len       = 28
         id        = 24923
         flags     =
         frag      = 0
         ttl       = 64
         proto     = icmp
         chksum    = 0x4364
         src       = 10.0.0.1
         dst       = 10.0.0.2
         \options   \
    ###[ ICMP ]###
            type      = echo-request
            code      = 0
            chksum    = 0xf7ff
            id        = 0x0
            seq       = 0x0
    ###[ Padding ]###
               load      = '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'


    В заголовке IP-пакета есть поле «идентификатор». Заполним его символами «A» и «B»:

    payload = ord("A") * 0x100 + ord("B")
    pkt = IP(src="10.0.0.1", dst="10.0.0.2", id = payload) / ICMP(type = 8)
    

    Более того, в заголовке ICMP есть точно такое же поле, в которое так же можно загрузить два байта.

    Изменим слушателя для вывода на экран полученных данных:

    from scapy.all import *
    import sys
    
    packets = sniff(filter="icmp", timeout = 10, count = 100, iface="eth0")
    
    for pkt in packets:
        if pkt[ICMP].type != 8:
            continue
        # Разделяем два символа
        a, b = divmod(pkt[IP].id, 0x100)
        sys.stdout.write(chr(a))
        sys.stdout.write(chr(b))
        sys.stdout.flush()
    

    По образу и подобию можно заполнить практически любое поле, отмеченное ранее как пригодное для стегано.

    Ping: Сложный вариант


    Передача данных из предыдущего пункта была не самая очевидная, но мы можем сделать ещё более неочевидной. Можно спрятать данные в поле для контрольной суммы. Согласно RFC1071 контрольная сумма является (внезапно!) побитовой инверсией чуть более сложной арифметической суммы.

    Объяснение с примером
    Допустим, у нас есть заголовок, для которого мы хотим вычислить контрольную сумму. На время расчётов поле checksum обнуляется.

    4500 003c 000a 0000 8001 [checksum] c0a8 000d c0a8 000d

    1. Складываем все 16-битные слова, запоминая перенос из старшего разряда:

    4500 + 003c + 000a + 0000 + 8001 + [checksum=0000] + c0a8 + 000d + c0a8 + 000e = 
    = (2) 46b2

    2. Складываем результат с переносами:

    46b2 + 2 = 46b4

    3. Инвертируем:

    ~(46b4) = b94b

    b94b — искомая нами контрольная сумма. Для проверки можно подставить в заголовок и выполнить пункты 1 и 2. Если получится FFFF, то сумма найдена верна.

    Проверка:

    1. 4500 + 003c + 000a + 0000 + 8001 + [checksum=b94b] + c0a8 + 000d + c0a8 + 000e = 
    = (2) FFFD
    2. FFFD + 2  = FFFF
    


    Нам известно, что контрольная сумма пакета изменяется при прохождении узлов в сети, так как изменяется TTL. Так же при прохождении NAT в пакете подменяется «адрес источника», что так же влияет на контрольную сумму. И на сколько уменьшится TTL при достижении нашего слушателя… Вишенкой на торте является то, что разрядность «идентификатора» совпадает с разрядностью контрольной суммы. Этот факт позволяет нам влиять на контрольную сумму и изменять её на любое значение из области определения. Так как контрольная сумма (полезная нагрузка) будет подсчитана только при прохождении последнего узла в маршруте, важно при расчётах учесть всё, что может быть изменено в пакете за время прохождения маршрута.

    Алгоритм нахождения «идентификатора», который даст нам желаемую контрольную сумму:

    1. Настраиваем пакет как при прохождении последнего узла (IP, TTL, etc)
    2. В «идентификатор» записываем полезную нагрузку
    3. Подсчитываем контрольную сумму
    4. Результат необходимо записать в «идентификатор» отправляемого пакета

    Напишем функцию, которая по количеству хопов, айпишникам за NAT'ом и двум байтам полезной нагрузки сформирует пакет.

    # src - адрес отправителя
    # src_nat - адрес отправителя за NAT
    # dst - адрес получателя
    # dttl - количество узлов на пути в получателю
    # a, b -- по одному байту полезной информации
    def send_stegano(src, src_nat, dst, dttl, a, b):
        # Формируем полезную нагрузку из двух байт
        payload = ord(a)*0x100 + ord(b)
        # Создаём состояние пакета при прохождении последнего узла маршрута
        pkt = IP(dst=dst, src=src_nat, ttl=64-dttl, id = payload) / ICMP(type=8)
        # Заставляем Scapy вычислить chksum
        pkt = IP(raw(pkt))
        # Готовим пакет к отправке
        pkt[IP].src = src
        pkt[IP].ttl = 64
        pkt[IP].id = pkt[IP].chksum
        # Стираем поле chksum, чтобы Scapy перерасчитал его
        del pkt[IP].chksum
        # Scapy вновь вычисляет все контрольные суммы
        pkt = IP(raw(pkt))
        # Отправляем пакет и ждём ответ
        sr1(pkt)
    

    Доработки?


    • поля chksum, seq, id в заголовке протокола ICMP так же могут использоваться для передачи данных
    • ToS можно использовать для идентификации пакетов «от своих» и игнорировать чужие echo-request.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 12

      +5
      Представляет чисто академический интерес, ИМХО.
      Проще в безобидный на вид payload стеганограммы добавлять: в картинки, в текст, в звук — вариантов куча.

      Ну, или на крайний случай, действительно в ICMP — помнится на заре проводного интернета у Корбины между разными районами столицы ходили только пинги. И мы с друганом гоняли траффик через pingtunnel :)
        0
        Не всегда имеется доступ к полезной нагрузке. Например при стеке ip/esp/ip/tcp/http всё, что после esp шифруется(типичный стек для ipsec туннеля). Поэтому в таком случае только и остается, что модифицировать внешний ip-заголовок.
          0
          Какой-либо real life use case можете показать где это бы пригодилось?
            0
            ну тут уж вопросы к автору статьи, где ему это нужно)лично я встречал передачу нескольких лишних байт информации в сетевых пакетах, где длина, указанная в заголовке, несколько меньше реальной длины. При это глазами это очень сложно заметить, даже тот же Wireshark даже не подсвечивает. Что там передает супостат, остается только гадать
              0
              Нормальные роутеры, по идее, должны такие пакеты дропать нафиг как битые.
                0
                В том то и дело, что в наиболее широко используемых протоколах типа ip/udp/tcp/icmp все отлично, поле длины совпадает с длиной байтового массива, а вот протоколы уровня представления уже не настолько невинны. Так что для роутера они вполне пригодны к употреблению.
                  0
                  А, я думал речь об IP-пакетах где длина в заголовке не совпадала с фактической — такие бы роутеры дропнули. А более глубоко копать большинство, конечно, не будет — только всякие специализированные девайсы вроде ASA с функционалом L7-инспекции. И прочий DPI.
        0
        Стеганография предполагает сокрытие самого факта передачи информации.
        Здесь это условие не соблюдается.
          0
          Сократе факта, а не способа
            +1
            В смысле что все остальные известные способы можно выявить, просто не любое сетевое ПО это делает по умолчанию, а значит факт сокрытия передачи есть.
          0
          Скорее это изучение возможностей Scapy в создании своих пакетов.
            0
            Все уже придумано и отлажено — «Hans — IP over ICMP»

            Only users with full accounts can post comments. Log in, please.