Как стать автором
Обновить
250.22
Бастион
Проводим пентесты, проектируем защищенные системы

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

Уровень сложностиСредний
Время на прочтение11 мин
Количество просмотров4.4K
Автор оригинала: Spaceraccoon

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

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

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

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

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

Теги:
Хабы:
+30
Комментарии9

Публикации

Информация

Сайт
bastion-tech.ru
Дата регистрации
Дата основания
2014
Численность
201–500 человек
Местоположение
Россия
Представитель
Игорь Santry