• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Штурмуем отладочную консоль «Яндекс Станции»

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,178
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Эта статья появилась в процессе реверса прошивок «Яндекс Станций». Я провел реверс U-Boot процессора умной колонки и создал утилиту для его извлечения из расшифрованного загрузчика. Способ универсален для всех подобных устройств и, возможно, других гаджетов с чипом Amlogic.
Разработанный мной метод подойдет:

  • для «Станции Mini2» (YNDX-00020/21);
  • «Станции Lite» (YNDX-00025);
  • «Станции Max» (YNDX-00053/0008);
  • других устройств на Amlogic S905X2 (G12) и A113X (AXG).
В процессе я расскажу о паре интересных плагинов для IDA: с помощью Diaphora мы позаимствуем информацию из схожего опенсорсного проекта, а FindCrypt поможет нам отыскать криптографические константы.

U-Boot login​

Кто подключался к UART «Яндекс Станций», тот наверняка встречал надписи «RH challenge» и «RH response». На том загрузка колонки прекращается, девайс нужно перезагрузить или ввести ответ. Запрос всегда разный и является не чем иным, как авторизацией при входе в консоль U-Boot.

Для просмотра ссылки Войди или Зарегистрируйся
Вход в консоль U-Boot происходит прерыванием по какой‑либо клавише, например Enter или Ctrl-C (классический вариант), далее уже может потребоваться авторизация. Поддерживается стандартный тип авторизации — bootstopkeysha256 (по хешу пароля). Отличное описание способов приведено в статье Для просмотра ссылки Войди или Зарегистрируйся. «Яндекс» пошел немного дальше стандартного варианта и добавил свой способ авторизации, дав ему имя Rabbit Hole. Давай посмотрим, как он устроен.

Для начала нужно получить прошивку устройства — считать ее с помощью программатора или любым другим способом. В качестве примера будем изучать колонку Lite (YNDX-00025). Для извлечения прошивки я использовал утилиту update, разработанную Amlogic для работы со своими загрузчиками через USB. Она подходит для устройств без пароля. Загрузчик состоит из двух частей — BL2 и TPL, что соответствует стандартной структуре U-Boot.

Если ты соберешь U-Boot вручную для чипа A113X, то на выходе получишь два файла: u-boot.bin.bl2 и u-boot.bin.tpl, оба незашифрованные. У нас же, к сожалению, используются зашифрованные версии.

Расшифровать их все‑таки возможно благодаря известной Для просмотра ссылки Войди или Зарегистрируйся.

Она позволяет расшифровывать загрузочные файлы при подключении через USB-порт без пароля. На колонках Lite, Mini2 и Max пароль по умолчанию отсутствует, хотя со временем «Яндекс» начал устанавливать его удаленно. Мне повезло: платы, с которыми я работаю, пока не защищены. Поэтому я смогу воспользоваться уязвимостью и расшифровать файл TPL, а BL2 для анализа не потребуется.


Строение загрузчика​

Для защиты процесса загрузки используется Для просмотра ссылки Войди или Зарегистрируйся с дополнительными элементами, в основном касающимися аппаратных особенностей, добавляемыми производителем процессора. Первым стартует BL1 (Boot ROM), жестко прошитый в процессор с завода, — замене он не подлежит. Следом за ним стартуют другие части:

  • BL2 готовит систему к старту;
  • BL31, BL32 — защищенная часть для обеспечения безопасности;
  • BL33 (U-Boot), который нам и нужен.
Загрузчик начального уровня BL2 нам сейчас неинтересен, а вот TPL содержит остальные уровни загрузки, в том числе BL33 (U-Boot).

У колонок Max с процессором Amlogic S905X2 загрузчик представлен одним файлом — bootloader.bin, который включает все этапы загрузки: от начальной инициализации до запуска U-Boot. В моделях на чипах A113X (например, Lite и Mini2) загрузчик разделен на два отдельных файла — с расширениями .bl2 и .tpl.

info​


Сигнатуры

Неполные строки
Дополнительное подтверждение я нашел в Для просмотра ссылки Войди или Зарегистрируйся Фредерика Бассе, в которой выполняется упаковка U-Boot через стандартные инструменты Amlogic со сжатием LZ4.

aml_encrypt_g12a --bl3sig --input u-boot.bin --compress lz4 --output u-boot.enc --level v3 --type bl33

Пытаемся извлечь U-Boot BL33​

Перед нами компонент BL33, давай попробуем извлечь его. Готовых решений в сети не нашлось, поэтому пришлось разбираться самостоятельно. В качестве первого шага — самый простой способ: с помощью утилиты dd отрезаем нужную область файла, а затем пробуем распаковать полученное архиватором LZ4.

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

Для просмотра ссылки Войди или Зарегистрируйся
Изучение я начал с заголовка, попытавшись определить его формат и получить полезные для распаковки данные. В разделе Doc исходников есть описание двух форматов:

  • LZ4 Block Format — блоки данных;
  • LZ4 Frame Format — заголовки данных.
Описания, которые удалось найти, касаются в основном структуры данных до и после сжатия, но не содержат информации о начальном заголовке. Тем не менее кое‑что полезное все же выяснилось: LZ4-распаковка может работать не только с файлами, но и с буферами, для чего необходимо явно указывать размеры до и после сжатия. А значит, эти значения должны где‑то храниться.

Отложив изучение исходников LZ4, я продолжил поиски — и на одном из форумов Для просмотра ссылки Войди или Зарегистрируйся на нужную зацепку: ссылку на проект Для просмотра ссылки Войди или Зарегистрируйся. Этот репозиторий, в свою очередь, указывает на три других, где лежат «разобранные» утилиты Amlogic (именно то, что нужно!):

В итоге я остановился на репозитории meson64-tools. В его составе уже есть библиотека LZ4, а также разобранная утилита подписи bl3sig.c, в которой наконец‑то нашлось все необходимое: заголовок в виде C-структуры и указание на используемый алгоритм сжатия — LZ4HC. Применяя эту структуру к имеющимся данным, нужно учитывать порядок байтов: в нашем случае процессор использует little-endian, так что многобайтовые значения нужно читать в обратном порядке.

Для просмотра ссылки Войди или Зарегистрируйся
Для просмотра ссылки Войди или Зарегистрируйся
Для просмотра ссылки Войди или Зарегистрируйся
Для просмотра ссылки Войди или Зарегистрируйся

Сводка LZ4​

В итоге удалось установить, что сигнатура LZ4C обозначает начало заголовка длиной 0x80 байт, за которым следует сжатый буфер данных. Для сжатия используется алгоритм LZ4HC, а все необходимые параметры для распаковки — включая размеры буфера до и после сжатия — уже содержатся внутри заголовка.


Извлечение​

Теперь, когда у нас есть все данные о заголовке и алгоритме, пробуем распаковать загрузчик. Для этого я набросаю утилиту на C#, он мне наиболее близок.

Помимо стандартных библиотек, будем использовать пакет K4os.Compression.LZ4. Привожу основную часть, а Для просмотра ссылки Войди или Зарегистрируйся ты найдешь на моем GitHub.

Код:
Console.WriteLine("LZ4C найден");

// Считываем размер данных до запаковки
fstream.Seek(0x8 + offset, SeekOrigin.Begin);
fstream.Read(buffer, 0, 4);

var target_size = BitConverter.ToUInt32(buffer, 0);
target = new byte[target_size];
fstream.Read(buffer, 0, 4);

// Считываем размер запакованных данных
source = new byte[BitConverter.ToUInt32(buffer, 0)];
fstream.Seek(0x80 + offset, SeekOrigin.Begin);
// Читаем запакованные данные
fstream.Read(source, 0, source.Length);

// Распаковываем
var decoded = LZ4Codec.Decode(source, 0, source.Length, target, 0, target.Length);

if (decoded == -1)
{
    throw new InvalidOperationException("Ошибка извлечения.Данные повреждены");
}

Console.WriteLine("target size {0:x} / source size {1:X} \r\n Распаковано {2:X}", target.Length, source.Length, decoded);

using (FileStream fstream = new FileStream(out_path, FileMode.OpenOrCreate))
{
// Преобразуем строку в байты

// Пишем массив байтов в файл
await fstream.WriteAsync(target, 0, target.Length);
Console.WriteLine("Файл записан в файл");
}

Работа проста, получаем размеры данных до (original_size) и после (compressed_size) сжатия, считываем сжатые данные одним большим куском и передаем на распаковку, после удачного завершения записываем буфер с распакованными данными в файл. Готово!

Для просмотра ссылки Войди или Зарегистрируйся

Переходим к дизассемблеру​

Как видишь, строки теперь не «битые» и IDA/Ghydra теперь может анализировать и «разбирать» BL33. Собственно, для этого и нужно было извлечение.

Целые строки
IDA

Сразу скажу, что поправить и затолкать назад U-Boot не выйдет (даже при наличии ключей шифрования). Да, он запакуется и встроится в прошивку, но не пройдет проверку из‑за наших изменений, для того‑то и нужен Trusted Firmware. Однако польза от наших упражнений все‑таки есть: теперь можно анализировать нетиповые функции, которые в «Яндексе» добавили для своих устройств, и наконец отыскать Rabbit Hole, а может, даже и нырнуть в нее.


Исследуем и ищем​

Чтобы дизассемблер верно обработал прошивку, выбираем архитектуру ARM little-endian. Базовый адрес и создание секций пока что не настраиваем, принимаем все параметры по умолчанию. Точка входа находится в самом начале прошивки, ставим курсор на 0x00 и жмем С — IDA автоматически создаст довольно много функций.

Для просмотра ссылки Войди или Зарегистрируйся

Всё в автоматическом режиме не распознается, поэтому, пробежавшись по прошивке вручную, обрабатываем неопределенные участки (в основном тыкая С) и переходим к размещению образа по правильному адресу (базовый адрес), чтобы получить наиболее правильные ссылки на функции и данные. Определить верный адрес нам помогает лог загрузки колонки:

Код:
U-Boot 2015.01 (Sep 29 2023 - 16:13:15)
DRAM: 256 MiB
Relocation Offset is: 0eda0000

Вносим правки через меню Edit → Segments → Rebase Program и указываем нужное значение. После этих действий увидим изменения. Часть ссылок ранее была неверно интерпретирована, но теперь все должно соответствовать тому, как U-Boot расположен в памяти устройства.

Для просмотра ссылки Войди или Зарегистрируйся Измененный вид

Поиск норы​

Подготовив прошивку к анализу, наконец‑то приступаем к поиску Rabbit Hole. Из лога загрузки мы знаем текст сообщения: «RH challenge», за него и будем цепляться. Идем в список строк с надеждой, что там будет ссылка.

Для просмотра ссылки Войди или Зарегистрируйся
Нам повезло, и ссылка действительно есть. Сразу назовем функцию Rabbit Hole. Выглядит код несложно (всего 63 строки и 5 функций), но все функции с неизвестными именами, их нужно как‑то различать.

Для просмотра ссылки Войди или Зарегистрируйся
Для просмотра ссылки Войди или Зарегистрируйся
Есть два способа идентифицировать функции:

  1. Зайти в каждую, попытаться проанализировать логику и дать понятное название.
  2. Собрать похожую версию ПО с отладочной информацией и при помощи плагина Для просмотра ссылки Войди или Зарегистрируйся (есть и другие) поискать стандартные функции, например printf, sha256.
Первый способ я применяю в начале изучения, но если вижу возможность применить второй способ, то обязательно им пользуюсь. Поскольку мы имеем дело с U-Boot, вероятно, большинство функций сможем определить вторым способом.


Подготовка среды компиляции U-Boot​

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

При сборке в секцию данных включаются строки с версией U-Boot и используемого компилятора. Ищем строки U-Boot и GCC (для компилятора) и находим:

  • aarch64-elf-gcc (Linaro GCC 4.9-2017.01) 4.9.4;
  • ld (Linaro_Binutils-2017.01) 2.24.0.20141017 Linaro 2014_11-3-git;
  • U-Boot 2015.01 (Sep 29 2023 - 19:21:39) — то же самое видно в логе загрузки.
Для просмотра ссылки Войди или Зарегистрируйся Компилятор

Среда сборки​

Данные о версии и компиляторе найдены, теперь ищем исходники, качаем компилятор. Я буду использовать Ubuntu. Обратимся к Для просмотра ссылки Войди или Зарегистрируйся по сборке U-Boot. Для кросс‑компиляции предлагается такая последовательность действий:

Код:
wget https://releases.linaro.org/archive/13.11/components/toolchain/binaries/gcc-linaro-aarch64-none-elf-4.8-2013.11_linux.tar.xz
wget https://releases.linaro.org/archive/13.11/components/toolchain/binaries/gcc-linaro-arm-none-eabi-4.8-2013.11_linux.tar.xz
tar xvfJ gcc-linaro-aarch64-none-elf-4.8-2013.11_linux.tar.xz
tar xvfJ gcc-linaro-arm-none-eabi-4.8-2013.11_linux.tar.xz
export PATH=$PWD/gcc-linaro-aarch64-none-elf-4.8-2013.11_linux/bin:$PWD/gcc-linaro-arm-none-eabi-4.8-2013.11_linux/bin:$PATH
git clone https://github.com/BayLibre/u-boot.git -b n-amlogic-openlinux-20170606 amlogic-u-boot
cd amlogic-u-boot
make axg_s400_v1_defconfig
make
export FIPDIR=$PWD/fip

Но версия компилятора здесь отличается от нашей, к тому же их два. Если с версией все ясно (просто поищем нашу), то с количеством требуется пояснение:

  • gcc-linaro-arm-none-eabi — для сборки кода, работающего в 32-битном режиме ARMv7;
  • gcc-linaro-aarch64-none-elf — для сборки под 64-разрядный режим AArhc64.
В версии 4.9 наименования немного изменились и, судя по справке, будет следующее:

  • aarch64-none-elf — gcc-linaro-*x86_64_aarch64-elf.tar.xz (Linux 64-bit binaries for the AArch64 bare-metal cross-toolchain);
  • arm-none-eabi — gcc-linaro-*x86_64_arm-eabi.tar.xz (Linux 64-bit binaries for the ARMv7 bare-metal cross-toolchain).
Почему две архитектуры? Дело в том, что процессор Amlogic A113X включает в себя сразу два ядра с разными архитектурами: основное — Cortex-A53, относящееся к 64-битной архитектуре ARMv8-A (AArch64), и вспомогательное — Cortex-M3, использующее 32-битную архитектуру ARMv7-M. Первое отвечает за основную загрузку и выполнение ОС, второе — за задачи реального времени, вроде управления питанием или работы с аудио.

info​

gcc-linaro-aarch64-none-elf я все же использовал другой версии из‑за проблем со сборкой, но это в нашем случае ничего не изменит.
Для просмотра ссылки Войди или Зарегистрируйся
Чтобы упростить рутинные действия при итерациях сборки, я набросал небольшой скрипт — так меньше придется стучать по клавишам. Внутри оставлены закомментированные строки на случай, если понадобится сменить версию U-Boot или использовать другую версию компилятора arm-none-eabi. В качестве основы я выбрал репозиторий Khadas — производителя отладочных плат, который активно сопровождает нужную нам ветку U-Boot и регулярно ее обновляет.

Код:
#/bin/bash
mkdir ~/lite_uboot
cd ~/lite_uboot
wget https://github.com/khadas/u-boot/archive/refs/tags/khadas-vims-u-boot-v2015.01-v1.5.2-release.tar.gz
tar xvf khadas-vims-u-boot-v2015.01-v1.5.2-release.tar.gz
#git clone https://github.com/khadas/u-boot.git
#cd ~/lite_uboot1/u-boot
#git checkout  ff7d3af наиболее близкий по дате коммит
mkdir ~/cross_compile
cd ~/cross_compile
wget -c https://releases.linaro.org/components/toolchain/binaries/latest-4/aarch64-elf/gcc-linaro-4.9.4-2017.01-x86_64_aarch64-elf.tar.xz
tar xvfJ gcc-linaro-4.9.4-2017.01-x86_64_aarch64-elf.tar.xz
export PATH=$PWD/gcc-linaro-4.9.4-2017.01-x86_64_aarch64-elf/bin:$PATH
#tar xvfJ gcc-linaro-4.9.4-2017.01-x86_64_arm-eabi.tar.xz
#export PATH=$PWD/gcc-linaro-4.9.4-2017.01-x86_64_arm-eabi/bin:$PATH
wget -c https://releases.linaro.org/archive/13.11/components/toolchain/binaries/gcc-linaro-arm-none-eabi-4.8-2013.11_linux.tar.xz
tar xvfJ gcc-linaro-arm-none-eabi-4.8-2013.11_linux.tar.xz
export PATH=$PWD/gcc-linaro-arm-none-eabi-4.8-2013.11_linux/bin:$PATH
cd ~/lite_uboot/u-boot-khadas-vims-u-boot-v2015.01-v1.5.2-release
export ARCH=arm
export CROSS_COMPILE=aarch64-elf-
make axg_skt_v1_defconfig
make

info​

Код:
Я не привожу полную инструкцию по настройке среды сборки, так что тебе могут понадобиться дополнительные пакеты и библиотеки.
Результат сборки забираем в папке build, нас интересует файл u-boot. Загрузим его в «Иду», и она автоматически определит тип ELF 64 bit и проведет полный анализ. У всех функций есть имена, заполнена информация о сегментах.

Для просмотра ссылки Войди или ЗарегистрируйсяДля просмотра ссылки Войди или Зарегистрируйся

Запускаем Diaphora​

Теперь нужно запустить плагин, который определит сигнатуры и строки и соберет всю нужную информацию в свою базу (SQLite), чтобы сравнить с U-Boot версии «Яндекса».

Для просмотра ссылки Войди или Зарегистрируйся
Процесс может слегка затянуться: моему MacBook Air десятилетней давности потребовалось около 30 минут на каждую из прошивок (собранную и в исполнении «Яндекса»).

Для просмотра ссылки Войди или Зарегистрируйся
После завершения индексации необходимо вернуться к прошивке «Яндекса» и повторно запустить плагин. На этот раз в строке базы сравнения следует указать SQLite-базу, созданную при индексации скомпилированного U-Boot. Внимание: не запускай повторную индексацию! Если согласиться, то плагин снова выполнит ту же работу и ты зря потратишь еще полчаса.

Загрузка базы

Отказ от повторной индексации

После сканирования плагин выдаст несколько вкладок с результатами:

  • Best matches — наиболее точно совпадающие функции;
  • Partial matches — более‑менее совпадающие функции с небольшими отличиями (степень совпадения помечена цветом: ярко‑зеленый — высокое, темно‑фиолетовый — низкое);
  • Problematic matches — плохо совпадающие результаты;
  • Unmatched — несколько вкладок полностью уникальных функций.
Результат работы

Низкая степень совпадения

Сначала переходим на вкладку с полностью совпадающими функциями, выделяем их и импортируем в проект — после этого база IDA будет обновлена. Затем открываем вкладку Partial и с осторожностью импортируем еще часть функций — я, например, использую только те, что подсвечены зеленым. Желательно просматривать содержимое перед импортом, чтобы не затянуть лишнего. Остальные вкладки можно не трогать — они нас не интересуют.

Для просмотра ссылки Войди или Зарегистрируйся

После импорта в исследуемом U-Boot появились осмысленные имена функций и переменных.

Для просмотра ссылки Войди или Зарегистрируйся

Для получения более полного результата можно собрать и проиндексировать другие версии U-Boot — каждая из них потенциально может помочь обработать новые функции. Никаких гарантий нет, но иногда такой подход позволяет значительно дополнить базу.

Я ограничился одним проходом и перешел к следующему плагину — Для просмотра ссылки Войди или Зарегистрируйся. Он ищет криптографические константы — это поможет определить алгоритмы шифрования. Найдя некоторые константы, плагин автоматически переименует их в соответствии с назначением.


Теперь уже точно анализ​

Итак, в Rabbit Hole пять функций:

  • sub_EDC96CC;
  • sub_EDC1BBC;
  • sub_EDC1B74;
  • sub_EDC1A14;
  • sub_EDC9734.
К сожалению, собранный мной U-Boot и проведенные манипуляции не дали им имен. Возможно, я использовал неподходящую версию исходников — или же в «Яндексе» написали часть кода самостоятельно. В итоге я стал разбирать каждую функцию вручную — анализировал их поведение, обращал внимание на используемые строки и на основе этого присваивал имена:

  • sub_EDC96CC — challenge_gen;
  • sub_EDC1BBC — print;
  • sub_EDC1B74 — getconsole;
  • sub_EDC1A14 — getc;
  • sub_EDC9734 — challenge_chk.
Рассмотрим только первую и последнюю.

Код:
challenge_gen
bool __fastcall challenge_gen(unsigned __int64 a1)
{
  int v2; // w20
  int v3; // w21
  int v4; // w22
  int v5; // w0

  v2 = sub_EE2218C();
  v3 = sub_EE2218C();
  v4 = sub_EE2218C();
  v5 = sub_EE2218C();
  return string_format(a1, "%08X%08X%08X%08X", v2, v3, v4, v5) == 32;
}

bool __fastcall challenge_chk(const uint8_t *a1, __int64 a2)
{
  sha256_context ctx;

  sha256_starts(&ctx);
  sha256_update(&ctx, a1, 0x20u);
  sha256_finish(ctx.total, 0xEE89720);
  return sub_EE0EBA8(&dword_EE370C0, a2, 0x100u, 0xEE89720, &unk_EE593E0) == 0;
}

Анализ первой функции и вызова sub_EE2218C() показал, что в регистре a1 формируется challenge — я сразу переименовал его соответствующим образом. Запрос каждый раз получается уникальным, поскольку для его создания используется аппаратный генератор случайных чисел. Это подтверждается содержимым функции sub_EE2218C(), где единственная строка — return MEMORY[0xFF634018];. Согласно документации на A113X, этот адрес действительно принадлежит области RNG.

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

Код:
result = challenge_gen(challenge);
  if ( result )
  {
    while ( get_console() )
      getc();
    print("RH challenge: %s\n", challenge);
С challenge_chk повезло немного больше — почти все функции в этой области уже получили осмысленные имена. По ним сразу становится ясно, что для хеширования входных данных используется SHA-256, а полученный хеш затем передается в sub_EE0EBA8. Заглянем в нее.

bool __fastcall challenge_chk(const uint8_t *a1, __int64 a2)
{
  sha256_context ctx; // [xsp+38h] [xbp+38h] BYREF

  sha256_starts(&ctx);
  sha256_update(&ctx, a1, 0x20u);
  sha256_finish(ctx.total, 0xEE89720);
  return sub_EE0EBA8(&dword_EE370C0, a2, 0x100u, 0xEE89720, &unk_EE593E0) == 0;
}
В коде этой функции видны какие‑то преобразования, но имен тоже нет. Ныряем в каждую номерную функцию и пробуем найти намеки на то, что она делает.

__int64 __fastcall sub_EE0EBA8(int *a1, const void *a2, unsigned int a3, const void *a4, __int64 a5)
{

  v5 = *a1;
  v7 = *a1 - 2048;
  result = 0xFFFFFFF2LL;
  if ( v7 <= 0x800 )
  {
    v9 = v5 >> 5;
    sub_EE0E8B0(&v14, *(a1 + 1), v9);
    sub_EE0E8B0(&v14, *(a1 + 2), v9);
    v15[0] = v9;
    v15[1] = a1[1];
    v18 = *(a1 + 3);
    v16 = &v14;
    v17 = &v14;
    return sub_EE0E8E4(v15, a2, a3, a4, a5);
  }
  return result;
}

Итак, sub_EE0E8B0 «переворачивает» байты входного массива и складывает в другой.

Назовем ее array_bswap32. Возможно, она нам еще где‑то встретится и не придется повторно в ней копаться.

Код:
__int64 __fastcall sub_EE0E8B0(__int64 result, __int64 a2, int a3)
{
  __int64 i; // x3

  for ( i = 0; a3 > i; ++i )
    (result + 4 * i) = bswap32((a2 + 4LL * a3 - 4 * i - 4));
  return result;
}

С функцией sub_EE0E8E4 нам повезло не так, она уже имеет разветвленную структуру, видно много операций над данными. При изучении листинга я зацепился взглядом за функцию subM — ее имя появилось в результате работы Diaphora. Берем это имя и пробуем искать его в исходниках U-Boot от Khadas, которые мы компилировали.

Для просмотра ссылки Войди или Зарегистрируйся

Как видим, эта функция встречается только в файле avb_rsa.с, нам очень повезло!

Для просмотра ссылки Войди или Зарегистрируйся

Подробно изучаем этот файл и сверяем с тем, что видим в IDA. В результате я наткнулся на вызов avb_rsa_verify. Эта функция очень напоминает начало sub_EE0E8E4.

Для просмотра ссылки Войди или Зарегистрируйся
Код:
__int64 __fastcall sub_EE0E8E4(unsigned int *a1, const void *a2, unsigned int a3, const void *a4, __int64 a5)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]

  if ( !a1 || !a2 || !a4 || !a5 )
    return 0xFFFFFFFBLL;
  if ( 4LL * *a1 != a3 || a3 > 0x200 )
    return 0xFFFFFFEALL;

Очень похожая проверка в начале, но при этом тестовые сообщения не выводятся, просто возвращаются коды ошибок: всё, что не 0, — плохо. Идем дальше и находим больше сходств, поэтому возвращаемся на пару уровней вверх и даем название функции sub_EE0EBA8: rsa_yndx. Теперь функция проверки ответа выглядит так:

Код:
bool __fastcall challenge_chk(const uint8_t *a1, __int64 a2)
{
  sha256_context ctx; // [xsp+38h] [xbp+38h] BYREF

  sha256_starts(&ctx);
  sha256_update(&ctx, a1, 0x20u);
  sha256_finish(ctx.total, 0xEE89720);
  return rsa_yndx(&dword_EE370C0, a2, 0x100u, 0xEE89720, &unk_EE593E0) == 0;
}

Здесь:

  • dword_EE370C0 — содержит открытую часть ключа RSA;
  • a2 — ответ (который мы вводим);
  • 0x100u — размер проверяемых данных;
  • 0xEE89720 — ячейка памяти, содержащая SHA-256 от challenge;
  • unk_EE593E0 — должно быть padding.
Глядя на прототип avb_rsa_verify, можно сопоставить параметры rsa_yndx: они не выглядят один в один, но анализ позволит полностью их сверить.

Код:
bool avb_rsa_verify(const uint8_t* key,
                    size_t key_num_bytes,
                    const uint8_t* sig,
                    size_t sig_num_bytes,
                    const uint8_t* hash,
                    size_t hash_num_bytes,
                    const uint8_t* padding,
                    size_t padding_num_bytes)

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

Для просмотра ссылки Войди или Зарегистрируйся
Собираем всё воедино. При попытке входа в консоль U-Boot Rabbit Hole генерирует текстовый запрос (challenge). Пользователь должен подписать этот запрос своей закрытой частью RSA-ключа и ввести полученную подпись обратно в консоль. Далее вызывается функция rsa_yndx, которая проверяет подпись по аналогии с avb_rsa_verify, используя алгоритм RSA-2048. В случае успешной проверки функция возвращает 0.

Код:
/* Verify a RSA PKCS1.5 signature against an expected hash.
 * Returns false on failure, true on success.
 */

Мы рассмотрели основные механизмы Rabbit Hole. Остальные части довольно простые, привожу листинг c комментариями:

Код:
bool Rabbit Hole()
{
  result = challenge_gen(challendge);
  if ( result )
  {
    while ( get_console() )
      getc();
    // Печатаем запрос
    print("RH challenge: %s\n", challendge);
    v1 = 0;
    // Печатаем приглашение и ожидаем 344 символа ответа
    print("RH response:\n");
    while ( v1 != 344 )
    {
      v2 = getc();
      // Если не пробел, то сохраняем в буфер
      if ( (ctype[v2] & 0x20) == 0 )
        // Проверяем введенные символы, если они из ASCII-таблицы, то помещаем в буфер 0xEE89470
        *(0xEE89470 + v1++) = v2;
    }
    v3 = 0;
    v4 = 0;
    v5 = 0;
    // Теперь прогоняем каждый введенный символ
    while ( v3 != 344 )
    // Сравниваем с таблицей Base64
    {
      v6 = 0;
      while ( 1 )
      {
        v7 = v6;
        // Base64_table найден FindCrypt
        if ( Base64_table[v6] == *(v3 + 0xEE89470) )
          break;
        if ( ++v6 == 64 )
        {
          v7 = 64;
          break;
        }
      }
      v8 = (v5 + 1);
      v12[v5 + 4048] = v7;

   // Продолжаем преобразование Base64 в двоичный вид
   if ( v8 == 4 )
 {
        v9 = v13;
        v10 = v4 + 1;
        *(0xEE89740LL + v4) = 4 * (v12[4048] & 0x3F) + (v13 >> 4);
        v11 = v14;
        if ( v14 != 64 )
        {
          *(0xEE89740LL + v10) = 16 * (v9 & 0xF) + (v14 >> 2);
          v10 = v4 + 2;
        }
        v4 = v10;
        if ( v15 != 64 )
        {
          v4 = v10 + 1;
          *(0xEE89740LL + v10) = v15 + (v11 << 6);
        }
        v8 = 0;
      }
      ++v3;
      v5 = v8;
    }
    // Запускаем проверку подписи
    return challenge_chk(challendge, 0xEE89740);
  }
  return result;
}

Вот полный алгоритм авторизации:

  1. При загрузке системы нажимаем Enter или Ctrl-C, что провоцирует вызов Rabbit Hole.
  2. Генерируется случайный запрос, который выводится в консоль.
  3. Пользователь должен подписать запрос закрытой частью RSA-ключа, преобразовать в Base64.
  4. Дальше вводим ответ в виде Base64, который преобразует его в бинарный вид и отправляет на проверку в challenge_chk.
  5. Если результат проверки совпал, то попадаем в интерактивный режим U-Boot.
Скриншот с примером привожу для «Станции Max».

Пример от «Станции Max»

Мне удалось получить доступ к этой консоли — как и некоторым моим знакомым. Однако, если пароль уже установлен, попасть в консоль становится заметно сложнее.

Список команд

Выводы​

Инженеры «Яндекса» в части защиты консоли U-Boot применили очень надежный алгоритм, не позволяющий получить вход даже при наличии расшифрованной прошивки, — асимметричный RSA-2048 со случайными входными данными. Брутфорс тут бессилен, еще и попытка ввода только одна — неверный ответ перезагружает устройство, и запрос будет уже новый. В лоб Rabbit Hole не обходится.

Зато по дороге я создал инструмент для извлечения BL33 и выяснил, как защищена прошивка. Надеюсь, и ты тоже почерпнул что‑то для себя полезное.

Я не профессиональный реверсер, скорее — увлеченный любитель. Это мой первый подобный опыт, и, если я где‑то напорол чуши, не стесняйся и расскажи мне об этом!
 
Activity
So far there's no one here