Учебник IPTables U32

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

Однако, есть вариант при котором не требуется уметь писать программы. Дон Коэн был настолько добр, что написал модуль для IPtables, который выделяет любой требуемый набор байт из пакета, делает нужные преобразования и проверяет укладывается ли результат в заданный диапазон. Например, можно взять значение поля Fragmentation из заголовка IP пакета, выкинуть все, кроме флага More Fragments и узнать поднят флаг или нет.

Не написав ни строчки кода на С 🙂

Я расскажу об основных подходах и надеюсь что примеров будет достаточно, чтобы вы могли писать собственные проверки.

Я не буду концентрироваться на том что поля значат или почему вам нужно их проверять, для этого есть множество (внимание! бесстыдная ссылка на моего работодателя!) ресурсов. Если вам просто хочется узнать список заголовков пакета, то читайте.

В этой статье считается что нумерация байт начинается с нуля. Например, в IP заголовке байт «0» хранит 4 бита поля «Version» и 4 бита поля «IP Header Length», байт «1» хранит поле «TOS» и т.д.

Проверка значения двухбайтового поля

В простейшей форме, u32 вырезает блок из 4 байт начиная со Start, применяет к ним маску Mask и сравнивает результат с Range. Синтаксис параметров выглядит так:

iptables -m u32 --u32 "Start&Mask=Range"

Обычно мы будем брать значение для «Start» на 3 меньше, чем последний интересующий нас байт. Иными словами, если вам нужны байты 4 и 5 из IP заголовка (поле IP ID), Start должно быть 5-3=2. Маска вырезает все что нам не нужно, это обычная битовая маска, максимальное значение которой 0xFFFFFFFF. Чтобы получить наши целевые байты 4 или 5, нам нужно выбросить байты 2 и 3. Для этого используем маску: 0x0000FFFF. В команде можно писать сокращенный вариант 0xFFFF.

То есть, чтобы получить IPIDы с 2 по 256, нам нужно использовать такую команду:

iptables -m u32 --u32 "2&0xFFFF=0x2:0x0100"

Если читать слева направо: «Загрузи модуль u32, и выполни следующие u32-проверки для пакета: возьми 4 байта, начиная с второго (байты 2 и 3 это поле Total Length, а байты 4 и 5 — IPID), примени к ним маску 0x0000FFFF (это сбросит первые два байта на 0, а последние два оставит неизменными) и проверь, попадает ли их значение — IPID — в интервал от 2 до 256. Если да, то верни истину, в противном случае — ложь»

В IPTables нет отдельной проверки на IPID, но это эквивалент для bpf фильтра "ip[2:2] >= 2 and ip[2:2] <= 256"в tcpdump.

В примере я опустил действие, но это может быть что-то вроде:

-j LOG --log-prefix "ID-in-2-256" -j DROP

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

Например, чтобы выяснить больше ли или равна 256 общая длина пакета Дон предлагает следующую проверку. Общая длина хранится в байтах 2 и 3 IP заголовка, значит наша стартовая позиция это 3-3=0. Так как нам опять нужно два байта, то оставим маску без изменений. Тогда выражение выглядит вот так:

iptables -m u32 --u32 "0&0xFFFF=0x100:0xFFFF"

Что эквивалентно:

iptables -m length --length 256:65535

или для bpf фильтра

"len >= 256"

Проверка значения однобайтового поля

В основном все так же, кроме маски, которая будет 0x000000FF (или в сокращенной форме 0xFF) и позволит выделить 1 байт из тех 4-х, что изначально берет u32. Пусть мы хотим определить не трейсроутит ли нас кто-нибудь, для этого можно, например, выяснить меньше ли значение поля TTL трех или нет. Конечно для этого можно применить модуль ttl, но давайте посмотрим как это делается с помощью u32.

Мы хотим получить из заголовка восьмой байт, значит стартовая позиция должна быть 8-3=5. Для этого нужно сделать вот такое:

iptables -m u32 --u32 "5&0xFF=0:3"

Что аналогично:

iptables -m ttl --ttl-lt 4

или для bpf фильтра

"ip[8] <= 3"

Проверка всех 4-х байт

Чтобы проверить весь IP адрес назначения мы исследуем байты 16-19. Так как нам нужны все 4 байта, то маска не нужна. Выясним, совпадает ли адрес назначения с 224.0.0.1:

iptables -m u32 --u32 "16=0xE0000001"

Что эквивалентно:

iptables -d 224.0.0.1/32

Если нам требуется проверить только первые три байта (чтобы выяснить входит ли исходящий адрес в заданную подсеть С класса), то нужно наложить маску. В этом случае, нам требуется отбросить последний октет, тогда она будет выглядеть вот так: 0xFFFFFF00. Давайте проверим входит ли исходящий адрес (байты 12-15, но 15-й байт мы отбросим) в подсеть класса С 192.168.15.0 (0xC0A80F00):

iptables -m u32 --u32 "12&0xFFFFFF00=0xC0A80F00"

Что соответствует:

iptables -s 192.168.15.0/24

Исследование младших байт в заголовке

Очевидно, что для получения значения поля TOS (байт 1 в заголовке), нам нужно начать с байта 1-3=-2. Вместо этого, мы начнем с байта 0, выделим нужный байт и сместим его ниже на последнюю позицию для упрощения проверки. Не обязательно идти именно таким путем, но мы используем эту возможность для демонстрации функции, которая вскоре понадобится.

Чтобы получить поле TOS мы сначала попросим у u32 байты 0-3, потом выделим байт 1 (второй байт в блоке) с помощью маски 0x00FF0000. Нам нужно сдвинуть значение TOS ниже на крайнюю правую позицию, чтобы упросить сравнение. Чтобы это сделать, мы используем функцию, которая имеет очевидное название «сдвиг вправо». Функция обозначается как «>>», после этих символов пишется количество бит на которые нужно сдвинуть аргумент (то что написано слева от «>>») вправо. Если вы не знакомы с правым сдвигом, то читайте этот учебник из Харпер Колледж.

Мы хотим сдвинуть TOS на 2 байта, или 16 бит, вправо. То есть функция выглядит как «>>16». Теперь, имея TOS на нужной позиции, мы сравним его с 0х08 (Maximize Throughput):

iptables -m u32 --u32 "0&0x00FF0000>>16=0x08"

или:

iptables -m ttl --tos 8

Исследование отдельных битов

Пусть мы хотим узнать состояние флага More Fragments, значение этого флага нельзя проверить с помощью iptables (-f использует 2-й и последующий аргументы, нам нужно сравнить все фраменты, кроме последнего). Флаг хранится в байте 6, значит нам нужно использовать смещение 3 и выбросить байты 3-5. Мы могли бы как обычно воспользоваться маской 0x00000FF, но нам требуется сохранить единственный бит: третий слева (0010 0000), поэтому используем маску 0x00000020. Теперь у нас есть два варианта: сдвинуть бит на крайнюю правую позицию или оставить на месте.

В первом случае, сдвинем его вправо на 5 битов. Это выглядит так:

iptables -m u32 --u32 "3&0x20>>5=1"

Если мы решим сохранить бит на месте, то нам нужно выбрать верное значение для сравнения. Пусть мы хотим узнать поднят ли этот бит, тогда сравнить нужно с 0x20.

iptables -m u32 --u32 "3&0x20=0x20"

Оба подхода вернут истину, в том случае если флаг More Fragments поднят.

Комбинация тестов

Если мы хотим проверить несколько элементов пакета, то нужно использовать оператор:

&&

между тестами.

Переходим к заголовку TCP

И здесь начинаются сложности. Пусть мы хотим посмотреть на байты 4-7 в заголовке TCP (TCP sequence number). Давайте сначала сделаем примитивную реализацию, которую постепенно улучшим.

Для начала, предположим что длина IP заголовка 20 байт, что обычно так и есть. Тогда наша стартовая позиция это 40-й байт TCP заголовка, который следует сразу на IP заголовком. Простая проверка совпадает ли номер с числом 21 (29h) может выглядеть как:

iptables -m u32 --u32 "24=0x29"

Это так же сработает и для пакетов с заголовком длиннее 20 байт, но тут могут быть проблемы. Давайте их исправим одну за другой.

Во-первых, мы нигде не проверяем является ли наш пакет TCP пакетом. Эта информация хранится в байте 9 IP заголовка. Возмем 4 байта, начиная с байта 5, отбросим байы 6-8 и посмотрим не совпадает ли 9-й с 6h. Новое правило, которое проверяет пакет на TCP и сравнивает Sequence Number с 41:

iptables -m u32 --u32 "6&0xFF=0x6 && 24=0x29"

Во-вторых, мы проигнорировали длину IP заголовка. Обычно она имеет равна 20 байтам, но она может быть больше, если заданы опции IP.

Что мы сделаем. Мы найдем длину IP заголовка (полубайт, который дает количество четырехбайтовых слов в заголовке, чаще всего это 5). Мы умножим это число на 4, чтобы определить число байт в IP заголовке. Полученное значение даст количества байт на которые нужно перейти, чтобы попасть на начло заголовка TCP. И добавим 4 байта, чтобы получить Sequence number.

Чтобы получить длину заголовка нам нужно сделать сдвиг на 24 бита: "0>>24", но нам требуется только младший полубайт умноженный на 4, чтобы получить значение байт в заголовке. Чтобы получить операцию умножения на 4, сдвинем не на 24, а на 22 бита. Одновременно со сдвигом используем маску 0x3C вместо 0x0F. Тогда выражение будет выглядеть как: "0>>22&0x3C". Для IP заголовка без опций оно, как и ожидалось, вернет 20. Теперь нам нужно убедить u32 использовать это число и сдвинуться на это количество байт внутри пакета, это делается с помощью оператора «@».

iptables -m u32 --u32 "6&0xFF=0x6 && 0>>22&0x3C@4=0x29"

Оператор «@» берет число слева от себя (обычно 20) и переходит на соответствующее количество байт вперед (мы можем выполнить эту операцию несколько раз, см. о данных TCP ниже). Число 4 справа от него говорит u32 взять байты 4-7, но u32 достаточно умен и берет их с учетом пропущенных байт. Это дает Sequence Number, даже если IP заголовок увеличился из-за опций. Уф!

Последняя особенность это фрагментация. Пока мы работали с IP заголовками, то такой проблемы не возникало, IP спроектирован так, чтобы его заголовок никогда не фрагментировался. А вот заголовок TCP и данные приложения фрагментироваться могут и если нам попался второй и последующий фрагменты, то получая 4-7 байты мы будем брать значения из других частей TCP заголовка или, что более вероятно, данные с прикладного уровня.

Чтобы удостовериться что мы смотрим на нужную часть TCP заголовка мы проверим является ли пакет первым фрагментом (или нефрагментированным пакетом, что в нашем случае одно и тоже). Для этого выясним равно ли нулю значение смещения фрагмента в IP заголовке: "4&0x1FFF=0".

Итоговое выражение (проверка на TCP, проверка на фрагментацию, переход на конец IP заголовка, проверка равны ли 4-7-й байты числу 41) выглядит так:

iptables -m u32 --u32 "6&0xFF=0x6 && 4&0x1FFF=0 && 0>>22&0x3C@4=0x29"

Если пакет все-таки фрагментирован, то нам нужно учесть еще одну вещь, фрагмент может быть настолько мал, что наше поле было помещено в следующий фрагмент! Конкретно в этом случае это не проблема, так как IP соединение должно обрабатывать пакеты с длиной как минимум в 68 байт, даже если IP заголовок будет иметь максимально возможную длину (60 байт), то первые 8 байт TCP заголовка будут включены в фрагмент.

Когда мы начнем разбирать поля дальше в пакете, нам придется полагаться на то, что u32 просто возвращает ложь, если мы попытаемся запросить значение, которое выходит за пределы исследуемого пакета.

Проверка значений в ICMP заголовке

Давайте посмотрим на ICMP Host Unreachables (ICMP, тип 3, код 1). Так же как и в предыдущем примере, нам нужно проверить значение поля Protocol (в случае ICMP это должно быть 1) и что мы получили как минимум первый фрагмент: "6&0xFF=1 && 4&0x1FFF=0"

Чтобы проверить значение ICMP Type и ICMP Code, мы опять пропустим IP заголовок ("0>>22&0x3C@..."). Возьмем первые 2 байта, начнем со смещения 0 и сдвинем вправо на 16 бит. В результате получим:

iptables -m u32 --u32 "6&0xFF=1 && 4&0x1FFF=0 && 0>>22&0x3C@0>>16=0x0301"

Проверка значений в данных UDP

Давайте попробуем углубиться в данные пакета и выясить, являются ли UDP пакеты DNS запросами. Мы не только проверим что порт назначения равен 53, но и посмотрим на старший бит второго байта данных. Если он поднят, то это DNS запрос.

Проверим что это UDP пакет: "6&0xFF=17". Добавим уже знакомую проверку на фрагментацию: "4&0x1FFF=0".

Чтобы выяснить порт назначения, возьмем 2 и 3 байты из заголовка UDP (пропустив IP заголовок, как делали раньше): "0>>22&0x3C@0&0xFFFF=53".

Если пакет прошел все предыдущие проверки, вернемся назад и проверим данные (напомню, нам нужно сместиться на длину IP заголовка и 8 байт UDP заголовков "0>>22&0x3C@8 ...") чтобы убедиться что у нас DNS запрос, а не ответ. Для получения старшего бита байта 2, мы используем смещение 8, правым сдвигом на 15 бит мы сместим первые 4 байта данных (где и находится Query) на крайнюю правую позицию, далее выбросим все остальные биты с помощью маски 0x01: "0>>22&0x3C@8>>15&0x01=1".

В результате тест выглядит так:

iptables -m u32 --u32 "6&0xFF=17 && 4&0x1FFF=0 && 0>>22&0x3C@0&0xFFFF=53 && 0>>22&0x3C@8>>15&0x01=1"

Вот жеж. Мне попадался белый шум с меньшим количеством энтропии 🙂 Обратите внимание что мы все делаем проверки с помощью u32, мы можем передать проверки на udp, фрагментацию и порт другим модулям и в результате получить более читабельную версию:

iptables -p udp --dport 53 \! -f -m u32 --u32 "0>>22&0x3C@8>>15&0x01=1"

Проверка значений из данных TCP

Как и с данными udp в предыдущем примере, мы должны быть абсолютно уверены в положении интересующих данных. В этом примере, мы попытаемся обнаружить сессии ssh, даже если они на порту отличном от 22. Погодите-ка, соединения ssh зашифрованы! Это же невозможно? А вот и нет.

При инициализации ssh соединении, первое что посылается от сервера клиенту это строка протокола, которая выглядит как: SSH-protoversion-softwareversion comments

Значение protoversion это, например, 1.99, 2.0 или 1.5. Вся строка, включая конечные перевод строки и возврат на начало строки, должны быть меньше чем 255 байт. Перед этой строкой может быть другая, но это нарушит совместимость с клиентами ssh версии 1.0, будем считать, что это случается нечасто. Более детально об этом можно можно прочитать в draft-ietf-secsh-transport-17.txt

Наиболее важная часть здесь это строка «SSH-«, так как она попадает в первые 4 байта соединения. Ура! Мы можем использовать модуль u32 чтобы заглянуть в эти байты на ранних этапах соединения, это значительно менее ресурсоемкая проверка, чем с помощью модуля string и позволяет искать ssh соединения на любом порту без дополнительной нагрузки на фаервол.

Давайте разберемся с самым простым. Нам нужно проверять только tcp пакеты, нефрагментированные, нам нужны только пакеты из первых 255 байт соединения, относящиеся к установленному соединению и, очевидно, общая длина пакета должна быть между 45 и 375 байтами (учитывайте минимальные и максимальные значения для длин заголовков IP и TCP, а так же длины строки ssh протокола):

iptables -p tcp \! -f -m connbytes --connbytes 0:255 -m state --state ESTABLISHED -m length --length 46:375

С такими ограничениями нам потребуется обработать очень малое количество пакетов, и  просматривая лишь несколько первых пакетов мы сможем разрешать или запрещать ssh соединения.

Теперь вступает в действие магия u32. Построим все с нуля. Так как мы уже определили что это TCP протокол, то опустим 6&0xFF=0x6. Пропустим IP заголовок:

0>>22&0x3C@

Теперь нам нужно пропуститьTCP заголовок, получим его длину из первой половины байта 12. Хотя нам и нужно сдвинуться вправо на 28 бит, чтобы поместить значение длины на последние 4 бита нашего 32-х битового буфера, нам так же нужно умножить его на 4, чтобы получить количество 4-х байтовых слов, поэтому мы сместим вправо на 26 бит и опять применим маску 0x3C:

0>>22&0x3C@ 12>>26&0x3C@

Это переносит нас в раздел данных TCP пакета. Так как байты, которые нам нужно изучить, это первые 4 байта пакета, мы просто возьмем 4 байта, начиная с 0 и сравним их с числом 0x5353482D (это шестнадцатеричный эквивалент строки «SSH-«):

0>>22&0x3C@ 12>>26&0x3C@ 0=0x5353482D

Вот все выражение, для экономии места слитое в одну строку:

iptables -p tcp \! -f -m connbytes --connbytes 0:255 -m state --state ESTABLISHED -m length --length 46:375 -m u32 --u32 "0>>22&0x3C@ 12>>26&0x3C@ 0=0x5353482D"

Сделав это, мы можем выполнить проверку на конкретную версию SSH протокола -m string --string "SSH-1.99" или еще более жестко -m string --string "SSH-1.99-OpenSSH_3.7.1p2". Хотя такое сравнение строк обычно очень дорогая операция в терминах процессорного времени на пакет, в этом случае все не так плохо, так как благодаря предыдущим фильтрам iptables этот пакет почти наверняка содержит строку протокола SSH. Явно требуя наличия строки «SSH-» в первых 4-х байтах соединения, мы избегаем нужды проверять наличие строки протокола для каждого пакета, когда кто-нибудь качает это статью через порт 80.

В заключение

Как сказал Дуглас Адамс: «Без паники»

Примеры, которые мы разобрали, ужасающе сложные. Что хорошего? Не нужно их писать когда каждый раз, когда требуется получить что-то еще в ICMP заголовке, данных TCP или где-то еще. Просто берите нужную проверку выше или из списка ниже и дополните его нужным полем, значение которого требуется оценить.

Проверки

Соберем все пройденное раньше и добавим несколько новых.

  • "2&0xFFFF=0x2:0x0100" Проверка того, что IPIDы между 2 и 256
  • "0&0xFFFF=0x100:0xFFFF" Проверяет что длина пакета 256 или более байт
  • "5&0xFF=0:3" Ищет пакеты с TTL меньшим или равным 3
  • "16=0xE0000001" IP адрес назначения совпадает с 224.0.0.1
  • "12&0xFFFFFF00=0xC0A80F00" Исходный IP принадлежит подсети 192.168.15.X.
  • 0&0x00FF0000>>16=0x08 Равно ли поле TOS 8 (Maximize Throughput)?
  • "3&0x20>>5=1" Поднят ли флаг More Fragments?
  • "6&0xFF=0x6" Это TCP пакет?
  • "4&0x1FFF=0" Равно ли смещение фрагмента 0? (Если да, то либо это нефрагментированный пакет, либо первый фрагмент).
  • "0>>22&0x3C@4=0x29" Равно ли TCP Sequence числу 41? (Это ребует наличия двух предыдущих проверок на протокол TCP и фрагментацию)
  • "0>>22&0x3C@0>>16=0x0301" Равны ли ICMP type=3 и ICMP code=1 (требует проверок на UDP и фрагментацию)
  • "0>>22&0x3C@0&0xFFFF=53" Равен ли UDP порт назначения 53? (требует проверок на UDP и фрагментацию)
  • "0>>22&0x3C@8>>15&0x01=1" Проверяет поднят ли бит UDP DNS запроса (требует проверок на UDP, фрагментацию и порт назначения 53).
  • "0>>22&0x3C@ 12>>26&0x3C@ 0=0x5353482D" Совпадают ли первые 4 байта данных TCP пакета с строкой «SSH-«? (требует дополнительных проверок, вся команда такая: iptables -p tcp \! -f -m connbytes --connbytes 0:255 -m state --state ESTABLISHED -m length --length 46:375 -m u32 --u32 "0>>22&0x3C@ 12>>26&0x3C@ 0=0x5353482D"

А теперь новые проверки:

  • "6&0xFF=1" Это ICMP пакет? (взято из документации Дона Коэна)
  • "6&0xFF=17" Это UDP пакет?
  • "4&0x3FFF=0" Равно ли смещение фрагмента 0 и опущен ли MF? (если да, то это нефрагментированый пакет).
  • "4&0x3FFF=1:0x3FFF" Больше ли нуля смещение фрагмента или поднян MF? (если да, то это фрагмент).
  • 0>>22&0x3C@12>>26&0x3C@-3&0xFF=0:255 Есть ли какие-нибудь данные в TCP пакете (требуется проверка на TCP и фрагментацию)? Это элегантное решение было предложено Доном Коэном когда я рыскал в поисках данных в SYN пакете. Просто проверяя есть ли в нулевом байте данных число от 0 до 255, мы получим истину если нулевой байт существует (что значит данные есть) или ложь в противном случае.
  • "0>>22&0x3C@8=3000" Равно ли значение поля ACK TCP 3000? (требует проверок на TCP и фрагментацию)
  • "0>>22&0x3C@10&0x80=0x80" Поднят ли старший бит (CWR) у поля TCP flag байта 13? (требует проверок на TCP и фрагментацию). Чтобы выяснить опущен ли он, то используйте =0 вместо =0x80 в конце.
  • "0>>22&0x3C@10&0x40=0x40" Поднят ли следующий бит (ECN-Echo) ? Аналогично, =0 если нужен опущенный флаг.
  • "3&0xE0=0x20" Поднял ли бит More Fragments и опущены ли флаги Reserved и Don’t fragment?
  • "3&0x20=0x20" Тоже проверяет поднят ли бит MF, но игнорирует Reserved или Don’t fragment.
  • "3&0xE0=0x60" Подняты ли More Fragments и Don’t fragment и опущен ли Reserved?
  • "3&0xE0=0x80" Поднят ли Reserved и опущены ли More Fragments и Don’t fragment?
  • "0>>22&0x3C@2&0xFFFF=0" Равен ли ICMP ID (внутри эхо-запроса или ответного пакета) 0? Требует проверку на ICMP, фрагментацию и эхо-запрос/ответ.
  • "0>>22&0x3C@4&0xFFFF=35" Равен ли номер ICMP echo Sequence 35?

Дон Коэн написал модуль u32 и (уж простите меня) несколько запутанную документацию внутри кода модуля. Вильям Стернс написал этот текст, используя некоторые примеры и подходы из документации Дона. Большое спасибо Дону за оценку раннего черговика этой статьи. Спасибо так же Гэри Кесслеру и компании Sans, за то, что они сделали документацию по TCP/IP пакетам общедоступной.

Оригинальный текст