Однажды, перед защитой очередной лабораторной работы мне задали вопрос: какие поля 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.

Комментарии (12)


  1. blind_oracle
    11.06.2018 23:18
    +4

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

    Ну, или на крайний случай, действительно в ICMP — помнится на заре проводного интернета у Корбины между разными районами столицы ходили только пинги. И мы с друганом гоняли траффик через pingtunnel :)


    1. ukhegg
      12.06.2018 14:22

      Не всегда имеется доступ к полезной нагрузке. Например при стеке ip/esp/ip/tcp/http всё, что после esp шифруется(типичный стек для ipsec туннеля). Поэтому в таком случае только и остается, что модифицировать внешний ip-заголовок.


      1. blind_oracle
        12.06.2018 14:48

        Какой-либо real life use case можете показать где это бы пригодилось?


        1. ukhegg
          12.06.2018 15:46

          ну тут уж вопросы к автору статьи, где ему это нужно)лично я встречал передачу нескольких лишних байт информации в сетевых пакетах, где длина, указанная в заголовке, несколько меньше реальной длины. При это глазами это очень сложно заметить, даже тот же Wireshark даже не подсвечивает. Что там передает супостат, остается только гадать


          1. blind_oracle
            12.06.2018 16:29

            Нормальные роутеры, по идее, должны такие пакеты дропать нафиг как битые.


            1. ukhegg
              12.06.2018 16:32

              В том то и дело, что в наиболее широко используемых протоколах типа ip/udp/tcp/icmp все отлично, поле длины совпадает с длиной байтового массива, а вот протоколы уровня представления уже не настолько невинны. Так что для роутера они вполне пригодны к употреблению.


              1. blind_oracle
                12.06.2018 16:36

                А, я думал речь об IP-пакетах где длина в заголовке не совпадала с фактической — такие бы роутеры дропнули. А более глубоко копать большинство, конечно, не будет — только всякие специализированные девайсы вроде ASA с функционалом L7-инспекции. И прочий DPI.


  1. DistortNeo
    12.06.2018 12:43

    Стеганография предполагает сокрытие самого факта передачи информации.
    Здесь это условие не соблюдается.


    1. Protos
      13.06.2018 05:49

      Сократе факта, а не способа


      1. Protos
        13.06.2018 05:52
        +1

        В смысле что все остальные известные способы можно выявить, просто не любое сетевое ПО это делает по умолчанию, а значит факт сокрытия передачи есть.


  1. rautate
    12.06.2018 15:50

    Скорее это изучение возможностей Scapy в создании своих пакетов.


  1. Saymon
    14.06.2018 07:40

    Все уже придумано и отлажено — «Hans — IP over ICMP»