Защита целостности управления потоком в V8
Защита целостности управления потоком (Control-flow integrity, CFI) — это функция безопасности, направленная на предотвращение атак, связанных с захватом управления потоком. Идея заключается в том, что даже если злоумышленнику удается повредить память процесса, дополнительные проверки целостности могут предотвратить выполнение произвольного кода. В этом посте мы обсуждаем нашу работу по внедрению CFI в V8.
Популярность Chrome делает его ценным объектом для атак с использованию уязвимостей нулевого дня, и большинство эксплойтов, встречающихся в реальной жизни, нацелены на V8 для получения первоначального выполнения кода. Эксплуатация V8 обычно следует аналогичной схеме: начальная ошибка приводит к повреждению памяти, но часто первоначальное повреждение ограничено, и злоумышленнику необходимо найти способ произвольно читать/записывать весь адресный пространство. Это позволяет им захватить управление потоком и выполнить шелл-код, который служит следующим шагом цепочки эксплуатации, пытаясь выйти из песочницы Chrome.
Чтобы предотвратить превращение повреждения памяти в выполнение шелл-кода, мы внедряем защиту целостности управления потоком в V8. Это особенно сложно в условиях наличия JIT-компилятора. Если вы превращаете данные в машинный код во время выполнения, теперь необходимо гарантировать, что поврежденные данные не превратятся в вредоносный код. К счастью, современные аппаратные функции предоставляют нам основу для разработки JIT-компилятора, который остается устойчивым даже при обработке поврежденной памяти.
Далее мы рассмотрим проблему, разделив ее на три части:
- CFI для перенаправления вперед (Forward-Edge CFI) проверяет целостность непрямых передач управления, таких как указатели на функции или вызовы таблицы виртуальных функций.
- CFI для перенаправления назад (Backward-Edge CFI) обеспечивает достоверность адресов возврата, читаемых из стека.
- Целостность памяти JIT-компиляции проверяет все данные, записанные в исполняемую память во время выполнения.
CFI для перенаправления вперед
Есть две аппаратные функции, которые мы хотим использовать для защиты непрямых вызовов и переходов: площадки приземления и аутентификация указателей.
Площадки приземления
Площадки приземления — это специальные инструкции, которые могут использоваться для маркировки допустимых целей переходов. Если они включены, непрямые переходы могут перемещаться только на инструкцию площадки приземления, все остальное вызовет исключение.
Например, в ARM64 площадки приземления доступны благодаря функции Branch Target Identification (BTI), введенной в Armv8.5-A. Поддержка BTI уже включена в V8.
На x64 площадки приземления были введены с функцией Indirect Branch Tracking (IBT), частью технологии Control Flow Enforcement Technology (CET).
Однако добавление площадок приземления для всех потенциальных целей непрямых переходов предоставляет лишь грубую защиту целостности управления потоком и всё ещё оставляет злоумышленникам много свободы. Мы можем дополнительно ужесточить ограничения, добавив проверки подписей функций (типов аргументов и возвращаемых значений на месте вызова должны соответствовать вызываемой функции), а также динамически удаляя ненужные инструкции площадок приземления во время выполнения. Эти функции являются частью недавнего FineIBT предложения, и мы надеемся, что оно получит поддержку со стороны ОС.
Аутентификация указателей
Armv8.3-A представил аутентификацию указателей (Pointer Authentication, PAC), которая может использоваться для внедрения подписи в верхние неиспользуемые биты указателя. Поскольку подпись проверяется перед использованием указателя, злоумышленники не смогут предоставить произвольно сфабрикованные указатели для непрямых переходов.
CFI для перенаправления назад
Для защиты адресов возврата мы также хотим воспользоваться двумя отдельными аппаратными функциями: теневыми стеками и PAC.
Теневые стеки
При использовании теневых стеков Intel CET и защищенного стека управления (Guarded Control Stack, GCS) в Armv9.4-A мы можем иметь отдельный стек только для адресов возврата, который имеет аппаратную защиту от вредоносных записей. Эти функции обеспечивают довольно сильную защиту от перезаписи адресов возврата, но нам нужно будет разобраться с случаями, когда мы легитимно модифицируем стек возврата, например, во время оптимизации / деоптимизации и обработки исключений.
Аутентификация указателей (PAC-RET)
Так же, как и для непрямых переходов, аутентификация указателей может использоваться для подписывания адресов возврата перед их записью в стек. Это уже включено в V8 для процессоров ARM64.
Побочным эффектом использования аппаратной поддержки для CFI перенаправления вперед и назад является то, что это позволит нам минимизировать влияние на производительность.
Целостность памяти JIT-компиляции
Уникальной проблемой для CFI в JIT-компиляторах является то, что нам необходимо записывать машинный код в исполняемую память во время выполнения. Мы должны защитить память таким образом, чтобы JIT-компилятор мог записывать данные, но примитив записи памяти атакующего не мог этого делать. Простым подходом было бы временное изменение разрешений страницы для добавления/удаления доступа на запись. Однако это по своей сути опасно, поскольку мы должны предполагать, что атакующий может одновременно инициировать произвольную запись из другого потока.
Разрешения памяти на поток
На современных процессорах можно использовать различные представления разрешений памяти, которые применяются только к текущему потоку и могут быстро изменяться в пользовательском режиме. На процессорах x64 это можно реализовать с помощью ключей защиты памяти (pkeys), а ARM объявила расширения наложения разрешений в Armv8.9-A. Это позволяет нам детально переключать доступ на запись к исполняемой памяти, например, добавляя отдельный pkey.
Страницы JIT теперь больше не доступны для записи атакующему, но JIT-компилятору все еще необходимо записывать сгенерированный код в них. В V8 сгенерированный код находится в AssemblerBuffers в куче, который вместо этого может быть поврежден атакующим. Мы могли бы защитить AssemblerBuffers таким же образом, но это просто смещает проблему. Например, нам также нужно будет защитить память, где хранится указатель на AssemblerBuffer. На самом деле, любой код, который включает доступ на запись к такой защищенной памяти, составляет поверхность атаки для CFI и должен быть написан с высокой степенью осторожности. Например, любая запись в указатель, который приходит из незащищенной памяти, является критической, поскольку атакующий может использовать его для повреждения исполняемой памяти. Таким образом, наша цель дизайна состоит в том, чтобы максимально сократить количество таких критических секций и сделать код внутри них коротким и автономным.
Валидация управления потоком
Если мы не хотим защищать все данные компилятора, мы можем считать их недоверенными с точки зрения CFI. Перед записью чего-либо в исполняемую память, мы должны убедиться, что это не приводит к произвольному управлению потоком. Это включает, например, то, что записанный код не выполняет инструкции системных вызовов или не переходит в произвольный код. Конечно, нам также нужно убедиться, что он не изменяет разрешения pkey текущего потока. Заметьте, что мы не пытаемся предотвратить повреждение произвольной памяти, поскольку если код поврежден, мы можем предположить, что у атакующего уже есть такая возможность. Для безопасного выполнения такой проверки нам также потребуется хранить необходимую метаинформацию в защищенной памяти, а также защищать локальные переменные в стеке. Мы провели предварительные тесты для оценки влияния такой проверки на производительность. К счастью, проверка не происходит в критических с точки зрения производительности путях, и мы не наблюдали никаких регрессий в тестах jetstream или speedometer.
Оценка
Исследования в области наступательной безопасности являются важной частью любого проекта по разработке механизмов защиты, и мы постоянно ищем новые способы обхода наших защит. Вот некоторые примеры атак, которые, как мы думаем, будут возможны, и идеи их решения.
Поврежденные аргументы системных вызовов
Как упоминалось ранее, мы предполагаем, что атакующий может инициировать примитив записи памяти одновременно с другими работающими потоками. Если другой поток выполняет системный вызов, некоторые аргументы могут быть под контролем атакующего, если они считываются из памяти. Chrome работает с ограниченным фильтром системных вызовов, но все же есть несколько системных вызовов, которые могут быть использованы для обхода защиты CFI.
Например, системный вызов sigaction используется для регистрации обработчиков сигналов. В ходе нашего исследования мы обнаружили, что вызов sigaction в Chrome достижим при соблюдении CFI. Поскольку аргументы передаются в памяти, атакующий мог бы инициировать этот путь выполнения кода и указать обработчику сигналов произвольный код. К счастью, мы можем легко решить эту проблему: либо заблокировать путь к вызову sigaction, либо заблокировать его фильтром системных вызовов после инициализации.
Другие интересные примеры — системные вызовы управления памятью. Например, если поток вызывает munmap на поврежденном указателе, атакующий может размонтировать страницы только для чтения, а последующий вызов mmap может использовать этот адрес снова, фактически добавляя разрешения на запись к странице. Некоторые операционные системы уже предоставляют защиту от этой атаки с помощью тех, как запечатывание памяти: платформы Apple предоставляют флаг VM_FLAGS_PERMANENT, а OpenBSD имеет системный вызов mimmutable.
Коррупция фрейма сигнала
Когда ядро выполняет обработчик сигнала, оно сохраняет текущее состояние процессора на пользовательский стек. Второй поток может повредить сохраненное состояние, которое затем будет восстановлено ядром. Защита от этого в пространстве пользователя кажется сложной, если данные фрейма сигнала не доверенные. В такой момент придется всегда выходить или перезаписывать фрейм сигнала известным сохраненным состоянием для возврата. Более многообещающим подходом было бы защищать стек сигналов с использованием разрешений на память для каждого потока. Например, стек sigaltstack с пометкой pkey защитил бы от злонамеренных перезаписей, но это потребовало бы временного разрешения записи ядром при сохранении состояния процессора на нем.
v8CTF
Это были лишь несколько примеров потенциальных атак, над которыми мы работаем, и мы также хотим больше узнать от сообщества по безопасности. Если это вас заинтересует, попробуйте свои силы в недавно запущенном v8CTF! Эксплуатируйте V8 и получите вознаграждение, эксплойты, нацеленные на уязвимости н-день, явно входят в область охвата!