С течением времени появляется все больше защитных технологий, из-за которых хакерам приходится туже затягивать пояса. Однако у этой медали две стороны: защитные технологии также создают дополнительную поверхность атаки, а чтобы их обойти, нужно только задействовать уязвимости в их коде. Посмотрим на одну из таких технологий — ARM TrustZone. Ее реализации содержат огромный объем кода, и чтобы искать в них уязвимости, нужен какой-то автоматический способ. Задействуем старый проверенный метод — фаззинг. Но умный! Будем фаззить специальные приложения, которые появились с введением технологии TrustZone — трастлеты. Чтобы подробно описать выбранный нами способ фаззинга, сначала обратимся к теории про TrustZone, доверенные операционные системы и взаимодействие с обычной операционной системой. Это недолго. Поехали! Технология TrustZone в процессорах ARM позволяет перенести обработку конфиденциальной информации в изолированную защищенную среду. Такой обработкой занимаются, например, сервисы Keystore, Fingerprint в ОС Android, технологии защиты авторских прав DRM и др. Про устройство TrustZone уже написано очень много, поэтому мы только кратко напомним.  TrustZone разделяет "мир" (в терминах TrustZone — World) на два — Normal World и Secure World — и добавляет в процессор целых четыре режима исполнения: На SoC с технологией TrustZone могут одновременно работать сразу две операционные системы. Одна, которая работает в Normal World, называется Rich OS, а вторая — из Secure World — TEE (Trusted Execution Environment) OS. Существует уже не один десяток этих доверенных операционных систем. Мы сосредоточимся на одной конкретной — Trustonic Kinibi. Она, в частности, используется на телефонах Samsung с SoC Exynos включительно до Galaxy S9. Компания Trustonic была создана компаниями ARM, Gemalto и Giesecke & Devrient (G&D) и продолжила развитие операционной системы Mobicore компании Giesecke & Devrient (G&D) под именем Kinibi. Операционная система Kinibi поддерживает стандарты Global Platform Trusted Execution Environment. Ее структурная схема приведена на рисунке.  Как видно, имплементация TrustZone включает компоненты не только в "защищенном мире", но и компоненты в "нормальном мире". Чтобы разобраться с основными из них, лучше посмотреть на схему этой системы с точки зрения разработчика.  На низком уровне в защищенном мире, кроме микроядра, работают драйверы и менеджер рантайма. А в нормальном мире работает специальный драйвер, обеспечивающий переход процессора в защищенный мир по запросу приложений. На уровне userspace работают приложения и компоненты обеспечивающие API для связи приложений нормального и защищенного миров. Также в нормальном мире работает специальный демон, который обеспечивает начальный запуск некоторых трастлетов, и через который проходят все запросы к трастлетам из клиентских приложений. Наборов API в Kinibi два: Global Platform API (обозначен зеленым цветом) и Legacy API (красный). Оба набора обеспечивают примерно одинаковый набор функций, только первый построен по стандартам Global Platform, а второй, похоже, был еще до стандарта, и поэтому именуется Legacy. Несмотря на то, что, судя по названию, от его использования надо уходить, в трастлетах Samsung используется только Legacy API. Чтобы использовать возможности, предоставляемые технологией TrustZone, приложения в нормальном мире, называемые клиентскими приложениями, общаются с доверенными приложениями — трастлетами. Трастлеты реализуют различные функции: аутентификация, управление ключами, работа с аппаратными компонентами, реализующими функции безопасности, и др. Запросы к трастлетам передаются с помощью специальной общей памяти. Нормальный мир и защищенный мир, по технологии TrustZone, на верхних уровнях (EL0 и S-EL0) изолированы друг от друга по памяти, и чтобы создать такой регион общей между ними памяти, называемой World Shared Memory (WSM), используется API, предоставляемый защищенным миром. Общая схема взаимодействия клиентского приложения и трастлета выглядит примерно так:  Псевдокоды сессии взаимодействия для клиентского приложения и для трастлета выглядят довольно шаблонно. Для клиентского приложения: Для трастлета: mcNotify/tlApiNotify и mcWaitNotification/tlApiWaitNotification — это и есть те самые функции уведомления о том, что запрос/ответ готов для получения в другом мире, и функции ожидания обработки запроса. Также у клиентского приложения есть возможность использовать функцию mcMap. Она позволяет создать еще один буфер WSM, если это требуется. Всего с помощью этой функции можно создать только четыре таких буфера. С клиентскими приложениями понятно — для телефонов Samsung это обычные приложения в Android. А что же представляют собой трастлеты? Трастлеты находятся в обычной файловой системе устройства и являются файлами, содержащими исполняемый код. Это не привычный для Android формат ELF или APK. Трастлеты в операционной системе Kinibi имеют собственный формат MobiCore Load Format (MCLF). Он описан в открытых исходных кодах компонентов уровня userspace, которые Trustonic разместил на Github. Схематично структуру файла трастлета можно изобразить на такой картинке (трастлет — слева).  Для трастлетов можно выделить следующие особенности: Как видим, трастлеты довольно ограничены в возможностях. Кроме этого, они используют некоторые защитные механизмы, типа различных атрибутов памяти, и также большинство трастлетов используют stack canaries для защиты от эксплуатации перезаписи стека. А вот ASLR в Kinibi нет, хотя он планируется в новых версиях. Несмотря на все ограничения, трастлеты — очень интересная цель для атакующего по следующим причинам: В качестве подопытного устройства мы использовали Samsung Galaxy S8. Если поискать в нем трастлеты, окажется, что их там довольно много.  То есть и кода очень много. Использовать для поиска уязвимостей статический анализ бинарного кода кажется не очень хорошей идеей. Задействовать динамический анализ так просто не получится, хотя бы потому что трастлеты имеют свой формат, отличный от того, что может запускаться на традиционных операционных системах. Хорошо бы еще использовать зарекомендовавший себя метод фаззинга с обратной связью, и каким-либо образом ловить краши трастлетов, когда они будут случаться. Попробуем решить эту интересную задачку. Для тех, кто еще не познакомился с замечательным инструментом AFL и с его многочисленными надстройками, рекомендуем к прочтению эту хорошую статью. А все остальные, вероятно, знают, что AFL умеет фаззить файлы формата ELF. Причем даже бинарные файлы, скомпилированные изначально без инструментации AFL. Это достигается за счет режима qemu mode. AFL использует специальную сборку эмулятора qemu, в которой к режиму qemu user mode добавлена функциональность бинарной инструментации инструкций ветвления. Это позволяет ему проводить фаззинг с контролем покрытия кода даже для бинарных файлов. А бонусом к этому идет возможность фаззить исполняемые файлы не только родной архитектуры, но и всех архитектур, которые поддерживает qemu. Но для того чтобы использовать этот режим в нашей задаче, надо как-то сконвертировать трастлеты в формат ELF. Посмотрим повнимательнее на файлы трастлетов. Благодаря открытому формату, для IDA Pro существует загрузчик для них. Если открыть любой трастлет, кроме, собственно, его кода, можно увидеть, что он использует функции библиотеки mclib. Интересно то, что все вызовы таких функций проходят через одну функцию по адресу, записанному в заголовке трастлета. Например, так выглядит функция tlApiLogvPrintf в коде трастлета, которая, очевидно, занимается выводом строк.  Видно, что она пробрасывает все параметры дальше другой функции. Это функция диспетчеризации mclib, адрес которой записан в заголовке MCLF в поле, которое называется tlApiLibEntry. То есть библиотечные функции, вызываемые таким образом, это единственная зависимость для трастлетов, других ссылок вовне трастлеты не имеют. Значит, если мы реализуем заглушки для функций API, сможем исполнить код трастлета в обычной среде Linux, конечно, сначала как-то преобразовав его в файл ELF. А значит, и сможем отлаживать и фаззить его. Чтобы превратить трастлет в файл формата ELF, можно взять уже готовый файл, например, скомпилировать пустое приложение с функцией main, и добавить в него секции трастлета вместе с его заголовком. Easy! Также необходимо как-то передать управление на код трастлета. С этим тоже нет проблем, в заголовке трастлета есть адрес его точки входа. Определим этот адрес в нашей функции main как адрес функции и вызовем ее. Подумав и поэкспериментировав, можно набросать следующий план решения нашей задачи: Так как нам нужно превратить в эльфы сразу много трастлетов, надо подумать над автоматизацией этих задач. Для каждого трастлета нужно автоматически определять следующие параметры: точку входа, адреса секций трастлета и размер входного буфера WSM. Добавим это в план. 1) Точка входа Первый пункт плана легко реализовать следующим кодом. Его можно добавить в функцию main для нашего исходного файла ELF. Компилируем наш код в объектный файл. Символ sym_tlMain надо добавить в объектный файл. Это можно сделать с помощью objcopy. В результате получаем tlrun.o.1 — скомпилированный исходник с функцией main, передающей управление на код трастлета. 2) Библиотечные функции Чтобы реализовать библиотечные функции, для начала нам нужен список всех этих функций. Когда-то давно случилась утечка из Qualcomm с кучей материалов для мобильных устройств на базе их процессоров. Среди этих материалов также были некоторые образы, заголовочные файлы и отладочные образы некоторых компонентов для операционной системы mobicore. Оттуда мы взяли прототипы библиотечных функций с их номерами, передаваемые как параметр в функцию диспетчеризации. Для функций с известным предназначением типа tlApiMalloc или tlApiLogvPrintf мы сделали соответствующие реализации с помощью аналогичных функций из libc. А функции не настолько понятные, например, tlApiSecSPICmd мы заменили простыми заглушками, выводящими свое имя и возвращающими статус ОК. Весь API компилируется в файл tllib.o 3) Функция диспетчеризации Аналогично адресу точки входа добавляем символ, его адрес, одинаковый для всех трастлетов: Реализация функции диспетчеризации тривиальна. Нужно учесть только, что ее адрес необходимо записать в заголовок. Так как мы не знаем заранее, по какому адресу после линковки и запуска расположится наша функция диспетчеризации, записывать ее адрес в заголовок трастлета надо уже в рантайме. Например, при запуске файла до начала исполнения функции main. 4) Секции Добавить секции к объектному файлу, также используем objcopy. Здесь .tlbin_text — название секции трастлета, а .text.bin — название файла с дампом этой секции. Дамп в файл можно сделать с помощью все той же IDA. В результате этого преобразования, к исходному файлу ELF добавится трастлет в бинарном виде. 5) Автоматизация Для всей сборки мы решили использовать один большой Makefile общий для всех трастлетов и еще по одному маленькому, подключаемому к нему для каждого отдельного трастлета с его параметрами. Для каждого трастлета нужно определить точку входа, адреса секций и размер буфера WSM. Первые два параметра легко получить с помощью простого скрипта для IDA, а определение размера буфера иногда не так просто автоматизировать. Можно автоматизировать и эту задачу, а можно потратить 10 минут, чтобы определить его для всех трастлетов, анализируя их код вручную. Эти параметры можно задавать как переменные в своем маленьком Makefile. А в большом Makefile использовать эти параметры таким образом: Итак, мы превратили трастлет в файл формата ELF с правильным расположением секций трастлета в памяти и правильными адресами в заголовке. В теории он даже может корректно исполняться и дальше фаззиться. Ну так давайте это проверим! Так как AFL использует qemu для исполнения кода неродной архитектуры, для начала неплохо бы проверить, исполняется ли вообще наш эльф под эмулятором. И тут сразу же начались проблемы. Для компиляции кода и сборки файла мы использовали тулчейн arm-linux-gnueabihf. "hf" в конце означает использование компилятором аппаратной поддержки Hard float в процессорах ARM. При попытке запустить наш файл под эмулятором qemu сразу падал, выдавая "Segmentation fault". Учитывая, что у нас в коде нигде не было работы с числами с плавающей точкой, причина этого падения была абсолютно непонятна. Немного подумав, мы решили попробовать использовать тулчейн без Hard float arm-linux-gnueabi. И нам повезло! Файл заработал, и в консоли стал появляться вывод от него.  Значит можно фаззить. Запускаем AFL и тут...  Почему-то AFL не видит инструментацию. Сначала было абсолютно непонятно, в чем же проблема. qemu собрана правильно, опция -Q (qemu mode) установлена. Чертыхаясь, пришлось влезть в исходники патчей AFL для qemu. Оказывается, в патчах AFL при загрузке файла ELF qemu ищет кодовую секцию и устанавливает границы адресов, в которых собирается производить инструментацию. Проблема в том, что если кодовых секций несколько, инструментироваться будет почему-то только первая из них. Баг это или фича, но у нас кодовых секций две, и точка входа — main — находится во второй. Очевидно, инструментацию он не видит при запуске, потому что во второй секции ее нет! Шерстя дальше исходники, можно заметить, что при включенной переменной среды AFL_INST_LIBS границы инструментации становятся бесконечными. Включаем ее и запускаем.  Фаззинг работает! Идея подтвердилась! Мы запустили фаззинг с обратной связью на бинарных файлах кастомного формата. Как видим, он даже находит какие-то краши. Таким образом, мы получили надежный способ фаззить такие бинари, отлавливать ошибки в их коде и к тому же запускать их в обычном Линуксе и удобно отлаживать существующими инструментами. Класс! За несколько дней мы провели фаззинг всех трастлетов. В результате у нас появилось очень много входных данных, генерирующих краши, и задача по анализу всех этих крашей. Суммарно для 23 трастлетов AFL нашел 477 тесткейсов, генерирующих краши. Огромный объем, который совершенно не хочется обрабатывать вручную. Среди этого множества тесткейсов есть почти одинаковые, генерирующие краш в одном и том же месте. Чтобы убрать избыточность тесткейсов, можно воспользоваться инструментом afl-cmin. После прохода им по всем трастлетам осталось 225 случаев, которые предстоит проанализировать. Все равно очень много! Чтобы как-то облегчить себе задачу, мы решили использовать инструменты динамического анализа, которые помогут более точно идентифицировать программную ошибку и какие-либо ее свойства. Это поможет оценить эксплуатабельность баги и трудоемкость ее эксплуатации. Итак, чтобы использовать какие-то инструменты динамического анализа, нам надо по крайней мере запустить наши переделанные трастлеты на нативной системе ARM, а не под виртуализацией qemu. Для этого могут подойти Linux или Android. Мы решили взять 32-битную систему с Linux, т.к. трастлеты 32-битные, а Linux более удобен и содержит больше инструментов для динамического анализа, чем Android. И вот тут оказалось, что при запуске наши эльфы сразу выдают Segmentation fault. Выяснилось, что проблема в необычности наших бинарей. При их создании надо располагать секции трастлета по нужным адресам, где адрес кодовой секции трастлета всегда 0x1000. Это первая секция в файле, а перед ней еще располагается заголовок ELF по адресу 0x0. А в системе Linux первые две страницы адресного пространства, до адреса 0x2000, зарезервированы для служебных задач, поэтому, когда загрузчик пытается спроецировать туда секцию, и возникает ошибка. Как оказалось, выход из этой ситуации есть. На 64-битном ядре такого резервирования первых страниц в памяти не происходит, и становится возможным такое расположение секций. Так как наши файлы 32-битные, то удобно предварительно создать на 64-битной системе 32-битную среду. Для этих целей отлично подходит пакет debootstrap. Теперь, когда наши переделанные трастлеты работают на нативной системе ARM, надо попробовать на них инструменты динамического анализа. Среди методов динамического анализа бинарных файлов есть отладка и динамическая бинарная инструментация (DBI). Для первого отлично подходит gdb. А для второго не так много опций: под ARM есть по сути только три стабильных фреймворка DBI — DynamoRIO, Valgrind и Frida. Первый имеет много хороших инструментов для трассировки и отлова ошибок, но загрузчик файлов ELF, который в нем реализован, не справился с загрузкой наших файлов. Valgrind — довольно мощный фреймворк, и у него есть подходящие нам инструменты callgrind для трассировки и memcheck для мониторинга операций с памятью. Оказалось, что они выдают не очень удобные для парсинга результаты, поэтому не подходят для использования в автоматическом режиме на множестве файлов. А Frida мы не успели попробовать. Если кто-нибудь имел опыт использования в Linux на ARM, напишите ваши впечатления в комментариях. Как видим, нам остается довольствоваться только отладчиком. Но с использованием скриптов для gdb даже это уже значительно упрощает нам работу. Еще одна проблема, которая была понятна с самого начала, это библиотечные функции, которые использует трастлет. Мы их заменили заглушками, за исключением функций, которые можно заменить аналогичными из libc. Очевидно, что если в логике трастлета некоторый код обрабатывает результат работы одной из таких функций-заглушек, очень вероятно, что он упадет с ошибкой из-за того, что ожидает совершенно другие данные, и это необязательно будет означать ошибку в коде. Таких функций, для которых нельзя так просто сымитировать поведение реальной функции, довольно много: Чтобы не тратить время на изучение таких сомнительных случаев, мы решили просто не рассматривать тесткейсы, в которых используются эти функции. В автоматическом режиме с помощью скриптов мы собрали следующую информацию по всем трастлетам: Всю эту информацию, как оказалось, очень удобно поместить в базу данных, и дальше запросами SQL выбирать наиболее интересные для анализа случаи и добавлять информацию по результатам анализа.  Например, таким запросом можно показать все тесткейсы, на которых происходит ошибка Segmentation fault: А таким отфильтровать тесткейсы, в которых используется функция tlApiSecSPICmd, которая у нас реализована заглушкой: Таким образом, во всех трастлетах были найдены ошибки разных типов. Некоторые из них не приводили к уязвимостям, но были и те, которые являются уязвимостями и могут быть использованы атакующим. Рассмотрим самую интересную из найденных уязвимостей.  Уязвимость была найдена в трастлете keymaster в коде обработки содержимого буфера TCI при парсинге структуры ASN.1, закодированной по правилам DER. Два поля в этой структуре используются как размеры: один при выделении динамической памяти, а другой — при ее копировании. Очевидно, что если второй размер больше первого, происходит переполнение кучи. Такие уязвимости обычно приводят к возможности выполнения кода атакующим, поэтому мы попытались сделать полноценный эксплойт для этой уязвимости. При оценке возможности эксплуатации надо также учитывать все ограничения трастлетов, перечисленные выше. Имея на руках переполнение кучи и исходя из данных ограничений, можно представить следующую стратегию эксплуатации: Чтобы это сделать, конечно, надо понимать в деталях, как работает куча в операционной системе Kinibi. Для этого нам пришлось провести реверс-инжиниринг функций аллокации и освобождения памяти mclib, но теперь хорошее описание работы кучи можно посмотреть в этом докладе с конференции ZeroCon, прошедшей в апреле. Еще одним немаловажный вопрос — как создать блок памяти кучи в секции .bss. Для этого нужно сначала с помощью переполнения создать в оригинальной куче блок, указывающий в .bss как на следующий доступный для аллокации блок. Но для того чтобы следующая аллокация произошла по этому адресу и не спровоцировала падение приложения, необходимо, чтобы данные, которые уже там есть, могли быть истолкованы как корректные метаданные кучи.  Таким образом, мы нашли необходимый адрес в секции .bss, который можно использовать для релокации кучи. Перезаписав так указатель функции и инициировав ее вызов, можно передать исполнение на некоторый адрес. Причем, это не может быть адрес нашего шеллкода, так как вся память, доступная нам для записи, в том числе куча и секция .bss, не может исполняться. Поэтому единственной возможностью исполнения своего кода является использование техник code-reuse. На ум сразу приходит ROP. Действительно, мы можем использовать ROP, так как стэк в трастлетах располагается в самых последних адресах секции .bss. Чтобы перезаписать адрес возврата в стэке, придется перезаписать всю секцию неинициализированных данных, начиная с найденного адреса и до нужного места в стэке. Это очень ненадежно, так как мы портим данные, которые могут еще использоваться в промежуток между аллокацией памяти и вызовом перезаписанной функции. Мы все же это попробовали, и, конечно, ничего не сработало. Кроме ROP, можно еще использовать JOP. JOP — это Jump Oriented Programming. В JOP адрес для передачи исполнения извлекается из адреса в памяти или из регистра. Чтобы найти подходящие для JOP гаджеты, подойдет классный инструмент ROPGadget. Он покажет все найденные гаджеты, а чтобы оставить только JOP, можно использовать регулярное выражение типа такого: Нам повезло! В нашем трастлете нашлось довольно много гаджетов.  Теперь нам надо собрать их в цепочку. В ROP программировании для продвижения по цепочке используется встроенная в процессор логика возврата из функций. Можно сказать, что ROP-программа пишется для weird machine, заключенной в архитектуре процессора. А в JOP для этого используется некий супергаджет, который загрузит параметры для вызова из памяти и произведет вызов. В архитектуре ARM, на которой и работает наш трастлет, есть инструкция, подходящая на такую роль — LDMIA (Load Memory Increment Address).  В качестве параметров она принимает регистр, содержащий адрес, данные по которому будут использоваться для загрузки в регистры, и набор регистров, которые необходимо заполнить. Затем регистр с адресом увеличивается на размер прочитанных данных, так что при втором вызове этой инструкции будут читаться уже следующие данные. Это как раз то что надо для JOP! Теперь нужно найти гаджет с инструкцией LDMIA в нашем наборе. Почему-то capstone, который используется в ROPGadget, интерпретирует эту инструкцию как LDMLO.  И нам опять повезло! Такие гаджеты есть. Выберем из них один и используем его как супергаджет, а для построения цепочки будем использовать гаджеты из набора, полученного ранее. Чтобы украсть stack cookie из стека, можно составить такую цепочку. Hello, world из защищенного мира будет выглядеть вот так.  Писать "Hello, world!" из защищенного мира, конечно, весело, но трастлет keymaster, определенно, более интересная цель. Так как он имеет доступ к ключевой информации, мы можем попробовать прочитать ее. Например, в этом посте Gal Beniamini на TEE Qualcomm смог вытащить промежуточные ключи шифрования из защищенного мира и, таким образом, провести offline-атаку на полнодисковое шифрование в Android. Другим вариантом постэксплуатации может быть эскалация привилегий до ядра TEE OS и дальше до монитора EL-3, что откроет практически неограниченные возможности по закреплению в системе. ARM TrustZone и его реализации на различных платформах сейчас довольно актуальная тема для поиска уязвимостей, так как их код еще не так хорошо исследован и содержит ошибки. При этом со стороны Secure World можно добиться повышения привилегий и в обычной системе Android, которая сама по себе уже не такая легкая цель для поиска и эксплуатации багов. Кроме этого, например, Samsung имеет программу bug bounty и награждает исследователей за уязвимости TrustZone, в которой мы тоже поучаствовали.  Благодаря таким классным инструментам как AFL и qemu, возможен фаззинг бинарных файлов кастомного формата даже для "неродных" процессорных архитектур. Фаззинг с обратной связью зарекомендовал себя как очень надежный способ поиска уязвимостей. А с развитием символьного исполнения и инструментов для работы с промежуточным представлением бинарного кода, он станет еще более мощным и универсальным методом. Используйте надежные методы поиска уязвимостей!