ComoDoS: удаленный BSOD через один IPv6-пакет в драйвере Comodo Firewall
Я отправил в службу безопасности Comodo полный отчет, анализ первопричины, рекомендации по исправлению и proof-of-concept. Ответа не последовало. После этого я дважды отправлял повторные письма, последнее из которых содержало лишь просьбу подтвердить получение отчета, но в ответ — полная тишина.На момент публикации уязвимость все еще остается zero-day.
Учитывая отсутствие у компании программы bug bounty, я не готов бесконечно тратить собственное время на попытки достучаться до вендора, который, судя по всему, не заинтересован в обратной связи. Если они захотят исправить проблему, они смогут сделать это независимо от публикации статьи. Более того, в одном из материалов ZDI утверждается, что исследователи почти два года пытались добиться исправления другой уязвимости у этого же поставщика.
Хотя данный баг позволяет удаленно вызвать как выход за границы массива при чтении (OOB Read), так и при записи (OOB Write) в ядре Windows, ограничения обоих примитивов, на мой взгляд, делают создание полноценного RCE-эксплойта маловероятным.
Тем не менее, уязвимость позволяет удаленно вызвать критический сбой системы (BSOD) всего одним TCP/IP-пакетом, даже если брандмауэр настроен на блокировку всех портов.
От исследований BYOVD к удаленной эксплуатации ядра
Два месяца назад я начал разрабатывать автономную систему поиска уязвимостей повышения привилегий в сторонних драйверах ядра Windows. Большие языковые модели отлично справляются с обнаружением относительно простых ошибок, которые затем активно эксплуатируются злоумышленниками в атаках класса BYOVD (Bring Your Own Vulnerable Driver). Мне показалось, что это идеальный сценарий использования ИИ для моей основной работы в Expel, где я занимаюсь MDR.Хотя моя система пока умеет находить в основном очевидные ошибки, меня удивило, насколько часто она отмечала драйверы различных поставщиков решений по кибербезопасности, особенно производителей антивирусов и межсетевых экранов. Получается довольно ироничная ситуация: защитный продукт потенциально можно обойти, используя его же собственный драйвер для повышения привилегий.
В какой-то момент я подумал: если ИИ способен "потрясти" драйвер случайного вендора и из него буквально высыпаются zero-day уязвимости локального повышения привилегий, то ручной анализ наверняка позволит найти что-нибудь интереснее.
Inspect.sys — драйвер файрвола Comodo Internet Security
На драйвер файрвола Comodo я наткнулся случайно. Из-за ошибки в моем конвейере анализа ИИ обработал очень старую версию Inspect.sys — драйвера брандмауэра Comodo. Система должна была анализировать только актуальные версии, поэтому драйвер образца 2014 года получил значительно более высокий рейтинг потенциальной эксплуатируемости по сравнению с остальными кандидатами.Изучая давно исправленные уязвимости, я заметил характерную тенденцию к неудачным архитектурным решениям, что вселило надежду найти zero-day и в актуальной версии драйвера.
Найти баг легко. Найти полезный баг — гораздо сложнее
Первая найденная ошибка обнаружилась буквально в первом месте, куда я посмотрел, — в парсере заголовков IP.Уязвимости в обработке IP особенно интересны, поскольку брандмауэр обязан разобрать пакет еще до принятия решения о его блокировке или пропуске. Это означает, что обработка выполняется до применения любых правил фильтрации, а значит, подобные ошибки могут эксплуатироваться даже тогда, когда закрыты абсолютно все порты.
Ошибка в IPv4-парсере настолько проста, что сложно поверить, как ее могли пропустить. Правда, практическая ценность у нее невелика из-за требований RFC.
Кратко суть такова: IPv4-пакет содержит два поля длины:
- длину самого IP-заголовка (IHL);
- общую длину заголовка и данных (Total Length).
Парсер просто вычитает размер заголовка (IHL) из общего размера пакета. Если указать IHL больше Total Length, происходит целочисленное переполнение вниз (integer underflow), и драйвер вычисляет размер полезной нагрузки как примерно 4 миллиарда байт (0xFFFFFFFC).
К сожалению, RFC требует, чтобы маршрутизаторы проверяли соответствие этих полей. Поэтому большинство маршрутизаторов должны отбросить такой пакет задолго до того, как он достигнет цели.
Локальная эксплуатация или атака внутри одной локальной сети все еще возможны (если пакет не проходит через маршрутизатор или L3-коммутатор), но это не особенно интересно. Всех интересуют удаленные эксплойты.
IPv6 никогда не бывает ответом... если только вопрос не звучит как "где найти более интересные уязвимости?"
С IPv6 ситуация гораздо интереснее по двум причинам.Во-первых, обработка IPv6 значительно сложнее, чем IPv4, а сложный код обычно содержит больше ошибок.
Во-вторых, спецификация IPv6 допускает несколько больше нестандартных конструкций, не требуя немедленного отклонения пакета. Это важно, поскольку интернет-пакет проходит через множество маршрутизаторов, прежде чем достигнет целевой системы.
Стоит отметить, что некоторые провайдеры идут гораздо дальше требований RFC. Облачные платформы нередко полностью блокируют отдельные типы IPv6-заголовков или проводят дополнительную валидацию.
Этот вывод стоил мне целого дня попыток протестировать эксплойт на виртуальной машине Azure, поскольку мой интернет-провайдер вообще не поддерживает IPv6.
Любопытно, что предыдущая исследованная мной уязвимость тоже была связана с IPv6. Правда, тогда речь шла об ошибке в самом ядре Windows, найденной путем анализа патча. На этот раз приятно было обнаружить что-то самостоятельно.
Краткое введение в IPv6
An IPv6 header with an example optional header attached.
Серая область пакета представляет собой фиксированный заголовок IPv6 — обязательную часть любого IPv6-пакета. Его размер всегда составляет ровно 40 байт.
Кроме него существуют расширенные (extension) заголовки, позволяющие добавлять дополнительную информацию. На схеме они выделены синим цветом.
Поле Next Header определяет тип следующего заголовка. Для TCP оно содержит значение 6.
Также оно может принимать значения различных расширений IPv6:
- 0 — Hop-by-Hop Options
- 43 — Routing Header
- 44 — Fragment Header
- 50 — Encapsulating Security Payload
- 51 — Authentication Header
- 60 — Destination Options
- 135 — Mobility Header
- 139 — Host Identity Protocol
- 140 — Shim6
- 253 — Experimental
- 254 — Experimental
Более того, такие заголовки можно комбинировать и выстраивать в цепочки.
Обычно существует два подхода к их обработке:
- Если размер IPv6-пакета отличается от 40 байт — сразу отбросить его. Лично я считаю этот подход наиболее разумным.
- Последовательно обходить всю цепочку расширений, суммируя размеры каждого заголовка.
IPv6-парсер: "по крайней мере, вы попытались"
two consecutive snippets of code illustrating the entire parse function.
Парсер IPv6 в Inspect.sys использует структуру, которую я условно назвал packet_desc.
В конце первого фрагмента кода значение packet_desc->payload_length to ext_hdr_len получает длину полезной нагрузки из фиксированного IPv6-заголовка (Payload Length). В норме это поле должно содержать суммарный размер всех данных после фиксированного заголовка.
Для поиска заголовка транспортного уровня драйвер проходит по всей цепочке расширений и вычисляет размер каждого из них.
Уязвимость находится в строке:
Код:
packet_desc->payload_length -= ext_hdr_len
При обработке каждого расширенного заголовка его длина вычитается из payload_length.
Проблема заключается в том, что значение payload_length полностью контролируется атакующим через поле Payload Length IPv6-заголовка.
Никакой проверки корректности этого значения не выполняется.
В коде присутствует множество защит от переполнений буфера и даже проверок теоретически невозможных ситуаций, однако отсутствует самая очевидная — проверка того, что размер полезной нагрузки действительно соответствует суммарному размеру расширенных заголовков.
Если указать Payload Length меньше суммы всех extension headers, переменная будет уменьшаться до тех пор, пока не станет отрицательной.
Но поскольку это беззнаковое 64-битное число, вместо отрицательного значения произойдет переполнение, и оно превратится в:
0xFFFFFFFFFFFFFFF8
или приблизительно 18 квинтиллионов байт.
Proof-of-Concept
PoC получился настолько маленьким, что его можно было бы разместить в одном сообщении социальной сети:
Код:
ext = IPv6ExtHdrDestOpt(nh=6, options=[PadN(optdata=b"\x00" * 8)])
tcp = TCP(sport=1337, dport=80, flags="S", seq=0, ack=1, window=0x2000)
ipv6 = IPv6(dst=dst_ip, nh=60, hlim=64, plen=8)
pkt = ipv6 / ext / tcp
send(packet)
Эксплойт, который я назвал ComoDoS, позволяет вызвать BSOD удаленной системы всего одним IPv6-пакетом.
Поскольку ошибка находится непосредственно в драйвере файрвола, совершенно неважно, открыт целевой порт или закрыт.
Если система не поддерживает IPv6, воспроизвести сбой можно локально, включив опцию Filter IPv6 traffic в Comodo Firewall и отправив IPv6-пакет напрямую на MAC-адрес сетевой карты.
Как одна переменная превращает потенциальный RCE в обычный DoS
Хотя существует достижимый путь выполнения, приводящий к OOB-записи, его ограничения делают практическую эксплуатацию крайне сомнительной.OOB-чтение возникает в функции, которая, судя по всему, ищет в сетевом трафике артефакты, связанные с WebDAV.
A snippet from part of the scanner function looking for WebDAV headers.
The parent function which calls the HTTP scanner function.
Переменная payload_len, передаваемая в функцию ScanForHTTPArtifacts, — это та самая величина, которая подвергается integer underflow в IPv6-парсере.
На этом этапе она усекается до 16 бит, уменьшаясь с 16 эксабайт до вполне обычных 64 КБ.
Если происходит полное чтение этих 64 КБ, существует высокая вероятность выхода за пределы выделенной памяти ядра и возникновения page fault.
Поскольку драйвер работает на уровне DISPATCH_LEVEL, любой page fault приводит к немедленному аварийному завершению всей системы.
Однако анализ функции показывает, что OOB-чтения можно избежать.
A snippet from ScanForHTTPArtifacts.
Сканер просматривает TCP-полезную нагрузку до тех пор, пока не встретит символы 0xD или 0xA (\r или \n), после чего пытается обнаружить HTTP-заголовок.
Минимальная строка:
Код:
GET / HTTP/1.0\r\n\r\n
Поскольку здесь не используется отслеживание TCP-соединений, полноценное TCP-рукопожатие не требуется. Достаточно отправить одиночный TCP-пакет с вредоносным IPv6-заголовком и строкой HTTP.
Само OOB-чтение удаленно практически бесполезно, поскольку полученные данные нигде не используются. Однако предотвращение сбоя таким способом открывает путь к OOB-записи.
Почему это, скорее всего, нельзя превратить в RCE
Единственное место, где поврежденное значение размера используется для записи, — это вызов memcpy.A snippet of the memcpy which uses our corrupted size value.
Для достижения этого участка кода требуется полноценное TCP-соединение, а значит, на целевой системе должен существовать хотя бы один открытый TCP-порт.
Параметр размера (arg3) представляет собой нашу переполненную переменную, а источником копирования служат TCP-данные пакета.
Проблема заключается в том, что здесь значение усекается уже до 32 бит, а не до 16.
В результате размер операции memcpy составляет около 4 ГБ, что гарантированно приводит к краху системы.
Теоретически, если бы величину underflow можно было уменьшить до более реалистичного значения, уязвимость могла бы стать основой для полноценного RCE. Однако максимальный размер стандартного сетевого пакета ограничен примерно 65 КБ, а этого совершенно недостаточно, чтобы превратить переполнение на 4 ГБ в управляемую запись.
Я вполне готов признать поражение — но само исследование определенно того стоило.
Proof of Concept
Ниже в оригинальной публикации приведены демонстрационное видео работы PoC и ссылка на исходный код.Полный Proof-of-Concept:
GitHub - MalwareTech/ComoDoS: A remote denial-of-service zero day vulnerability in Comodo Internet Security firewall
Источник