Повышение привилегий на уровне ядра в TrustZone (CVE-2016-2431)

Повышение привилегий на уровне ядра в TrustZone (CVE-2016-2431)

В этой статье мы продолжим наше путешествие, начиная от неограниченных привилегий и заканчивая выполнением кода на уровне ядра в TrustZone.

Автор: laginimaineb
В этой статье мы продолжим наше путешествие, начиная от неограниченных привилегий и заканчивая выполнением кода на уровне ядра в TrustZone. После того как в предыдущей статье мы повышали привилегии в среде QSEE (Qualcomm Secure Execution Environment), у нас осталась задача по эксплуатации уязвимостей в ядре в TrustZone.
Существует несколько интересных фишек, которые доступны из контекста ядра в TrustZone. Вот некоторые из них:

  • Прямой и полный контроль над любым приложением, работающим в среде QSEE, включая всю конфиденциальную информацию. Например, отпечатки пальцев или ключи шифрования (более подробно об этих техниках будет рассказано в следующих статьях).
  • Отключение аппаратных защит, после чего становится возможным чтение и запись напрямую во все области динамической памяти (DRAM), включая память периферийных устройств, подключенных к плате (например, модема).
  • Как мы уже смогли убедиться, можно получить доступ к компоненту QFuse, который отвечает за некоторые функции различных устройств. В некоторых случаях становится возможной разблокировка загрузчика (в зависимости от схемы реализации блокировки).

Теперь, когда контекст обозначен, перейдем к практической части!
https://2.bp.blogspot.com/-3FB8d3r0WVo/V11pogLjoeI/AAAAAAAAD-I/VD7aqKzuyn4zLalnA8Udi6GRcCx00QSpQCLcB/s1600/minas_tirith_2.png
Схема эксплуатации
В операционной системе QSEOS (Qualcomm Secure Environment Operating System), как и во многих ОС, доступны службы для приложений, работающих на базе системных вызовов.
В каждой операционной системе реализованы различные методы защиты от вредоносных программ. В случае с системными вызовами ОС следует всегда проверять информацию, поступающую от приложений. Другими словами, должны существовать «границы доверия» между операционной системой и работающими приложениями.
Теперь посмотрим, как обстоят дела на самом деле.
В зоне безопасности («Безопасном мире»), так же как и в «Обычном мире», пользовательские приложения могут запускать системные вызовы при помощи инструкции SVC. В QSEE все системные вызовы запускаются через функцию, которую я назвал «qsee_syscall»:
https://3.bp.blogspot.com/-zU775jk_280/V1W_V6hHsVI/AAAAAAAAD7M/x8_voDv-LmEAyL3tkRKEBUeIzx_IchPFACLcB/s1600/Screenshot%2Bfrom%2B2016-06-06%2B21%253A19%253A37.png
Рисунок 1: Перечень инструкций функции qsee_syscall
Как видно из рисунка выше, функция qsee_syscall представляет собой простейшую обертку и делает следующее:

  • Сохраняет номер системного вызова в регистре R0.
  • Сохраняет аргументы системного вызова в регистрах R4-R9.
  • Вызывает инструкцию SVC с кодом 0x1400.
  • Возвращает результаты работы системного вызова через регистр R0.

Теперь, когда мы знаем, как запускаются системные вызовы, рассмотрим код ядра в TrustZone, который используется для обработки запросов инструкции SVC. При запуске инструкции в «Безопасном мире», так же как и в «Обычном мире», необходима регистрация адреса вектора, к которому впоследствии перейдет процессор.
В отличие от инструкций SMC (применяемых для запроса служб «Безопасного мира» из «Обычного мира»), которые используют регистр MVBAR (Monitor Vector Base Address Register) для базового адреса вектора, инструкции SVC просто используют «Безопасную» версию регистра VBAR (Vector Base Address Register).
https://2.bp.blogspot.com/-yXbzmQylPsg/V1XJO4h4zYI/AAAAAAAAD7c/9FEDsItbkiM7uFQGArcORyydDJk4OaknQCLcB/s1600/Screenshot%2Bfrom%2B2016-06-06%2B21%253A46%253A16.png
Рисунок 2: Соответствие инструкций и регистров, используемых для базовых адресов векторов
Доступ к регистру VBAR осуществляется при помощи опкодов MRC/MCR со следующими операндами:
https://3.bp.blogspot.com/-uHy0umvcdOA/V1XJy4qeSPI/AAAAAAAAD7k/m1H9uF3A-wEX6UjJRYqsfpvp2XUaL23YwCLcB/s1600/Screenshot%2Bfrom%2B2016-06-06%2B22%253A06%253A36.png
Рисунок 3: Операнды, используемые в опкодах MRC/MCR, для чтения и записи в регистр VBAR
То есть мы можем просто поискать внутри ядра в TrustZone опкод MCR с операндами, указанными выше, для того, чтобы найти адрес безопасной копии регистра VBAR. Поиск внутри образа TrustZone привел к следующим результатам:
https://2.bp.blogspot.com/-yR266QxuEFk/V1XKmSwy4eI/AAAAAAAAD70/yoQCoaFxZaEzBgmhvyYmgiRxl_9yA3NaACLcB/s1600/Screenshot%2Bfrom%2B2016-06-06%2B22%253A10%253A08.png
Рисунок 4: Результаты поиска опкода MCR
Согласно документации на архитектуру ARM, «Безопасный вектор» имеет следующую структуру:
https://1.bp.blogspot.com/-RTNrbVEg8Ec/V1XLNnv9K5I/AAAAAAAAD78/UtP8AMKrqfQOiDQhnPLpD3NS4DX4BnUCQCLcB/s1600/Screenshot%2Bfrom%2B2016-06-06%2B22%253A12%253A05.png
Рисунок 5: Структура «Безопасного вектора» в архитектуре ARM
Отсюда мы можем начать отслеживать выполнение кода, начиная от обработчика SVC в таблице векторов.
Вначале в коде выполняются стандартные процедуры, такие как сохранение аргументов и контекста, после чего начинается обработка запрашиваемого системного вызова. Разработчики компании Qualcomm проявили любезность и оставили строку «app_syscall_handler», идентифицирующую функцию обработки, и мы тоже будем использовать это имя. Рассмотрим общую структурную схему данной функции:
https://2.bp.blogspot.com/-athOsF4ef9Q/V11iBzuw7NI/AAAAAAAAD9E/NnlqPDSSLAIiFgbPugEZ1pcpW49nckaiwCLcB/s1600/Screenshot%2Bfrom%2B2016-06-12%2B16%253A20%253A34.png
Рисунок 6: Структурная схема функции app_syscall_handler
Достаточно приличный объем кода.
Однако при более внимательном рассмотрении оказывается, что структура очень поверхностная, и каждая ветка кода относительно проста. На самом деле, данная функция представляет собой огромную структуру switch-case, которая использует командный код системного вызова, заносимого пользователем в регистр R0, для запуска нужного системного вызова.
https://2.bp.blogspot.com/-fZ_JQsg79-g/V11k_8Zj9MI/AAAAAAAAD9g/EBWOtP-cJtc98IGQhwkD8MKnN0wy9nTKgCLcB/s1600/Screenshot%2Bfrom%2B2016-06-12%2B16%253A34%253A28.png
Рисунок 7: Часть конструкции switch-case в функции app_syscall_handler
При изучении кода выше выясняется, что кое-что упущено, а конкретно проверка аргументов передаваемых пользователем. Поскольку в app_syscall_handler подобного функционала не наблюдается, остается вариант, что нужные проверки делаются внутри системных вызовов на уровне ядра. Настало время копнуть чуть глубже.
Как видно из рисунка выше, большинство системных вызовов запускаются не напрямую, а при помощи набора глобальных указателей, каждый из которых указывает на различные таблицы, поддерживаемые системные вызовы.
https://4.bp.blogspot.com/-gMhdEanZxnQ/V11mZtyIQpI/AAAAAAAAD9w/in9KZHDq3Y03R58UEmTBnR9T9D-s_oPFgCLcB/s1600/Screenshot%2Bfrom%2B2016-06-12%2B16%253A39%253A56.png
Рисунок 8: Перечень указателей на таблицы с системными вызовами
Через перекрестные ссылки было найдено местонахождение таблиц, на которые ссылаются данные указатели. Оказалось, что структура таблиц довольно проста. Каждая таблица содержи 32-битный номер, представляющий собой номер системного вызова, за которым следует указатель на функцию обработки системного вызова. Ниже показана одна из таких таблиц:
https://1.bp.blogspot.com/--KrsPs3TU0Q/V11nTGDKPKI/AAAAAAAAD98/Tjohb2duZIse1xTg6pjzj_5yZhzyP7XAgCLcB/s1600/Screenshot%2Bfrom%2B2016-06-12%2B16%253A43%253A49.png
Рисунок 9: Одна из таблиц с системными вызовами
Как видно из рисунка выше, существует некоторая логика группировки каждого набора системных вызовов. Например, в таблице 6 (той, что показана выше) содержатся системные вызовы, имеющие отношение к управлению памяти (хотя, остальные системные вызовы в других таблицах структурированы менее жестко).
Давайте рассмотрим один из системных вызовов, внутри которого должна выполняться проверка аргументов. Хорошим кандидатом для проверки станет системный вызов, принимающий указатель в качестве аргумента и записывающий информацию в то место, куда указывает переданный указатель. Очевидно, что при подобном функционале должны выполняться строгие проверки на предмет того, что указатель находится в областях памяти, принадлежащим приложениям среды QSEE.
Изучая приложение widevine, находим следующий системный вызов:
https://3.bp.blogspot.com/-TdY-Zz5Oe_A/V11skc7QVkI/AAAAAAAAD-c/kh9zq62zMCw0KEwInnVKTrV_KqnF22flwCLcB/s1600/Screenshot%2Bfrom%2B2016-06-12%2B17%253A07%253A07.png
Рисунок 10: Системный вызов qsee_cipher_get_param
Этот системный вызов принимает четыре аргумента:

  • Указатель на объект «cipher», который ранее был инициализирован при вызове функции «qsee_cipher_init».
  • Тип параметра, который будет получен из объекта cipher.
  • Адрес, куда будет записан полученный параметр.
  • Неизвестный аргумент.

Конечно, приложения в среде QSEE всегда отрабатывают корректно и устанавливают правильный адрес выходного указателя, но что происходит внутри ядра в TrustZone? На данный момент у нас есть достаточно знаний, чтобы исследовать этот вопрос. Внутри конструкции switch-case функции app_syscall_handler находим таблицу с нужным системным вызовом и смещение функции qsee_cipher_get_param на уровне ядра. В итоге выходим на функцию qsee_cipher_get_param:
https://2.bp.blogspot.com/-d6Wdyoej4e0/V111XOepCpI/AAAAAAAAD-8/cXAkBWBy0vstOezFAWCzs_jw5ugu1oPkQCLcB/s1600/qsee_cipher_get_param_.png
Рисунок 11: Реализация системного вызова qsee_cipher_get_param на уровне ядра
Выясняется, что на уровне ядра в TrustZone отсутствуют какие-либо проверки практически всех параметров, передаваемых пользователем. Несмотря на то, что в функции проверяется, чтобы указатели не были пустыми, и переменная param_type находилась внутри допустимого диапазона, аргумент output используется как есть. Более того, если мы используем тип параметра (param_type) с номером 3, функция запишет один байт из объекта cipher по переданному указателю!
Сей факт не является просто случайностью. Если посмотреть другие системные вызовы, реализованные на уровне ядра, то можно увидеть, что на уровне ядра в TrustZone не выполняется вообще никаких проверок аргументов, переданных из среды QSEE (если быть более точным, то используется любые переданные указатели). То есть на момент написания статьи абсолютно все системные вызовы были уязвимыми.
При написании эксплоита мы будем пользоваться функцией qsee_cipher_get_param.
Полное чтение/запись
Как всегда при написании эксплоита мы попробуем оптимизировать отдельные этапы. Чем большее время мы тратим на улучшение базовых функций, тем чище и надежнее будет наш эксплоит, что в долгосрочной перспективе сильно сэкономит время.
На данный момент мы можем записать неконтролируемые данные из объекта cipher в контролируемую область памяти. Естественно, было бы неплохо контролировать и ту информацию, которую мы будем записывать.
Поскольку функция «qsee_cipher_get_param» используется для чтения параметра из объекта cipher, соответственно, должна существовать функция, которая устанавливает этот параметр. Результаты поиска функции «qsee_cipher_set_param» в приложении widevine подтверждают наши догадки:
https://2.bp.blogspot.com/-bNECIu2uyRE/V11417P2dbI/AAAAAAAAD_Y/Upe0e8qdSSc1SJ8kD-C0FIC822QvN1bsACLcB/s1600/Screenshot%2Bfrom%2B2016-06-12%2B17%253A59%253A26.png
Рисунок 12: Функция qsee_cipher_set_param
Посмотрим на реализацию этого системного вызова на уровне ядра:
https://4.bp.blogspot.com/-ddqGTJ9ZJgo/V12XEfAXPsI/AAAAAAAAEAQ/E7tSA4CMTfoCM4ty3aTqLk082v66fqGTQCLcB/s1600/qsee_cipher_set_param.png
Рисунок 13: Реализация системного вызова qsee_cipher_set_param на уровне ядра
Кажется, мы можем установить значение параметра, используя то же самое значение аргумента param_type (3) и передавая указатель на управляемую область памяти, которая будет содержать байт для записи, внутри среды QSEE. Ядро в TrustZone без каких-либо проблем будет хранить переданное значение внутри объекта cipher, позволив нам в дальнейшем записать данное значение по любому адресу при помощи функции qsee_cipher_get_param и целевого указателя.
Собирая все вместе, получаем следующий алгоритм:

  • Инициализируем объект cipher при помощи функции qsee_cipher_init.
  • Выделяем буфер в среде QSEE.
  • Записываем нужный байт в выделенный буфер.
  • Вызываем функцию qsee_cipher_set_param и в аргументе param_value передаем выделенный буфер.
  • Вызываем функцию qsee_cipher_get_param и передаем целевой адрес в качестве аргумента output.

https://1.bp.blogspot.com/-TFWKs1ggDcs/V12dObPJ0RI/AAAAAAAAEAg/TlN2_VEul1kEzCfJgh7qdBGTV9jcfCyjgCLcB/s1600/Screenshot%2Bfrom%2B2016-06-12%2B20%253A34%253A04.png
Рисунок 14: Схема работы с памятью (чтение/запись)
Внимательные читатели могут заметить, что схему алгоритма можно перевернуть, чтобы считать произвольную область памяти. Для решения этой задачи необходимо передать в аргументе param_value в функции qsee_cipher_set_param адрес области памяти, которую мы хотим прочитать. Далее ядро в TrustZone считает значение по тому адресу и сохранит в объекте cipher. Затем мы можем получить данное значение посредством вызова функции qsee_cipher_get_param.
Написание эксплоита
Используя алгоритм, описанный выше, мы получаем полный доступ на чтение/запись. Осталось научить запускать произвольный код на уровне ядра в TrustZone.
Первая мысль, которая приходит в голову, - записать шелл-код в сегменты кода внутри ядра в TrustZone. Однако здесь есть одна загвоздка – сегменты кода на уровне ядра в TrustZone на современных устройствах защищены блоками, называемыми XPU, которые не дают модифицировать как код ядра, так и некоторые другие области памяти. Сей факт значительно осложняет модификацию кода ядра.
С другой стороны, у нас есть доступ к динамически выделенному коду в «Безопасном мире», который используется приложениями среды QSEE. Мы можем обойти защитные биты на страницах кода (поскольку эти страницы помечены на чтение/выполнение) и напрямую модифицировать код из контекста ядра в TrustZone. Далее мы просто переходим к созданному коду из контекста ядра  и запускаем ту часть, которую пожелаем.
Обойти защитные биты можно без модификации трансляционной таблицы (translation table), но используя удобную функцию в ARM MMU (Memory management unit, блок управления памятью) под названием «domains» (домены).
В трансляционной таблице в архитектуре ARM каждая запись имеет поля с перечнем прав доступа и 4-битное поле, идентифицирующее домен, которому принадлежит таблица.
В ARM MMU есть 32-битный регистр DACR (Domain Access Control Register) с 16 парами битов (одна пара для каждого домена). Данные биты используются для отметки того, стоит ли генерировать предупреждения при чтении/записи во время трансляции внутри выбранного домена.
https://1.bp.blogspot.com/-qBU0YGqLo8w/VcZUhIgjRiI/AAAAAAAACcU/DicJQWfsF2M/s640/Screenshot%2Bfrom%2B2015-08-08%2B22%253A11%253A54.png
Рисунок 15: Формат регистра DACR
Когда процессор пытается получить доступ к определенному адресу, MMU вначале проверяет права доступа. Если доступ разрешен, предупреждение не генерируется.
В противном случае MMU проверяет, установлены ли биты, соответствующие выбранному домену в регистре DACR. Если биты установлены, тогда предупреждение игнорируется и доступ разрешается.
Вышесказанное означает, что установка значения 0xFFFFFFFF в регистр DACR позволит получить доступ к любому адресу выделенной памяти при чтении/записи без генерации предупреждений и, что более важно, без модификации трансляционной таблицы.
Более того, на уровне ядра в TrustZone уже есть кусок кода, используемый для установки значения в регистр DACR. То есть нам остается лишь указать нужное значение (0xFFFFFFFF).
https://2.bp.blogspot.com/-lvYXMys_2oc/V13TZjGul9I/AAAAAAAAEA0/zarYJ98eTh8diq_p46HtNeJ0z5pJeXsKACLcB/s1600/Screenshot%2Bfrom%2B2016-06-13%2B00%253A24%253A43.png
Рисунок 16: Функция для установки значения в регистр DACR
После того как мы получили доступ на чтение/запись на уровне ядра в TrustZone, нам осталось научиться выполнять произвольные функции и возобновлять поток выполнения, что позволит нам изменить регистр DACR при помощи гаджетов, указанных выше, и впоследствии записать и выполнить шелл-код в «Безопасном мире».
Подмена системных вызовов
Как вы уже знаете, большинство системных в среде QSEE запускаются неявно при помощи глобальных указателей, каждый из которых указывает на соответствующую таблицу с системными вызовами.
Сами по себе таблицы размещены в областях памяти, которые защищены XPU, но указатели не защищены никак, поскольку используются во время выполнения приложения и должны находиться в области памяти, доступной для модификации.
Сей факт позволяет значительно упрощает подмену и управление выполнением кода на уровне ядра.
Все, что нам нужно, - создать собственную «поддельную» таблицу с системными вызовами. Созданная таблица будет идентичной настоящей за исключением одной записи, которая будет указывать на функцию по нашему выбору (вместо обработчика системного вызова).  
Следует отметить, что поскольку мы не хотим спровоцировать нежелательных спецэффектов для других приложений, работающих в среде QSEE, важно выбрать запись, соответствующую неиспользуемому (или редко используемому) системному вызову.
После создания поддельной таблицы используем ранее описанный алгоритм для добавления в глобальную таблицу с системными вызовами ссылки на нашу поддельную таблицу.
Далее при запуске «заряженного» системного вызова из среды QSEE наша функция будет запускаться внутри ядра в TrustZone. Более того, при помощи функции app_syscall_handler мы получим результаты работы запущенного кода в среду QSEE после отработки вызова SVC.
https://3.bp.blogspot.com/-SOQWNV76RxY/V13hSajet5I/AAAAAAAAEBE/CgEBEHiMHj0hpTfBvqiwFKvXL1BLBiNKACLcB/s1600/Screenshot%2Bfrom%2B2016-06-13%2B01%253A24%253A19.png
Рисунок 17: Алгоритм добавления и использования поддельного системного вызова
Собираем все вместе
На данный момент у нас есть все необходимое для написания простейшего эксплоита, который будет записывать шелл-код в «Безопасный мир», выполнять записанное в контексте ядра вTrustZone и возобновлять выполнение.
Нам нужно:

  • Разместить поддельную таблицу с системными вызовами в среде QSEE.
  • Использовать алгоритм для перезаписи настоящей таблицы так, чтобы было указание на нашу поддельную таблицу.
  • Добавить запись «заряженного» системного вызова в поддельную таблицу для указания на функцию, модифицирующую регистр DACR на уровне ядра в TrustZone.
  • Выполнить «заряженный» системный вызов для вызова функции, модифицирующей регистр DACR в ядре TrustZone. Таким образом, мы устанавливаем в регистр DACR значение 0xFFFFFFFF.
  • Использовать гаджет для записи шелл-кода напрямую в кодовую страницу, принадлежащую приложению, работающему в среде QSEE.
  • Сделать недействительным кэш инструкций (во избежание проблем с новым кодом).
  • Добавить запись в запись в поддельную таблицу для указания на шелл-код.
  • Вызвать «заряженный» системный вызов для перехода к написанному шелл-коду в контексте ядра в TrustZone!

На рисунке ниже показана иллюстрация всех шагов:
https://3.bp.blogspot.com/-YFzZS8TyCq4/V2E6ks1drHI/AAAAAAAAECM/JMlqMdR_YDUeFjd6-u61rfdSI6h_s4TIwCLcB/s1600/Screenshot%2Bfrom%2B2016-06-15%2B14%253A20%253A39.png
Рисунок 18: Графическое представление полного алгоритма эксплоита
Демонстрация эксплоита
Полную версию эксплоита можно взять здесь: https://github.com/laginimaineb/cve-2016-2431.
Эксплоит написан на базе предыдущего с целью выполнения кода в среде QSEE. Если вы хотите поэкспериментировать, обратите внимание на следующие функции:

  • tzbsp_execute_functionвызывает указанную функцию с нужными аргументами внутри ядра в TrustZone.

https://2.bp.blogspot.com/-VahpwNj76KE/V13mUOQawjI/AAAAAAAAEBU/oV45L8KiU9Iu6ywd4Ti3g4siJLZ0xzyIACLcB/s1600/Screenshot%2Bfrom%2B2016-06-13%2B01%253A45%253A07.png
Рисунок 19: Прототип функции tzbsp_execute_function

  • tzbsp_load_and_exec_file загружает шеллкод из указанного файла и запускает загруженное внутри ядра в TrustZone.

https://4.bp.blogspot.com/-urZBsLoPTsc/V13mt8YNiBI/AAAAAAAAEBc/-NL5vwCkF-wPuGSjWtlroZk9n14SgKU9gCLcB/s1600/Screenshot%2Bfrom%2B2016-06-13%2B01%253A46%253A26.png
Рисунок 20: Прототип функции tzbsp_load_and_exec_file
Кроме того, я добавил небольшой скрипт «build_shellcode.sh», который можно использовать для сборки шелл-кода из файла "shellcode.S" и последующей записи в бинарный блоб, который затем загружается и выполняется при помощи функции выше.
Хронология событий

  • 13.10.2015: Обнаружена уязвимость и отправлен базовый алгоритм эксплуатации.
  • 15.10.2015: Получен ответ от компании Google.
  • 16.10.2015: В Google отправлена полная версия эксплоита.
  • 30.03.2016: Появилось описание в CVE.
  • 02.05.2016: Проблема устранена и выпущен публичный бюллетень для устройств Nexus.

Насколько мне известно, уязвимость присутствовала во всех устройствах и всех версиях QSEOS до тех пор, пока 02.05.2016 проблема не была устранена. До того момента контроль над выполнением кода в среде QSEE был эквивалентен контролю над выполнением кода на уровне ядра в TrustZone. То есть, по сути, можно было контролировать выполнение кода во всех контекстах и аспектах.
Поскольку на тот момент не было публичных исследований среды QSEE, данная проблема не предавалась огласке. Надеюсь, что в будущие исследования QSEE и TrustZone помогут выявить схожие проблемы и, таким образом, граница между QSEOS и QSEE станет безопаснее.

Где кванты и ИИ становятся искусством?

На перекрестке науки и фантазии — наш канал

Подписаться