Однажды, перед защитой очередной лабораторной работы мне задали вопрос: какие поля IP-пакета можно использовать для стегано? Я не знал и лишь пожал плечами. Но вскоре я всё же решил изучить этот вопрос.
Под катом вас ждёт изучение заголовков IP-пакетов, собственная утилита ping на Python и несколько способов передать данные, не привлекая внимания.
Выделим поля, изменение которых не сильно повлияет на пакет:
IHL может изменяться от 5 до 15.
Поле ToS используется для приоритизации трафика и уведомлениях о заторах без отбрасывания пакетов. Чаще всего это поле 0. Теоретически можно использовать для передачи целого байта информации.
Длина пакета прекрасное поле для передачи чисел от 20 до 65535.
TTL может передавать до 7 бит информации. Необходимо знать количество хопов до принимающего и учитывать это.
Для повторения эксперимента потребуются две машины с Python и фреймворком scapy.
Установить оный можно следуя инструкции из документации. В моём случае это были два дроплета на DO со включенной локальной сетью. Для проверки работоспособности стегано были выбраны два маршрута: через локальную сеть за 1 хоп и через интернет за 2 хопа.
Сначала реализуем sender.py, который будет отправлять ICMP пакеты без скрытых сообщений.
Scapy перед отправкой заполнит остальные поля значениями по умолчанию и подсчитает контрольную сумму.
На стороне принимающего напишем listener.py, который будет прослушивать и выводить на экран все приходящие ICMP-пакеты.
В заголовке IP-пакета есть поле «идентификатор». Заполним его символами «A» и «B»:
Более того, в заголовке ICMP есть точно такое же поле, в которое так же можно загрузить два байта.
Изменим слушателя для вывода на экран полученных данных:
По образу и подобию можно заполнить практически любое поле, отмеченное ранее как пригодное для стегано.
Передача данных из предыдущего пункта была не самая очевидная, но мы можем сделать ещё более неочевидной. Можно спрятать данные в поле для контрольной суммы. Согласно RFC1071 контрольная сумма является (внезапно!) побитовой инверсией чуть более сложной арифметической суммы.
Нам известно, что контрольная сумма пакета изменяется при прохождении узлов в сети, так как изменяется TTL. Так же при прохождении NAT в пакете подменяется «адрес источника», что так же влияет на контрольную сумму. И на сколько уменьшится TTL при достижении нашего слушателя… Вишенкой на торте является то, что разрядность «идентификатора» совпадает с разрядностью контрольной суммы. Этот факт позволяет нам влиять на контрольную сумму и изменять её на любое значение из области определения. Так как контрольная сумма (полезная нагрузка) будет подсчитана только при прохождении последнего узла в маршруте, важно при расчётах учесть всё, что может быть изменено в пакете за время прохождения маршрута.
Алгоритм нахождения «идентификатора», который даст нам желаемую контрольную сумму:
Напишем функцию, которая по количеству хопов, айпишникам за NAT'ом и двум байтам полезной нагрузки сформирует пакет.
Под катом вас ждёт изучение заголовков IP-пакетов, собственная утилита ping на Python и несколько способов передать данные, не привлекая внимания.
Содержание
Структура 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 обнуляется.
1. Складываем все 16-битные слова, запоминая перенос из старшего разряда:
2. Складываем результат с переносами:
3. Инвертируем:
b94b — искомая нами контрольная сумма. Для проверки можно подставить в заголовок и выполнить пункты 1 и 2. Если получится FFFF, то сумма найдена верна.
Проверка:
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 при достижении нашего слушателя… Вишенкой на торте является то, что разрядность «идентификатора» совпадает с разрядностью контрольной суммы. Этот факт позволяет нам влиять на контрольную сумму и изменять её на любое значение из области определения. Так как контрольная сумма (полезная нагрузка) будет подсчитана только при прохождении последнего узла в маршруте, важно при расчётах учесть всё, что может быть изменено в пакете за время прохождения маршрута.
Алгоритм нахождения «идентификатора», который даст нам желаемую контрольную сумму:
- Настраиваем пакет как при прохождении последнего узла (IP, TTL, etc)
- В «идентификатор» записываем полезную нагрузку
- Подсчитываем контрольную сумму
- Результат необходимо записать в «идентификатор» отправляемого пакета
Напишем функцию, которая по количеству хопов, айпишникам за 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.