Недавно я наткнулся на один сабмишен для kernelCTF, в котором автор упоминает новую технику расширения окон гонок в ядре Linux:
kCTF submission
Позже я узнал, что на самом деле эта техника не является новой — она уже была описана в блоге от Starlabs. В этом посте также объясняется немного больше деталей, чем в сабмишене kernelCTF, о том, как именно можно использовать эту технику.
Однако ни один из этих двух ресурсов не дал мне полного понимания. Мне хотелось действительно разобраться, как именно работает эта техника… И именно этим я в итоге и занялся!
Это будет короткий и простой пост в блоге (в основном для себя), в котором я опишу то, что узнал.
Содержание
PoC и TL;DR
Вот минимальный PoC, который я написал, чтобы продемонстрировать эту технику на Ubuntu 25.04 (kernel 6.14.11). Закомментируйте второй вызов fallocate(), чтобы увидеть разницу во времени доступа.
Краткое объяснение:
Введение в /dev/shm
В Linux /dev/shm — это файловая система в оперативной памяти, предназначенная для низколатентного временного хранения данных. Она также широко используется для IPC через разделяемую память.
В рамках этого поста её можно рассматривать как обычный файл. Его можно открыть и отобразить в память процесса для чтения и записи. Ключевое отличие заключается в том, что page fault’ы и некоторые системные вызовы обрабатываются специализированными обработчиками /dev/shm.
Выделение страниц с помощью fallocate()
После открытия /dev/shm и отображения страницы физическая память ещё не выделена. Реальное выделение происходит только при первом доступе к странице, что приводит к page fault.
Для /dev/shm обработкой page fault занимается shmem_fault(), которая вызывает shmem_get_folio_gfp() — функция, которая ищет страницу в кэше, подкачке или выделяет новую.
Существуют ситуации, в которых page fault может завершиться неудачей, например при нехватке памяти или дискового пространства, что может привести к OOM.
Для более контролируемого поведения используется fallocate(), который немедленно выделяет физические страницы и возвращает ошибку в userland при невозможности выполнить операцию.
Для /dev/shm этот вызов обрабатывается функцией shmem_fallocate().
Освобождение страниц с помощью FALLOC_FL_PUNCH_HOLE
fallocate() также позволяет освобождать ранее выделенные страницы с помощью флага FALLOC_FL_PUNCH_HOLE(обязательно вместе с FALLOC_FL_KEEP_SIZE).
Во время hole punch происходят следующие действия:
После этого любые обращения к этим адресам снова будут обрабатываться через обычный page fault.
Блокирование других потоков
Если во время hole punch другой поток сталкивается с page fault, shmem_fault() проверяет inode->i_private. Если он установлен, поток усыпляется через shmem_falloc_wait() до завершения hole punch.
Таким образом, операции с памятью могут быть принудительно приостановлены на значительное время.
Расширение окон гонок
Итоговые наблюдения:
Обязательное условие: внутри окна гонки должен происходить доступ к userland-адресу, например через copy_from_user() или copy_to_user().
Типовой сценарий использования:
Заключение
PoC из раздела PoC и TL;DR легко адаптируется под большинство race-condition эксплойтов в Linux. Достаточно заменить доступ к fmap на действие, которое приводит ядро во внутреннее уязвимое окно гонки.
kCTF submission
Не удалось прогрузить медиа
Позже я узнал, что на самом деле эта техника не является новой — она уже была описана в блоге от Starlabs. В этом посте также объясняется немного больше деталей, чем в сабмишене kernelCTF, о том, как именно можно использовать эту технику.
Однако ни один из этих двух ресурсов не дал мне полного понимания. Мне хотелось действительно разобраться, как именно работает эта техника… И именно этим я в итоге и занялся!
Это будет короткий и простой пост в блоге (в основном для себя), в котором я опишу то, что узнал.
Содержание
[]PoC и TL;DR
[]Введение в /dev/shm
[]Выделение страниц с помощью fallocate()
[]Освобождение страниц с помощью FALLOC_FL_PUNCH_HOLE
[]Блокирование других потоков
[]Расширение окон гонок- Заключение
PoC и TL;DR
Вот минимальный PoC, который я написал, чтобы продемонстрировать эту технику на Ubuntu 25.04 (kernel 6.14.11). Закомментируйте второй вызов fallocate(), чтобы увидеть разницу во времени доступа.
Краткое объяснение:
[]Открытие /dev/shm и передача файлового дескриптора в fallocate() приводит к вызову shmem_fallocate().
[]Флаг FALLOC_FL_PUNCH_HOLE заставляет shmem_fallocate() размэпить диапазон адресов, выделенный первым fallocate(), а также освободить физические страницы обратно системе. Это занимает заметное время.
[]Если во время этого процесса происходит page fault (например, при доступе к fmap), вызывается shmem_fault().
[]Если shmem_fault() обнаруживает, что выполняется hole punch, он вызывает shmem_falloc_wait() и усыпляет поток до завершения операции.- Если поток ядра попадает в shmem_fault() во время hole punch, он будет спать значительно дольше обычного page fault, что позволяет расширять окна гонок.
Введение в /dev/shm
В Linux /dev/shm — это файловая система в оперативной памяти, предназначенная для низколатентного временного хранения данных. Она также широко используется для IPC через разделяемую память.
В рамках этого поста её можно рассматривать как обычный файл. Его можно открыть и отобразить в память процесса для чтения и записи. Ключевое отличие заключается в том, что page fault’ы и некоторые системные вызовы обрабатываются специализированными обработчиками /dev/shm.
Выделение страниц с помощью fallocate()
После открытия /dev/shm и отображения страницы физическая память ещё не выделена. Реальное выделение происходит только при первом доступе к странице, что приводит к page fault.
Для /dev/shm обработкой page fault занимается shmem_fault(), которая вызывает shmem_get_folio_gfp() — функция, которая ищет страницу в кэше, подкачке или выделяет новую.
Существуют ситуации, в которых page fault может завершиться неудачей, например при нехватке памяти или дискового пространства, что может привести к OOM.
Для более контролируемого поведения используется fallocate(), который немедленно выделяет физические страницы и возвращает ошибку в userland при невозможности выполнить операцию.
Для /dev/shm этот вызов обрабатывается функцией shmem_fallocate().
Освобождение страниц с помощью FALLOC_FL_PUNCH_HOLE
fallocate() также позволяет освобождать ранее выделенные страницы с помощью флага FALLOC_FL_PUNCH_HOLE(обязательно вместе с FALLOC_FL_KEEP_SIZE).
Во время hole punch происходят следующие действия:
[]В inode->i_private записывается маркер, указывающий, что hole punch активен.
[]Вызывается unmap_mapping_range(), который удаляет PTE и размэпливает страницы во всех процессах.
[]Вызывается shmem_truncate_range(), освобождающий физические страницы.
[]Все ожидающие потоки пробуждаются с помощью wake_up_all().
После этого любые обращения к этим адресам снова будут обрабатываться через обычный page fault.
Блокирование других потоков
Если во время hole punch другой поток сталкивается с page fault, shmem_fault() проверяет inode->i_private. Если он установлен, поток усыпляется через shmem_falloc_wait() до завершения hole punch.
Таким образом, операции с памятью могут быть принудительно приостановлены на значительное время.
Расширение окон гонок
Итоговые наблюдения:
[]Page fault’ы для /dev/shm обрабатываются shmem_fault().
[]Страницы можно как выделять, так и освобождать через fallocate().
[]Page fault во время hole punch приводит к засыпанию потока.
[]Это можно использовать для искусственного расширения окон гонок в ядре.
Обязательное условие: внутри окна гонки должен происходить доступ к userland-адресу, например через copy_from_user() или copy_to_user().
Типовой сценарий использования:
[]Открыть /dev/shm и выделить максимальный объём памяти через fallocate() (64 МБ на Ubuntu 25.04).
[]В потоке T1 войти в уязвимое окно гонки.
[]В потоке T2 вызвать fallocate() с FALLOC_FL_PUNCH_HOLE.
[]Если T1 попадает на copy_to_user() / copy_from_user() во время hole punch, он засыпает, тем самым расширяя окно гонки.
Заключение
PoC из раздела PoC и TL;DR легко адаптируется под большинство race-condition эксплойтов в Linux. Достаточно заменить доступ к fmap на действие, которое приводит ядро во внутреннее уязвимое окно гонки.
👍4
😮1
🤔1
LIMB