Как я взломал миллионы умных весов через уязвимости в API и железе

от автора

Зачем взламывать устройства по одному, если можно хакнуть сразу все?

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

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

Весы с подключением к интернету… Серьезно? 

Находясь в отпуске, я заметил на экране весов в спортзале отеля любопытную иконку. Это был значок Wi-Fi. Да, кто-то решил, что подключить к интернету весы — это хорошая идея 🙂

Я пошарился по Amazon и нашел множество аналогичных устройств с подключением по Wi-Fi или Bluetooth. Некоторые из них имели подозрительно схожие мобильные приложения.

Weighing Machines on Amazon

Оказалось, что многие из них были изготовлены одним и тем же OEM-производителем. Даже если они создавались разными OEM с разными кодовыми базами, беглый анализ связанных Android-приложений показал, что во многих из них использовались одни и те же библиотеки — например, com.qingniu.heightscale. Вероятно, это связано с тем, что написать совместимую библиотеку с нуля труднее.

Qingniu Library on Arboleaf App
Qingniu Library on Renpho App

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

Мы должны идти глубже

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

Этот процесс сложно защитить. Еще начиная с завода, у каждого девайса должен быть уникальный идентификатор/секрет устройства, чтобы вы не могли случайно связаться с весами X при сканировании QR-кода на весах Y. Самый ненадежный способ защиты — применение статической строки, например, UUID, MAC-адреса или серийного номера. Хотя такая строка вполне может использоваться в качестве идентификатора, она плохо работает как секрет аутентификации. Даже если они сгенерированы случайно и слабо поддаются перебору, их будет сложно отозвать в случае утечки.

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

То есть типичный процесс привязки будет выглядеть так:

  1. Пользователь устанавливает мобильное приложение и создает аккаунт.

  2. Через приложение пользователь подключается к устройству.

  3. Секрет устройства передается мобильному приложению.

  4. Мобильное приложение передает серверу секрет пользователя (например, токен сессии) и секрет устройства.

  5. Сервер подтверждает подлинность секретов и привязывает аккаунт пользователя к устройству.

  6. Теперь пользователь может управлять устройством и получать от него данные удаленно через интернет.

Звучит вполне разумно. Что же может пойти не так?

Инъекция SQL в OEM (обход BT-WAF) 

OEM-производитель допустил ошибку буквально на старте. Мне даже не пришлось покупать само устройство — достаточно было проанализировать мобильное приложение и перебрать доступные API-эндпоинты. 

Среди прочего мне попался любопытный эндпоинт api/ota/update, через который, как я предположил, можно было бы вытащить прошивку, чтобы еще лучше разобраться в устройстве. Благодаря декомпиляции Java-кода Android-приложения я без особого труда восстановил структуру необходимых JSON-параметров в теле запроса. Увы, даже с корректными запросами оказалось, что производитель не собирался делиться чем-то интересным.

Куда интереснее оказались другие API. Во время их изучения я наткнулся на несколько эндпоинтов, уязвимых к SQL-инъекциям. Что любопытно, сервер прикрывался китайским файрволом Baota Cloud WAF (BT-WAF). Он оказался куда суровее большинства файрволов, с которыми мне доводилось иметь дело. 

Особенно выделился эндпоинт /api/device/getDeviceInfo: он позволял запрашивать серийные номера устройств. Ошибка производителя заключалась в том, что этот номер одновременно выступал и идентификатором, и секретом аутентификации. Более того, тот же серийный номер использовался в запросе к /api/device/bindv2 — и этот запрос привязывал (или перепривязывал) устройство к аккаунту отправителя! Под «серийным номером» же скрывалось не что иное, как случайно сгенерированный MAC-адрес, записанный прямо в памяти устройства.

Вот исходное тело запроса для уязвимого эндпоинта:

{   "serialnumber":"'001122334455" }

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

{   "serialnumber":"'or\n@@version\nlimit 1\noffset 123#" }

Разберем, что здесь происходит. Если эта инъекция вставляется в SQL-запрос вроде:

SELECT * FROM devices WHERE serial = 'INJECTION'

то мы получим такой инъецированный SQL: 

SELECT * FROM devices WHERE serial = 'INJECTION'or\n@@version\nlimit 1\noffset 123#'

Здесь присутствует два ключевых механизма обхода:

  1. @@version всегда равно true и может использоваться вместо более очевидного 1=1.

  2. \n (символ новой строки) позволяет разбить SQL-инструкцию на строки вместо использования пробелов — еще один способ обойти фильтрацию.

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

Получение доступа к отладочной консоли по UART на весах Withings WBS06

Когда я начал изучать другие модели устройств, мне попались весы Withings Body. Как и другие подобные гаджеты, они поддерживали подключение по Wi-Fi и Bluetooth и использовали собственное мобильное приложение. При этом Withings — довольно уважаемый бренд, и, что любопытно, устройство также продавалось под маркой Nokia Body.

Извлечь через API приложения прошивку для дальнейшего анализа оказалось несложно. Однако в отличие от более «тяжелых» прошивок (например, у роутеров), где обычно используется полноценная файловая система и Linux, здесь была низкоуровневая прошивка для ARM без ОС — так называемый baremetal.

Хотелось бы мне в этом посте пролить больше света на искусство реверс-инжиниринга baremetal ARM, но, судя по видео и блогам специалистов с гораздо большим опытом, чем у меня, можно прийти к выводу, что заниматься им крайне тяжело.

Тем не менее, я взялся за задачу и пошел по пути, описанному в статье о реверсе baremetal-прошивок в Ghidra. В итоге мне удалось определить модель микроконтроллера, установленного в WBS06 (здесь помогли фотографии из документации FCC) и настроить распределение памяти.

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

Connection Manager Shell Command Usage:   wifi <wifi_sync_flags>             Попытка синхронизации по Wi-Fi с указанными флагами.             wifi_sync_flags - это комбинация следующих флагов:                 0x01 (разрешить обновления), 0x02 (сохранить DbLib),                  0x04 (отправить DbLib), 0x08 (отправить сырые данные),                 0x10 (отправить wlog), 0x20 (отправить события),                  0x40 (отправить дополнительные данные)   wifi_no_update <wifi_sync_flags>             Попытка синхронизироваться по Wi-Fi, обновления не разрешены              (даже если указаны во флагах).   wifi_update <wifi_sync_flags>               Попытка синхронизации, разрешает обновление,                если доступно (даже если не указано во флагах).   bt    Попытка синхронизации по Bluetooth   do    Попытка синхронизации по Wi-Fi/мобильной связи и откат к Bluetooth в случае неудачи.

Порыскав по вебу, я обнаружил пост другого исследователя на Reddit о том, что он разобрался с контактами UART на более старой модели WBS05.

Объяснение оказалось довольно простым, поэтому я нетерпеливо приступил к попытке воспроизведения на WBS06. Самой важной подсказкой стало наличие на нижней поверхности WBS06 тех же трех отверстий, соответствующих контактам UART Tx, Rx и GND; сравнив их с фотографиями внутренностей из документации FCC, я убедился в этом наверняка.

Exterior of WBS06 for UART pins

Выводы для UART на весах WBS06
Interior of WBS06 for UART pins

Те самые контакты GND, Rx и Tx

Первые заходы не увенчались успехом. Несмотря на то, что с помощью логического анализатора мне удалось правильно определить скорость передачи данных (baud rate), последовательное соединение выдавало абракадабру. После нескольких часов мучений я понял, что проблема была в моем дешевом USB-TTL-конвертере CP2102. Замена его на более надежный FT232 наконец дала нужный результат.

Logic Analyzer

Logic Analyzer

Теперь, когда у меня появился доступ к отладочной оболочке, я мог исследовать все хранящиеся в устройстве данные, включая сертификат, секретные ключи и многое другое. Разумеется, несмотря на всю увлекательность, это было не особо важно — пока что я мог «хакнуть» только уже купленное мной устройство.

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

Чтобы всерьез протестировать удаленные векторы атаки, мне нужно было досконально разобраться, как устройство аутентифицируется на API-сервере и как именно происходит привязка пользователя к конкретному устройству.

Например, команда connection_manager wifi пыталась установить соединение с API-серверами, при этом выводя подробные отладочные логи. 

shell>connection_manager wifi  [info][CM] Connection manager request, action = 3, wifi sync flags = 0xffffffff [VAS] t:15 [info][CM] Start with cnlib action = 3 [VAS] t:15 [CNLIB] Recovered LastCnx from DbLib [AM] Defuse id 4 [TIME] Current time (timestamp) 0 , 8h 0min 0sec [TIME] Waking up in 16h 90min 60sec [TIME] Add random time 0 [AM] Set id 3 at 63060 [AM] Set id 1 at 600 [CNLIB] Try to connect via wifi (1) [DBLIB][ERASEBANK] Bank 1 [info][DBLIB][SUBSADD] 14 0 [info][CM] Initializ[VAS] t:15 e Wifi [WIFIM] Request [WIFIM] init [VAS] t:15 wifi_chip_enable bcm43438_request == Set dcdc_sync == bcm43438_request: pwron module [WIFIMFW] current_fw == FW_2 1 version 1 size 80 [WIFIMFW] wifi_crc: 0 [WIFIMFW] Take current bank [WIFIMFW] Firmware block 1a8000 : OK [WIFIMFW] Wifi Offset 21a370, lenght 58d1d [WWD] HT Clock available in 31 ms [WWD] mac: a4:7e:fa:19:2c:f6 supported channels: 13 [WIFIM] init OK [info][CM] Wifi initialized [WIFIM] join_configured_ap [VAS] t:15 [WIFIM] ssid = ... [WIFIM] key  = ... [WIFIM] WPA key already saved [WWD] join: ssid=<...>, sec=0x00400004, key=<...> [WDM] wwdm_join_event_handler: state=1, wifim_err=9, stopped=0 [WDM] wwdm_join_event_handler: state=2, wifim_err=9, stopped=0 [WDM] wwdm_join_event_handler: state=2, wifim_err=0, stopped=1 [WDM] wwdm_join_event_handler: stopped [WWD] join: wiced_res=0, wifim_res=0 [info][WIFIM] join: attempt #0, rc=0 [info][WIFIM] join: SSID <...> join rc=0 after 1 attempts [VAS] t:15 [VAS] t:15 [info][WIFIM] join: RSSI=-64 [VAS] t:15 [WIFIM] connect: use static ip [WIFIM] Interface UP (Status : 0xf) [WIFIM] netif_up: use DHCP [WIFIM] Interface UP (Status : 0xf) [WIFIM] netif_up: [WIFIM] IP=192.168.0.9 [WIFIM] Mask=255.255.255.0 [WIFIM] Gw=192.168.0.1 [WIFIM] DNS[0]=192.168.0.1 [WIFIM] DNS[1]=0.0.0.0 [WIFIM] connect_cfg_ap: success [info][CM] Joined configured AP successfully [VAS] t:15 [info][CM] Store DbLib... [VAS] t:15 [DBLIB][ERASEBANK] Bank 2 [info][CM] Store DbLib done [HTT[VAS] t:15  S_CLIENT] Init [HTTPS_CLIENT] Init [info][CM] Wslib init successful, carry on [VAS] t:15  [WS] WsLib_StartSession  [WS] __WsLib_Once [WS] Https_client browsing <https://wbs06-ws.withings.net/once?appliver=1181&appname=WBS06&apppfm=device> [HTTPS_CLIENT] New connection or Adress/Security Changed [HTTPS_CLIENT] Close [HTTPS_CLIENT] Init [HTTPS_CLIENT] Handshake started {"status":0,"body":{"user":[{"userid":...,"screens":[{"id":66,"deactivable_status":6,"src":1,"embid":11,"rk":1}]},...]}} > [DBLIB][ERASEBANK] Bank 1 [WS] WSLIB_OK [WS] Https_client browsing <https://wbs06-ws.withings.net/v2/summary?appliver=1181&appname=WBS06&apppfm=device> [HTTPS_CLIENT] Socket already opened [WS] Params <action=getforscale&sessionid=...> {"status":0,"body":[{...}]} > [WS] WSLIB_OK [USLIB] FLUSH STORED MEASURE [USLIB] 0 measure(s) flushed [WS] Https_client browsing <https://wbs06-ws.withings.net/v2/weather?appliver=1181&appname=WBS06&apppfm=device> [HTTPS_CLIENT] Socket already opened [WS] Params <action=getforecast&sessionid=...short=1&enrich=t> ...

Я также попытался заменить хранящиеся в устройстве mTLS-сертификаты, чтобы упростить перехват трафика по Wi-Fi. Однако сервер отклонил подписанные мной сертификаты, как и положено.

Тем не менее, благодаря отладочным логам и различным данным о состоянии из памяти, мне удалось восстановить основную схему аутентификации:

  1. Получив учетные данные Wi-Fi по Bluetooth от мобильного приложения, устройство может самостоятельно подключиться к API-серверу.

  2. Устройство предъявляет свой сертификат и устанавливает соединение с API-сервером при помощи mTLS.

  3. API-сервер возвращает одноразовый код (nonce).

  4. Устройство подписывает nonce локальным приватным ключом и отправляет его серверу.

  5. Сервер проверяет подпись, и если всё в порядке — возвращает токен сессии устройства.

  6. Теперь устройство может обращаться к API-серверу, используя этот токен в качестве аутентификации.

Особый интерес представляет схема привязки пользователя к устройству. Ее можно реализовать двумя способами.

Первый способ (инициируется мобильным приложением пользователя):

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

  2. Оно получает токен сессии устройства по Bluetooth.

  3. Приложение выполняет аутентификацию перед API-сервером с Session-Id: USER_SESSION_TOKEN и отправляет полезную нагрузку запроса userid=USER_ID& sessionidtoken=DEVICE_SESSION_TOKEN. userid — это простое инкрементируемое число.

  4. API-сервер подтверждает валидность Session-Id и sessionidtoken, а затем привязывает userid к ID устройства, которому принадлежит DEVICE_SESSION_TOKEN.

Второй способ (инициируется самим устройством):

  1. Устройство уже имеет свой токен сессии.

  2. Оно получает токен сессии пользователя от приложения по Bluetooth.

  3. Устройство выполняет аутентификацию перед API-сервером с Session-Id: DEVICE_SESSION_TOKEN и отправляет полезную нагрузку запроса deviceid=DEVICE_ID& sessionidtoken=USER_SESSION_TOKEN. deviceid — это простое инкрементируемое число.

  4. API-сервер подтверждает валидность Session-Id и sessionidtoken, а затем привязывает deviceid к ID пользователя, которому принадлежит USER_SESSION_TOKEN.

Оба способа должным образом защищены; попытки заменить userid в первом сценарии или deviceid во втором приводили к сбою, потому что они не соответствовали токену сессии Session-Id.

Однако в логике приложения всё же обнаружилась критическая уязвимость. Возможно, лучше всего ее объяснит приближенная схема валидации на сервере:

if (req.session.isValid) {   if (!validateSession(req.body.sessionidtoken)) {     return error   }    const targetSession = fetchSession(req.body.sessionidtoken)   // инициированный приложением пользователя процесс   if (targetSession.type === 'device') {     associate(req.body.userid, targetSession.id)    // инициированный устройством процесс   } else if (targetSession.type === 'user') {     associate(req.body.deviceid, targetSession.id)   } }

В чем здесь ошибка? Представьте, что атакующий отправляет запрос, где и Session-Id, и sessionidtoken — это его пользовательский токен, но при этом в deviceid подставляется ID устройства, которым он не владеет.

Логика на сервере сочтет, что этот процесс был инициирован устройством — и не потребует подтверждения подлинности deviceid, то есть не проверит, что устройство действительно принадлежит владельцу сессии. Это позволяет перепривязать чужое устройство к своей учетной записи.

Безопасный код должен включать дополнительную проверку:

if (req.session.isValid) {   if (!validateSession(req.body.sessionidtoken)) {     return error   }    const targetSession = fetchSession(req.body.sessionidtoken)    // инициированный приложением пользователя процесс, валидирующий,       что привязываемый пользователь соответствует заголовку токена сессии   if (req.body.userid === req.session.id && targetSession.type === 'device') {     associate(req.body.userid, targetSession.id)    // инициированный устройством процесс, валидирующий,       что привязываемое устройство соответствует заголовку токена сессии   } else if (req.body.deviceid === req.session.id && targetSession.type === 'user') {     associate(req.body.deviceid, targetSession.id)   } }

Благодаря этой ошибке, зная доступные ID устройств, злоумышленник, по моим оценкам, мог бы привязать к своему аккаунту более миллиона девайсов.


Вендор отреагировал на уязвимость молниеносно. 29 декабря 2024 года я отправил вендору отчет, а уже 3 января компания ответила, что уязвимость исправлена.

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

Чему нас учит этот кейс? Когда речь идет о взломе «железа», масштабировать атаку за пределы одного устройства до полностью удаленного эксплойта бывает сложно. Привязка пользователя к устройству — критический участок, который может обходить аппаратные и сетевые средства защиты, ведь уязвимость находится на стороне API, а не на стороне устройства. Это особенно актуально для потребительских IoT-устройств, в которых ставка делается на простоту настройки и удобство. 

PURP — телеграм-канал, где кибербезопасность раскрывается с обеих сторон баррикад

t.me/purp_sec — инсайды и инсайты из мира этичного хакинга и бизнес-ориентированной защиты от специалистов Бастиона


ссылка на оригинал статьи https://habr.com/ru/articles/910492/