[Перевод] Очиститель воздуха скачивал корневой сертификат по HTTP: как я выгнал из него облако Coway

Очиститель Coway Airmega IconSОчиститель Coway Airmega IconS

Очиститель вполне приличный, а вот приложение к нему мне не нравится.

На дворе 2017 год. Западное побережье США год за годом накрывают лесные пожары. Днём небо выглядит вот так:

Город под оранжевым небомГород под оранжевым небом

Мой дом, как и многие дома в районе залива Сан-Франциско, не утеплён и толком не герметичен. Чтобы не задыхаться от копоти, многие тогда стали покупать очистители воздуха вроде этого:

Реклама Coway: чёрный и белый AirmegaРеклама Coway: чёрный и белый Airmega

Почтенный Coway Airmega

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

Со временем очиститель стал у нас дома незаменимым: жена начала включать его на ночь на полную мощность как генератор белого шума. Я привык, и теперь сам без него не засыпаю.

Кнопка «Спокойной ночи»

Освещение у нас умное и подключено к Apple Home. Перед сном я всегда нажимаю кнопку «Спокойной ночи», и весь свет гаснет. Мне очень хотелось привязать к ней и вентилятор: нажал кнопку, свет выключился, вентилятор раскрутился до той громкости белого шума, которую любит жена.

Кнопка сцены «Спокойной ночи» в Apple HomeКнопка сцены «Спокойной ночи» в Apple Home

У старого Airmega не было Wi-Fi, так что простого способа подключить его к Apple Home не существовало. Когда он в конце концов сломался, я купил Coway Airmega IconS (сейчас его уже сняли с производства). Это всё тот же вентилятор с HEPA-фильтром, только заметно дороже, стилизованный под дизайнерскую мебель середины прошлого века и, главное, управляемый по Wi-Fi (более или менее).

Бежевый очиститель на деревянных ножках и телефон с приложением CowayБежевый очиститель на деревянных ножках и телефон с приложением Coway

Coway Airmega IconS

Готовая интеграция для Home Assistant

Приложение Coway для iPhone — классика IoT. Загружается целую вечность и почти при каждом запуске просит сменить пароль. К счастью, один смельчак разобрал это приложение и написал интеграцию для Home Assistant. Интеграция, по сути, притворяется телефонным приложением: опрашивает облако Coway, получает оттуда состояние и через него же отправляет команды вентилятору. Home Assistant пробрасывает всё это в Apple Home, и готово.

Облако Coway IoCare (серверы Coway где-то в интернете) ↑ команды ↓ состояние ↑ состояние ↓ командыМой дом: Home Assistant (интеграция IoCare) Очиститель (Airmega IconS, Wi-Fi) ↓Apple Home (та самая кнопка «Спокойной ночи»)

Зачем уходить из облака

Минус такого подхода в том, что ты по-прежнему зависишь от милости облачного сервиса Coway (точнее, от её отсутствия). Похоже, как минимум раз или два в год что-нибудь да ломается.

Вот что ломалось на моей памяти:

  • Coway не понравилось, что интеграция долбит их серверы, и они её заблокировали. После долгого спора с сообществом всё-таки согласились её разрешить.

  • Coway переписали приложение и переименовали его из IoCare в IoCare+, так что интеграцию тоже пришлось переписывать.

  • Coway слегка поменяли вёрстку HTML, который интеграция парсит, и она сломалась.

  • Серверы Coway время от времени лежали, и управлять устройством удалённо было нельзя.

  • Однажды Coway заблокировали мой аккаунт за старую версию интеграции. Несколько недель я не мог управлять вентилятором удалённо, пока поддержка снимала бан.

К тому же вряд ли Coway будет поддерживать этот сервис вечно. Подозреваю, что рано или поздно, как и все производители IoT, они либо сделают его платным, либо закроют. Моя модель уже снята с производства (хотя другие модели с Wi-Fi продаются, так что кто знает).

Лазейка

Признаюсь, я ленился. Годами откладывал попытку избавиться от облака Coway: если устройство по-настоящему закрыто, это будет тяжело. Скорее всего, при включении вентилятор подключается к coway.com (или куда там ещё) и проверяет, что SSL-сертификат сервера валиден. Поддельный сертификат, который пройдёт такую проверку, я, скорее всего, сделать не смогу, и к моему серверу вентилятор просто не подключится.

Кроме того, поднимать стенд для атаки «человек посередине» (MitM) — та ещё морока. Вентилятор должен подключиться к изолированной сети Wi-Fi, весь трафик которой идёт в интернет через мой сервер. Для этого нужна куча правил маршрутизации и файрвола, перенаправляющих трафик. Ничего невозможного (я такое уже делал), но возни много, и настроить всё правильно всегда непросто. А когда стенд готов, это только первый шаг.

Тем не менее за это время изменились две важные вещи.

Во-первых, недавно (в октябре 2026 года) случился инцидент безопасности: выяснилось, что eMudhra, один из доверенных удостоверяющих центров интернета, использовал для части своих закрытых ключей очень слабый параметр.

Ian Carroll (@iangcarroll), 20 октября 2026: «Ближайшие дни или недели корневой УЦ есть у любого — спасибо eMudhra. Самое время поразвлекаться с MiTM несговорчивых устройств!»

Таблица промежуточных УЦ eMudhra и значений d их закрытых ключей: крошечные числа вроде 156, 230, 115, 56, 155 и 4Таблица промежуточных УЦ eMudhra и значений d их закрытых ключей: крошечные числа вроде 156, 230, 115, 56, 155 и 4

Так что если устройство не обновлялось, И в нём зашиты корневые сертификаты, И оно не привязывает (pinning) сертификат облачного сервиса, то… возможно, это и есть лазейка. Многовато «если», согласен, но если в устройстве нет грубых ошибок безопасности, это может стать нужной точкой опоры.

Во-вторых, просто появился ИИ. LLM может настроить всё, что нужно для MitM, и можно сразу переходить к разбору того, как работает вентилятор. Причём сделать это без присмотра и за считаные минуты.

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

Наш человек посередине

Сначала я собрал такую схему:

Очиститель (Airmega IconS) │ Wi-FiRaspberry Pi — точка доступа Wi-Fi (Pi в режиме AP, своя изолированная сеть), │ весь трафик перенаправлен в mitmproxy, который расшифровывает и пишет его в логМой домашний роутер (обычная домашняя сеть) │Облако Coway IoCare (в интернете)

Это не сработало: у Raspberry Pi маленькая антенна дорожкой на плате, и сигнал у неё слабый. Пока Pi стоял в одной комнате с роутером, до вентилятора он не добивал. Можно было, наверное, перенести вентилятор в комнату с роутером, но зачем, если задачу можно решить запутанным техническим способом?

Ноутбук, переходник USB-C на Ethernet, Raspberry Pi и USB-C аккумулятор, соединённые друг с другомНоутбук, переходник USB-C на Ethernet, Raspberry Pi и USB-C аккумулятор, соединённые друг с другом

Raspberry Pi своим радиомодулем Wi-Fi поднимает точку доступа, к которой подключается вентилятор. Одновременно с этим подключаться по Wi-Fi к домашней сети ему непросто. Поэтому я перенёс Pi в комнату с вентилятором и воткнул его Ethernet-порт в ноутбук, а ноутбук уже по Wi-Fi соединяет его с домашней сетью:

Очиститель (Airmega IconS) │ Wi-Fi, в той же комнатеRaspberry Pi — точка доступа Wi-Fi (изолированная сеть), │ весь трафик перенаправлен в mitmproxy (расшифровка и лог) │ EthernetМой ноутбук (общий доступ к интернету: мост Wi-Fi ↔ Ethernet) │ Wi-FiМой домашний роутер (обычная домашняя сеть) │Облако Coway IoCare (в интернете)

Дальше нужно настроить Pi.

Чат с Claude: просьба настроить Pi как точку доступа с mitmproxy заблокирована с пометкой Category: cyberЧат с Claude: просьба настроить Pi как точку доступа с mitmproxy заблокирована с пометкой Category: cyber

Не берусь судить, честно ли такое блокировать, но Claude выполнять просьбу отказался, даже «для тестирования».

К счастью, у ChatGPT таких угрызений совести нет.

ChatGPT спокойно относится к категории CyberChatGPT спокойно относится к категории Cyber

После нескольких итераций получились вот такие правила nftables:

scottjg@pi:~ $ sudo nft -a list rulesettable inet mitmproxy_ap { # handle 8set client_interfaces { # handle 4type ifnameelements = { "wlan0" }}chain prerouting { # handle 1type nat hook prerouting priority dstnat; policy accept;meta nfproto ipv4 iifname @client_interfaces tcp dport { 80, 443 } counter packets 2 bytes 120 redirect to :8080 # handle 6}chain input { # handle 2type filter hook input priority filter - 10; policy accept;iifname != "lo" iifname != @client_interfaces tcp dport 8080 counter packets 0 bytes 0 reject with tcp reset # handle 7iifname @client_interfaces meta nfproto ipv6 counter packets 9 bytes 2397 drop # handle 8iifname @client_interfaces ct state established,related accept # handle 9iifname @client_interfaces udp dport { 53, 67 } accept # handle 11iifname @client_interfaces tcp dport { 53, 8080 } accept # handle 13iifname @client_interfaces ip protocol icmp accept # handle 14iifname @client_interfaces counter packets 23 bytes 4697 reject with icmpx admin-prohibited # handle 15}chain forward { # handle 3type filter hook forward priority filter - 10; policy accept;iifname @client_interfaces meta nfproto ipv6 counter packets 0 bytes 0 drop # handle 16oifname @client_interfaces meta nfproto ipv6 counter packets 0 bytes 0 drop # handle 17iifname @client_interfaces udp dport 443 counter packets 1 bytes 57 reject # handle 18iifname @client_interfaces oifname != "eth0" counter packets 0 bytes 0 reject with icmpx admin-prohibited # handle 19}}table ip nm-shared-wlan0 { # handle 12chain nat_postrouting { # handle 1type nat hook postrouting priority srcnat; policy accept;ip saddr 10.42.0.0/24 ip daddr != 10.42.0.0/24 masquerade # handle 2}chain filter_forward { # handle 3type filter hook forward priority filter; policy accept;ip daddr 10.42.0.0/24 oifname "wlan0" ct state { established, related } accept # handle 5ip saddr 10.42.0.0/24 iifname "wlan0" accept # handle 6iifname "wlan0" oifname "wlan0" accept # handle 7iifname "wlan0" reject # handle 8oifname "wlan0" reject # handle 9}}

Если коротко, правило masquerade выпускает трафик изолированной сети Wi-Fi в интернет через домашнее подключение, а правило redirect отправляет HTTP и HTTPS в запущенный mitmproxy.

Там же, на Pi, mitmproxy работает со своим веб-интерфейсом mitmweb примерно с такой командной строкой:

mitmweb  --mode transparent --showhost  --listen-host 0.0.0.0 --listen-port 8080  --web-host 127.0.0.1 --web-port 8081  --set web_open_browser=false  --emudhra-old

Да, в mitmproxy прямо из коробки есть опция (--emudhra-old), которая автоматически перехватывает трафик с корневым сертификатом eMudhra.

Подглядываем за вентилятором

Дальше я через приложение на iPhone стал подключать вентилятор к нашей сети Pi-MITM-Test. На середине приложение сообщило, что настройка не удалась. Заглянем в веб-интерфейс mitmproxy:

mitmweb с единственным запросом: обычный HTTP GET за AmazonRootCA1.pemmitmweb с единственным запросом: обычный HTTP GET за AmazonRootCA1.pem

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

И запрос этот очень подозрительный. Похоже, вентилятор скачивает корневой сертификат удостоверяющего центра Amazon по обычному HTTP. Интерфейс показывает только соединения, которые дошли до настоящего HTTP-запроса. Если, например, вентилятор не принимает сертификат eMudhra, которым мы выдаём себя за настоящие серверы, его HTTPS-соединения обрываются раньше и в списке не появляются.

Гипотеза: устройство скачивает этот публичный сертификат по HTTP, чтобы было чем проверять последующие (пока невидимые) HTTPS-соединения. Встроенных корневых сертификатов у него нет вообще.

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

"""Serve this lab's public RSA CA only to the authorized Coway test device."""from pathlib import Pathfrom mitmproxy import ctx, httpdef response(flow: http.HTTPFlow) -> None: if ( flow.client_conn.peername[0] == "10.42.0.93" # device IP and flow.request.scheme == "http" and flow.request.method == "GET" and flow.request.pretty_host == "rootca-fs.iocareus.com.s3.ap-northeast-2.amazonaws.com" and flow.request.path.split("?", 1)[0] == "/AmazonRootCA1.pem" and flow.response is not None and flow.response.status_code == 200 ): certificate = Path("/var/lib/mitmproxy-ap/mitmproxy-ca-cert.pem").read_bytes() flow.response = http.Response.make( 200, certificate, { "Content-Type": "application/x-pem-file", "Cache-Control": "no-store", }, ) flow.metadata["coway_test_ca_served"] = True ctx.log.info("Served lab RSA CA to the Coway's HTTP CA bootstrap request")

И действительно, теперь устройство подключается до конца, и появляются новые запросы:

mitmweb показывает HTTPS-запросы к серверу сертификатов Coway, включая скачивание сертификата устройства и закрытого ключаmitmweb показывает HTTPS-запросы к серверу сертификатов Coway, включая скачивание сертификата устройства и закрытого ключа

Гипотеза подтвердилась.

Мы можем перехватывать трафик между вентилятором и сервером, а значит, скорее всего, сможем написать свой сервер вместо облака Coway IoCare. Заодно выяснилось, что трюк с eMudhra вообще не нужен. Вентилятор скачивает корневой сертификат по обычному HTTP. Перехватить его трафик можно было всегда.

Не хватает одного: ни один из этих запросов не связан ни со скоростью вентилятора, ни с чем-либо ещё, что можно сделать в приложении. Но это легко выяснить. Если запустить на Pi tcpdump и пощёлкать кнопками в приложении, видно зашифрованный трафик на порту 8883 — стандартном порту MQTT поверх TLS. MQTT — протокол шины сообщений, его часто используют в IoT-устройствах.

Вернёмся к POST из списка запросов выше. Вот что вернул этот вызов API:

{ "header": { "success": true, "result": "S1000", "message": "SUCCESS" }, "payload": { "macAddr": "", "certToken": "", "host": "a29vouc2qruduj-ats.iot.ap-northeast-2.amazonaws.com", "port": 8883, "serial": "", "certSvrUrl": "https://cert.iotp.coway.com/iot/v1/cert/r0", "caUrl": "http://rootca-fs.iocareus.com.s3.ap-northeast-2.amazonaws.com/AmazonRootCA1.pem", "privateUrl": "https://icert-fs.iocareus.com//55d66923e9-private.pem.key", "certUrl": "https://icert-fs.iocareus.com//55d66923e9-certificate.pem.crt", "svcCode": "002AIR007", "countryCd": "US" }}

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

Пока что можно написать ещё один плагин для mitmproxy, который подставляет в этот ответ наш собственный ключ, и через него проксировать MQTT-трафик. Код плагина получился длинноватым, поэтому я его сюда не включил, но ИИ генерирует его довольно быстро. MQTT mitmproxy не декодирует, но сырой поток показать может:

mitmweb показывает сырой поток MQTT между вентилятором и AWS IoT, видны имена топиков вроде IoCare/MControlmitmweb показывает сырой поток MQTT между вентилятором и AWS IoT, видны имена топиков вроде IoCare/MControl

Данные выглядят как смесь бинарной и текстовой абракадабры, но это стандартный поток протокола MQTT. Когда я меняю скорость в приложении, видно, как сервер Coway присылает устройству новое сообщение. Обычно я бы загрузил это в анализатор трафика и составил таблицу: какая абракадабра приходит на каждую кнопку в приложении. Но теперь…

Моё сообщение ИИ: список всех кнопок, которые я нажимал в приложении, по порядку (режим, скорость, таймер, чувствительность, подсветка, цикл префильтра), с просьбой сопоставить их с командами MQTTМоё сообщение ИИ: список всех кнопок, которые я нажимал в приложении, по порядку (режим, скорость, таймер, чувствительность, подсветка, цикл префильтра), с просьбой сопоставить их с командами MQTT

…и через несколько минут:

Ответ ИИ с заметками о расшифрованных командах: таймер в минутах, цикл фильтра — недели, умноженные на 56, сопоставления сохранены в coway-command-map.jsonОтвет ИИ с заметками о расшифрованных командах: таймер в минутах, цикл фильтра — недели, умноженные на 56, сопоставления сохранены в coway-command-map.json

Этого уже достаточно, чтобы собрать собственный веб-сервер, который при настройке выдаёт вентилятору сертификаты и конфигурацию, и собственный MQTT-сервер для отправки команд. В обычной работе облако Coway полностью выпадает из схемы, но для настройки устройства мы всё ещё пользуемся приложением Coway. И нам всё ещё нужен довольно надоедливый MitM-стенд, чтобы вентилятор подключался к нам, а не к Coway.

Настройка по Bluetooth

При настройке через приложение Coway ищет устройство по Bluetooth. Я надеялся найти способ настраивать устройство самому, без приложения. А если повезёт, то и наткнуться по пути на что-нибудь ещё полезное.

Реверсом Bluetooth-устройств я уже занимался. Раньше моя стратегия была такой: взять Android-версию приложения, декомпилировать её и просто корпеть над кодом. Это не всегда так просто, как звучит. Разработчики часто обфусцируют приложения, и декомпилированный код читать труднее. К тому же часть байт-кода не декомпилируется обратно в исходник чисто, и приходится читать сырой байт-код.

И всё же, на мой взгляд, обычно это проще, чем возиться с приложениями для iPhone. Android-приложения обычно пишут на Java (или Kotlin), а Java-приложения реверсить проще, чем то, что скомпилировано из Swift или Objective-C.

На этот раз я попробовал новый подход: встроенные отладочные логи Bluetooth в Android.

Фото меню разработчика на Android-телефоне с включённым журналом HCI BluetoothФото меню разработчика на Android-телефоне с включённым журналом HCI Bluetooth

Потом я подключил телефон по USB и попросил ИИ управлять телефоном через adb, изучить логи Bluetooth и восстановить протокол. С этой задачей ИИ возился довольно долго. В итоге он установил, что сообщение с настройками Wi-Fi зашифровано. Раз всё равно придётся выяснять алгоритм шифрования, без декомпилированного приложения, видимо, не обойтись. И точно: следующим шагом он стянул приложение Coway с телефона, декомпилировал его и начал сопоставлять исходный код с логами Bluetooth.

В результате выясняется, что настройка устроена так:

  • Прочитать по Bluetooth идентификационные данные вентилятора из характеристики FFA0 («характеристика» — это конечная точка Bluetooth, из которой можно читать или в которую можно писать). Там есть базовые сведения об устройстве, включая его MAC-адрес.

  • Записать в характеристику FFA2 ключ шифрования (20 случайных строчных латинских букв):

"P001" + protobuf { command: 1, key: Base64(random_letters)}
  • Составить AES-ключ: ключ из шага 2, к которому приписаны 12 символов MAC-адреса в верхнем регистре ASCII.

  • Собрать конфигурацию, которая выглядит так:

{ "cmd": 2, "ssid": "Example Wi-Fi", "pwd": "example-wifi-password", "bssid": "AABBCCDDEEFF", "type": 1, "macAddr": "112233445566", "caUrl": "http://192.0.2.1:8080/ca.pem", "certSvrUrl": "https://192.0.2.1:9443/iot/v1/cert/r0", "certToken": "example-enrollment-token", "serial": "EXAMPLE000000000001"}

Её заворачивают в protobuf, дополняют по PKCS#7 и шифруют AES-ключом из шага 3. Затем добавляют спереди строку «P002» и тоже отправляют в FFA2.

Само собой, восстанавливать всё это от начала до конца по декомпилированному Android-приложению вручную очень долго. Это как минимум часы работы (скорее, дни). Тут ИИ здорово выручает: у него ушло около 20 минут.

Приятно и то, что все данные для расшифровки этой полезной нагрузки есть в логах Bluetooth, снятых с телефона, так что правильность нашей реализации можно проверить только по ним. Дальше ИИ быстро переписал весь процесс настройки в небольшой скрипт на Python.

Я довольно внимательно следил за ходом работы. Полную полезную нагрузку ИИ мне так и не показал, но я видел её в промежуточном выводе скриптов, которые он писал. И тут я кое-что заметил. Два URL из нашего перехвата задаются прямо в этой нагрузке (caUrl и certSvrUrl). Первый говорит вентилятору, откуда скачивать корневой сертификат по обычному HTTP, второй — куда отправлять POST по HTTPS за конфигурацией MQTT.

Раз эту нагрузку теперь отправляем мы, эти URL можно направить куда угодно. А самое главное, MitM больше не нужен. Достаточно запустить свой сервер и при настройке вентилятора указать его адреса.

Интеграция с Home Assistant

На основе всего этого я написал собственную интеграцию для Home Assistant. У моего сервера Home Assistant нет Bluetooth, и стоит он далеко от вентилятора, поэтому настройку я сделал через Web Bluetooth. Так можно поднести ноутбук к вентилятору и настроить его на прямую работу с Home Assistant.

Страница настройки coway-local в Chrome, в окне выбора Bluetooth вентилятор отмечен как сопряжённыйСтраница настройки coway-local в Chrome, в окне выбора Bluetooth вентилятор отмечен как сопряжённый

После этого устройство общается с Home Assistant напрямую по Wi-Fi.

Управление очистителем в Home Assistant: вентилятор на 67%, кнопки скорости High, Medium, Low и OffУправление очистителем в Home Assistant: вентилятор на 67%, кнопки скорости High, Medium, Low и Off

А главное, Home Assistant пробрасывает его в мой Apple Home.

Раздел «Спальня» в Apple Home: плитка очистителя рядом со светомРаздел «Спальня» в Apple Home: плитка очистителя рядом со светомУправление очистителем в Apple Home: высокий синий ползунок на ручных 67%Управление очистителем в Apple Home: высокий синий ползунок на ручных 67%

Вот и вся работа за день. Теперь я наконец могу нажать «Спокойной ночи».

Источник: habr.com

0 0 голоса
Рейтинг новости
1
0
Подписаться
Уведомить о
0 комментариев