С течением времени появляется все больше защитных технологий, из-за которых хакерам приходится туже затягивать пояса. Однако у этой медали две стороны: защитные технологии также создают дополнительную поверхность атаки, а чтобы их обойти, нужно только задействовать уязвимости в их коде.
Посмотрим на одну из таких технологий — ARM TrustZone. Ее реализации содержат огромный объем кода, и чтобы искать в них уязвимости, нужен какой-то автоматический способ. Задействуем старый проверенный метод — фаззинг. Но умный!
Будем фаззить специальные приложения, которые появились с введением технологии TrustZone — трастлеты. Чтобы подробно описать выбранный нами способ фаззинга, сначала обратимся к теории про TrustZone, доверенные операционные системы и взаимодействие с обычной операционной системой. Это недолго. Поехали!
ARM TrustZone
Технология TrustZone в процессорах ARM позволяет перенести обработку конфиденциальной информации в изолированную защищенную среду. Такой обработкой занимаются, например, сервисы Keystore, Fingerprint в ОС Android, технологии защиты авторских прав DRM и др.
Про устройство TrustZone уже написано очень много, поэтому мы только кратко напомним.
TrustZone разделяет "мир" (в терминах TrustZone — World) на два — Normal World и Secure World — и добавляет в процессор целых четыре режима исполнения:
- EL3 — режим монитора — режим, в котором система стартует, и который является самым привилегированным режимом исполнения;
- S-EL2 — режим доверенного гипервизора;
- S-EL1 — режим доверенной операционной системы;
- S-EL0 — режим доверенных приложений (trusted applications, TAs, trustlets), или трастлетов.
На SoC с технологией TrustZone могут одновременно работать сразу две операционные системы. Одна, которая работает в Normal World, называется Rich OS, а вторая — из Secure World — TEE (Trusted Execution Environment) OS. Существует уже не один десяток этих доверенных операционных систем. Мы сосредоточимся на одной конкретной — Trustonic Kinibi. Она, в частности, используется на телефонах Samsung с SoC Exynos включительно до Galaxy S9.
Trustonic Kinibi
Компания 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, предоставляемый защищенным миром.
Общая схема взаимодействия клиентского приложения и трастлета выглядит примерно так:
- Клиентское приложение обращается к демону с UID трастлета, с которым хочет установить сессию;
- Демон посредством драйвера обращается к доверенной операционной системе с запросом на загрузку трастлета;
- Доверенная операционная система загружает трастлет в адресное пространство защищенного мира;
- Клиентское приложение опять через запрос к демону создает буфер WSM и записывает в него данные для запроса к трастлету;
- Клиентское приложение уведомляет защищенный мир о готовности запроса;
- В защищенном мире запрос отправляется на обработку нужному трастлету, и трастлет записывает в буфер WSM результат своей работы;
- Цикл запроса и ответа может повториться;
- Клиентское приложение завершает сессию с трастлетом.
Псевдокоды сессии взаимодействия для клиентского приложения и для трастлета выглядят довольно шаблонно. Для клиентского приложения:
void main()
{
uint8_t* tciBuffer;
uint32_t tciLength;
uint8_t* mem;
uint32_t mem_size;
mcOpenDevice(MC_DEVICE_ID_DEFAULT);
mcMallocWsm(MC_DEVICE_ID_DEFAULT, 0, tciLength, &tciBuffer, 0);
session.deviceId = MC_DEVICE_ID_DEFAULT;
mcOpenSession(&session, &uuid, tciBuffer, tciLength);
mcMap(&session, mem, mem_size, &mapInfo);
mcNotify(&session);
mcWaitNotification(&session, -1);
mcUnmap(&session, mem1, &mapInfo1);
mcCloseSession(&session);
mcFreeWsm(MC_DEVICE_ID_DEFAULT, tciBuffer);
mcCloseDevice(MC_DEVICE_ID_DEFAULT);
}
Для трастлета:
void tlMain(uint8_t *tciData, uint32_t tciLen)
{
// Check TCI size
if (sizeof(tci_t) > tciLen) {
// TCI too small -> end Trusted Application
tlApiExit(EXIT_ERROR);
}
// Trusted Application main loop
for (;;) {
// Wait for a notification to arrive
tlApiWaitNotification(INFINITE_TIMEOUT);
// Process command
// Notify the TLC
tlApiNotify();
}
}
mcNotify
/tlApiNotify
и mcWaitNotification
/tlApiWaitNotification
— это и есть те самые функции уведомления о том, что запрос/ответ готов для получения в другом мире, и функции ожидания обработки запроса. Также у клиентского приложения есть возможность использовать функцию mcMap. Она позволяет создать еще один буфер WSM, если это требуется. Всего с помощью этой функции можно создать только четыре таких буфера.
С клиентскими приложениями понятно — для телефонов Samsung это обычные приложения в Android. А что же представляют собой трастлеты?
Трастлеты Kinibi
Трастлеты находятся в обычной файловой системе устройства и являются файлами, содержащими исполняемый код. Это не привычный для Android формат ELF или APK. Трастлеты в операционной системе Kinibi имеют собственный формат MobiCore Load Format (MCLF). Он описан в открытых исходных кодах компонентов уровня userspace, которые Trustonic разместил на Github. Схематично структуру файла трастлета можно изобразить на такой картинке (трастлет — слева).
Для трастлетов можно выделить следующие особенности:
- исполняются в изолированном адресном пространстве, то есть один трастлет не видит другой;
- не имеют доступа к памяти нормального мира, за исключением буферов WSM, к памяти операционной системы TEE и к физической памяти;
- располагаются в памяти по секциям с разными правами на чтение, запись и исполнение;
- буферы WSM располагаются в неисполняемой памяти;
- загружаются без ASLR;
- в своей работе используют API, предоставляемый mclib — библиотекой, реализующей Global Platform API и Legacy API для защищенного мира;
- могут обращаться к защищенным драйверам с помощью функции
tlApi_callDriver
.
Как видим, трастлеты довольно ограничены в возможностях. Кроме этого, они используют некоторые защитные механизмы, типа различных атрибутов памяти, и также большинство трастлетов используют stack canaries для защиты от эксплуатации перезаписи стека. А вот ASLR в Kinibi нет, хотя он планируется в новых версиях.
Несмотря на все ограничения, трастлеты — очень интересная цель для атакующего по следующим причинам:
- это окно в TrustZone с уровня userspace в Android;
- они могут служить отправной точкой для эскалации привилегий до ядра операционной системы TEE;
- трастлеты имеют доступ к защищенной информации, куда не имеет доступ даже ядро Android.
В качестве подопытного устройства мы использовали 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. Добавим это в план.
- определить точку входа, адреса секций и размер буфера WSM.
Собираем эльфа
1) Точка входа
Первый пункт плана легко реализовать следующим кодом. Его можно добавить в функцию main
для нашего исходного файла ELF.
typedef void (*tlMain_t)(const void* tciBuffer, const uint32_t tciBufferLen);
tlMain_t tlMain = sym_tlMain;
tlMain(tciBuffer, tciBufferLen);
Компилируем наш код в объектный файл.
$(CC) $(INCLUDE) -g -c tlrun.c
Символ sym_tlMain
надо добавить в объектный файл. Это можно сделать с помощью objcopy.
arm-linux-gnueabi-objcopy --add-symbol sym_tlMain=$(TLMAIN) tlrun.o tlrun.o.1
В результате получаем tlrun.o.1
— скомпилированный исходник с функцией main
, передающей управление на код трастлета.
2) Библиотечные функции
Чтобы реализовать библиотечные функции, для начала нам нужен список всех этих функций. Когда-то давно случилась утечка из Qualcomm с кучей материалов для мобильных устройств на базе их процессоров. Среди этих материалов также были некоторые образы, заголовочные файлы и отладочные образы некоторых компонентов для операционной системы mobicore. Оттуда мы взяли прототипы библиотечных функций с их номерами, передаваемые как параметр в функцию диспетчеризации. Для функций с известным предназначением типа tlApiMalloc
или tlApiLogvPrintf
мы сделали соответствующие реализации с помощью аналогичных функций из libc. А функции не настолько понятные, например, tlApiSecSPICmd
мы заменили простыми заглушками, выводящими свое имя и возвращающими статус ОК. Весь API компилируется в файл tllib.o
$(CC) $(INCLUDE) -g -c tllib.c
3) Функция диспетчеризации
Аналогично адресу точки входа добавляем символ, его адрес, одинаковый для всех трастлетов:
arm-linux-gnueabi-objcopy --add-symbol sym_tlApiLibEntry=0x108c tlrun.o tlrun.o.1
Реализация функции диспетчеризации тривиальна. Нужно учесть только, что ее адрес необходимо записать в заголовок. Так как мы не знаем заранее, по какому адресу после линковки и запуска расположится наша функция диспетчеризации, записывать ее адрес в заголовок трастлета надо уже в рантайме. Например, при запуске файла до начала исполнения функции main
.
void (*sym_tlApiLibEntry)(int num) __attribute__((weak));
void tlApiLibEntry(int num) __attribute__((noplt));
__attribute__((constructor)) void init()
{
sym_tlApiLibEntry = tlApiLibEntry;
}
4) Секции
Добавить секции к объектному файлу, также используем objcopy
.
arm-linux-gnueabi-objcopy --add-section .tlbin_text=.text.bin \
--set-section-flags .tlbin_text=code,contents,alloc,load \
--add-section .tlbin_data=.data.bin \
--set-section-flags .tlbin_data=contents,alloc,load \
--add-section .tlbin_bss=.bss.bin \
--set-section-flags .tlbin_bss=contents,alloc,load \
tlrun.o.1 tlrun.o.2
Здесь .tlbin_text
— название секции трастлета, а .text.bin
— название файла с дампом этой секции. Дамп в файл можно сделать с помощью все той же IDA.
В результате этого преобразования, к исходному файлу ELF добавится трастлет в бинарном виде.
5) Автоматизация
Для всей сборки мы решили использовать один большой Makefile общий для всех трастлетов и еще по одному маленькому, подключаемому к нему для каждого отдельного трастлета с его параметрами. Для каждого трастлета нужно определить точку входа, адреса секций и размер буфера WSM. Первые два параметра легко получить с помощью простого скрипта для IDA, а определение размера буфера иногда не так просто автоматизировать. Можно автоматизировать и эту задачу, а можно потратить 10 минут, чтобы определить его для всех трастлетов, анализируя их код вручную. Эти параметры можно задавать как переменные в своем маленьком Makefile.
TLMAIN := 0x98F5D
TLTEXT := 1000
TLDATA := c0000
TLBSS := c10e0
TLTCI_LEN := 4096
А в большом Makefile использовать эти параметры таким образом:
$(CC) $(INCLUDE) -g -DTCILEN=$(TLTCI_LEN) -c tlrun.c
# ...
$(CC) -g tlrun.o.2 tllib.o --section-start=.tlbin_text=$(TLTEXT),--section-start=.tlbin_data=$(TLDATA),--section-start=.tlbin_bss=$(TLBSS) -o tlrun
Итак, мы превратили трастлет в файл формата 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. Очевидно, что если в логике трастлета некоторый код обрабатывает результат работы одной из таких функций-заглушек, очень вероятно, что он упадет с ошибкой из-за того, что ожидает совершенно другие данные, и это необязательно будет означать ошибку в коде.
Таких функций, для которых нельзя так просто сымитировать поведение реальной функции, довольно много:
- tlApiSecSPICmd;
- tlApi_callDriver;
- tlApiWrapObjectExt;
- tlApiUnWrapObjectExt;
- tlApiCipherDoFinal;
- tlApiSignatureSign;
- ...
Чтобы не тратить время на изучение таких сомнительных случаев, мы решили просто не рассматривать тесткейсы, в которых используются эти функции.
Результаты фаззинга
В автоматическом режиме с помощью скриптов мы собрали следующую информацию по всем трастлетам:
- UID трастлета;
- идентификатор краша;
- тип ошибки (тип сигнала при краше);
- адрес, на котором происходит ошибка;
- используемые трастлетом функции API.
Всю эту информацию, как оказалось, очень удобно поместить в базу данных, и дальше запросами SQL выбирать наиболее интересные для анализа случаи и добавлять информацию по результатам анализа.
Например, таким запросом можно показать все тесткейсы, на которых происходит ошибка Segmentation fault:
select * from main where type = "SIGSEGV";
А таким отфильтровать тесткейсы, в которых используется функция tlApiSecSPICmd
, которая у нас реализована заглушкой:
select * from main where api not like "tlApiSecSPICmd";
Таким образом, во всех трастлетах были найдены ошибки разных типов. Некоторые из них не приводили к уязвимостям, но были и те, которые являются уязвимостями и могут быть использованы атакующим. Рассмотрим самую интересную из найденных уязвимостей.
SVE-2019-14126
Уязвимость была найдена в трастлете keymaster в коде обработки содержимого буфера TCI при парсинге структуры ASN.1, закодированной по правилам DER. Два поля в этой структуре используются как размеры: один при выделении динамической памяти, а другой — при ее копировании. Очевидно, что если второй размер больше первого, происходит переполнение кучи. Такие уязвимости обычно приводят к возможности выполнения кода атакующим, поэтому мы попытались сделать полноценный эксплойт для этой уязвимости. При оценке возможности эксплуатации надо также учитывать все ограничения трастлетов, перечисленные выше.
Имея на руках переполнение кучи и исходя из данных ограничений, можно представить следующую стратегию эксплуатации:
- найти какой-нибудь указатель на функцию в доступном для перезаписи месте, например, в секции .bss;
- с помощью найденного переполнения создать в этом месте блок памяти кучи;
- инициировать выделение памяти в данном месте и перезапись указателя на функцию;
- инициировать вызов функции по перезаписанному указателю.
Чтобы это сделать, конечно, надо понимать в деталях, как работает куча в операционной системе Kinibi. Для этого нам пришлось провести реверс-инжиниринг функций аллокации и освобождения памяти mclib, но теперь хорошее описание работы кучи можно посмотреть в этом докладе с конференции ZeroCon, прошедшей в апреле.
Еще одним немаловажный вопрос — как создать блок памяти кучи в секции .bss. Для этого нужно сначала с помощью переполнения создать в оригинальной куче блок, указывающий в .bss как на следующий доступный для аллокации блок. Но для того чтобы следующая аллокация произошла по этому адресу и не спровоцировала падение приложения, необходимо, чтобы данные, которые уже там есть, могли быть истолкованы как корректные метаданные кучи.
Таким образом, мы нашли необходимый адрес в секции .bss, который можно использовать для релокации кучи.
Перезаписав так указатель функции и инициировав ее вызов, можно передать исполнение на некоторый адрес. Причем, это не может быть адрес нашего шеллкода, так как вся память, доступная нам для записи, в том числе куча и секция .bss, не может исполняться. Поэтому единственной возможностью исполнения своего кода является использование техник code-reuse.
На ум сразу приходит ROP. Действительно, мы можем использовать ROP, так как стэк в трастлетах располагается в самых последних адресах секции .bss. Чтобы перезаписать адрес возврата в стэке, придется перезаписать всю секцию неинициализированных данных, начиная с найденного адреса и до нужного места в стэке. Это очень ненадежно, так как мы портим данные, которые могут еще использоваться в промежуток между аллокацией памяти и вызовом перезаписанной функции. Мы все же это попробовали, и, конечно, ничего не сработало.
Кроме ROP, можно еще использовать JOP. JOP — это Jump Oriented Programming. В JOP адрес для передачи исполнения извлекается из адреса в памяти или из регистра.
Чтобы найти подходящие для JOP гаджеты, подойдет классный инструмент ROPGadget. Он покажет все найденные гаджеты, а чтобы оставить только JOP, можно использовать регулярное выражение типа такого:
ROPgadget --binary tlrun --thumb --range 0x1000-0xbeb44 | grep -E "; b.+ r[0-9]+$"
Нам повезло! В нашем трастлете нашлось довольно много гаджетов.
Теперь нам надо собрать их в цепочку. В ROP программировании для продвижения по цепочке используется встроенная в процессор логика возврата из функций. Можно сказать, что ROP-программа пишется для weird machine, заключенной в архитектуре процессора. А в JOP для этого используется некий супергаджет, который загрузит параметры для вызова из памяти и произведет вызов. В архитектуре ARM, на которой и работает наш трастлет, есть инструкция, подходящая на такую роль — LDMIA (Load Memory Increment Address).
В качестве параметров она принимает регистр, содержащий адрес, данные по которому будут использоваться для загрузки в регистры, и набор регистров, которые необходимо заполнить. Затем регистр с адресом увеличивается на размер прочитанных данных, так что при втором вызове этой инструкции будут читаться уже следующие данные. Это как раз то что надо для JOP!
Теперь нужно найти гаджет с инструкцией LDMIA в нашем наборе. Почему-то capstone, который используется в ROPGadget, интерпретирует эту инструкцию как LDMLO.
И нам опять повезло! Такие гаджеты есть. Выберем из них один и используем его как супергаджет, а для построения цепочки будем использовать гаджеты из набора, полученного ранее. Чтобы украсть stack cookie из стека, можно составить такую цепочку.
*(int*)&mem1[offset] = SUPER_GADGET; // r2
*(int*)&mem1[offset + 4] = 0; // r3
*(int*)&mem1[offset + 8] = 0; // r4
*(int*)&mem1[offset + 12] = SUPER_GADGET; // r5
*(int*)&mem1[offset + 16] = 0x9560b; // r7
offset += 0x14;
*(int*)&mem1[offset] = 0; // r2
*(int*)&mem1[offset + 4] = 0; // r3
*(int*)&mem1[offset + 8] = 0; // r4
*(int*)&mem1[offset + 12] = 0; // r5
*(int*)&mem1[offset + 16] = 0x96829; // r7
offset += 0x14;
*(int*)&mem1[offset] = SUPER_GADGET; // r2
*(int*)&mem1[offset + 4] = 0; // r3
*(int*)&mem1[offset + 8] = 0x3d5f4; // r4
*(int*)&mem1[offset + 12] = mapInfo3.sVirtualAddr; // r5
*(int*)&mem1[offset + 16] = 0x218c7; // r7
Hello, world из защищенного мира будет выглядеть вот так.
strcpy(mem3 + 0x100, "Hello world from TEE!\n");
*(int*)&mem1[offset] = 0x7d081b1; // r2
*(int*)&mem1[offset + 4] = 0; // r3
*(int*)&mem1[offset + 8] = mapInfo3.sVirtualAddr + 0x100; // r4
*(int*)&mem1[offset + 12] = 0; // r5
*(int*)&mem1[offset + 16] = 0x9545b; // r7
Писать "Hello, world!" из защищенного мира, конечно, весело, но трастлет keymaster, определенно, более интересная цель. Так как он имеет доступ к ключевой информации, мы можем попробовать прочитать ее. Например, в этом посте Gal Beniamini на TEE Qualcomm смог вытащить промежуточные ключи шифрования из защищенного мира и, таким образом, провести offline-атаку на полнодисковое шифрование в Android. Другим вариантом постэксплуатации может быть эскалация привилегий до ядра TEE OS и дальше до монитора EL-3, что откроет практически неограниченные возможности по закреплению в системе.
Заключение
ARM TrustZone и его реализации на различных платформах сейчас довольно актуальная тема для поиска уязвимостей, так как их код еще не так хорошо исследован и содержит ошибки. При этом со стороны Secure World можно добиться повышения привилегий и в обычной системе Android, которая сама по себе уже не такая легкая цель для поиска и эксплуатации багов. Кроме этого, например, Samsung имеет программу bug bounty и награждает исследователей за уязвимости TrustZone, в которой мы тоже поучаствовали.
Благодаря таким классным инструментам как AFL и qemu, возможен фаззинг бинарных файлов кастомного формата даже для "неродных" процессорных архитектур. Фаззинг с обратной связью зарекомендовал себя как очень надежный способ поиска уязвимостей. А с развитием символьного исполнения и инструментов для работы с промежуточным представлением бинарного кода, он станет еще более мощным и универсальным методом. Используйте надежные методы поиска уязвимостей!
Полезные ссылки
- Reverse Engineering Samsung S6 SBOOT
- Unbox Your Phone
- Trust Issues: Exploiting TrustZone TEEs
- TEE Exploitation: Exploiting Trusted Apps on Samsung’s TEE at Zer0con 2019
- BREAKING SAMSUNG'S ARM TRUSTZONE at BlackHat USA 2019
- The road to Qualcomm TrustZone apps fuzzing
- Reverse-engineering Samsung Exynos 9820 bootloader and TZ