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

Всем привет!

Исправления давно в проде, а конфа 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. Проверять скомпилированный код и реверсить/изучать все линкуемые либы? Да. Нет.

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

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

Конец.

Конец.

© Habrahabr.ru