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

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

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

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

Находясь в отпуске, я заметил на экране весов в спортзале отеля любопытную иконку. Это был значок 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 — инсайды и инсайты из мира этичного хакинга и бизнес-ориентированной защиты от специалистов Бастиона

Комментарии (11)


  1. mmMike
    20.05.2025 09:55

    Ужасно корявый первод.
    На степень невычитанности "и наплевать и забыть" от переводчика, намекает перевод примера строки из прошивки.
    Читать лучше оригинал.

    Хотя и он несколько пафосный. Громкий заголовок.. Но по сути нашел уязвимость (типичный "и так сойдет от разработчиков") на хосте с регистарцией весов и выдернул "серетный" ключ из прошивки, а не внедрил трояна в прошивку весов.


  1. pnmv
    20.05.2025 09:55

    умные весы, умные утюги... мир сошел с ума.

    (мне, пожалуйста, двадцать пар умных трусов)


    1. LeVoN_CCCP
      20.05.2025 09:55

      мне, пожалуйста, двадцать пар умных трусов

      Вам тоже их рекламу во сне показали?


      1. pnmv
        20.05.2025 09:55

        хочу понять, чем от обычных отличаются.


        1. Wesha
          20.05.2025 09:55

          Анализируют выхлопные газы!


    1. JediPhilosopher
      20.05.2025 09:55

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

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


      1. pnmv
        20.05.2025 09:55

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


      1. Yuriy_krd
        20.05.2025 09:55

        Со стиралками дело обстоит гораздо хуже, чем с чайниками) Некоторые производители стали делать так, что некоторые режимы доступны ТОЛЬКО в мобильном приложении. Типа, ради минимализма во внешнем виде основные можете включать и так, а если хотите какой-то особенный режим - велком ту мобилка апп.


    1. 3263927
      20.05.2025 09:55

      главное чтобы весы не были умнее их хозяина


  1. RoasterToaster
    20.05.2025 09:55

    Вот кому то новый год здорово подпортили


  1. Furriest
    20.05.2025 09:55

    Ок, ну сломал чувак весы. Миллионы весов привязал к своему аккаунту. Ущерб-то от этого какой?
    Он теперь знает вес миллионов человек на планете? О ужас, человечество этого не переживёт!