
Linux LPE - Надёжный выход из изоляции/контейнера
Надёжная proof-of-concept для непривилегированного побега из контейнера / изоляционной среды для CentOS / RHEL 10.
Использует уже исправленную ошибку фрагментации IPv6 в __ip6_append_data() (исправлена в основной ветке коммитом 38becddc, CVE не присвоен) — внутрислэбовое линейное переполнение в skb_shared_info в хвосте собственного заголовка пакета. Этот README описывает только цепочку эксплуатации. Триггер не рассматривается.
6.12.0-242.el10).Это proof-of-concept, сознательно не являющаяся готовым к применению оружием. Каркас надёжности намеренно отсутствует: нет подстройки PCP на процессор, нет подготовки SLUB-кэшей, а заголовок skb размещён в общем, не таком уж тихом кэше. Предоставляемый код срабатывает достаточно часто, чтобы доказать цепочку, но не более. Он ограничен только CentOS / RHEL 10.
CONFIG_INIT_ON_ALLOC_DEFAULT_ON выключен — значение по умолчанию в RHEL / CentOS. Данная PoC помещает устаревший указатель в неинициализированные байты слэба; с init_on_alloc=1 этот слот обнуляется, и данный конкретный приём приводит лишь к падению ядра (разыменование NULL). Это ограничение метода, а не ошибки: полноценный эксплойт, покрывающий и такие ядра, с другой техникой, будет опубликован после того, как исправление и CVE станут общедоступны./sys/kernel/btf/vmlinux присутствует и доступен для чтения всем (стандартно в RHEL / CentOS).| Дистрибутив | Ядро | Результат |
|---|---|---|
| CentOS Stream 10 | 6.12.0-242.el10 | root, побег из контейнера |
| RHEL 10 | 6.12.0-228.el10 | побег из контекста httpd_t |
Внутрислэбовое переполнение до self-UAF. Ошибка даёт контроль над единственным байтом nr_frags в skb_shared_info. Путь освобождения (skb_release_data()) проходит по frags[0 .. nr_frags) и вызывает put_page() для каждого элемента, при этом frags[] никогда не инициализируется. Мы заранее помещаем struct page * для страницы буфера канала (pipe) в этот слот слэба через контролируемое повторное использование кэша, затем устанавливаем nr_frags в 1, так что при освобождении снимается ссылка со страницы, которой мы всё ещё владеем. Линейное переполнение превращается в use-after-free страницы. Переполнение никогда не затрагивает frags[0], поэтому его не нужно синхронизировать по времени с распылением — это и делает метод прощающим.
Page UAF → Dirty-Pagetable. Освобождённая страница канала (pipe) повторно используется как таблица страниц последнего уровня при помощи сбоя (fault) на свежем анонимном отображении. Одна физическая страница теперь одновременно является действующей листовой таблицей страниц и страницей, которую канал по-прежнему читает и пишет. Запись восьми байт в канал устанавливает поддельный PTE; чтение из канала возвращает содержимое таблицы. Это конечный произвольный физический доступ на чтение/запись, примерно на 460 окон PTE на одну таблицу.
Обход KASLR. С помощью физического чтения мы сканируем фиксированную таблицу страниц SMP-трамплина в нижней памяти, которую KASLR никогда не перемещает; её запись половины ядра указывает на level4_kernel_pgt — физическую базу ядра. Оттуда init_top_pgt (узнаваемая по самореферентной записи 511) служит универсальным переводчиком виртуальных адресов в физические, восстанавливая виртуальную базу ядра и привязывая наше адресное пространство к физической памяти.
Конечный доступ → бесконечный чтение/запись. Мы подделываем один PTE в листовой таблице, указывающий на собственный физический адрес таблицы. Соответствующее виртуальное окно тогда алиасит саму таблицу страниц, так что PTE становятся обычной памятью: неограниченный, произвольный доступ к ядру на чтение/запись, без использования канала в цикле. Когерентность TLB кольца 3 обеспечивается принудительным полным сбросом mm с помощью чрезмерного mprotect().
Вычисление смещений и получение учётных данных. Смещения полей структур считываются во время выполнения из /sys/kernel/btf/vmlinux, а не зашиты жёстко. Мы находим собственный task_struct через обход struct page в vmemmap и mm_struct.owner, обнуляем идентификаторы учётных данных и заполняем все наборы возможностей в cred и real_cred. Адреса символов получаются из внутреннего резолвера, разбирающего собственные таблицы kallsyms ядра через наш произвольный доступ; только если это не удаётся, мы прибегаем к запасному варианту — указываем cred.user_ns на init_user_ns (чтобы CAP_SYSLOG срабатывал в начальном пространстве имён) и читаем реальные адреса из /proc/kallsyms.
Отключение SELinux без переключения enforcing. Мы перезаписываем пролог avc_denied() заглушкой xor eax, eax ; ret (перешагивая endbr64 при IBT). Теперь каждый отказ возвращает разрешение, действующее на весь процесс, при этом getenforce по-прежнему сообщает Enforcing. Это также необходимое условие для побега: перезапуск обработчика core-dump через границы доменов в противном случае был бы отклонён.
Побег через core_pattern. Мы перезаписываем глобальный core_pattern обработчиком с префиксом |, указывающим на наш собственный бинарный файл через /proc/<PID>/root (корневой каталог контейнера сбойной задачи), после чего вызываем сбой дочернего процесса. Ядро запускает обработчик как usermodehelper, от root в начальных пространствах имён, и мы передаём интерактивную оболочку root через Unix-сокет, привязанный внутри контейнера. Обработчик рождается в начальных пространствах имён, поэтому не требуется хирургия пространств имён на месте, и нет связанных с этим рисков пространства имён PID.