Побег из тюрьмы. О способах обхода изоляции FreeBSD Jails
Ниже приведу текст доклада. Оригинал здесь.
FreeBSD Jails, несмотря на их зрелость и широкое использование, содержат критические уязвимости, позволяющие злоумышленнику, получившему права root внутри jail, сбежать на хост-систему.
Это стало возможным из-за фундаментального несоответствия между исторической моделью доверия к root и современными требованиями безопасности контейнеров.
Ключевые аспекты вывода:
- Наследие кода как главный риск: значительная часть кода ядра, особенно в сетевых подсистемах и драйверах, была написана десятилетия назад, когда единственной защитой был принцип «только root может это делать». Этот код не проектировался с учётом модели угроз, где root внутри jail — это враждебный актор. В результате он изобилует классическими уязвимостями (переполнения буфера, чтение за границами, утечки памяти), которые легко эксплуатируются.
- Модель безопасности не выдержала проверки: аудит привилегий, доступных jail, показал, что многие опасные операции (например, чтение памяти ядра — kmem) формально являются «осведомлёнными о jail», хотя на практике их почти никогда не стоит включать. Это создаёт избыточную и рискованную поверхность атаки.
- Эксплуатация — не ракетостроение: создание полноценного эксплойта для побега из jail оказалось удивительно простой задачей даже для исследователей с небольшим опытом в этой области. Этому способствовали:
- Отсутствие современных механизмов защиты (ASLR для кода ядра, теневые стеки).
- Наличие удобных примитивов для обхода существующих защит (например, лёгкий способ получить указатели на структуры ядра из пользовательского пространства через sysctl).
- Возможность загрузки собственных модулей ядра (KLD), что превращает сложную разработку шелл-кода в написание тривиального C-кода с полными привилегиями.
- Необходимы системные изменения: точечные исправления найденных багов — лишь полумера. Для реального усиления защиты требуются фундаментальные изменения:
- Полный аудит и рефакторинг устаревшего кода, особенно в интерфейсах, доступных из jail.
- Внедрение современных механизмов защиты (ASLR для ядра, CET) на уровне всей системы.
- Пересмотр архитектуры: отказ от практики передачи указателей ядра в пользовательское пространство, ужесточение политики загрузки модулей.
- Постепенный переход к memory-safe языкам (например, Rust) для написания нового кода ядра и драйверов, что кардинально снизит количество уязвимостей, связанных с памятью.
Исследование демонстрирует, что «проверенность временем» не равнозначна безопасности. Технологии изоляции, созданные в одну эпоху, должны постоянно адаптироваться к реалиям и инструментам другой. Безопасность FreeBSD Jails требует не просто патчей, а переосмысления подходов к безопасности ядра в условиях, где root больше не является безусловно доверенной сущностью.
Илья: Хорошо, наше выступление называется «Побег из тюрьмы: анализ безопасности FreeBSD Jails». Прежде чем углубиться в тему, позвольте нам представиться. Привет, меня зовут Илья. Я работаю в компании IOActive. Мы занимаемся компьютерной безопасностью. Конференция CCC — моя самая любимая конференция в мире. Я посещаю каждую из них, начиная с 18C3. Думаю, для меня это уже 21-я или 22-я. Я выступал здесь несколько раз ранее — по фаззингу, анализу кода, некоторым аспектам Linux, PST, Windows и другим темам. А это — мое следующее выступление.
Майкл: Привет, я Майкл. Я независимый программист и системный администратор с 20-летним стажем. Моя первая конференция была в 2004 году. А вторая — вот эта, спустя 21 год. И на обеих я был с Ильей. Так что да, спасибо. Компьютеры — это и мое хобби, и работа. Они переплетены. Я не слишком привязан к конкретным языкам программирования или операционным системам. Мне просто нравится переворачивать биты, понимаете? Круто. Ладно.
Илья: Итак, для кого это выступление? По сути, если вы фанат безопасности и любите вопросы безопасности, анализ кода, или вам нравятся операционные системы, или защитная безопасность, или наступательная безопасность — здесь будут моменты, которые вам понравятся. Но даже если вы не фанат безопасности, если вы парень с BSD, вам что-то да понравится. Возможно, не все, но что-то интересное для вас здесь будет. Если вы занимаетесь виртуализацией и контейнеризацией, здесь тоже есть материал. Если вы просто помешанный на ОС гик, здесь тоже есть что-то для вас. Думаю, разные части этого выступления заинтересуют широкую аудиторию.
Да, о чем же это? В двух словах, по сути, это техническое глубокое погружение в границы безопасности FreeBSD Jails, как и сказано в названии. И мы фокусируемся на том, можем ли мы сбежать из них? Какова поверхность атаки? Как выглядит «побег из тюрьмы»? А ближе к концу у нас есть некоторые наблюдения, выводы и призыв к действию в плане будущего усиления защиты.
Наша программа на самом деле очень проста. Я сделаю некоторое введение. А затем мы пройдемся по некоторым проблемам, которые мы обнаружили. И примерно на середине мы рассмотрим некоторые из этих проблем и покажем вам эксплойт и в общих чертах, как мы его создали. А последняя часть нашего выступления — это, по сути, наблюдения, выводы, призыв к действию, моменты, где, как мы считаем, можно улучшить ситуацию в будущем.
Да, прежде чем продолжить, я хочу поблагодарить команду безопасности FreeBSD. Они были невероятно добры и отзывчивы к нам. Взаимодействие с FreeBSD было потрясающим. Я чуть позже расскажу об этом процессе подробнее. Но взаимодействие и почти что общение в реальном времени было действительно очень, очень здорово. И я уверен, что кого-то упустил, но вот люди, от которых я получал ответы и электронные письма и так далее. Все они были абсолютно замечательны. С ними было фантастически работать. Они абсолютно понимают, чем мы занимаемся. Они были очень supportive. Они исправляли ошибки и так далее. Так что весь этот процесс был потрясающим. И да, мы бы не смогли сделать ничего из этого без них, правда.
Что подводит меня ко второму слайду. Итак, это, так сказать, защитники, а это — атакующие, хакеры безопасности. Это, конечно, мы, и, безусловно, для части с эксплойтами, мы, знаете ли, опирались на тех, кто был до нас. Некоторые из этих работ уходят на 25 лет назад. Некоторые — более свежие. Некоторые связаны со взломом PS4, PS5 и целой кучей всего посередине. И, без сомнения, я упустил кое-кого из людей. Если вы занимались интересными исследованиями по безопасности PSD или FreeBSD, и вашего имени здесь нет — это моя вина. Я должен был добавить ваше имя. И затем, да, мы решили добавить сюда ChatGPT. Он был полезен в нескольких случаях, когда не галлюцинировал.
Да, итак, зачем мы это сделали? Верно? Для меня это было путешествие, которое длится около 25 лет. 25 лет назад я был подростком и сидел в IRC-канале с моим другом, который был большим поклонником FreeBSD, и он обожал FreeBSD4. И одной из вещей, которые ему очень нравились, были jail’ы. И с того момента до сегодняшнего дня я всегда думал: насколько сложно было бы сломать jail? И вот, наконец, в начале этого года я нашел немного времени и начал копаться в FreeBSD jail’ах.
Я больше исхожу из опыта работы с кодом. А затем, мы с Майклом знаем друг друга, не знаю, с конца 90-х, 20 с лишним лет, 25. И мы какое-то время не общались. А затем в начале этого года мы снова связались. И Майкл рассказывал мне, что занимается FreeBSD, DevOps и администрированием, безопасностью в одном ISP. И это было как раз в тот момент, когда я изучал часть кода. И я подумал: о, было бы здорово сотрудничать над этим? Я мог бы привнести свои знания по коду, а Майкл мог бы все это собрать, сделать красиво, заставить работать все эти скрипты, заставить работать все эти эксплойты и так далее. И да, через несколько дней мы начали сотрудничать. И мы представим в основном то, чем мы занимались последние три-четыре месяца.
Итак, ладно, давайте начнем с одного большого предположения, верно? Мы предполагаем, что jail скомпрометирован, верно? Итак, у вас есть jail, в котором, не знаю, запущен какой-то сервис, и кто-то получил root в вашем jail, верно? Насколько вы в безопасности, верно? Можно ли сбежать из jail с правами root и получить доступ к хосту, верно? И каковы риски, связанные с этим, верно? Это то, что мы исследуем в этом выступлении.
Немного истории о jail’ах. Как я уже сказал, они были представлены в FreeBSD4, в начале 2000-х. Основная идея заключалась в том, что мы хотим запускать обычные Unix-сервисы на одной машине, но ваш root недостаточно хорош, и нам нужно что-то лучшее. И Пауль Хеннингкамп подумал: давайте я создам jail’ы. И он их создал. И, безусловно, для того времени они были в каком-то смысле ahead of their time. Сейчас у всех есть какая-то подобная технология, но 25 лет назад это определенно было не так.
Другое дело с jail’ами, конечно, что первая версия никогда не идеальна и имеет множество проблем, но они определенно созрели и стали более защищенными со временем. Было исправлено множество вещей, добавлены новые функции и так далее. Ранние jail’ы имели очень ограниченные возможности в сети. Это было исправлено. Конфигурации теперь динамичны и гибки. Раньше так было не всегда. У нас есть гораздо лучшие инструменты для управления jail’ами. Так что со временем произошла эволюция. И, безусловно, с точки зрения удобства использования и того, что можно делать с jail’ами, ситуация улучшилась по сравнению с 25 годами назад. Но все началось в FreeBSD4.
На высоком уровне, как работают jail’ы, верно? По сути, вы создаете конфигурацию jail’а, а затем на уровне системного вызова вызываете `jail_set()`, и есть прослойки посередине, которые преобразуют одно в другое. Но по сути в вашем `jail.conf` вы можете определить имя хоста, корневую файловую систему, точки монтирования, rlimits, открытые устройства, имя хоста и все такого рода вещи, верно? Вот как вы его настраиваете. И затем, когда вы создаете jail и подключаете к нему что-то, внутри создается структура `prison`, и она говорит: даже если в ядре у этого процесса права root, он все равно ограничен этой тюрьмой (prison), и ему не разрешено делать все, что может делать root. Он обеспечивает chroot, контролирует доступ к файловой системе, контролирует точки монтирования и делает ряд других вещей.
Одна интересная вещь, своего рода примечание, это то, что `jail_attach()` — это, очевидно, полезный и необходимый системный вызов. Он требует некоторой осторожности. Вообще, если смотреть на это с абстрактной точки зрения, можно увидеть некоторое сходство с `execve`, где если у вас есть SUID-бинарник (set user ID), и вы запускаете программу, ресурсы, которые у вас есть, передаются вашему потомку. Это в основном верно и для jail’ов. Есть исключения, есть некоторая проверка. Но, например, если у меня есть файловый дескриптор для устройства или для файла, и я делаю `jail_attach()`, этот файловый дескриптор переходит вместе. И, скажем, у вас есть конфиг, и в нем говорится, что этот jail не имеет доступа к этому устройству, но вам удалось передать этот файловый дескриптор, и внезапно ваш jail получает доступ к этому устройству, верно? Так что здесь требуется некоторая осторожность, если вы используете `jail_attach()`. Вообще, если вы занимаетесь просто администрированием, все в порядке, потому что инструменты делают это за вас. Но если вы создаете свои собственные инструменты, вам нужно быть здесь немного осторожнее.
Итак, суть jail’ов заключается в посредничестве системных вызовов, верно? После создания jail’а у вас есть все эти системные вызовы. И для каждого системного вызова, если ему нужно выполнить привилегированную операцию, ядро в основном смотрит на него и говорит: окей, ты, да, ты в jail’е, да, ты root, но ты в jail’е. Разрешено ли тебе в этом jail’е выполнять эту конкретную операцию, верно? Основная точка — это функция `prison_priv_check()`, которая вызывается `priv_check_cred()`, которая является основным API для проверки привилегий в ядре FreeBSD. И именно здесь политика jail’ов ограничивает привилегии и обеспечивает, чтобы получение root в jail’е не означало получение root на хосте, верно? Эта политика обеспечивается для jail’ов. Она может быть использована только jail’ом, или, по сути, она проверяет привилегии, и привилегии, которые вам потенциально разрешено иметь, относятся к подсистемам и драйверам, которые «осведомлены» о jail’ах. Вообще, если что-то не «осведомлено» о jail’ах, вы не получите эту привилегию. Хотя иногда могут быть несоответствия, но в целом так это работает.
Что также очень удобно, так это то, что эта функция имеет очень, очень большое switch-выражение для всех этих привилегий, которые потенциально разрешены для jail’а, и это была наша отправная точка для аудита этого всего. Мы подумали: окей, вот все, что jail может делать. Давайте поиграем с ними.
Помимо этого, эти проверки привилегий потенциальны, верно? То есть, вам потенциально разрешено это делать, но вы все еще ограничены `jail.conf`, верно? И если в конфигурации jail’а сказано, например, что одной из разрешенных привилегий является чтение kmem, верно? Но это концептуально верно. Но чтобы фактически делать чтение kmem, конфиг все еще должен говорить: я открываю для вас, как для jail’а, доступ к чтению kmem, что большинство конфигов не делают, слава богу, потому что это ужасная идея. Но технически, чтение kmem «осведомлено» о jail’ах.
В любом случае, это список привилегий, и он даже не полный, но это около 120 штук. Множество и множество привилегий, которые потенциально разрешены для jail’ов. Неудивительно, что многое вращается вокруг сетевых вещей. Все три брандмауэра «осведомлены» о jail’ах. Если интерфейс «осведомлен» о jail’ах, NFS «осведомлен» о jail’ах, IP-стек «осведомлен» о jail’ах. Wi-Fi стек «осведомлен» о jail’ах. Учетные данные, процессы, планирование и так далее. Как я сказал, чтение kmem «осведомлено» о jail’ах, на что есть свои причины, но это просто плохая идея. Подсистема Linux «осведомлена» о jail’ах, что, я думаю, тоже проблематично, но я могу понять, что в этом может быть необходимость.
Да, так что в основном моя работа заключалась в просмотре этих привилегий, а затем я в каком-то смысле выбирал те, которые выглядели наиболее интересными для меня, и затем начинал экспериментировать с ними. Я не завершил все. Например, я не проводил полный аудит подсистемы Linux, потому что она просто огромна. Большая часть работы была сосредоточена на сетях, потому что это казалось очень интересным.
В основном я смотрел на обработчики, которые говорили: окей, только root должен делать эти вещи, верно? И идея заключалась в том, что если вы находитесь на хосте и вы не root, вы не можете трогать эти вещи, верно? И мои рассуждения были таковы: этот код, вероятно, был написан 20-30 лет назад. Кто-то писал его с мыслью: ну, это только для root, кому это действительно важно? Действительно ли нам нужна безопасность? И так все нормально. Итак, другое дело — старый код обычно содержит более тривиальные ошибки безопасности, потому что есть вещи, которые мы знаем сегодня, когда пишем код, особенно на C, которые мы не так хорошо понимали, скажем, в середине или конце 90-х.
Да, итак, проблемы. Мы нашли около 40 проблем. Не все из них исправлены, хотя довольно многие уже исправлены, и это отлично. Мы не будем говорить о тех, которые еще не исправлены, но я расскажу о тех, которые уже исправлены. Как я уже упоминал, большая часть того, на что мы смотрели, была в IPsec, CARP, Wi-Fi, интерфейсах, правилах IOC, NFS, и, в основном, в брандмауэрах. И именно в брандмауэрах происходят очень интересные вещи.
Как я сказал ранее, я упоминал команду безопасности FreeBSD. Я и раньше сообщал им о проблемах, и обычно это работает так: вы знаете, не только в FreeBSD, но и в любой команде безопасности, у них есть адрес электронной почты, вы просто отправляете им свой список, и в какой-то момент они пишут вам обратно и говорят: мы исправили их, все в порядке, вот рекомендации. Но в этот раз мы с Майклом использовали GitHub для отслеживания нашей работы, потому что мы сотрудничали. И мы поместили все наши ошибки туда, а также наши PoC (Proof of Concept), наши заметки, нашу документацию, наши ВМ и все эти вещи. И когда мы написали ребятам из FreeBSD, мы сказали: кстати, у нас есть этот GitHub. Хотите, чтобы мы добавили вас туда? Они сказали да, и из этого вышло то, что они начали комментировать наши вещи, а мы смотрели на их вещи, они указывали нам на исправления по мере их написания. И так мы получили что-то вроде сотрудничества в реальном времени с их командой безопасности. Это было фантастически. Мне очень понравилось. Я не думаю, что когда-либо работал, сообщая о внешних ошибках безопасности, с таким хорошо работающим процессом. Так что я невероятно, невероятно рад, что ребята из FreeBSD были готовы работать с нами и комментировать некоторые вещи на GitHub, которые мы делали.
Да, так, примеры проблем. Я пройдусь по ним очень быстро, потому что действительно интересные вещи — это то, что покажет Майкл.
Итак, одна из первых проблем, которые мы нашли, была в IPFW, одном из брандмауэров. Это ioctl, который добавляет правило, выделяет память (malloc), затем копирует данные из пользовательского пространства в нее. Ошибка при копировании перехватывается, но никогда не используется. Так что ошибка отбрасывается, и копирование может завершиться неудачей. Выделенная память не была инициализирована, так что теперь вы работаете с неинициализированной памятью ядра. Это, очевидно, плохо. Могут быть побочные каналы, и может произойти утечка неинициализированных данных ядра. Не здорово.
Похожий код в том же брандмауэре IPFW: формат правил изменился между FreeBSD 7 и 8. И для версии 8 была добавлена функция, которая говорит: эй, возьмите эти старые правила версии 7 и преобразуйте их в правила версии 8. И в этом преобразовании, посмотрите на этот цикл `for`, там есть встроенные поля длины, и отсутствует проверка границ для этого `f_len`, и когда происходит `bcopy`, может произойти повреждение памяти. Итак, источник из вашего правила версии 7 в приемник в правиле версии 8, приемник в правиле версии 8 может быть меньше, и может произойти повреждение памяти. Очевидно, это плохо. Эта ошибка была исправлена просто тем, что ребята из FreeBSD сказали: нам это больше не нужно. Это действительно, действительно старый код. FreeBSD 7, его никто не использует. И они просто вырезали все это, и это исправило ошибку, что отлично.
Да, это была не ошибка, которую мы нашли. Она была в CARP. Это было на самом деле очень мило. Итак, это ioctl, и вы можете использовать `SIOCGVH`. И этот ioctl в основном говорит: эй, дай мне информацию об этом интерфейсе CARP для каждого интерфейса CARP, который у тебя есть. И у них есть структура `carpbreq`, которая является записью CARP, и вы копируете ее туда, а затем она возвращается в пользовательское пространство. Проблема в том, что запись CARP может хранить только одну запись. И тем не менее, функция позволяет указывать количество записей. И мой первый вопрос был: что происходит? Они как бы... как это работает? Портится ли память? И фактическая работа выполняется `carp_ioctl_get()`. И `carp_ioctl_get()`, я полагаю, все еще пытается делать несколько записей, но где-то по пути кто-то подумал: мы не можем копировать больше одной записи, потому что это испортит память. Так что они делают: они перечисляют все записи, а затем продолжают копировать их в самую первую запись, так что все остальные теряются. Но затем они все равно дают вам обновленное количество. Так что когда приходит время копировать вашу структуру `carpbreq` обратно в пользовательское пространство, вы копируете за границы. И это может звучать не очень захватывающе, но мы сейчас покажем вам, почему это действительно круто. Этот конкретный примитив позволяет невероятно надежно утечь «печеньку» стека (stack cookie), что является ключевым элементом информации, который нам понадобится для некоторых наших эксплуатаций.
В частности, эта ошибка, которая также есть в брандмауэре IP filter, одном из более старых, восходящем к середине 90-х. В нем есть устройство, при записи в которое, вы в основном записываете в структуру, и в структуре есть связанное поле, и они используют это связанное поле, и копируют в эту структуру данных в стеке. Это 2048 байт, без какой-либо проверки границ, и над этим стоит прекрасный комментарий, который гласит: это должно быть достаточно большим, чтобы хранить любой возможный тип данных. Типа, хм, отлично.
Есть... я немного спешу, так что я... здесь есть другие примеры, но я просто пробегусь по ним очень быстро. Итак, неограниченный malloc вызывает панику. Раскрытие информации, раскрытие информации, целочисленное переполнение (underflow). Чтение за границами, чтение за границами, раскрытие информации. Чтение за границами и запись за границами. Это в исправлении, довольно милая ошибка. Та же запись за границами. Больше памяти. О, это удаленное раскрытие памяти через NFS. Да. И это... спецификация говорит, что когда вы выполняете запись через NFS, сервер отвечает и говорит: эй, ты фактически записал столько-то байт. И спецификация говорит, что число, которое вы даете, должно быть беззнаковым целым (`unsigned int`). И функция, которую они используют для извлечения, работает с беззнаковым целым, но они хранят его в знаковом целом (`signed int`). И затем они проверяют: о, если оно больше буфера, то они выходят. Но если оно меньше, они просто вычитают его. Итак, что происходит: вы говорите, что записали минус пять байт. И тогда происходит так: мы знаем буфер, минус пять. И затем она пишет перед буфером в сеть. И затем вы можете, вы можете, в конце концов, вы выйдете за границы и вызовете крах. Но вы можете делать байт за байтом, и просто продолжать говорить минус один, минус два, минус три. И так вы можете утечь столько данных, сколько захотите, пока не произойдет крах. Эта была действительно милой.
Когда я нашел эту ошибку и стал искать информацию, оказалось, что мы нашли ее в сентябре этого года. Оказывается, ребята из OpenBSD наткнулись на эту ошибку в 2019, исправили ее, назвали исправлением надежности, а не исправлением безопасности. Они сказали, что это может вызвать крах. Не думаю, что они поняли, что это также может привести к утечке информации. Потому что, очевидно, если ваше отрицательное значение слишком велико и выходит за пределы страницы, вы падаете. Но если оно недостаточно большое, вы в конечном итоге отправляете неинициализированную память из ядра по сети. Итак, да, OpenBSD исправили это. Никто не знал. Это NFS, так что код общий. Он есть в Solaris. Он есть в illumos. Он был в OpenBSD. Он был в FreeBSD. Он был в NetBSD. Когда мы сказали ребятам из FreeBSD, они ответили: да, да, дайте нам поговорить с ребятами из NetBSD, и мы убедимся, что они тоже знают. Итак, патч внизу — это не патч FreeBSD. Это патч OpenBSD шестилетней давности. Или шести с половиной. Ладно. Так что да.
Это только примеры. Есть много других ошибок, которые еще не исправлены. Многое из этого — сочные вещи, повреждение памяти и так далее. Пока мы изучали этот код, мы нашли некоторые ошибки в других местах, не связанные с jail’ами. Так, мы нашли некоторые ошибки в ELF, некоторые в exec, некоторые в netmap. И они интересные, но я не буду говорить о них, потому что это не совсем связано с jail’ами.
Майкл: Отлично. Продолжим. Сейчас начнутся сочные части. Да. Хорошо. Дай-ка посмотреть. Я не профессионал в этом деле. Я многому научился у Ильи и очень благодарен ему за то, что он позволил мне отправиться в это путешествие с ним в последние пару месяцев. Я занимаюсь разработкой и системным администрированием 20 лет. Итак, я попытался использовать часть этого опыта для создания работающих proof of concept эксплойтов. К этому мы и перейдем дальше.
Итак, идея в том, чтобы определить несколько кандидатов в ошибки, для которых мы можем фактически создать рабочий proof of concept. Чтобы нам было что показать. И они могут иметь несколько уровней интереса, например, вызвать отказ в обслуживании. Да. Хм-м. Неплохо. Но раскрытие конфиденциальной информации. М-м-м. И затем полный компрометация хоста. Да. Еще лучше.
Итак, первое, что мы сделали, — настроили стабильную среду разработки, потому что, ну, если у вас хорошая рабочая среда, вы настроены на успех, как я понял за 20 лет создания вещей, а теперь переключившись на их взлом. Итак, мы использовали отличные инструменты. QEMU — отличный эмулятор. У нас были некоторые проблемы с моим MacBook, потому что через HDMI, а это тот, который я использую. И на нем... на нем хорошо работать. Но когда вы исследуете архитектуру x86, он работает через эмуляцию, и это довольно медленно. Так что я компилировал на машине x86, и это было удобно — просто перекидывать образы дисков QEMU туда-сюда.
Отладчик ядра, конечно. Мы также перешли на... мы скомпилировали отладочное ядро FreeBSD. Но нам пришлось отключить проверки (asserts), потому что мы не хотели, чтобы они нас останавливали. SSHFS-монтирования, потому что не хочется вручную копировать файлы. Когда мы собрали наше собственное ядро... то есть, просто... нет такого понятия, как совпадение. Да. Это было забавно. Ладно. Итак, первая попытка — отлаживать офлайн. Мы экспериментируем, система падает, хост падает, и затем мы изучаем дамп краха в отладчике. К сожалению, это приводило к падению отладчика. Ну, да. Точно. Так что нам пришлось придумать что-то другое.
Итак, мы перешли к онлайн-отладке. Наша вторая попытка заключалась в переходе к онлайн-отладке. Это значительно облегчило установку точек останова и, знаете, для просмотра того, что происходит. Очень хорошо объяснено в руководстве FreeBSD, конечно. Онлайн-отладка использует последовательное соединение. Так что мы просто эмулируем его через TCP с двумя виртуальными машинами, и тогда мы можем приступать к работе. Когда делаете так, с несколькими виртуальными машинами, следите за MAC-адресами, потому что если они совпадут, то вы попадете в мир боли. И, да, пытайтесь понять, что вы делаете. Это был мой первый раз, когда я настраивал среду отладки для FreeBSD. И, да, это привело к нашей третьей попытке. Не просто соберите ядро на одной машине и затем просто скопируйте его в отладчик, который не знает о правильных символах, и вы получите несоответствие, и это превращается в огромный беспорядок. И я потратил, типа, неделю или две на эту штуку, и это было раздражающе.
Итак, что мы сделали — я просто настроил отладчик, полностью готовый отладчик, а затем просто клонировал образ диска и использовал его в качестве цели. И тогда вы получаете некоторую полезную информацию, верно? Вы можете устанавливать точки останова, видеть, что происходит. И, эй, вот снова этот интересный комментарий внизу, который появляется.
Итак, я быстро пробегусь по этим proof of concept эксплойтам, потому что хочу перейти к самому интересному. Кроме первого, это был мой первый эксплойт для ядра в жизни. И это, ну, это просто эксплуатация одной из найденных ошибок. Илья нашел этот неограниченный malloc. В общем, да, вот снова код. Вот этот... у меня есть мышь? Нет, нету. Итак, длина для выделения памяти приходит от пользователя. И ядро с радостью выделяет то количество памяти, которое запрашивает пользователь. И делает это с флагом `M_WAITOK`. Это означает, что оно просто, знаете ли, пытается это сделать и ждет, пока... да, пока не найдет достаточно памяти для этого. Так что это приводило к панике. И, да, ладно. Может быть использовано для DDoS-атаки или чего-то подобного. Не слишком интересно. Ну, интересно, но мы хотим большего.
Второй эксплойт, второй proof of concept, то же самое. Но только наоборот, вместо `getsockopt()` мы использовали `setsockopt()`. Тоже только что было рассказано Ильей. То же самое, мы просто передаем огромное значение длины в этот вызов. Итак, оно пытается выделить это огромное количество памяти. Мы также смешали это с многопоточностью, чтобы это происходило немного быстрее и надежнее на машинах с большим объемом памяти. То же самое. Что это? А, да. Этот — это разрушение стека (stack smash), о котором только что говорил Илья. Да, классическая вещь, вернется в нашем комплексном эксплойте.
Илья также упоминал это. Это запись за границы массива. Попытка избежать записи за границы на самом деле приводит к обратному эффекту и заставляет ядро возвращать больше памяти, чем должно. Это может быть, да, использовано для утечки «печеньки» стека, и так и будет. Вот что это делает. На этот раз без паники, но мы утекаем «печеньку» стека, которая нам понадобится через секунду. А вот отладчик показывает, что эти две вещи на самом деле совпадают. Так что ядро было очень радо отдать нам это.
Теперь, комплексный proof of concept эксплойт — это тот, который мы действительно хотели, знаете, объединить все эти вещи. И цель — полностью захватить хост из скомпрометированного jail’а. Итак, это использует эксплуатацию нескольких ошибок, потому что они нам понадобятся через секунду. И это хороший proof of concept, чтобы показать, что, знаете, что может пойти не так, когда допущения о jail’ах не оправдываются.
Нам пришлось иметь дело с несколькими ограничениями. Прежде всего, защита стека. Итак, когда вы разрушаете стек в ядре FreeBSD и хотите захватить поток выполнения, вы не можете просто перезаписать указатель инструкции, не перезаписав также и «печеньку» стека. Ядро не имеет исполняемых страниц в том месте, где мы находимся. Так что мы не можем внедрить наш собственный шеллкод из пользовательского пространства. Мы также не можем сказать ядру перепрыгнуть в пользовательское пространство, где у нас есть инструкции, из-за SMAP и SMEP, предотвращения доступа в режиме супервизора. Мы не можем читать или записывать данные из пользовательского пространства в пользовательское пространство. Так что есть некоторые ограничения, но мы нашли способы обойти их.
Первое — защита стека. Итак, да, как я только что сказал, есть значение, вычисляемое при загрузке и помещаемое в стек. И если вы разрушаете стек, перезаписываете его и хотите перезаписать указатель инструкции, вам нужно пройти через место, где находится «печенька». И когда вы это делаете, система фактически это обнаружит и вызовет панику, что приведет к концу игры. К счастью, «печенька» стека — это глобальное значение, установленное при загрузке. Так что мы просто утекаем ее и заменяем значение во время разрушения стека. И ошибка номер 50, найденная Ильей, для утечки памяти ядра была идеальным кандидатом для этого. Итак, вот наш код, который мы использовали для получения «печеньки» стека. Он использует уязвимость интерфейса carp и утекает «печеньку» стека, которую мы затем помещаем в наш буфер в пользовательском пространстве, используемый для разрушения стека.
Теперь у нас все еще есть неисполняемая память ядра. Так что нам нужно найти способ обойти это. Мы не можем дать ему наши собственные инструкции. Мы не можем перепрыгнуть куда-то в пользовательское пространство. Но мы можем перепрыгнуть на функции ядра. Итак, мы придумали: почему бы нам не попытаться перепрыгнуть на функцию загрузки ядерного модуля (KLD load) и просто загрузить наш собственный модуль ядра. И тогда мы можем, знаете, продолжить эксплуатацию с помощью C-кода. Итак, адрес этой функции был довольно легко найти из-за всей детерминированности в ядре FreeBSD. Мы просто провели статический анализ образа ядра, и он дал нам адрес текущей функции загрузки KLD, так что мы можем перепрыгнуть на нее. И мы помещаем его в наш буфер, используемый для разрушения стека. И мы не хотим перепрыгивать точно в начало этой функции, потому что там куча проверок привилегий. Это часть механизма jail’ов. Мы фактически хотим перепрыгнуть на 69 байт внутрь функции. Итак, да, мы хотим перепрыгнуть прямо туда, чтобы обойти проверки уровня безопасности и проверки привилегий там.
Итак, теперь мы говорим ядру вызвать функцию загрузки KLD. И у функции три параметра. Ей нужен дескриптор текущего потока для потока, который фактически это делает. Нужен путь к модулю ядра, который вы хотите загрузить. И есть третий параметр, который нам не очень интересен. Но проблема в том, что мы не можем заполнить эту информацию в нашем буфере, потому что ядро не может читать из нашего буфера. Ну, извините, нет, оно может читать из нашего буфера, но у нас нет этой информации. Например, мы не можем получить адрес дескриптора потока. Или можем? Потому что Илья нашел в статье Phrack 2002 года, что существует этот sysctl, который можно использовать из пользовательского пространства, и ядро FreeBSD будет с радостью дать вам адрес текущего потока. Итак, мы, да, из пользовательского пространства запрашиваем адрес, получаем его, используем и помещаем в наш буфер, который ядро затем копирует обратно в память ядра, куда можно перепрыгнуть. Это была интересная штука. Итак, вот наш код, который мы использовали для этого. Это просто код пользовательского пространства, который получает это значение. А затем, когда у нас есть это значение, мы помещаем его в наш буфер и получаем доступ к нему в ядре.
Затем аргумент файла — то же самое. Итак, нам нужна строка, путь к модулю ядра, который мы хотим загрузить, но нет способа передать ее в ядро, потому что ядро не может читать из пользовательского пространства, и у него нет доступа к адресу, указывающему на буфер в пользовательском пространстве. Итак, что мы делаем — то же самое. Вызываем нашего хорошего друга sysctl снова. И это фактически способ для пользовательского пространства просто запросить указатель в памяти ядра, указатель на аргументы программы, которую вы только что запустили. Так что это универсальный способ получить любого рода строку в память ядра.
Илья: Да, но это дает вам надежный указатель ядра на нее.
Майкл: Итак, мы именно это и делаем. Мы передаем параметр нашему эксплойту с путем к модулю ядра, который хотим загрузить. И, да, мы получаем адрес для этого в памяти ядра из пользовательского пространства. И помещаем его в наш буфер, который попадает в ядро. Итак, да, идентификатор файла (file ID) не очень интересен, так что мы просто устанавливаем его в ноль. Теперь у нас есть этот огромный буфер, который мы используем для разрушения стека, и в нем есть вся информация, необходимая для выполнения этого действия.
Следующий шаг — модуль ядра. Причина, по которой мы хотим его, в том, что мы хотим, знаете, продолжить, а не прыгать по разным инструкциям. Мы просто хотим иметь некоторый удобный C-код сейчас. Модуль ядра имеет полные привилегии ядра, и нам нужно очистить стек, потому что, да, я вернусь к этому через секунду. И мы хотим получить полный контроль над хостом, что мы можем сделать с модулем ядра. Мы также хотим сообщить эксплойту в пользовательском пространстве, что происходит, и выйти из jail’а, а затем очиститься после эксплойта.
Ладно, итак, отчет о прогрессе обратно в пользовательское пространство. Илья нашел очень умный способ сделать это. Мы просто заставляем эксплойт отобразить некоторую память пользовательского пространства, а затем заставляем модуль установить доступ к этой памяти, и затем мы используем нечто вроде механизма IPC, чтобы просто передавать сообщения. Это предотвращает появление следов нашей деятельности на консоли хоста, что нежелательно, когда вы пытаетесь быть немного скрытным. И эксплойт затем показывает эти сообщения атакующему. Вот этот код. Вот что делает эксплойт. Он просто отображает некоторую память. А затем в модуле ядра мы получаем доступ к этой памяти. И затем у нас есть наша пользовательская функция логирования, которая просто передает сообщения через это. А затем в пользовательском пространстве эксплойт извлекает эти сообщения и показывает их пользователю.
Следующий шаг — выйти из jail’а. Это то, что мы на самом деле хотим сделать. Итак, мы хотим освободить от jail’а два процесса, а именно сам эксплойт, но также и оболочку (shell), которая запустила эксплойт, потому что тогда мы можем фактически что-то делать. И мы хотим изменить корневой путь для текущего процесса и родительского процесса, потому что мы не хотим быть в этом контейнере jail’е в файловой системе. Это некоторый стандартный код, который изменяет процесс jail’а и просто делает его процессом вне jail’а.
Затем изменение chroot — это было не так просто, потому что оно защищено кучей магии с указателями в ядре, чтобы заставить вас использовать API. Но мы пишем какой-то мусорный C-код, играем с указателями и разыменованием, и тогда можем обойти всю эту штуку.
Теперь нам также нужно очиститься после эксплойта, потому что нельзя просто так ходить, разрушая стек и портя память в ядре. В какой-то момент нужно немного это почистить. Итак, очистка заключается в сообщении обратно в пользовательское пространство, что мы закончили. Затем нам нужно отменить отображение той памяти пользовательского пространства. Это как если бы кто-то перебросил вам голубя через стену jail’а и сказал: используй это для связи. Ну, вам нужно убрать этого голубя снова, если вы не хотите вызвать подозрения. Ладно. И затем нам нужно разблокировать «гиганта» (giant), потому что ядро блокируется (это общая блокировка ядра), и нам нужно ее разблокировать. Но нам пришлось разблокировать ее дважды по какой-то причине. Понятия не имею почему. Разблокируешь один раз — ничего не работает.
Илья: Да.
Майкл: Три раза — паника, два — магическое число. Понятия не имею почему. И затем мы взяли страницу из книги Linux и просто «упали» (oops) оттуда с помощью `thread_exit()`, потому что мы ленивые, не хотим убираться. И да, мы не хотим вызывать панику, конечно. Так что да, вот как мы сообщаем, что закончили, в пользовательское пространство. А затем вот очистка, так что мы возвращаем ту память, которую использовали для связи, разблокируем «гиганта», и да, затем делаем `thread_exit`.
Была одна интересная вещь, которая произошла во время выполнения нашего эксплойта: он работал, но затем примерно через 60 секунд внезапно все равно происходила паника. И потребовалось время, чтобы выяснить, почему это произошло, но оказалось, что наш эксплойт фактически создавал запись в таблице где-то, и да, происходило какое-то событие, автоматическая процедура очистки в ядре, которая пыталась бы очистить или обработать ее, и поскольку это была поврежденная запись, она просто вызывала панику. Решение было простым: мы просто изменили команду с создания записи на обновление записи, которой не существует, но это не важно, оно просто проигнорирует это.
Теперь у нас есть вся эта информация, поступающая в буфер, который мы используем для переполнения стека, но дело в том, что мы не знали, куда именно поместить эту информацию в нашем буфере переполнения, например, указатель на строку пути модуля ядра. Где именно в нашем буфере переполнения мы должны его поместить? Есть некоторые техники для этого, это основы разрушения стека, вы просто создаете так называемый циклический паттерн, и это паттерн, который не повторяется слишком быстро, и затем вы можете использовать его, вы переполняете все этим, и затем вы можете фактически использовать его для вычисления позиции в буфере определенной интересующей вас вещи. Таким образом, мы смогли использовать значения регистров или другие интересные значения, чтобы вычислить, где в нашем буфере ядро будет это читать.
Разные версии FreeBSD и разные ядра имеют разные смещения, так что, ну, мы храним их в эксплойте и обнаруживаем во время выполнения. Вот эта функция, очень простая, но интересно было то, что мы просто заставили ChatGPT делать нашу работу, так что мы сбросили эту функцию в ChatGPT, и затем могли просто сказать ему: где здесь такая-то вещь, мы просто, например, получили значение регистра, и мы просто сказали ему: вот что у нас есть, вот наша функция паттерна, где это в нашем буфере переполнения? И он просто говорил нам, и это значительно упрощало процесс вычисления всех этих смещений. Мы могли бы рассчитать это вручную, но да, мы же ленивые, верно? Теперь у нас есть эти... это была идея Ильи — поместить некоторые структуры в код, чтобы сделать его, знаете, мультиплатформенным, например, FreeBSD 14.3, 15, разные ядра, мы можем просто добавить больше смещений. Итак, платформа определяется эксплойтом во время выполнения, и затем он просто выбирает правильные смещения.
Хорошо, теперь время демонстрации, надеюсь, это будет...
Илья: Да, боги демонстраций были не с нами сегодня. У нас есть это на живой системе, но та не хотела подключаться через HDMI, так что нам придется показать заранее записанную демонстрацию. Итак, давайте посмотрим, это играет? Так, подождите. Справа вы видите jail. Вы внутри оболочки в jail’е, а слева консоль хоста jail’а, главной машины. Вот так. Итак, мы заходим в наш эксплойт, собираем наш модуль ядра, который выполнит работу за нас после того, как мы запустим эксплойт, и затем копируем этот модуль ядра просто на одну папку назад, где находится эксплойт, потому что мы любим делать лишнюю работу без причины. Ладно, мы собираем эксплойт, и затем запускаем его. И вот оно. Это еще... Да. Для верности мы просто перезагрузим хост.
Илья: Круто.
Майкл: Ладно. Фантастически. Круто.
Илья: Итак, это последняя часть презентации, которая, знаете, давайте... Каковы наши наблюдения? Каковы наши выводы? Что можно сделать лучше?
Наблюдения были таковы: с точки зрения аудита, мы не завершили все, но мы нашли около 40 ошибок. Типы ошибок были самыми разными. Это были чтения и записи за границами, разрушения стека, повреждения кучи, целочисленные переполнения, состояния гонки, утечки информации, неограниченные выделения памяти. Просто обычный стандартный набор вещей, которые вы видите, когда разрабатываете ядро на языке C. Это в каком-то смысле в рамках ожидаемого. Это не здорово, но так есть.
С точки зрения эксплойтов, последний раз я писал эксплойт в 2019. А до этого — в 2013. Так что это два эксплойта за 12 лет. Так что я очень заржавел. Это был первый эксплойт для Майкла, кстати.
Майкл: Да. Последний раз? Никогда. Но это было *слишком* просто.
Илья: Да. В этом и дело. Да. Я был очень заржавевшим. Он никогда этого не делал, и это заняло у нас пару недель. Так не должно быть. Сейчас почти 2026. Это не должно быть так просто. Это было слишком, слишком, слишком легко. Так каковы же выводы здесь?
Проверенное временем часто путают с тем, что значит «более безопасное». Но это не так, верно? Сетевой стек отличный. Он проверен временем, но это не значит, что в нем нет ошибок, верно? Есть разница между, знаете, быть укрепленным против случайных ошибок, которые случаются время от времени, и ошибок, которые никогда не случаются случайно, если только кто-то их специально не спровоцирует, и это не одно и то же, верно?
Другое дело — много кода, на который мы смотрели, особенно тех интерфейсов, которые предназначены только для root и были созданы до появления jail’ов, этот код датируется другим временем, знаете, середина-конец 90-х, 25-30 лет назад. Старый код обычно содержит более тривиальные ошибки. Он просто не создавался с учетом современной безопасности. Мы узнали кое-что за последние 30 лет о написании более безопасного C-кода, чего мы не знали, как нам казалось, в 90-х, но, конечно, не знали. Так что да, код, написанный с учетом только root, был довольно небрежным в этом плане. И я бы сказал, что люди делают это не нарочно, но в глубине души думают: о, только администратор может это сделать, знаете, это не так уж важно. И так что да, модель угроз для jail’ов, знаете, не предвиделась, когда это всё создавалось, верно? Или когда подсистемы и драйверы писались. И да, как мы сказали ранее, эксплуатация была просто слишком легкой, верно?
Теперь я хочу сделать оговорку, потому что может показаться, что мы указываем пальцем и смеемся, но это не так. Потому что реальность такова, что построение операционной системы невероятно сложно. Это очень, очень тяжело, требует много упорной работы. Я имею в виду, только то, что мы делали с отладчиком и всеми этими прыжками через обручи. Типа, если вы тот парень, который строит отладчик, и у вас нет отладчика, чтобы отлаживать ваш отладчик, ваша жизнь в тысячу раз хуже, чем была у нас, верно? Я имею в виду, это лишь крошечная часть, типа, ОС огромна, это десятилетия работы, наложенные слоями, и как-то кто-то должен держать все это в голове и заставить работать. Так что я не здесь, чтобы указывать пальцем и смеяться. Есть довольно серьёзные вещи, но это очень, очень сложно, и я понимаю, что это нелегко. И если кто-то думает, что это просто, кстати, идите постройте свою собственную операционную систему и посмотрите, насколько это сложно. Это невероятно мучительно. Теперь, сказав все это, у нас есть некоторые предложения.
Итак, первое — да, потребуется больше аудита. Думаю, я точно не завершил всё, и даже то, что я завершил, уверен, кое-что я упустил. Одна вещь, которая бросается в глаза, — это то, что было слишком легко получить необходимые указатели ядра. Было слишком легко получить указатель на поток. Было слишком легко получить указатель ядра на произвольный кусок памяти, который мы можем надежно использовать и помещать в него любое содержимое. И мы понимаем, что эти интерфейсы используются реальными программами, но что если мы, исторически это, может, и имело смысл, но что если мы создадим лучшие интерфейсы или другие интерфейсы, которые дадут этим программам то, что им нужно, без предоставления им указателей ядра, верно? Да, программы нужно будет модифицировать, но это в любом случае должно было быть сделано, потому что вытаскивание указателей из ядра по своей природе подвержено состояниям гонки и является ужасной идеей. И это было нормально в 1970-х и 1980-х, но сейчас 2025. Это уже не нормально.
Загрузка модулей, да, загрузка модулей крута. Она очень полезна. Она, конечно, облегчает жизнь, но она также облегчает жизнь тому, кто разрабатывает эксплойты. Это значительно более надежно, мощно и гибко, чем шеллкод. И так что это одна из тех вещей, да, иногда это нужно, но в других случаях, например, OpenBSD в основном сказали: больше никаких модулей ядра. Вы собрали ядро, и всё. Я не говорю, что это должно быть вашим единственным вариантом, но, возможно, это может быть опцией по умолчанию. Не знаю. Думаю, здесь есть пространство для улучшений.
Да, средства защиты от эксплойтов. Как вы видели, когда мы получили этот указатель на `current_kld_load`, это ключевой момент. Этот указатель одинаков. Если у вас x86, если у вас x86 FreeBSD и та версия, этот адрес работает везде. Он одинаков, верно? Это слишком просто, верно? Я не говорю, что ASLR решает все проблемы, но это хотя бы испортило бы нам день, верно? У нас не было такого плохого дня. Мы просто взяли адрес и закончили, верно?
Другое дело — «печеньки» стека хороши, но они такие... 2003-го года. Intel придумала теневые стеки и контроль целостности потока кода и все эти вещи. И на современном x86 это доступно. Я хочу, чтобы FreeBSD использовала это, и я хочу, чтобы они использовали это в ядре, верно? Да, будут сложности, но технология есть. Давайте использовать её, верно?
Еще одно предложение, и это, в зависимости от того, с кем вы говорите, может быть спорным, но C — хороший и мощный язык, и он служил нам хорошо последние 50 с лишним лет, но, по моему мнению, он должен умереть. Преимущества больше не перевешивают недостатки. Слишком много рисков. Я упомянул Rust, потому что он, кажется, набирает обороты. Не обязательно Rust. Есть и другие языки. Если хотите писать на Ada, отлично. Но, знаете, Rust, кажется, хороший кандидат. И я думаю, пора, знаете, мы начинаем видеть это в ядре Linux, и Windows кое-что делает в этом направлении, и мы видим это в некоторых браузерах и на некоторых мобильных устройствах и так далее. Пора начать создавать наши новые низкоуровневые вещи на безопасных в отношении памяти языках, таких как Rust. Я понимаю, это непросто. Это многолетнее усилие. Без сомнения, будут религиозные войны. Нужно убедить нужных людей. Нужно построить инфраструктуру для этого, а затем работать поэтапно. Нужно, знаете, сначала новые подсистемы, может быть, драйвер, потом, может, подсистема. В конце концов, надеюсь, в какой-то момент вы сможете перенести часть вашего кода. Мы говорим легко о пяти, десяти годах в будущем, прежде чем это даст реальный эффект, но эффект будет реальным, верно? Rust — не серебряная пуля, но мы знаем из данных, которые видели до сих пор, что он резко снижает количество проблем, связанных с памятью. По умолчанию Rust безопасен в отношении памяти. Конечно, когда вы делаете низкоуровневые вещи, в какой-то момент вы можете делать небезопасные вещи, и это все еще может вызывать проблемы. Но, я имею в виду, для итерации по списку или чего-то подобного вам не нужны сырые указатели. Rust может обеспечивать безопасность памяти там, верно? Он может обеспечивать границы памяти, все такого рода вещи, верно? Так что это резко уменьшит количество проблем, которые у вас были бы. Круто.
Да, всё. Всё.