Packet Crafting

Александр sinister подготовил для linkmeup новую интереснейшую статью.

=====================================



Создание пакетов или packet crafting — это техника, которая позволяет сетевым инженерам или пентестерам исследовать сети, проверять правила фаерволлов и находить уязвимые места.
Делается это обычно вручную, отправляя пакеты на различные устройства в сети.
В качестве цели может быть брандмауэр, системы обнаружения вторжений (IDS), маршрутизаторы и любые другие участники сети.
Создание пакетов вручную не означает, что нужно писать код на каком-либо высокоуровневом языке программирования, можно воспользоваться готовым инструментом, например, Scapy.

Scapy — это один из лучших, если не самый лучший, инструмент для создания пакетов вручную.
Утилита написана с использованием языка Python, автором является Philippe Biondi.
Возможности утилиты практически безграничны — это и сборка пакетов с последующей отправкой их в сеть, и захват пакетов, и чтение их из сохраненного ранее дампа, и исследование сети, и многое другое.
Всё это можно делать как в интерактивном режиме, так и создавая скрипты.
С помощью Scapy можно проводить сканирование, трассировку, исследования, атаки и обнаружение хостов в сети.
Scapy предоставляет среду или даже фреймворк, чем-то похожий на Wireshark, только без красивой графической оболочки.
Утилита разрабатывается под UNIX-подобные операционные системы, но тем не менее, некоторым удается запустить ее и в среде Windows.
Эта утилита так же может взаимодействовать и с другими программами: для наглядного декодирования пакетов можно подключать тот же Wireshark, для рисования графиков — GnuPlot и Vpython.
Для работы потребуется права суперпользователя (root, UID 0), так как это достаточно низкоуровневая утилита и работает напрямую с сетевой картой.
И что важно, для работы с этой утилитой не потребуются глубокие знания программирования на Python.

Приступаем



Официальный сайт проекта — www.secdev.org/projects/scapy/
Установку можно провести разными способами, например apt-get install python-scapy, в случае дистрибутивов на основе Debian.
Так же можно просто скачать свежую версию с сайта разработчиков:
# cd /tmp
# wget scapy.net
# unzip scapy-latest.zip
# cd scapy-2.*
# sudo python setup.py install

После этого запуск происходит непосредственно командой scapy.
На экране отобразится примерно так:



Мы видим стандартное приглашение для ввода, все действия будут выполняться в интерактивном режиме.
Выход происходит комбинацией Ctrl+D, либо набрав функцию exit().

Изучаем инструмент


На самом деле Scapy сильно отличается от привычных утилит. Он работает в текстовом режиме, но любое взаимодействие осуществляется не через привычные ключи и параметры командной строки, а через интерпретатор Python'а.
Такой подход вначале может показаться несколько неудобным и непривычным, но со временем и с практикой приходит понимание того, что это было правильным решением, и что это действительно удобно.

Вначале посмотрим на поддерживаемые протоколы, для этого вызовем функцию ls().



Вывалится более 300 разнообразных протоколов, с которыми можно работать, включая прикладные вроде HTTP, транспортные TCP и UDP, сетевого уровня IPv4 и IPv6 и канального уровня Ether (Ethernet).
Важно обращать внимание на регистр: большинство протоколов пишутся в Scapy с заглавными буквами.
Для того чтобы подробно рассмотреть поля определенного протокола, можно вызвать функцую ls() с указанием протокола: ls(TCP)



В результате будут выведены все поля, которые можно модифицировать в процессе создания пакетов. В скобках показаны значения, которые используются по умолчанию, можно заметить, что порт отправителя 20 (это ftp-data) и порт получателя – 80 (это естественно HTTP), так же установлен флаг SYN (flags = 2).

К примеру, если рассмотреть канальный уровень (Ethernet), то тут возможностей будет поменьше:



В дополнение к функции ls(), есть полезная функция lsc(), которая выведет практически весь основной функционал Scapy:



Для того чтобы получить более подробную информацию о каждой функции, можно использовать help(имя_функции), например:



Видим нечто похожее на MAN страницу в Unix системах.
Для выхода можно использовать опять же привычную в Linux клавишу Q.

Мы посмотрели на протоколы и функции, теперь можно перейти к делу — к созданию пакетов.

Крафтим


Можно создавать сразу пакеты высоких уровней (сетевого и прикладного), и Scapy автоматически дополнит низлежащие уровни, а можно вручную собирать, начиная с канального уровня.
Разделяются уровни модели OSI символом прямого слэша (/).
Нужно обратить внимание на то, что Scapy читает пакет от нижнего уровня слева, до более высокого справа. Поначалу это может немного сбивать с толку, но после небольшой практики всё станет вполне привычно.
К слову, в терминологии Scapy сетевой пакет разделяется на слои, и каждый слой представляется как экземпляр объекта.
Собранный пакет в упрощенном виде может выглядеть как:
Ether()/IP()/TCP()/”App Data”

В большинстве случаев используется только уровень L3, предоставляя Scapy возможность самостоятельно заполнять канальный уровень, на основе информации из ОС.
Меняя значения полей каждого протокола мы меняем стандартные значения (их выводит функция ls()).

Теперь создадим какой-нибудь простой пакет.



Всё очень просто: мы указали адрес назначения, порт и вобщем-то нагрузку в виде слова «TEST».
Сам пакет был незамысловато назван packet, мы увидим очень подробно и развернуто наш свежесозданный пакет:
И теперь, выполнив знакомую уже функцию ls(packet):



Уровни в нем разделяются символами "--".

Вместо того, чтобы создавать пакет за один раз можно создавать его частями:



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

И теперь собираем всё в один пакет:



Видно, что результат получится аналогичный.

Углубляемся в пакеты


Мы уже смотрели на вывод функции ls(), но не всегда нужна такая подробная информация о пакете.
Достаточно просто набрать имя переменной и сразу увидеть краткую сводку:



Так же можно использовать метод summary():



Если же нужно чуть больше информации, то есть метод show():



Кроме того, можно просмотреть любое поле, просто указав его:



Разумеется, это работает только в том случае, если такие поля уникальны в пределах пакета.
Если, например, взять поле flags, которое присутствует как в TCP, так и в IP, тут уже нужно конкретизировать, что мы хотим увидеть. В противном случае Scapy выведет значение первого найденного поля (IP flags в нашем примере).
Конкретизация происходит путем указания протокола в квадратных скобках:



К слову, по умолчанию установленные флаги выводятся в цифровом представлении.
Если все управляющие биты будут включены (установлены в 1), то получим значение равное 255. В нашем случае значение 2 говорит о том, что установлен SYN бит.

Но существует возможность отобразить управляющие биты и в символьном отображении:



Как уже говорилось, в любой момент можно достаточно просто поменять значение любого поля:



А в случае, если поле не является уникальным, то нужно указать протокол:



Вторым способом является использование конструкции payload, которая позволяет перепрыгнуть через один уровень (через L3 в нашем случае):



Здесь мы вначале просматриваем вывод слоев над L3, затем просматриваем значение TCP флагов и устанавливаем для них новое значение.
Кстати, можно даже несколько раз вызвать payload, поднимаясь при этом выше и выше:



Можно еще посмотреть на содержимое пакета в шестнадцатеричном виде, для этого есть функция hexdump():



Разбираемся с адресацией


Scapy и в деле указания адреса получателя так же проявляет большую гибкость.
Масса вариантов — здесь и привычная десятичная форма, и доменное имя и CIDR нотация:



В последнем случае пакет будет отправлен на каждый адрес в подсети.

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



На этом этапе может возникнуть мысль: «А что если нужно задать множество портов?».
И тут Scapy предоставляет широкие возможности, можно указать как диапазон, так и просто перечислить множество:



Обращаю внимание на различие в скобках, в случае диапазона они круглые, а в случае множества – квадратные.
И завершая разговор про указание целей, рассмотрим ситуацию, когда нужно отправить множество пакетов на множество портов.
Для того, чтобы увидеть какие пакеты будут отправлены придется задействовать цикл for, не забываем, что язык программирования у нас Python.
На самом деле ничего сложного, всё очень логично:



Вначале мы уже привычно создаем пакет, в котором указываем подсеть и диапазон портов.
Затем, используя цикл, создаем список, где переменной «а» присваивается каждый элемент структуры пакета. В Python'е отсутствуют массивы в привычном понимании. Вместо них для хранения объектов используются списки.
Мы используем цикл for, для того чтобы «распаковать» всю структуру и отобразить ее в таком наглядном виде.

Отправляем пакеты в путь


С таким же размахом и широтой происходит и отправка пакетов:
  • функция send() – отправляет пакеты, используя сетевой (L3) уровень, никак не обрабатывая ответы. Используется принцип — отправили и забыли;
  • функция sendp() – отправляет пакеты, используя канальный (L2) уровень, учитываются указанные параметры и заголовки Ethernet кадров. Ответы всё так же не ожидаются и не обрабатываются;
  • функция sr() – является аналогичной send(), исключение составляет то, что она уже ожидает ответные пакеты;
  • функция srp() – отправляет и принимает пакеты, уровень L2
  • функция sr1() – отправляет пакет третьего уровня и забирает только первый ответ, множество ответов не предусматривается;
  • функция srp1() – аналогично sr1(), только уже канальный уровень.
Каждую из этих функций можно вызвать и без дополнительных параметров, просто указывая имя переменной, содержащей пакет.



Но вместе с тем существует много дополнительных опций, которые могут быть иногда полезны.
Например, timeout – укажет, сколько времени (в секундах) нужно ждать до получения ответного пакета, retry – сколько раз нужно повторно слать пакет, если ответ не был получен и одна из самых полезных опций – это filter, синтаксис которого очень похож на tcpdump.

В качестве наглядного примера отправим пакет в сеть:



Здесь мы используем функцию, которая после отправки ожидает ответ, устанавливаем таймаут 0.1 секунды и фильтруем ответы, которые подпадают под указанное правило.

Как поступать с ответными пакетами?
Можно взять и назначить переменную, которая и будет содержать ответ:


А смотреть уже привычным способом, просто вызывая переменную response.
Видно, что ответ сохранился в двух вариантах – Results и Unanswered, результаты и без ответа, соответственно.
Указывая смещение, можно вывести только необходимую часть ответа:



Или подробную информацию:



Если же пакет был отправлен в сеть без указания переменной (например, просто функцией sr()), то по умолчанию пакет будет числиться за переменной "_" (символ подчеркивания).

Чтобы достать оттуда эти пакеты, можно использовать конструкцию:



При этом разные результаты сохранятся в двух разных переменных (res и unans).

Более подробный вывод достигается опять же путем указания смещения:



Ловим ответные пакеты


Теперь рассмотрим ситуацию, если пакетов в ответ приходит много.



То, что мы увидели, было по сути, самое что ни есть сканирование портов.

Открытые порты будут с флагами SA (SYN/ACK), например:



Мы смотрим именно на пакет по номеру, счет традиционно начинается нуля.

Можно пойти дальше и распаковать этот результат:



Здесь мы извлекли из результата отправленный пакет (под номером 21) и ответ на него.
Но это только один пакет, а как быть, если нужно обработать все пакеты?
В таком случае придется вновь обращаться к циклу for:



Берем и разбиваем каждый элемент списка res на части a и b. Затем обрезаем часть “a”, заливая это всё в список “allsent”.
Аналогично создаем список allrec, только уже оставляем другую часть.
Всё это, конечно, хорошо, но хотелось бы в более удобном виде получить список открытых и закрытых портов.
Еще раз посмотрим на список res, a точнее на элемент res[0], который состоит из двух частей: пакет, который мы отправили res[0][0], и ответ, который получили res[0][1].



В ответе можно обнаружить три части — заголовок IP (res[0][1][0]), заголовок TCP (res[0][1][1]) и собственно сами данные (res[0][1][2]).



Используем цикл for для извлечения каждого элемента res[N] в переменную «а».



Теперь в переменной «a»содержится результат для каждого пакета. Другими словами «а» представляет собой ans[N].
Нам остается только проверить значения a[1][1], которые будут означать res[N][1][1] в заголовке TCP.
Если быть еще более точным, требуется значение 18, которое означает установленные флаги SYN-ACK.



В тех случаях, когда это условие сработает, мы еще выведем порт отправителя из заголовка TCP:



В итоге, получим результат в виде списка открытых портов.
Все вышеозначенные конструкции набираются за один раз, важно так же уделять внимание отступам (обычно это 4 пробела).

Мы только что вручную написали простой сканер портов, не больше и не меньше.

Листинг 1 Сканер портов


Сниффер и наоборот


В Scapy входит также и небольшой сниффер, за который отвечает функция sniff().
Естественно, с ним можно использовать фильтры (похожие на фильтры tcpdump), за это отвечает параметр filter, так же можно ограничивать количество пакетов с помощью параметра count.
Как всегда вызов help(sniff) выведет вполне подробную информацию по этой функции.
Не следует забывать, что это сильно упрощенный сниффер, и ожидать от него хорошей скорости особо не приходится.
Стандартная комбинация Ctrl+C прервет процесс захвата трафика и выведет результат.
Как и любая неопределенная переменная, результат попадет в "_".
Выполнив метод summary(), можно увидеть статистику по захваченным пакетам:



Вместо захвата трафика из сети, можно прочитать его из заранее сохраненного дампа (pcap файла).



Кроме того, можно и наоборот, записать пойманные пакеты в файл, используя функцию wrpcap():



И завершая тему сниффинга, можно вызвать Wireshark прямо из интерфейса Scapy, для этого можно использовать одноименную функцию wireshark().

Подробно про Wireshark можно в моей предыдущей статье по адресу http://linkmeup.ru/blog/115.html.

Автоматизация


Всё, что мы рассматривали, происходило непосредственно в интерактивном режиме.
Но, естественно, многие вещи можно автоматизировать, написав скрипты.
Для этого в начале скрипта нужно будет указать:
#!/usr/bin/python

Знакомый для пользователей ОС Linux, shebang. (http://en.wikipedia.org/wiki/Shebang_(Unix))
from scapy.all import *

Импортировать весь функционал Scapy.
После этого уже можно писать необходимые функции.
Важно делать отступы при написании циклов в скриптах, иначе будут появляться сообщения об ошибках, и скрипт не будет работать.
Тут же рассмотрим и подключение дополнительных модулей к Scapy на примере OSPF.
Изначально Scapy не умеет работать с протоколом OSPF.
Если выполнить load_contrib('ospf'), то будет сообщение об ошибке: «ERROR: No module named contrib.ospf».
Вначале скачаем модуль, его можно взять тут.
Затем нужно создать каталог contrib:
# mkdir /usr/lib/python2.7/dist-packages/scapy/contrib

И перенести модуль в свежесозданный каталог:
# cp ospf.py /usr/lib/python2.7/dist-packages/scapy/contrib/

Теперь если зайти в Scapy и просмотреть список подключенных сторонних модулей (за это, как вы догадались, отвечает функция list_contrib()):
>>> list_contrib()
ospf: OSPF status=loads
>>>
Казалось бы, что уже всё готово, но не тут то было.
При очередной попытке подгрузить ospf модуль:
>>> load_contrib('ospf'), получаем всё ту же ошибу «ERROR: No module named contrib.ospf»
Для того, чтобы модуль окончательно заработал, осталось создать скрипт инициализации (пустой файл):
touch /usr/lib/python2.7/dist-packages/scapy/contrib/__init__.py

После этого, уже можно будет создавать пакеты для OSPF.

Создаем трехэтапное TCP-соединение


Для этого нужно будет поймать SYN/ACK ответ, извлечь из него TCP sequence number, увеличить значение на единицу и, собственно, и поместить полученное значение в поле acknowledgement number.
Непростая задача на первый взгляд, но Scapy может справиться и с ней.
Вначале рассмотрим, что нам нужно, для того чтобы всё прошло успешно.

1) Отправить SYN принимающей стороне:
  • собрать заголовок IP, не забыть про адрес отправителя и получателя;
  • собрать TCP заголовок, в котором нужно будет указать TCP порты отправителя и назначения, установить TCP флаги (SYN бит) и сгенерировать ISN (Initial Sequence Number).
2) Поймать ответный пакет:
  • сохранить ответ;
  • извлечь из него TCP sequence number и увеличить это значение на единицу.
3) Создать подтверждение (ACK) на полученный ответный пакет:
  • собрать заголовок IP, содержащий такие же адреса отправителя и получателя, как в случае SYN пакета;
  • собрать TCP заголовок, с такими же номерами портов, как и в SYN сегменте, но уже установить ACK флаг, увеличить значение ISN на единицу и установить acknowledgement в извлеченный и увеличенный, на втором шаге, sequence number.

Для того чтобы стало еще понятней, рассмотрим уже более подробно, с использованием произвольно взятых значений.
К примеру, соединение прошло таким образом:
192.168.10.200 1024 > 192.168.10.50 80 flags=SYN seq=12345
192.168.10.50 80 > 192.168.10.200 1024 flags=SYN, ACK seq=9998 ack=12346
192.168.10.200 1024 > 192.168.10.50 80 flags=ACK seq=12346 ack=9999
Что в итоге нужно было сделать.

1) Отправить SYN принимающей стороне:
  • собрать заголовок IP, в котором указать в качестве отправителя 192.168.10.200 и 192.168.10.50 в качестве получателя;
  • собрать TCP заголовок с портом источника (source) 1024 и портом назначения (destination) 80. Так же установить SYN флаг и сгенерировать ISN равный 12345.
2) Поймать ответный пакет:
  • сохранить ответ;
  • извлечь из него TCP sequence number (9998) и увеличить это значение на единицу, получим 9999.
3) Создать подтверждение (ACK) на полученный ответный пакет:
  • собрать заголовок IP, в котором указать в качестве отправителя 192.168.10.200 и 192.168.10.50 в качестве получателя;
  • собрать TCP заголовок с такими же портами источника и назначения (1024 и 80 соответственно), установить ACK флаг, увеличить ISN на единицу (12346) и установить acknowledgement в увеличенное значение пойманного seq number (9999).
Начнем собирать пакет:



Здесь уже всё должно быть знакомым: пакет собираем из двух частей, инкапсулируя TCP в IP.

Теперь помня о том, что нам нужно будет перехватить ответ, извлечь оттуда sequence number и увеличить на единицу, делаем:



Происходит следующее – функция sr1 отправляет ранее созданный пакет в сеть, а первый пришедший ответ помещается в переменную SYNACK.
А затем, используя конструкцию SYNACK.seq, извлекаем TCP sequence number, увеличиваем его на единицу и сохраняем в переменной my_ack.

Продвигаемся дальше:



Создаем новый заголовок TCP и называем его ACK. В нем устанавливается другой флаг (A — ACK) и увеличивается значение sequence number.
Кроме того, в качестве acknowledgement указывается переменная my_ack.
Затем собранный пакет выбрасывается в сеть командой send (помним, что это L3 команда, которая даже не слушает, что придет в ответ).
Если всё было сделано правильно, то классическое TCP-соединение состоялось.
Осталось только создать TCP сегмент без каких-либо флагов и тоже отправить в сеть.



Как можно увидеть, мы в очередной раз создали экземпляр TCP заголовка (в этот раз, назвав его PUSH), без флагов и со всеми остальными знакомыми уже значениями.
После этого добавили немного данных, используя переменную data, и отправили в сеть, используя ту же функцию send.
И соответственно от получателя должен будет прийти acknowledgement на этот сегмент.

Листинг 2 TCP-соединение


Но здесь есть и несколько подводных камней.
Если посмотреть на этот обмен в Wireshark, можно увидеть, что до того как ушел наш ACK пакет, внезапно был отправлен RST:



Дело в том, что Scapy работает мимо TCP/IP стека ОС. Это означает то, что ОС не подозревает о том, что Scapy отправляет какие-то пакеты.
Соответственно ОС не будет ожидать появления SYN/ACK пакетов. И, следовательно, соединение будет сразу же сброшено.
Очевидно, что это совсем не тот результат, который нам нужен.
Одним из решений такой проблемы будет использование функционала пакетного фильтра, в частности iptables, который сможет блокировать исходящие RST пакеты.
Например, таким образом:
# iptables -A OUTPUT -p tcp -d 192.168.10.50 -s 192.168.10.200 --dport 80 --tcp-flags RST RST -j DROP

Выполнение такой конструкции приведет к тому, что все исходящие пакеты с адресом назначения 192.168.10.50 и с адресом отправителя 192.168.10.200 на 80-й порт, с установленным RST флагом, будут отбрасываться.
Пакеты будут все так же генерироваться силами ОС, но они просто не будут вылетать за ее пределы.
В итоге уже ничего не будет мешать Scapy делать полноценную TCP-сессию:



Продолжаем исследования


Используя Scapy, можно находить хосты в сети, среди указанного множества адресов:



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



По умолчанию, установлен 8-й тип для ICMP, это и есть классический эхо-запрос.

Углубляясь в тему ИБ, попробуем определить версию ОС используя Scapy и nmap.



Итак, рассмотрим что было сделано.
Вначале был подключен внешний модуль, в данном случае nmap.
Затем проверяем, что у нас есть файл (nmap-os-fingerprints) с отпечатками различных ОС.
И запускаем непосредственно определение удаленной операционной системы, за это отвечает функция nmap_fp, где в качестве параметров помимо самой цели, можно еще указать открытый (oport) и закрытый (cport) порты.
Правильно указанные порты помогут сильно улучшить точность определения ОС.

Визуализируем пакеты


Все время мы смотрели на текстовый вывод, местами была псевдографика, но Scapy умеет и выводить некоторые результаты в графическом виде.
Посмотрим, что нам предлагается.
Самое простое — это метод conversations():



При его выполнении, запустится окно ImageMagick, в котором отрисуется схема нашего обмена пакетами, не ахти красиво, но достаточно наглядно.
Это способ, вероятно, лучше всего подойдет, для визуализации дампов с трафиком.

Второй способ заключается в построении 2D графиков, с последующим экспортом их в pdf-файл.



За это уже отвечает функция pdfdump().
Результаты выглядят примерно так:



В данном случае уже вполне неплохо.
Кроме того, функция graph() опять откроет окно ImageMagick, но уже с детальной прорисовкой:



Здесь мы видим результат трассировки, с подробным отображением автономных систем и прочей визуализацией.

И, завершая тему визуализации, а вместе с ней и статью, посмотрим на 3D отображения трассы.
Для этого потребуется VPython и команда trace3D().



Здесь отображена трасса из предыдущего графика.

Но иногда бывают и такие варианты:



В этом примере была проведена трассировка сразу нескольких целей, с использованием нескольких (80, 443) tcp портов.
Левый клик на любом объекте приведет к появлению IP-адреса над ним, а левый клик с зажатой клавишей CTRL – к отображению более подробной информации — портам, как в этом случае.

Эпилог


Итак, мы рассмотрели лишь малую часть утилиты Scapy, но уже это впечатляет.
Возможности, которые предоставляются действительно очень большие.
Статья призвана вызвать у читателя интерес в изучении сетевых протоколов, и не является исчерпывающим руководством по инструменту Scapy.
За использование этой утилиты в каких-либо противоправных целях ни редакция, ни автор ответственности не несут.



В процессе написания статьи использовались материалы Института SANS и официальная документация проекта.

11 комментариев

avatar
Спасибо! Весьма интересная статья .
avatar
Отличнейшая статья! Спасибо вам Александр :)
avatar
Рад, что статья вам понравилась. :-)
avatar
Зачетная статья!
avatar
Александр а с какой версией python(а) в данный момент работает scapy? У меня стоит 2.7, бывает в некоторых случаях тулза сбоит.
avatar
Я использовал
~# python --version
Python 2.7.3
Проблем не было. А разработчики говорят, что можно начиная с версии Python 2.5.
avatar
Спасибо! Значит дело в ядре) у меня точная версия такая же.
avatar
очень интересная статья, как раз сейчас занимаюсь настройкой IDS в сети, пригодится. Можно Вас попросить написать листинг SYN Flood или ICMP Flood. То есть, нужно, чтобы к одному хосту/порту отправить большое количество SYN-запросов или ICMP за короткое время.
Спасибо, огромное.
avatar
Для нагрузочного тестирования можно использовать и scapy, просто зациклив отправку пакетов:
>>>send(IP(dst=" 192.168.10.1 ")/ ICMP(),loop=1)

Но скорость отправки пакетов будет небольшой, поэтому эффективнее использовать например утилиту hping3, с ключами --icmp и --flood.
avatar
Спасибо. Замечательная статья, замечательный блог.
комментарий был удален
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.