habrahabr

Как я Капсулу Нео от VK взломал

  • пятница, 13 сентября 2024 г. в 00:00:10
https://habr.com/ru/companies/bizone/articles/840376/

Всем привет!

Исправления давно в проде, а конфа OFFZONE 2024, на которой я выступил с этим докладом, закончилась — пришло время и на Хабре рассказать об уязвимости, которую я нашёл в умном девайсе от VK под названием «Капсула Нео» (далее — «Капсула»). Бага оказалась критичной, с возможностью исполнять на колонке собственный код по сети (RCE).

О том, как мне удалось найти уязвимость, с чем пришлось столкнуться за время проекта и почему VK в итоге не виноваты, читайте под катом.

Старт проекта

Капсулы различной степени "готовности"
Капсулы различной степени "готовности"

Эта история началась не совсем так, как другие мои домашние проекты, когда я сам выбираю цель для исследования. В этот раз задачка пришла от человека, отвечающего у нас в компании за программу Bug Bounty (далее — BB). Мол, тут у VK новая колонка выходит, идёт программа бета-тестирования, и нужны хорошие реверс-инженеры, умеющие ковырять железки. Когда прозвучали слова о «хороших реверс-инженерах», мы с ребятами из нашего отдела (далее — Лаба) моментально согласились. От нас потребовались никнеймы и почта для регистрации в программе BB и резервирования части железок под конкретное число исследователей.

Чуть позже этот чел принёс в Лабу по колонке на каждого, и мы с азартом принялись их изучать. Как вы понимаете, возиться с тряпочками, пластиком и прочей ерундой вокруг основной платы обычно совсем не хочется, поэтому в ход сразу пошли и дремель, и термофен — лишь бы те самые тряпочки с пластиком снять. Конечно, это всё шутка, и ничего такого не применялось, просто выделять целый абзац на то, чтобы рассказать про откручивание винтиков мне не хотелось.

Куча БП от Капсул
Куча БП от Капсул

Так вот, когда доступ к основной плате «Капсулы» был получен, мы стали осматривать её на предмет того, что мы обычно ищем на рабочих проектах, а именно:

  • большие микросхемы (SoC, контроллеры),

  • флэш-память,

  • группы контактов (отладочные пины, пины с UART).

Основной камень оказался один: какой-то ноунейм BES2300, с которым до этого никому из нас не приходилось работать. Флэшек на плате не нашлось, зато обнаружились целых три группы подозрительных контактных площадок. Именно с них мы и решили начать.

Группы контактов
Группы контактов

UART есть, а толку ноль

Самый простой способ определить, какие из множества пинов отвечают за отладочные/информационные сообщения, — это подключить максимальное их количество к логическому анализатору, включить целевой девайс, и посмотреть, что происходит. Если вы видите в окне анализатора что-то типа сплошных длинных полосок на одном из каналов, скорее всего вы нашли UART TX. В нашем случае такой пин тоже нашёлся. И, так как он принадлежал к одной из тех групп по три пина в каждой, было сделано предположение, что соседние с ним — это земля (GND) и UART RX.

Такого вида "полоски" и нужны
Такого вида "полоски" и нужны

Определившись с пинами UART и подключив их к USB/UART-свистку, мы открыли Putty с бодрейтом 115200 в надежде увидеть там осмысленный текст. Но конечно же, там нас ожидала каша. Лично я бы стал перебирать все возможные варианты, чтобы найти нужный бодрейт. Но умные люди в Лабе научили меня некоторой магии, как это можно сделать красивше.

Скрин не из Putty, но "каша" там была такая же
Скрин не из Putty, но "каша" там была такая же

Алгоритм поиска

В том же лог-анализаторе при выбранной максимально возможной частоте дискретизации необходимо точно так же записать всё, что ходит на пине UART TX. Далее в этом трафике необходимо найти минимальный по ширине импульс и привести его, например, к микросекундам. Пускай это будет значение 8.64µs. Затем необходимо единицу (1) поделить на 8.64 и умножить полученное значение на 1_000_000 (для микросекунд). В итоге получится значение, приблизительно равное целевому бодрейту (в нашем примере — 115740115200).

Максимальный зум на "полоску". Минимальный импульс
Максимальный зум на "полоску". Минимальный импульс

В случае «Капсулы» итоговым бодрейтом оказалось значение 1_500_000. Подключившись на такой скорости к колонке, мы наконец получили читаемый текст.

Такой вот своеобразный спам
Такой вот своеобразный спам

Как полагается, что-либо выжать с UART в тот момент не удалось. На нажатия клавиш он не реагировал, а только беспощадно спамил.

В общем-то, на этом мы тогда и остановились. Работа, проекты — заниматься колонкой совсем не было времени. А потом я вообще стал удалёнщиком... и всё своё оборудование, которое я смог насобирать к моменту отлучения от офиса, я перевёз в другой город вместе со своей красавицей-кошкой Матильдой. Обосновался на лоджии, докупил всякого барахла — можно считать, собственная мини-Лаба у меня теперь есть.

Мадам кошька
Мадам кошька

Возвращаясь к «Капсуле»

Так вот, переехав и более-менее разгрёбшись в череде проектов, я снова смог выкроить время, чтобы заняться колонкой. Правда, пока другой — саундбаром от Yamaha, про который я писал на Хабр цикл статей: раз и два. Рекомендую почитать ;)

Про японский саундбар я вспомнил не просто так. Дело в том, что пока я им занимался, я успел облюбовать новый софт для работы с Serial COM-портами — Serial Port Monitor от Electronic Team. В нём оказалась одна интересная настройка, позволяющая изменять либо вовсе убирать окончание строки, добавляемое к каждой отправляемой команде: \n, \r, \r\n. Этого требовал саундбар — я это видел, так как мог изучать код обработчика команд.

Диалоговое окно с настройкой окончания строк
Диалоговое окно с настройкой окончания строк

К сожалению, кода обработчика нажатий в UART для «Капсулы» у меня не было. Но отправить команду с нормальным окончанием строки мне таки удалось — я точно так же воспользовался Serial Port Monitor, в котором уже был настроен правильный перенос строки: \n (Putty отправляет \r\n по умолчанию).

Нажимаю я Enter, совершенно ни на что не надеясь, и вижу: реакция в консоли точно есть! А мужики-то не знают... На самом деле, я решил лабовским пока не рассказывать — нужно сначала всё изучить, что тут есть.

UART — всё-таки толк есть

Первой командой, которую я отправил в консоль, была help. В ответе среди мусора, которым продолжала беспощадно спамить колонка, я получил следующий список команд:

«Меню» help
«Меню» help

Тут тебе и чтение/запись памяти, и чтение/запись физических портов — красота! Дополнительно по команде AT+HELP выдавался и второй список:

«Меню» AT+HELP
«Меню» AT+HELP

А что это тут у нас?! Кроме ещё одного варианта чтения/записи памяти, обнаружилась команда включения отладки по JTAG.

К этому моменту я не сделал ничего, чтобы запустить отладку на железке: пины не искал, JTAG не включал, возможности сдампить память устройства для последующего изучения тоже пока не имел. Поэтому, чтобы не переключаться с UART на разборки с отладкой, я решил попробовать сдампить прошивку прямо через консоль.

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

К сожалению, из-за постоянного спама в консоль процесс обещал быть очень небыстрым. Оставив скрипт работать на ночь, я пошёл спать.

Дамплю память 10 часов...
Дамплю память 10 часов...

Изучение дампа прошивки

Наутро, выспавшись и получив дамп прошивки «Капсулы», я взялся его изучать: открыл «Иду», распихал по регионам, кое-как разобравшись с дублированием некоторых из них по разным адресам.

К счастью, в дампе виднелись строки, адреса и константы, а значит, что-то из этого можно было совать в поиск Github и «гуго́л». И там и там мне удалось обнаружить кусочки SDK от вендора BES для нужной мне BES2300. К сожалению, на момент подготовки текста доклада выяснилось, что с гитхаба репозиторий был удалён. Тем не менее, был и второй ресурс, на котором данный SDK был почти в свободном доступе — CSDN. Это такой китайский файлообменник, на котором за китайскую же денежку ты можешь скачать много чего интересного.

Нужный мне SDK
Нужный мне SDK

Карты «Мир» там не принимали, но, к счастью, в интернетах есть дяди, которые скачают нужный файлик за рубли (стоило это мне 1500). Файлик оказался чуть более полезным, чем с гитхаба, хоть и всё равно неполным. Тем не менее, нужные мне функции и строки там были — бери да изучай.

Но прежде чем уйти в дебри кода, я решил-таки определиться: чего искать-то вообще?

Модель злоумышленника

Что касается возможных векторов атаки, разгуляться я не мог. В условиях программы BB есть список разрешённых сообщений о проблемах, поэтому мне были доступны и интересны только следующие направления:

  • поиметь колонку, имея физический доступ (локальное исполнение кода);

  • поиметь колонку, используя сетевой доступ (удалённое выполнение кода).

Начать я решил с первого — с локального выполнения кода по UART.

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

Вулна в функции AT+WSACONF
Вулна в функции AT+WSACONF

Я быстро накидал PoC и закинул его в консоль — ожидаемо, железка ребутнулась. Недолго думая, я решил сбацать свой первый в жизни репорт по программе BB: подробно описал, где находится уязвимая функция и как её эксплуатировать — и отправил на проверку.

Неожиданно — мой репорт не приняли. На скринах все детали.

Мой первый репорт
Мой первый репорт
Ответ на первый репорт
Ответ на первый репорт

Расстроившись и не очень-то желая раскручивать такой вектор, я решил переключиться на второй вариант — сетевой.

Сетевой вектор. Для определения доступных сетевых портов я запустил nmap -p- 192.168.0.123. Я всякое надеялся увидеть, но только не такое:

Одинокий порт
Одинокий порт
А вообще, курить - вредно!
А вообще, курить - вредно!

На самом деле, баловаться с доступом к «Капсуле» по сети я начал ещё до того, как смог полноценно работать в консоли по UART. Я видел открытый HTTP-порт и даже заходил на него через браузер. Там одна лишь страница настройки Wi-Fi на колонке. Уже не помню точную команду для привязки к сети, но это определённо был GET-запрос.

Так вот, однажды в ходе моих экспериментов я случайно воткнул в аргументы второй знак = («равно»). Получилось что-то типа:

http://192.168.0.123/setup?key1=value1&key2==value2

И тут «Капсула» почему-то перезагрузилась. Заглянув в консоль UART, я увидел портянку, которая выглядела как stacktrace после assert-а.

stacktrace
stacktrace

Тут были видны и адреса в стеке, и имена работавших потоков. Затем колонка перезагружалась и начинала спамить мусорными сообщениями по новой. Я пробовал вставлять разное количество знаков «равно» в надежде изменить выводимый в UART трейс.

Так у меня сформировался такой вот список:

Ловись, крэш, большой и маленький
Ловись, крэш, большой и маленький

То есть да, буквально: большой и маленький крэш, либо совсем без крэша. Такое нестабильное поведение натолкнуло меня на мысль: «А вдруг это можно раскрутить до RCE?».

Начальное понимание происходящего

Самое интересное, что среди мусора в логе была видна и реальная причина падения колонки: assert на нечётное количество пар key=value в запросе, к тому же с адресом памяти, где оно упало.

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

По сути есть некая структура keys_dict_t с максимальным количеством элементов key_value_t в ней и счётчиком этих элементов. В какой-то момент структура переполняется, и происходит падение.

Приблизительное представление о переполняемой структуре
Приблизительное представление о переполняемой структуре

К сожалению, я пока не понимал, как это эксплуатировать. Ну есть крэш, ну и что? В статике мне сложно было понять, как это раскручивать.

Отсылка на творчество Евы Морозовой
Отсылка на творчество Евы Морозовой

Тогда я принял решение наконец-то разобраться с отладкой.

Чёртова отладка

Да, именно чёртова, если не сказать грубее. Хуже того, что мне пришлось испытать, мне ощущать в реверс-инжиниринге не доводилось. И вот почему...

Снова картинка с группами пинов
Снова картинка с группами пинов

Для начала отладку необходимо было завести, а для этого — понять, на какие пины она выведена. Тут как с UART не выйдет — придётся перебирать. Хорошо, что вариантов для брута немного — всего две группы по 3 пина. Чтобы определиться, что перебирать, нужно понять, с чем мы имеем дело: либо JTAG, либо SWD. Отладка могла быть вообще никуда не выведена, но раз уж есть команда UART на её включение, скорее всего подключиться физически я смогу. Да и название команды AT+JTAG говорит о том, что нужно искать интерфейс JTAG.

Если отладка на устройстве реализована через JTAG, потребуется минимум 4 пина, а значит будут задействованы две группы. То, что они разнесены по плате, конечно, смущает, но всякое бывает. Отправляю команду на включение, пробую Jtagulator-ом набрутать правильное расположение пинов — безуспешно.

Более вероятный вариант: на колонке SWD. На него всего нужно два пина (Clock и I/O) и земля (GND). Подсоединяю все 6 пинов, перебираю — тишина. Неужели нет отладки? Я отказывался это принимать, поэтому попробовал просто замерить мультиметром напряжение на этих 6 пинах после включения отладки командой из консоли.

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

Говорит, мол, мало питалова
Говорит, мол, мало питалова

Я бы не назвал это успешным подключением. Забавно, что при каждой попытке подключиться ошибки были разные. Отлаживаться было просто невозможно!

Я настолько отчаялся, что даже рассказал товарищам из Лабы, как наконец отправил Enter, смог прочитать память, нашёл отладку. Потом мы долго смеялись над тем, что проект тупо не двигался из-за настройки переноса строки в софте. Бывает.

Почти успешное подключение
Почти успешное подключение

Так вот, я надеялся, что кто-то из товарищей попробует на такой же колонке точно так же подключиться по SWD, попробует отладиться, и у него всё получится. Не получилось.

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

Умные схемы подключения SWD
Умные схемы подключения SWD

Расследование, что называется, зашло в тупик. Бага не раскручивается — надо вернуться к анализу кода, авось чего другое найдётся. Спустя бесчисленное количество суток в «Иде» я наконец обратил внимание на команду, которая позволяет писать в физические адреса, а именно в управляющие регистры. Одним из регистров, с которым я решил поиграться, стал Watchdog. Если он включён, на устройстве будут отрабатывать прерывания, переключаться потоки и тому подобное. Моя мысль заключалась в том, что, скорее всего, отладке мешает какой-то код, который переключает назначение SWD-пинов (GPIO) на другое.

Для проверки гипотезы я отправил в регистр управления Watchdog значение для его остановки, после чего включил SWD. Случайно это получилось или я действительно что-то остановил, но впервые мне удалось нормально подключиться и прочитать память через JLink! Спойлер: получилось случайно :) Некоторое время я даже смог поотлаживаться, а потом снова всё сломалось.

Тем не менее, схема с некоторыми изменениями оказалась наиболее полезной:

  1. Отключить Watchdog.

  2. Включить SWD.

  3. Перейти к пункту 1 (повторять 100 раз).

Практически рабочая схема, отвечаю!
Практически рабочая схема, отвечаю!

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

Собственно, по поводу структуры я был прав
Собственно, по поводу структуры я был прав
  1. На стеке хранится структура со словарём полученных пар key=value из URL-запроса. У структуры есть максимальный размер max_count.

  2. При переполнении словаря key=value URL-запроса происходит перезапись поля с количеством элементов словаря count.

  3. Команда логирования принимаемых запросов поднимает со стека столько указателей на пары ключ-значение, сколько указано в поле count, хотя на стеке их количество будет меньшим (и ограниченным размером структуры).

  4. Происходит перезапись значений регистров, включая адрес возврата {R4-R11,PC}, значениями, которые я передаю.

На самом деле там есть множество других нюансов при формировании RCE-запроса: длина кода с полезной нагрузкой, количество знаков «равно», требуемых для выравнивания, количество пар key=value. Но это всё детали. Оставалась самая малость — отправить репорт.

Репорт

Описание уязвимости
Описание уязвимости
Steps to Reproduce
Steps to Reproduce

Как и ожидалось, отчёт, содержащий бинарную уязвимость, обрабатывали долго. Я был уверен, что мой репорт подтвердят, но не знал, как именно это происходит на платформе BB. Тут много нюансов:

  • Понимание разрабами того, как работают переполнения на стеке.

  • Качественный PoC, результат которого можно увидеть сразу.

  • Время на исправление бага.

Уже на первом пункте возникли трудности: далеко не все разработчики понимают, как переполняется стек. Да, многие знают, что нужно проверять размер памяти перед копированием, а вот что будет, если не проверять — нет. Но это ладно, ожидаемо.

Второй момент — скорее мой недочёт, хоть и не критичный: я сделал PoC, который ребутает колонку. С одной стороны, это наглядно. А с другой, если не смотреть на выхлоп в консоль, поведение девайса можно счесть за обычный крэш и последующий ребут. Поэтому мне написали с просьбой приложить ещё какое-нибудь подтверждение. Учитывая уже описанные сложности при формировании PoC, это потребует энного времени и отладки. Как бы то ни было, мне удалось сформировать такой запрос, который кирпичит колонку. Думаю, самое оно в качестве пруфа.

Ничего лучше не придумал
Ничего лучше не придумал

Ну а третий момент, время — это скорее про ожидание, нервы: хватило ли последнего пруфа или нужно ещё, и тому подобное. В конце концов я получил ответ, что бага будет исправлена и я получу вознаграждение.  И как раз 31 декабря оно пришло!

Выводы

Какие можно сделать выводы? Давайте порассуждаем:

  1. Бага есть? Есть.

  2. Багу внёс не ты? Не ты (не разработчик VK).

  3. Тебе дали SDK сразу с багой? Да.

  4. Проверять скомпилированный код и реверсить/изучать все линкуемые либы? Да. Нет (смотреть чужой код на тысячи строк, или того хуже — реверсить его, лично я не стал бы просто так).

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

Спасибо за то, что дочитали до конца.

Конец.
Конец.