Переход с протокола IPv4 на более новый, удовлетворяющий требованиям и тенденциям сегодняшнего цифрового мира, когда-нибудь да случится. Кандидатуру приемников IPv4 возглавляет IPv6. IPv6 действительно интересный и достаточно гибкий протокол, к сожалению, не получил заслуженного внимания в массах. Мы начинаем серию статей, раскрывающую особенности, влияющие на безопасность сети, главного кандидата – IPv6. Целью этой статьи является получение базовых знаний по IPv6, а также изучение фрагментации на практике. Фрагментация может позволить создавать пакеты таким образом, чтобы обходить системы обнаружения атак– это достигается за счёт перекрытия фрагментированных пакетов.

Чтобы понять,почему подобные атаки возможны, необходимо получить базовые навыки фрагментации пакетов. Понять IPv6 без какого-либо погружения в теорию не получится, тем не менее, я постараюсь сделать акцент на практической части. Тема адресации в IPv6-сетях не будет рассматриваться – все необходимые знания по этой теме можно получить из RFC4291.

Что нам потребуется?

Для изучения нам понадобится утилита Scapy и Wireshark. Scapy – это общедоступная утилита, написанная на языке python, которая позволяет создавать пакеты с нужными нам полями (packet crafting). Подробнее о самой утилите можно прочитать по следующей ссылке: http://www.secdev.org/projects/scapy/. Wireshark – утилита для сбора и анализа сетевого трафика.

Установка Scapy достаточно проста -предварительно требуется установить Python 2.7/3.3, после чего следует перейти по ссылке scapy.net. При переходе начнётся автоматическая загрузка архива с актуальной версии утилиты (настоятельно рекомендую устанавливать на Linux – для пользователей Windows можно воспользоваться следующим мануалом). Распаковав архив и перейдя внутрь распакованной папки, выполним следующие команды, после которых можно считать этап установки Scapy успешно пройденным:

sudo python setup.py install
sudo scapy

В чем отличия от IPv4?

Первым делом посмотрим на сам заголовок IPv6 и сравним его с IPv4.

Основной заголовок IPv6 имеет фиксированную длину в 40 байт, что в два раза больше минимального заголовка IPv4, однако, стоит помнить, что поля Source/Destination (src)/(dst) увеличились и стали по 128 бит для увеличения пула белых адресов, а также, что заголовок IPv4 может увеличиться до 60 байт.

Аналогом ttl в IPv6 является поле hop limit (hlim) – которое обозначает максимальное число переходов и последовательно уменьшается на единицу каждым маршрутизатором по пути следования пакета. Поскольку основной заголовок IPv6 имеет фиксированный размер, то поле ihl из IPv4 не является значимым. Аналогом поля length в IPv6 является поле payload length (plen), которое обозначает длину передаваемых данных, без учёта длины основного заголовка. Аналогом tos в IPv6 является поле traffic class (tc), на основании которого происходит разделение пакетов по классам.

Новшеством протокола IPv6 является поле flow label (fl) – данным полем можно выделить поток пакетов, который требует определенной обработки. Например, при помощи fl можно передавать информацию о необходимости использования одного и того же маршрута для данного потока пакетов.

Поле next header (nh) является аналогом protocol type из IPv4 и позволяет определять тип данных полезной нагрузки. Данный заголовок позволяет пересылать данные по необходимым протоколам вышестоящего уровня (например, TCP, UDP или ICPM).

Как можно заметить, часть полей заголовка IPv4 отсутствует в основном заголовке IPv6. Например, достаточно странным может показаться отсутствие полей identification, flags и offset (frag на Рисунке выше), отвечающих за фрагментацию пакетов, но на самом деле это не совсем так: они просто вынесены в заголовки расширения. Заголовки расширения были введены для поддержки большей функциональности, чем представлено основным заголовком IPv6 – передача дополнительной информации сетевого уровня. Они опциональны и располагаются между основным заголовком IPv6 и заголовком вышестоящего протокола. В пакете может быть несколько заголовков расширения – каждый заголовок имеет номер, который должен быть указан в предыдущем заголовке в поле next header (nh), начиная с основного заголовка.

На сегодняшний день RFC 2460 определяет 6 заголовков расширения, краткую информацию о которых можно получить из таблицы ниже.

Название заголовка расширения Идентификатор next header Описание
Hop-by-Hop Options header 0 Каждый узел по пути следования пакета, должен обработать содержащиеся параметры данного заголовка. (см. RFC 2460)
Destination Option 60 Произвольный набор опций, который должен быть обработан получателем. (см. RFC 2460)
Routing Header 43 Позволяет определить промежуточные узлы от отправителя до получателя, через которые следует пройти пакету. (см. RFC 2460)
Fragment header 44 Содержит информацию о фрагментации пакета. (см.RFC 2460)
Authentication header 51 Содержит информацию для поддержки целостности и проверки подлинности зашифрованных данных при использовании IPSec. (см. RFC 2402)
Encapsulating Security Payload Header 50 Обеспечивает шифрование данных с помощью IPSec (см. RFC 2406)

RFC 2460 устанавливает порядок следования заголовков в пакете – первым заголовком, очевидно, должен быть основной заголовок IPv6, последним – заголовок вышестоящего уровня, а вот порядок следования заголовков расширения между ними совпадает с тем порядком, в котором были описаны заголовки расширения в таблице выше.

Каждый из заголовков расширения не должен встречаться более одного раза в пакете – единственным исключением является Destination option header, который может встретиться два раза. Например, структура IPv6 пакета с заголовками расширения может выглядеть так:

Базовые знания по IPv6 получены и наконец-то настало время практических экспериментов!

Приступаем к практике!

По настройке тестовой среды хочется отметить лишь следующие пункты:

  • хост-отправитель – IPv6 address=fec0:1::a; prefix=64; gateway=fec0:1::a;
  • хост-получатель - IPv6 address=fec0:1::b; prefix=64; gateway=fec0:1::a;
  • по определенным причинам требуется отключить IPv4 (можно сделать отдельное IPv6 подключение, при этом один из тестируемых хостов можно развернуть на виртуальной машине) и установить MTU равным 1400 байт.

Итак, первым практическим шагом будет создание ICMPv6 Echo-запроса через Scapy. Используя следующую последовательность команд в Scapy, можно достичь поставленной цели:

header=IPv6(src="fec0:1::a",dst="fec0:1::b")
payload=ICMPv6EchoRequest(data="Hello, Deiteriy!")
packet=(header/payload)
packet.display()
send(packet)

На Рисунке ниже можно увидеть передачу ICMPv6 Echo-запроса и получение Echo-ответа.

Стоит отметить, что отправленный пакет был корректным, несмотря на то, что мы не задавали большинство требуемых полей (например, next header, plen, check sum). Дело в том, что Scapy при отправке автоматически подставляет все требуемые поля, которые не были заданы нами явно. Давайте вмешаемся в этот процесс и изменим, например, поле checksum в заголовке ICMPv6EchoRequest – для этого нам требуется выполнить следующие команды:

packet.cksum=777
send(packet)

Таким образом, мы, как и хотели, отправили пакет с неверной контрольной суммой и по этой причине не получили Echo-ответ.

Увеличим теперь размер данных в нагрузке Echo-запроса. Сделать это можно следующей командой:

packet.data="A"*1344+"B"*656+"C"*400
send(packet)

В результате мы должны были получить следующую ошибку: Scapy_Exception: cannot fragment this packet. Таким образом мы добрались до очередного теоретического подраздела IPv6 – фрагментации.

Фрагментация в IPv6

Как и в IPv4, перед отправкой данных, IPv6-хост должен знать Path MTU (RFC1981) – наименьший максимальный размер полезной нагрузки пакета по его пути следования, который может быть передан протоколом без разбития исходного пакета на несколько пакетов меньшего размера (то есть без фрагментации). При превышении установленного MTU, пакет должен быть фрагментирован. Однако, в отличие от IPv4, фрагментировать исходный пакет может только его источник (роутеры по пути следования теперь не могут этого делать). Кроме того, минимальный MTU, поддерживаемый IPv6 был поднят до 1280 байт! К сожалению, чтобы двигаться дальше, нам потребуется разобрать заголовок фрагментации протокола IPv6, но это совсем недолго!

Поле nh нам уже знакомо – это идентификатор следующего заголовка (а им может быть, как заголовок вышестоящего уровня, так и другой заголовок расширения), res1 – неиспользуемое зарезервированное поле, принимающее значение 0. Fragment offset (offset) – смещение данных этого фрагмента (которые cледуют за заголовком фрагментации), относительно начала фрагментированной части исходного пакета. Исходный пакет при фрагментации делится на две части:

  • нефрагментируемую – состоящую из основного заголовка IPv6 и всех тех заголовков расширения, которые должны быть обработаны промежуточными узлами по пути следования пакета (по этой причине каждый фрагментированный пакет несёт общую - нефрагментируемую часть и уже свой фрагмент исходного пакета);
  • фрагментируемую – часть, которую должен обрабатывать (собирать в исходный пакет) только получатель (а ещё средства защиты, но это не точно). Длина полезной нагрузки фрагмента (которая указывается в plen) должна быть кратна 8 и при этом длины фрагментированных пакетов должны соответствовать MTU. Естественно, данные могут не быть кратными 8, поэтому для последнего фрагмента (с флагом M равным 0) данное правило может не выполняться.

Поле M принимает два значение: 0 (данный фрагмент является последним) и 1 (данный фрагмент не последний и стоит ожидать ещё фрагменты). Поле identification аналогично одноименному полю из заголовка IPv4 – каждому исходному пакету, который будет разбиваться на фрагменты, источник генерирует уникальное значение (идентификатор, присваиваемый всеми фрагментами), которое должно отличаться от других фрагментированных пакетов, отправленных недавно (с теми же src и dst).

Теперь вернемся к пакету, который нам не удалось отправить раннее. Как мы и договаривались, MTU равно 1400 байт. Давайте подсчитаем общую длину передаваемого пакета: полезная нагрузка ICMPv6-запроса (2400 байт), кроме того, исходный пакет состоит из основного заголовка IPv6 (40 байт) и заголовка вышестоящего уровня (ICMPv6 - 8 байт). Поэтому исходный пакет имеет длину 2448 байт, что превышает MTU и для его передачи нам потребуется фрагментация.

Давайте разобьём его сначала на два фрагмента. На Рисунке ниже показана схема, которая отображает основную логику при разбитии исходного пакета IPv6 на два фрагментированных пакета.

Следующий python-скрипт, позволит нам отправить два, описанных выше на Рисунке, пакета:

from scapy.all import *                  
								 
payload1 = ""
for i in range(1344):
	payload1 = payload1 + "A"
payload2 = ""
for i in range(656):
	payload2 = payload2 + "B"
for i in range(400):
	payload2 = payload2 + "C"

IPv6Hdr1 = IPv6(src="fec0:1::a", dst="fec0:1::b", plen=1360)
IPv6Hdr2 = IPv6(src="fec0:1::a", dst="fec0:1::b", plen=1064)
FragmentHdr1=IPv6ExtHdrFragment(offset=0, m=1, id=31337, nh=58)
FragmentHdr2=IPv6ExtHdrFragment(offset=169, m=0, id=31337, nh=58)
ICMPv6Hdr = ICMPv6EchoRequest(data=payload1, cksum=0xbc07)

packet1=(IPv6Hdr1/FragmentHdr1/ICMPv6Hdr)
packet2=(IPv6Hdr2/FragmentHdr2/payload2)
send(packet1)
send(packet2)

На Рисунке ниже можно увидеть, как нам (на fec0:1::a) приходит Echo-ответ, так же разбитый на два фрагмента.

Давайте разобьем тот же исходный пакет уже на четыре фрагмента. Как и в прошлом примере, логика разбития исходного пакета на фрагменты будет показана на Рисунке ниже. Обращаю внимание на то, что в нашем первом фрагменте максимальный объем данных (максимальный отрезок полезной нагрузки ICMPv6, который мы можем поместить в первый фрагмент) составляет 1344 байт (по причине MTU равного 1400 байт). Для разбития на большее количество фрагментированных пакетов, мы отправим в первом фрагменте 744 байта, во втором 600 байт, 656 байт в третьем и 400 байт в четвертом. Отправляя подобные фрагментированные пакеты, мы нарушаем предписание RFC 2460: “The lengths of the fragments must be chosen such that the resulting fragment packets fit within the MTU of the path to the packets destination(s)”. То есть мы нарушаем требование к длине фрагментированного пакета.

Следующий python-скрипт, позволит нам отправить четыре, описанных выше на Рисунке, пакета:

# -*- coding: utf-8 -*-
from scapy.all import *                  
								 
payload1 = ''
for i in range(744):
	payload1 = payload1 + "A"
payload2 = ''
for i in range(600):
	payload2 = payload2 + "A"
payload3 = ''
for i in range(656):
	payload3 = payload3 + "B"
payload4=''
for i in range(400):
	payload4 = payload4 + "C"	

IPv6Hdr1 = IPv6(src="fec0:1::a", dst="fec0:1::b", plen=760)
IPv6Hdr2 = IPv6(src="fec0:1::a", dst="fec0:1::b", plen=608)
IPv6Hdr3 = IPv6(src="fec0:1::a", dst="fec0:1::b", plen=664)
IPv6Hdr4 = IPv6(src="fec0:1::a", dst="fec0:1::b", plen=408)

FragmentHdr1=IPv6ExtHdrFragment(offset=0, m=1, id=31337, nh=58)
FragmentHdr2=IPv6ExtHdrFragment(offset=94, m=1, id=31337, nh=58)
FragmentHdr3=IPv6ExtHdrFragment(offset=169, m=1, id=31337, nh=58)
FragmentHdr4=IPv6ExtHdrFragment(offset=251, m=0, id=31337, nh=58)

ICMPv6Hdr = ICMPv6EchoRequest(data=payload1, cksum=0xbc07)

packet1=(IPv6Hdr1/FragmentHdr1/ICMPv6Hdr)
packet2=(IPv6Hdr2/FragmentHdr2/payload2)
packet3=(IPv6Hdr3/FragmentHdr3/payload3)
packet4=(IPv6Hdr4/FragmentHdr4/payload4)
send(packet1)
send(packet2)
send(packet3)
send(packet4)

Обратите внимание на Echo-ответ: несмотря на то, что мы передавали 4 фрагментированных пакета, в ответ получили 2 фрагментированных пакета. Поскольку MTU равно 1400 байт, то отвечающая сторона рационально использовала эту длину и разбила собранный исходный пакет на два фрагмента.

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

Подведем итоги

Итак, мы получили теоретические знания о IPv6. Уже осознали, что IPv6 – это не “IPv4 всего лишь с более длинными адресами”. Получили практический опыт создания IPv6 пакетов с необходимыми нам полями, а также фрагментации этих пакетов и уже увидели одну некорректность в реализации протокола IPv6 относительно документации. Практический навык создания фрагментированных пакетов с нужными полями и смещениями важен с точки зрения безопасности. Например, использовать этот навык можно для проведения атаки перекрывающимися фрагментами (fragment overlapping), когда часть одного фрагмента частично или полностью затирает данные другого фрагмента при сборке исходного пакета, а также создания atomic фрагментов, когда исходный пакет фрагментируется в один пакет (то есть без нормальной фрагментации на несколько пакетов). Таким образом, злоумышленники могут манипулировать фрагментами с целью обхода сигнатуры атаки, при этом доставляя пакеты с необходимыми полями на удаленную систему. Чтобы провести подобную атаку с перекрывающими фрагментами стоит учитывать, что разные операционные системы имеют разные алгоритмы сборки пакетов. Поэтому, в нашей следующей статье мы разберем особенности сборки фрагментированных пакетов разными операционными системами.