Как я Капсулу Нео от VK взломал
- пятница, 13 сентября 2024 г. в 00:00:10
Всем привет!
Исправления давно в проде, а конфа OFFZONE 2024, на которой я выступил с этим докладом, закончилась — пришло время и на Хабре рассказать об уязвимости, которую я нашёл в умном девайсе от VK под названием «Капсула Нео» (далее — «Капсула»). Бага оказалась критичной, с возможностью исполнять на колонке собственный код по сети (RCE).
О том, как мне удалось найти уязвимость, с чем пришлось столкнуться за время проекта и почему VK в итоге не виноваты, читайте под катом.
Эта история началась не совсем так, как другие мои домашние проекты, когда я сам выбираю цель для исследования. В этот раз задачка пришла от человека, отвечающего у нас в компании за программу Bug Bounty (далее — BB). Мол, тут у VK новая колонка выходит, идёт программа бета-тестирования, и нужны хорошие реверс-инженеры, умеющие ковырять железки. Когда прозвучали слова о «хороших реверс-инженерах», мы с ребятами из нашего отдела (далее — Лаба) моментально согласились. От нас потребовались никнеймы и почта для регистрации в программе BB и резервирования части железок под конкретное число исследователей.
Чуть позже этот чел принёс в Лабу по колонке на каждого, и мы с азартом принялись их изучать. Как вы понимаете, возиться с тряпочками, пластиком и прочей ерундой вокруг основной платы обычно совсем не хочется, поэтому в ход сразу пошли и дремель, и термофен — лишь бы те самые тряпочки с пластиком снять. Конечно, это всё шутка, и ничего такого не применялось, просто выделять целый абзац на то, чтобы рассказать про откручивание винтиков мне не хотелось.
Так вот, когда доступ к основной плате «Капсулы» был получен, мы стали осматривать её на предмет того, что мы обычно ищем на рабочих проектах, а именно:
большие микросхемы (SoC, контроллеры),
флэш-память,
группы контактов (отладочные пины, пины с UART).
Основной камень оказался один: какой-то ноунейм BES2300
, с которым до этого никому из нас не приходилось работать. Флэшек на плате не нашлось, зато обнаружились целых три группы подозрительных контактных площадок. Именно с них мы и решили начать.
Самый простой способ определить, какие из множества пинов отвечают за отладочные/информационные сообщения, — это подключить максимальное их количество к логическому анализатору, включить целевой девайс, и посмотреть, что происходит. Если вы видите в окне анализатора что-то типа сплошных длинных полосок на одном из каналов, скорее всего вы нашли UART TX
. В нашем случае такой пин тоже нашёлся. И, так как он принадлежал к одной из тех групп по три пина в каждой, было сделано предположение, что соседние с ним — это земля (GND
) и UART RX
.
Определившись с пинами UART и подключив их к USB/UART-свистку, мы открыли Putty с бодрейтом 115200 в надежде увидеть там осмысленный текст. Но конечно же, там нас ожидала каша. Лично я бы стал перебирать все возможные варианты, чтобы найти нужный бодрейт. Но умные люди в Лабе научили меня некоторой магии, как это можно сделать красивше.
В том же лог-анализаторе при выбранной максимально возможной частоте дискретизации необходимо точно так же записать всё, что ходит на пине
UART TX
. Далее в этом трафике необходимо найти минимальный по ширине импульс и привести его, например, к микросекундам. Пускай это будет значение8.64µs
. Затем необходимо единицу (1
) поделить на 8.64 и умножить полученное значение на1_000_000
(для микросекунд). В итоге получится значение, приблизительно равное целевому бодрейту (в нашем примере —115740
≈115200
).
В случае «Капсулы» итоговым бодрейтом оказалось значение 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, совершенно ни на что не надеясь, и вижу: реакция в консоли точно есть! А мужики-то не знают... На самом деле, я решил лабовским пока не рассказывать — нужно сначала всё изучить, что тут есть.
Первой командой, которую я отправил в консоль, была help
. В ответе среди мусора, которым продолжала беспощадно спамить колонка, я получил следующий список команд:
Тут тебе и чтение/запись памяти, и чтение/запись физических портов — красота! Дополнительно по команде AT+HELP
выдавался и второй список:
А что это тут у нас?! Кроме ещё одного варианта чтения/записи памяти, обнаружилась команда включения отладки по JTAG.
К этому моменту я не сделал ничего, чтобы запустить отладку на железке: пины не искал, JTAG не включал, возможности сдампить память устройства для последующего изучения тоже пока не имел. Поэтому, чтобы не переключаться с UART на разборки с отладкой, я решил попробовать сдампить прошивку прямо через консоль.
Потыкавшись командой md
в различные регионы памяти, я выяснил, что если региона нет, то железка ребутается, иначе выплёвывается дамп региона. Для комфортной работы я смастерил скрипт, который дампит нужные регионы и раскладывает по файликам.
К сожалению, из-за постоянного спама в консоль процесс обещал быть очень небыстрым. Оставив скрипт работать на ночь, я пошёл спать.
Наутро, выспавшись и получив дамп прошивки «Капсулы», я взялся его изучать: открыл «Иду», распихал по регионам, кое-как разобравшись с дублированием некоторых из них по разным адресам.
К счастью, в дампе виднелись строки, адреса и константы, а значит, что-то из этого можно было совать в поиск Github и «гуго́л». И там и там мне удалось обнаружить кусочки SDK от вендора BES для нужной мне BES2300
. К сожалению, на момент подготовки текста доклада выяснилось, что с гитхаба репозиторий был удалён. Тем не менее, был и второй ресурс, на котором данный SDK был почти в свободном доступе — CSDN. Это такой китайский файлообменник, на котором за китайскую же денежку ты можешь скачать много чего интересного.
Карты «Мир» там не принимали, но, к счастью, в интернетах есть дяди, которые скачают нужный файлик за рубли (стоило это мне 1500). Файлик оказался чуть более полезным, чем с гитхаба, хоть и всё равно неполным. Тем не менее, нужные мне функции и строки там были — бери да изучай.
Но прежде чем уйти в дебри кода, я решил-таки определиться: чего искать-то вообще?
Что касается возможных векторов атаки, разгуляться я не мог. В условиях программы BB есть список разрешённых сообщений о проблемах, поэтому мне были доступны и интересны только следующие направления:
поиметь колонку, имея физический доступ (локальное исполнение кода);
поиметь колонку, используя сетевой доступ (удалённое выполнение кода).
Начать я решил с первого — с локального выполнения кода по UART.
Физический доступ. В одной из команд, доступных по UART
, мне удалось обнаружить уязвимость, которая будет очевидна всем, кто сталкивался с переполнениями на стеке.
Я быстро накидал 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
-а.
Тут были видны и адреса в стеке, и имена работавших потоков. Затем колонка перезагружалась и начинала спамить мусорными сообщениями по новой. Я пробовал вставлять разное количество знаков «равно» в надежде изменить выводимый в 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, попробует отладиться, и у него всё получится. Не получилось.
Далее в ход пошли различные схемы из интернетов о том, что нужно подключить конденсатор, резистор определённых номиналов — мол, это сгладит какие-то там недоподтягивания до нормальных логических уровней. Но всё это точно так же не помогло.
Расследование, что называется, зашло в тупик. Бага не раскручивается — надо вернуться к анализу кода, авось чего другое найдётся. Спустя бесчисленное количество суток в «Иде» я наконец обратил внимание на команду, которая позволяет писать в физические адреса, а именно в управляющие регистры. Одним из регистров, с которым я решил поиграться, стал Watchdog
. Если он включён, на устройстве будут отрабатывать прерывания, переключаться потоки и тому подобное. Моя мысль заключалась в том, что, скорее всего, отладке мешает какой-то код, который переключает назначение SWD-пинов (GPIO) на другое.
Для проверки гипотезы я отправил в регистр управления Watchdog
значение для его остановки, после чего включил SWD. Случайно это получилось или я действительно что-то остановил, но впервые мне удалось нормально подключиться и прочитать память через JLink! Спойлер: получилось случайно :) Некоторое время я даже смог поотлаживаться, а потом снова всё сломалось.
Тем не менее, схема с некоторыми изменениями оказалась наиболее полезной:
Отключить Watchdog
.
Включить SWD.
Перейти к пункту 1 (повторять 100 раз).
И не спрашивайте у меня, как это работает, — я не знаю, отладка всё равно иногда отваливалась. Одно могу сказать точно: через пару дней таких упражнений я таки смог раскрутить найденную багу до RCE. Вот какие факторы к ней приводят:
На стеке хранится структура со словарём полученных пар key=value
из URL-запроса. У структуры есть максимальный размер max_count
.
При переполнении словаря key=value
URL-запроса происходит перезапись поля с количеством элементов словаря count
.
Команда логирования принимаемых запросов поднимает со стека столько указателей на пары ключ-значение, сколько указано в поле count
, хотя на стеке их количество будет меньшим (и ограниченным размером структуры).
Происходит перезапись значений регистров, включая адрес возврата {R4-R11
,PC}
, значениями, которые я передаю.
На самом деле там есть множество других нюансов при формировании RCE-запроса: длина кода с полезной нагрузкой, количество знаков «равно», требуемых для выравнивания, количество пар key=value
. Но это всё детали. Оставалась самая малость — отправить репорт.
Как и ожидалось, отчёт, содержащий бинарную уязвимость, обрабатывали долго. Я был уверен, что мой репорт подтвердят, но не знал, как именно это происходит на платформе BB. Тут много нюансов:
Понимание разрабами того, как работают переполнения на стеке.
Качественный PoC, результат которого можно увидеть сразу.
Время на исправление бага.
Уже на первом пункте возникли трудности: далеко не все разработчики понимают, как переполняется стек. Да, многие знают, что нужно проверять размер памяти перед копированием, а вот что будет, если не проверять — нет. Но это ладно, ожидаемо.
Второй момент — скорее мой недочёт, хоть и не критичный: я сделал PoC, который ребутает колонку. С одной стороны, это наглядно. А с другой, если не смотреть на выхлоп в консоль, поведение девайса можно счесть за обычный крэш и последующий ребут. Поэтому мне написали с просьбой приложить ещё какое-нибудь подтверждение. Учитывая уже описанные сложности при формировании PoC, это потребует энного времени и отладки. Как бы то ни было, мне удалось сформировать такой запрос, который кирпичит колонку. Думаю, самое оно в качестве пруфа.
Ну а третий момент, время — это скорее про ожидание, нервы: хватило ли последнего пруфа или нужно ещё, и тому подобное. В конце концов я получил ответ, что бага будет исправлена и я получу вознаграждение. И как раз 31 декабря оно пришло!
Какие можно сделать выводы? Давайте порассуждаем:
Бага есть? Есть.
Багу внёс не ты? Не ты (не разработчик VK).
Тебе дали SDK сразу с багой? Да.
Проверять скомпилированный код и реверсить/изучать все линкуемые либы? Да. Нет (смотреть чужой код на тысячи строк, или того хуже — реверсить его, лично я не стал бы просто так).
Итог: не весь код, который компилируется, зависит от тебя, но изучать его как-то нужно.
Спасибо за то, что дочитали до конца.