Выполнение произвольного кода внутри загрузчика S-Boot в ОС Android. Часть 1

Выполнение произвольного кода внутри загрузчика S-Boot в ОС Android. Часть 1

Загрузчик S-Boot от компании Samsung для ОС Android является центральным звеном в цепочке системы безопасности.

Авторы: Nitay Artenstein (@nitayart) и Gilad Goldman (@gnull00)

Загрузчик S-Boot от компании Samsung для ОС Android является центральным звеном в цепочке системы безопасности. Тот, кто скомпрометировал S-Boot, потенциально может загрузить другое ядро и образ системы и, таким образом, обойти большинство механизмов защиты телефона.

Это хорошо известный вектор атаки, часто используемый для получения прав суперпользователя и модификации ОС Android, что, по нашим догадкам, пользуется особым спросом в правоохранительных и правительственных структурах.
Еще более интересным является тот факт, что S-Boot содержит несколько уязвимостей, связанных с нарушением целостности памяти, одну из которых можно использовать для выполнения произвольного кода внутри загрузчика.
На данный момент мы можем подтвердить, что брешь присутствует в чипсетах Exynos. Уязвимость универсальна для 90% ПЗУ в чипсетах Exynos в устройствах Galaxy S5, S6 и S7. В самых последних версиях ПЗУ для устройств S7 (по состоянию на февраль 2017 года) эта проблема, кажется, устранена. Более точная информация появится в скором будущем.

Тема, которая будет излагаться, достаточно обширна, и мы решили разделить материал на две части. В первой части будет рассказано о внутреннем устройстве S-Boot, схемах атаки и базовых возможностях отладки. В конце статьи будет продемонстрировано особо интересное направление атаки. Во второй части мы рассмотрим конкретную уязвимость, при помощи который можно добиться выполнения произвольного кода в S-Boot.

Мы не будем касаться азов реверс-инжиниринга, таких как загрузка в IDA или нахождения базового адреса. На эту тему скоро выйдет статья от Фернана Лоуна Сана (Fernand Lone Sang; @_kamino_).

Как устроен S-Boot
https://2.bp.blogspot.com/-Y2tk5gSUDtU/WLYNJRZn_eI/AAAAAAAAAng/ZgZud2t6ZxE-AdBmjDoDtA6BgbB1f7jOACLcB/s1600/bl2.jpg
Рисунок 1: Стадии загрузки на телефонах компании Samsung

Процесс загрузки ОС Android на устройствах, выпускаемых Samsung, начинается с запуска кода в загрузочной части ПЗУ (ROM), проверяющего целостность загрузчика для следующей стадии при помощи публичного OEM-ключа, который известен как SSBK (Samsung Secure Boot Key; Безопасный загрузочный ключ для устройств от Samsung). Затем в память загружаются два процесса: первый – S-Boot собственной персоной, второй - TrustZone TEE (Trusted Execution Environment; Достоверная среда выполнения), запускаемых в так называемом «Безопасном мире».

Далее два процесса начинают работать сообща. Операционной системой достоверной среды выполнения в случае с чипсетом Exynos является Trustonic (бывшая MobiCore), которая вызывается из S-Boot для проверки правильной подписи образов перед загрузкой. Таким образом, взлом либо S-Boot, либо TEE потенциально может привести к компрометированию всей системы.

Сам по себе S-Boot разделен на две части: BL1 - загрузчик, вызываемый из загрузочной части ПЗУ, который инициализирует низкоуровневые объекты системы. BL2 – небольшая операционная система, к которой происходит переход после проверка сигнатуры. Эта ОС содержит драйвера для USB, экрана и I/O (операции ввода/вывода).

Поскольку мы были заинтересованы в нахождении уязвимости, которая позволит нарушить процесс загрузки, было решено начать исследования процесса загрузки ядра. На этой стадии мы уже имеем инициализированную систему и можем довольно легко выполнять дисковые операции ввода/вывода, которые понадобятся для загрузки другого образа. Таким образом, мы решили сразу перейти к стадии BL2 (хотя в дальнейшем вполне можно заняться исследованием стадии BL1).

На стадии BL2 у нас не было никаких возможностей для отладки, а только блоб sboot.bin вместе со стандартным образом для чипсета Samsung Exynos. Мы открыли блоб в IDA и перешли к коду, относящемуся к стадии BL2.

https://2.bp.blogspot.com/-qza987PrSbc/WLYcQKRl2iI/AAAAAAAAAn8/Robw4zlPwHoPx3rn3YzFUF6giLYiQgbJQCLcB/s1600/Screenshot%2Bfrom%2B2017-03-01%2B02-53-50.png
Рисунок 2: Типичная функция в стадии BL2 (обратите внимание на количество строк)

Найти функции, относящиеся к стадии BL2, оказался довольно просто. Зная, что стадия BL1 отвечает за инициализацию низкоуровневых объектов, а на стадии BL2 появляется практически полноценная операционная система, мы сделали вывод, что функции, используемые в стадии BL2, должны быть больше по объему, а также содержать больше отладочных строк и ссылок на другие функции. После того как был найден переход к стадии BL2, мы использовали некоторые известные трюки для нахождения базового адреса образа в памяти.

С позиции высокого уровня на стадии BL2 обнаруживается несколько интересных функций (список неполный):

  1. Загрузка ядра.
  2. Прошивка образа нового образа.
  3. Отображения базового пользовательского интерфейса во время обновления прошивки.
  4. Отладка (если нам будет сопутствовать удача).

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

https://3.bp.blogspot.com/-VkTvPV-g4h8/WLYREEHStjI/AAAAAAAAAno/UrWzztsqhboupkLsdwEv6_HHGc9Z07GpgCLcB/s1600/Windows%2B7%2Bx64-2017-03-01-02-05-01.png
Рисунок 3: Приложение Odin для работы с прошивками в устройствах Samsung

Приложение Odin

Любой, кто занимался исследованием телефонов на базе ОС Android, выпускаемых компанией Samsung, знаком с приложением Odin. Эта старая, но в некоторой степени неидеальная, программа предназначена для загрузки прошивок на устройство.

На стороне устройства при загрузке новой прошивки вначале нужно переключить телефон в режим загрузки в S-Boot при помощи нажатия комбинации из трех клавиш. Затем устройство подключается через USB к компьютеру, на котором запущен клиент Odin. Затем клиент Odin отсылает выбранный образ прошивки на сервер Odin, запущенный на телефоне. Вы не можете загрузить любой образ. На заблокированном устройстве загрузчик не примет прошивку, не подписанный компанией Samsung.

https://4.bp.blogspot.com/-CgrjZSDbxcM/WLYgyBsUdGI/AAAAAAAAAoI/19tyX1vtLPEjtPY7RBaBRavn80zbPtMNgCLcB/s1600/download_mode.jpg

 

Рисунок 4: Режим загрузки. Заблокированный загрузчик не принимает неподписанные образы

На стороне загрузчика приложение Odin использует довольно универсальный протокол для получения и передачи данных через USB. Именно с этого места мы и начнем наши исследования.
Если вы хотите следовать нашим экспериментам, загрузите версию прошивки G930FXXU1APF2 (устройство Galaxy S7).

Ключевая функция в приложении Odin, которая практически полностью отвечает за обработку протокола - process_packet (по адресу 0x8F00A0A4). При просмотре функции тут же сталкиваемся с ошибкой:

https://1.bp.blogspot.com/-ZnUy5pf88_I/WLYkmriaVnI/AAAAAAAAAoU/7gv2hzX9glMVyFHaRfP_ikYT4aubQZmDgCLcB/s1600/bug1_cropped.png
Рисунок 5: Начало функции process_packet

Как видно из рисунка выше, в протоколе Odin происходит считывание ID и перехода к соответствующей ветке кода. Идентификатор пакета 0x65 говорит о том, что предполагается выполнение операции с PIT-файлом, содержащего информацию разделах (более подробную информацию можно получить в этой ветке на форуме XDA).

Когда код доходит до идентификатора 0x65, дальше либо происходит запись в буфер содержимого текущего PIT-файла, либо запись нового файла в специальный раздел, содержащий PIT-данные. Если второй байт пакета – 1, Odin переходит дальше и копирует текущий PIT-файл в буфер, который затем будет передан клиенту. Клиенту нужно определить, подходит ли новая прошивка под текущую схему разделов.

Однако не очень понятно, когда инициализируется буфер, в который копируется PIT-файл (xfer_data.pit_buf). Скорее всего, память выделяется только в этом случае:

https://1.bp.blogspot.com/-pIsOXmjYBf4/WLYqmjJC5vI/AAAAAAAAAog/wJc28oTjvFM6U_9Pee2yM7ryRvtLHXVpwCLcB/s1600/init_2.png
Рисунок 6: Выделение памяти для буфера pit_buf

Из рисунка выше видно, что вначале нужно отослать инициализационный пакет (ID 0x64) прежде, чем будет выделена память под буфер. В противном случае буфер будет указывать на 0. Если вы попытаетесь скопировать PIT-файл прежде, чем память будет выделена, код будет копировать в 0. Получается классическое разыменование пустого указателя.

Эта уязвимость похожа на многие другие, найденные нами в приложении Odin, приводящие к краху загрузчика. Минус в том, что, по всей вероятности, мы не можем использовать эти бреши в своих целях. На архитектуре ARM64 адрес 0 не является проекцией куда-либо, и любые попытки копирования приводят к немедленному краху. С архитектурой ARM32 дела обстоят получше, поскольку адрес 0 может содержать таблицу векторов исключений (Exception Vector Table; EVT), которая может быть перезаписана. Проблема заключается в том, что мы все еще не управляем тем, что записываем, поскольку не управляем PIT-данными.

Но найденная брешь дает нам нечто другое. На рисунке ниже показан экран во время краха загрузчика.

https://3.bp.blogspot.com/-f_-9PC9LRUE/WLYtgfDb3uI/AAAAAAAAAos/dLCklmsvmGA4chDpHTOid9Vcn-QuLQxEgCLcB/s1600/upload_mode_2.jpg
Рисунок 7: Внутри режима Upload

Выгрузка памяти

На рисунке выше видно, что информация выводится обработчиком исключений загрузчика, после чего осуществляется переход в так называемый режим Upload, который является полусекретным и уже несколько лет озадачивает пользователей сообщества XDA.

Некоторые пользователи говорят, что переключение в этот режим осуществляется после особых крахов ядра; другие говорят, что переход происходит из-за проблем с интегральной микросхемой, управляющей мощностью (Power management integrated circuit; PMIC). Теперь мы знаем, что переключение в этот режим происходит во время исключений.

Исследуя код, мы видим, что режим Upload реализован в виде встраиваемой функции (inline function) внутри usbd3_rdx_process (по адресу 0x8F028C1C). Код, показанный ниже, немного отредактирован и упрощен с целью повышения читабельности.

mode_switch = p_board_info->mode_switch;
if ( mode_switch & UPLOAD_MODE )
{
  if ( !transaction_data.response_buffer )
  {
    transaction_data.response_buffer = (char *)malloc(0x80000);
    if ( !transaction_data.response_buffer )
    {
      printf("%s: buffer allocation failed.\n", "usbd3_rdx_process");
      goto INFINITE_LOOP;
    }
  }
  if ( !strcmp(packet_buf, "PoWeRdOwN") )
  {
    goto POWERDOWN;
  }
  if ( !strcmp(packet_buf, "PrEaMbLe") )
  {
    memcpy(transaction_data.response_buffer, "AcKnOwLeDgMeNt", 15);
    goto SEND_RESPONSE;
  }
  if ( !strcmp(packet_buf, "PrObE") )
  {
    memcpy(transaction_data.response_buffer, log_location_buf, log_location_buf_size);
    goto SEND_RESPONSE;
  }
  ...
  dump_start_addr = strtol(packet_buf, NULL, 16);
  dump_end_addr = strtol(packet_buf + 9, NULL, 16);
  ...
  (some length checks)
  ...
  memcpy(transaction_data.response_buffer, dump_start_addr,   dump_end_addr - dump_start_addr);
  goto SEND_RESPONSE;

Это базовый протокол для выгрузки памяти с устройства. После отправки серии инициализационных пакетов, вы можете просто отослать начальный и конечный адрес для выгрузки и в ответ получить кусок памяти через USB.
Эту чрезвычайно полезную возможность можно использовать для отладочных и исследовательских целей, поскольку мы можем выгрузить образ памяти после краха, просмотреть содержимое регистров, стека и понять, как обстоит ситуация в целом. С целью более всестороннего исследования мы также можем выгрузить всю память, что и будет сделано во второй части.

Поскольку мы не смогли найти общедоступный инструмент для выгрузки памяти в режиме Upload, то написали свою собственную утилиту, которой вы свободно можете пользоваться в своих целях и экспериментах.

Фаззинг приложения Odin

На этом этапе исследований мы вернулись к протоколу Odin в надежде найти уязвимость, пригодную к эксплуатации. Обычно одним из первых шагов при изучении нового метода атаки является написание простых фаззеров для быстрого поиска брешей.

Эта задача решается чуть сложнее в случае с S-Boot, поскольку используется специальный протокол на базе технологии CDC ACM, которая может применяться для эмуляции последовательных портов через USB. Корректная работа с этим протоколом довольно сложна. Например, необходимо отсылать пустой пакет после каждого стандартного пакета. Некоторые пакеты должны быть размером 1024 байта, даже если содержат 4 байта полезных данных. Все эти и другие сложности затрудняли написание с нуля фаззера пакетов.

И тут нам на помощь приходит прекрасная утилита Heimdall, написанная Бенджамином Добелем (Benjamin Dobell). Heimdall – реализация протокола клиента Odin с открытым исходным кодом, берущая на себя всю рутину взаимодействия кодом Odin’а в загрузчике, которая была использована в качестве базы для написания фаззера.  

Мы добавили параметр командной строки с именем «fuzz», принимающим на входе набор сырых пакетов, предварительно сгенерированных при помощи кода на Python, после чего полученная информация отсылается в определенной последовательности. В процессе пересылки учитываются низкоуровневые нюансы. Доработанный фаззер можно скачать отсюда.

В процессе тестирования приложения Odin было спровоцировано несколько интересных крахов, но, на первый взгляд, в найденных находках не было ничего полезного. Перед тем как погрузиться глубже внутрь Odin’а мы решили расширить инструментарий для отладки. И тут нас ждала интересная находка.

UART консоль

В процессе поисков внутри бинарного файла по адресу 0x8F08BD78 мы нашли набор строковых указателей, наводящих на размышления:

https://4.bp.blogspot.com/-CeqhQoBOA2c/WLYzsnx-J3I/AAAAAAAAApA/97FYYTKrwVgu2IAuhwq1wbgvmFNcKa_1ACEw/s1600/comm_2.png
Рисунок 8: Предполагаемый список команд

На рисунке выше, предположительно, показаны команды и описания от терминального интерфейса,  оставленного в диагностических целях. Подобные вещи периодических находятся в других проектах по исследованию встроенных устройств, но здесь такой поворот событий оказался для нас сюрпризом.

Предполагая, что был найден в некотором роде последовательный интерфейс, при помощи которого можно подключиться к терминалу, мы обнаружили, что члены форума XDA уже побывали здесь до нас.

Выясняется, что разработчики компании Samsung оставили UART терминал, к которому можно получить доступ через загрузчик и который используется для выполнение низкоуровневых команд в служебных и диагностических целях. Некоторые команды позволяют загружать ядро со специальными параметрами, записывать / читать информацию из хранилища и активировать различные USB режимы.

Однако с момента первоначальной публикации в сообществе XDA в 2012 году, больше никто не делился информацией об использовании этих терминалов, и многие предположили, что этот интерфейс был отключен. Мы решили проверить эту гипотезу.

После изучения других источников и в частности презентации Майкла Оссмана и Кайла Осборна на конференции Black Hat от 2013 года мы решили, что в телефонах Samsung и Nexus использовалась микросхема мультиплексора между USB коннектором и USB контроллером. Детектируя напряжение между пином ID и земляным пином в USB коннекторе, мультиплексор переключал различные схемы соединения на устройстве.

Две схемы соединения официально задокументированы: обычный USB и USB OTG. Еще один режим, который не упомянут в общедоступной документации, - UART.

https://2.bp.blogspot.com/-N5stQ6L2UCo/WLY5zh61jvI/AAAAAAAAApU/G9MRaFtdZGUNDWyJ-27uaMREwGjPq9c7wCLcB/s1600/anyway.jpg
Рисунок 9: Устройство Samsung Anyway

Затем мы попытались подключиться к недокументированному UART терминалу. Наш первый пункт назначения - Samsung Anyway, устройство, которое компания Samsung держит в секрете. Этот аппарат, используемый инженерами компании Samsung, трудно найти в свободной продаже, но иногда можно купить на Ebay.

Устройство Samsung Anyway делает не что иное как настройку размеров сопротивлений к пину ID и обрыв линий D+/D- к DSUB коннектору, который затем может быть подсоединен к компьютеру через адаптер Serial-USB.

Получив подержанное устройство Anyway с аукциона eBay, мы протестировали различные комбинации переключателей, чтобы подключиться к UART терминалу. Эта методика работала на старых телефонах. Однако все, чего нам удалось добиться, - получить логи загрузчика и ядра, но не доступа к самому терминалу.

Было решено смастерить собственный кабель для интерфейса UART, схожий с тем, который Джошуа Дрейк сделал на базе UART кабеля для телефона Nexus 4. Мы собрали информацию из сообщества XDA относительно соответствий между сопротивлениями для пина ID и режимами, используемые производителем. Кроме того, некоторые подсказки были получены из DTS-файлов ядра. В итоге была получена следующая схема устройства:
https://1.bp.blogspot.com/-Fz3GAf0QruY/WLY8V_3BA4I/AAAAAAAAApg/yyV7LpvzJo8IvZxWXp2HunFk-frhLnQ5gCLcB/s1600/drawing.jpg

https://4.bp.blogspot.com/-tzt934DczMA/WLY-Fa8_nJI/AAAAAAAAApk/N-X1gqXgoAgRi4U4YBe711lmFg5atAtEQCLcB/s1600/jig.jpg
Рисунок 10: Самодельный переходник

Поскольку было необходимо управлять диапазоном сопротивлений, мы использовали переменный резистор для установки желаемого значения (измеряемого при помощи мультиметра) и подсоединения к устройству S7.
Наш адаптер довольно простой: переходник RS232-USB имеет свои собственные линии приема/передачи подсоединенные к линиям D+/D- в USB коннекторе. Пин ID заземлен через переменный резистор.

Выяснилось, что размер корректного сопротивления – 619 КОм. При таком значении резистора были получены некоторые выходные данные во время загрузки устройства. Однако мы до сих пор не добились желаемого, поскольку после получения нескольких строк, все заканчивается, и у нас нет доступа к терминалу.

https://1.bp.blogspot.com/-0I8cHco7WXo/WLY_RDohlII/AAAAAAAAAps/QUBqKCkiOv0cUpjx6O3uAyke5i35O5zOQCLcB/s1600/log.png
Рисунок 11: Выходные данные UART порта. После появления строки ifconn_com_to_open вывод информации прекращается

Погрузившись вглубь проблемы, мы нашли функцию, которую обозначили как get_initial_uart_str (адрес 0x8F006ECC). По-видимому, UART консоль запускалась только если эта функция возвращала не пустое значение:

https://2.bp.blogspot.com/-0oh23RMWzjk/WLZBvBfrn6I/AAAAAAAAAp0/keWnGiDch1s8BxvQBctet1aWpS9fk6zrwCLcB/s1600/termcode2.png
Рисунок 12: Функция get_initial_uart_str

С этого места и, особенно, начиная с метки LABEL_9, видно, что загрузчик ожидает последовательность возвратов каретки прежде, чем перейти в режим консоли.

Теперь было понятно, что делать дальше: путем нажатия клавиши «enter» на старте при подключенном переходнике и одновременном нажатии кнопок Громкости и Питания, нам удалось пройти проверку и в функции ifconn_com_to_open и в терминале.

Наконец, нас ожидал успех:

https://4.bp.blogspot.com/-rnd_8rlTeXg/WLZDgBC3vwI/AAAAAAAAAp8/5z8WPW0QtP4G2demIbWGCchyaozM5rvHwCLcB/s1600/got_terminal.jpg
Рисунок 13: UART консоль

Как видно из рисунка выше, в консоли уже выведены некоторые интересные команды, подробнее о которых мы поговорим во второй части.

Устали от того, что Интернет знает о вас все?

Присоединяйтесь к нам и станьте невидимыми!