Предисловие
История берет свое начало в мае, когда в один из непогожих питерских дней, местная котельная решила отключить отопление. За окном было +10, влажность зашкаливала, ветер дул, а тепленькое солнышко не светило в окна от слова совсем (чертова северная сторона). В квартире стало ощутимо холодно. Кот слезал с теплых колен только для опустошения миски. Мы же кутались в флиски и пледы. Через два дня такой жизни стало понятно - к черту все, нужен обогреватель! Требования были довольно просты - цена, определенная мощность, возможность эту самую мощность регулировать и какой-либо интерфейс (wi-fi\BT\ZigBee\485). Последнее хотелось больше для баловства и неведомого "а вдруг потребуется!". Оставлю за рамками статьи муки выбора, количество обогревателей на полках магазинов в конце весны и прочие приколы наших доставщиков. В итоге, я стал обладателем конвектора под брендом… впрочем, если не показывать шильдик, то можно найти картинки аж трех производителей "клепающих" одинаковые модели. А если погуглить чуть поглубже, то окажется что в РФ франшиза на выпуск конвекторов под этими брендам принадлежит одной единственной конторе. И все встает на свои места - конвектор один, а шильдик лишь влияет на цену. Впрочем, мне то какая разница - грело бы, да управлялось.
Что-то я отвлекся. Итак, конвектор модульный:
нагревательный блок
блок с инвертором для управления
Wi-fi свисток
Cобрано, запущено, кот согрет, самое время посмотреть, что там с интерфейсом. Приложение для мобилки подключилось к конвектору, залило настройки wi-fi сети и радостно предложило управлять обогревателем через интернет. И в принципе на этом можно было бы закончить, но в процессе эксплуатации появилось желание - время от времени использовать конвектор для просушки одного из помещений. Датчик влажности есть, к Home Assistant подключен, дело за малым, завести туда же конвектор. Тут меня ждало полнейшее разочарование - никакого API, никаких интеграций, вообще ни_че_го. Лан, инженером же работаю, не в первой городить программно-аппаратные решения.
Часть 0. Исходные данные
Первое, что делает любой инженер - берет отвертку и смотрит внутрь. А внутри USB-модуля оказалась железка HF-LPT220. Заботливый гугл находит:
Статью (часть 1 и часть 2) пользователя @avstepanov Краткое содержание: Кондиционер, модуль такой же, попробовали wi-fi, API нет, решили проблему через управление по IR.
Сайт производителя
Краткие характеристики модуля:
Support IEEE802.11b/g/n Wireless Standards
Based on High-Flying Cost Effective Wi-Fi SOC: MC300 chipset
Support UART/GPIO Data Communication Interface
Support Work As STA/AP Mode
Support Smart LinkFunction(APP program provide)
Support Wireless and Remote Firmware Upgrade Function
Support WPS Function(Reserved)
Support Internal/ExternalPinAntenna Option
Single +3.3V Power Supply
Smallest Size: 22mm x 13.5mm x3mm , SMT17 Package
FCC/CE Certificated
Закатываем рукава достаем чемоданчик с инструментарием:
Инструменты
Железки:
Mikrotik для перехвата пакетов
Логический анализатор (клон saleae Logic 8) - для анализа интерфейса, потребуется в третьей части
Различные кабели (USB-AM - USB-AF)
USB-UART преобразователь. В моем случае - CP2102
ПО:
Wireshark
Android Studio
Putty
Часть 1. Wi-Fi!
Раз мобильное приложение общается с железкой через wi-fi, логично запустить сниффер и посмотреть что там бегает. Хорошо что микротик умеет "зеркалить" траффик в Wireshark (wiki mikrotik, инструкция попроще). Запускаем wireshark, включаем обогреватель и наблюдаем.
После отрабатывания DHCP-клиента, железка запрашивает IP адрес dongle.***.ru, где *** представительство в РФ (не вендор). Просто примем эту информацию к сведенью и сделаем вывод - в других международных регионах скорее всего будут свои фирмы-представители и, соответственно, адреса.
Происходит установка TCP-соединения и начинается обмен AT-командами
< - запрос от сервера, конвектору
> - ответ от конвектора, серверу
< AT+NDBGL=0,0
< AT+APPVER
> +ok
< AT+WSMAC
> =<ver>-20170810 . +ok=<MAC>
Данные команды повторяются несколько раз, как и ответы на них, видимо, происходит не очень быстрая обработка со стороны сервера.
Идем в документацию с сайта hi-flying и действительно находим:
AT+NDBGL - Enable\Disable UART information
AT+WSMAC - Set\Query Module MAC address parameters. Setting is valid after reset.
AT+VER - Query module software version information. AT+APPVER в документе отсутствует, т.к. модуль и прошивка датированы 2017 г. а вот документация на сайте 2016.
Далее, происходит переход на свой, проприетарный, протокол, состоящий из двух секций. Первая, предположительно, инициализация. Про нее особо сказать нечего, т.к. при каждом запуске прилетают и улетают одинаковые данные.
А вот вторая, заставляет задуматься.
Честно говоря, в эпоху пристального внимания к безопасности IoT, не надеялся увидеть что-либо дельное, однако… Серьезно? Как-то это не похоже на зашифрованный TLS\SSL\Ipsec - траффик.
Cходу можно сделать предположение - последний байт, это контрольная, однобайтовая сумма. Открываем калькулятор и складываем AA+C+A+1+1A+1+6+5+1 = E8. Ясно, понятно. Начинаем жать все кнопки в приложении и обнаруживаем некоторые закономерности, попутно пишем на LUA dissector для Wireshark. Не парсить, же байты каждый раз =)
Получаем занятную штуку, на примере: aa 0c 0a 01 1a 01 06 00 00 00 00 05 00 01 e8
Наименование | Размер (байт) | Примечание и возможные значения | Пример |
Заголовок | 2 | Байты неизменны из пакета в пакет, возможно, это внутренний ID устройства. С MAC не совпадают. | aa 0c |
Команда или событие | 1 | 09 - произошло изменение состояния - от конвектора | 0a (установить) |
Статус | 1 | 00 - отключен | 01 (включить) |
Температура | 1 | Поддерживаемая температура. Задается в приложении или на устройстве. | 1a (установить 26 гр.) |
Режим | 1 | Режимы эксплуатации: | 01 (установить Comfort) |
Мощность | 1 | 01 - 1 ур. - минимальная | 06 (установить режим Auto) |
Таймер | 2 | Таймер на отключение, в минутах, минуты в hex, | 00 00 (таймер выставить в 00:00) |
Статус таймера | 1 | 00 - оключен | 00 (отключить) |
Температура в помещении | 1 | Температура с датчика | 00 |
Неизвестно | 2 | У меня не используются. Варианты: | 05 00 |
Дисплей | 1 | 00 - отключен | 01 (дисплей включить) |
Контрольная сумма | 1 | Сумма всех предыдущих байт. Формально, остаток от деления на 255. | e8 |
Отдельно хочется выделить запрос текущей конфигурации от сервера:
Data | Примечание |
AA 03 08 10 04 C9 | Конвектор отвечает пакетом выше, с кодом команды 88 |
* На этапе инициализации используются дополнительные пакеты и команды, но оставим это за рамками статьи.
Фуф, с данными, вроде как понятно. Но будет ли устройство принимать команды от стороннего сервера? Адрес сервера забит в прошивку. Мы конечно можем, подменить ответ DNS-сервера на локальном маршрутизаторе, но сначала стоит попробовать что-нибудь попроще.
Посмотрим, какие порты открыты на сервере конвекторе. Linux\WSL > Bash > Netcatnc -vnz <IP конвектора> 1-65000 2>&1 | grep succeeded
Через довольно продолжительное время, обнаружится открытый, 8899 порт. ONVIF у конвектора? Whaaat?! Но нам то что? Попробуем отправить пакет на включение и выставление настроек: echo -e "\xAA\x0C\x0A\x01\x17\x01\x01\x00\x00\x00\x00\x00\x00\x00\xDA" |
nc <IP конвектора> 8899
И, внезапно, оно работает!
Есть одно, НО. При отправке запроса о текущей конфигурации (AA 03 08 10 04 C9), ответ отправляется не тому кто запросил, а на сервер с которым установлено соединение. Как итог, управление возможно только в слепую, никаких данных о включении, отключении, режиме и температуре получить невозможно.
Есть два способа изменить это поведение (сборка своей прошивки не в счет).
Первый. Так как в прошивке зашито DNS-имя, можно изменить IP-адрес в ответе DNS-сервера, так чтобы dongle.*****.ru ссылался на 192.168.0.* - т.е. прописать статическую DNS запись.
Плюс данного способа - все настройки делаются на маршрутизаторе\DNS-сервере в пару кликов.
Минус - очевидно, способ нерабочий, если ваш DHCP-сервер выдает клиентам публичные DNS-сервера (8.8.8.8, 1.1.1.1 и прочие "семейные DNS").
Второй способ приведен ниже, в части посвященной UART и касается изменении адреса сервера в модуле wi-fi.
Плюсы - гарантированная работа в ЛВС.
Минусы - жесткая привязка к конкретной ЛВС - это надо помнить.
Итак, запускаем (или пишем на коленке) на локальной машине простенький tcp-сервер. Отправляем железке AA 03 08 10 04 C9
в ответ получаем
aa 0c 88 01 15 01 01 00 00 00 18 00 70 00 00 00 6e
Теперь, все работает как надо.
Промежуточные результаты: Можно начинать писать свой шлюз\OPC-сервер\плагин для системы управления умным домом.
Часть 2. Android
А что если есть более простой способ рулить конвектором? Например, подключить систему управления умным домом напрямую к серверу и перекидываться JSON'ами. Реализовать такое управление, безусловно, проще.
Что ж, стоит посмотреть, как мобильное приложение общается с сервером. Запускаем wireshark, настраиваем mikrotik и смотрим. А посмотреть есть на что.
Приложение обращается к тому же самому IP - 82.209.**.** (правда я не увидел запроса к DNS, но возможно ОС взяла значение из кэша). А так же смотрим первый TCP Stream. Да у нас тут целый запрос-ответ.
Ответ (К сожалению в ответе пришлось бы блюрить огромное количество данных, по этому вставка текстом, некоторые заголовки вырезаны):
HTTP/1.1 200 OK
Server: nginx/1.12.2
Content-Type: application/json;
…
X-Powered-By: PHP/7.0.27
X-Powered-CMS: <название одной популярной CMS>
…
{
"result": {
"token": "<токен>",
"user": {
"name": ""
},
"enc_key": "<ключ>",
"server": {
"host": "dongle.<вендор>.ru",
"port": "10001"
},
…
}
Что имеем? В запросе используется голый http, передающий в открытом виде:
appcode - бренд. Напомню, производитель изготавливает оборудование под разными брендами.
login - номер мобильного телефона, который является логином в приложении
password - пароль. Не хэш. Именно пароль, указываемый при регистрации, и использующийся для входа в приложение.
Интересное в ответе:
На той стороне используется би_cms_.
Нам передают токен
Забегая вперед скажу, что нам передают ключ шифрования - enc_key
*Б - безопасность. Но давайте посмотрим что происходит дальше, а потом проанализируем все вместе.
Сморим, что же происходит дальше. А дальше происходит небольшой обмен командами. Cервер запрашивает присвоенный токен, приложение его отправляет и в ответ получает это:
IoZAWjxg/vHjEBEZvX1zTkrmbT2k8tVG0T1NjHjw2Qx1fwChQ
…sv7PkRKKQ=6829b7302ad50470a5ddb3cd41566daf
IoZAWjxg/vHjEBEZvX1zTgwVJIsFn6VqMtnyYurmE3p9TjpjjgxfpJDlRMyTKNqmZto39j4X4Y8nKH/XPIdynRedAM/
…4gQ4s9fXbd402ed9aec13b9a3ab4521212294f7c
IoZAWjxg/vHjEBEZvX1zTgwVJIsFn6VqMtnyYurmE3p9TjpjjgxfpJDlRMyTKNqmZto39j4X4Y8nKH/XPIdynRedAM/
…4gQ4s9fXbd402ed9aec13b9a3ab4521212294f7c
Погодите, шифрование? Вот это поворот! Тут наши полномочия, как говорится все.
Но погодите, мы встречаем 3 одинаковых последовательности. Причем, одно из сообщений дублируется два раза. Открываем приложение, нажимаем разные кнопочки и обнаруживаем, что некоторые пакеты не меняются.
Делаем первый промежуточный вывод - шифрование не привязано ко времени.
Конец сообщения очень сильно что-то напоминает. Да и по закону жанра в конце сообщения должен быть хэш\CRC. Проводим некоторое время в калькуляторах перебирая алгоритмы и действительно, конец сообщения содержит свой хэш в MD5 (дайджест?). Ок.
Осталось определить алгоритм шифрования. Developer android заботливо подсказывает, что алгоритмов поддерживается дофига, но рекомендуется использовать AES256. Запомним.
На вопрос "Как определить алгоритм шифрования зная шифротекст?", гугл лаконично отвечает "Никак, анализируй приложение (де)шифровщик".
Забрались далеко, отступать не наши методы. Качаем APK, при помощи dex2jar конвертируем в jdk и открываем в JD.
Можно заняться полным реверсом (не ассемблер же), но быстрее будет запустить поиск по ключевому слову. Вопрос, что искать? Что-то отвечающее за шифрование - "encrypt"
Внезапно.
Библиотека от espressif
Библиотека от hyflying, с их SmartLink'ом.
Три класса реализующих AES. Причем во всех трех классах используются разные режимы шифрования.
Библиотеки от espressiа и hyflying можно отложить на потом, WifiUtils, логично предположить, отвечает за шифрование Wi-Fi. Остаются TcpServices, EncryptUtils и AES256Chipher. Заглянем в EncryptUtils и увидим:
public class EncryptUtils {
private static final String AES_MODE = "AES/CBC/PKCS7Padding";
private static final String CHARSET = "UTF-8";
private static final String ENC_PASSWORD;
private static final String HASH_ALGORITHM = "SHA-256";
private static final String TAG = EncryptUtils.class.getSimpleName();
private static final byte[] ivBytes;
static {
ENC_PASSWORD = EncryptUtils.class.getSimpleName().concat(EncryptUtils.class.getSimpleName());
ivBytes = new byte[] {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0 };
}
…
public static String decrypt(String paramString) {
try {
return decrypt(ENC_PASSWORD, paramString);
…
}
private static String decrypt(String paramString1, String paramString2) throws GeneralSecurityException {
try {
SecretKeySpec secretKeySpec = generateKey(paramString1);
byte[] arrayOfByte = Base64.decode(paramString2, 2);
return new String(decrypt(secretKeySpec, ivBytes, arrayOfByte), "UTF-8");
…
}
private static SecretKeySpec generateKey(String paramString) throws NoSuchAlgorithmException, UnsupportedEncodingException {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
byte[] arrayOfByte = paramString.getBytes("UTF-8");
messageDigest.update(arrayOfByte, 0, arrayOfByte.length);
return new SecretKeySpec(messageDigest.digest(), "AES");
}
…
Краткое содержание:
Переменной ENC_PASSWORD два раза присваиваем имя текущего класса ("EncryptUtilsEncryptUtils"). Получаем хэш и на его основе генерим ключ, которым и дешифруем данные.
Запускаем Android Studio и пишем простенький код (или пользуемся он-лайн дешифровщиками) и скармливаем наш шифротекст.
Fail. Пробуем по другому, снова фейл. И так и сяк - Fail.
Окей, посмотрим что лежит во втором классе - AES256Chipher
paramString1 - шифротекст
paramString2 - ключ
public static String decrypt(String paramString1, String paramString2) {
… <отрезание и проверка MD5, not null и все такое прочее> …
MessageDigest messageDigest = MessageDigest.getInstance("SHA384");
try {
byte[] arrayOfByte2 = messageDigest.digest(paramString2.getBytes("UTF-8"));
byte[] arrayOfByte1 = Arrays.copyOfRange(arrayOfByte2, 32, 48);
arrayOfByte2 = Arrays.copyOfRange(arrayOfByte2, 0, 32);
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
IvParameterSpec ivParameterSpec = new IvParameterSpec(arrayOfByte1);
cipher.init(2, new SecretKeySpec(arrayOfByte2, "AES"), ivParameterSpec);
return new String(cipher.doFinal(Base64.decode(paramString1, 0)));
…
}
Краткое содержание: берем пароль, считаем от него SHA384 (почему 384?), разбиваем на два байтовых массива [32-48] и [0-32].
Первый - вектор инициализации (IV)
Второй - ключ, который и скармливаем SecretKeySpec
Вроде, ничего криминального, все так делают. Внимание вопрос - что же у нас может выступать в роли пароля? Может быть, тот самый enc_key из JSONa передающийся открытым http?
Пробуем, и… вуаля, сообщения расшифровываются. Внутри нас ждет тот самый REST API с командами getDeviceParams, setDeviceParams, changedDeviceParams. Можно начинать писать плагин для системы управления.
Возникают смешанные чувства:
- Передача номера телефона и пароля в открытом виде. Я даже не знаю, что тут написать. Хэш? SSL\TLS? Не, не слышали. Зато, на сайте разработчиков гордо красуется "Входим в ТОП-100 разработчиков мобильных приложений".
- Ключ шифрования (по сути) передается в открытом виде при установлении каждой сессии. А сессия создается при каждом запуске приложения. Нет, он не сохраняется. Открыл приложение, выставил температуру, закрыл приложение - такой же жизненный цикл имеет ключ. Т.е. нам достаточно перехватить начало одной, любой сессии и все, можем рулить железкой.
- Зачем в приложении аж 3 класса реализующих AES? Ладно, тут ответ очевиден - "я его слепила из того что было"Однако!
- Благодаря этому, можно легко написать сторонне приложение\плагин\расширение для системы управления умным домом. Утечки ПД, нет, т.к. нет ФИО (оно вообще нигде не указывается, даже при регистрации)
Часть 3. UART
Как выяснилось в первой части, наш wi-fi свисток, это всего лишь "UART to Wi-Fi" преобразователь. Это значит, что мы можем при помощи Esp32\Arduino\RPI создать свою железку для управления конвектором - с MTTQ, bluetooth и прочими соединениями. Надо лишь узнать протокол.
Итак, свисток имеет разъем USB-AM, и подписи на плате (см. фото выше) +5v, Tx, Rx, GND. Дело за малым, взять кабель AM - AF, разрезать, зачистить и в параллель подключить логический анализатор. +5v, можно оставить в покое. Таким образом, сразу будем видеть и Тx и Rx - главное, определиться с какой стороны смотреть.
Запуск, тыканье в кнопочки на мобильнике и…
…что-то это напоминает.
UART to Wi-Fi оказался тупым конвертером. Никакой внутренней обработки, никакой логики. Формат сообщения такой же как и в первой части.
В Wireshark мы видели AT-команды. Да и в документации про это что-то было. Берем UART-USB, выставляем +5В и подключаем. Главное Tx и Rx подключить перекрестно (очевидно, но можно забыть). Запускаем Putty или любой другой терминальный клиент. Выбираем com-порт в документации указана скорость 115200, но по факту 9600.
Окей, железка что-то шлет в терминал, но на ввод не реагирует. Курим ман и находим - для перехода в режим настройки необходимо отправить "+++" (без enter'а), на что HF-LPT220 ответит "a" и нам надо в ответ послать такую же "a". Сделать это все надо за 3 с.
После данной эквилибристики, железка становится отзывчивой. Введем AT+H и увидим список доступных команд:
AT+H
AT+APPVER: Show application version.
AT+DCDC=on/off: Enable or disable DCDC Mode .
AT+SMEM: Show memory.
AT+FLSHRD: Show flash data.
AT+UART: Set/Get the UART0/UART1 Parameters.
AT+NDBGL:set/get debug level
AT+MDCH: Put on/off automatic switching WIFI mode.
AT+ENTM: Goto Through MOde.
AT+SMTLKST=mode,protocol: Setup smartlnk mode and protocol.
AT+RELD: Reload the default setting and reboot.
AT+MID: Get The Module ID.
AT+WRMID: Write Module ID.
AT+VER: Get application version.
AT+BVER: Get bootloader version.
AT+CFGRD: Get current system config.
AT+FCLR: Clear Fsetting.
AT+CFGTF: Save Current Config to Default Config.
AT+CFGW=on/off: Enable or disable write config to flash
AT+SRST:Soft Reset the Module.
AT+SLEEP=ms:Cpu sleep ms.
AT+E: Echo ON/Off, to turn on/off command line echo function.
AT+Z: Reset the Module.
AT+H:show help
AT+SOCKB: Set/Get Parameters of socket_b.
AT+TCPDISB: Connect/Dis-connect the TCP_B Client link.
AT+TCPTOB: Set/Get TCP_B time out.
AT+TCPLKB: Get The state of TCP_B link.
AT+RCVB: Recv data from socket_b
AT+SNDB: Send data to socket_b
AT+NETP: Set/Get the Net Protocol Parameters.
AT+TCPLK: Get The state of TCP link.
AT+TCPTO: Set/Get TCP time out.
AT+TCPDIS: Connect/Dis-connect the TCP Client link
AT+MAXSK: Set/Get MAX num of TCP socket (1~5)
AT+DISPS: Disable power saving mode of WIFI
AT+WSLQ: Get Link Quality of the Module (Only for STA Mode).
AT+SMTLK: Start Smartlink.
AT+WSSSID: Set/Get the AP's SSID of WIFI STA Mode.
AT+WAP: Set/Get the AP parameters.
AT+WAPMXSTA: Set/Get the Max Number Of Sta Connected to Ap.
AT+WSKEY: Set/Get the Security Parameters of WIFI STA Mode.
AT+WAKEY: Set/Get the Security Parameters of WIFI AP Mode.
AT+WIFI=UP/DOWN: Power down or up the wifi chip.
AT+WPSBTNEN:enable/disable wps button.
AT+WALKIND:enable/disable LED indication of AP connection.
AT+WALK:Show sta information of AP connection.
AT+WSCAN: Get The AP site Survey (only for STA Mode).
AT+WMODE: Set/Get the WIFI Operation Mode (AP or STA).
AT+WSLK: Get Link Status of the Module (Only for STA Mode).
AT+WIFI=UP/DOWN: Power down or up the wifi chip.
AT+WSMAC: Set/Get Module MAC Address.
AT+NTPSER: Set/Get NTP Server address.
AT+UDPLCPT: Set/Get local UDP port.
AT+WANN: Set/Get The WAN setting if in STA mode.
AT+LANN: Set/Get The LAN setting if in ADHOC mode.
AT+WADHCP:enable/disable AP dhcp server and set ip address pool
AT+ASWD: Set/Query WiFi configuration code.
AT+NTPRF: Set/Query NTP.
AT+NTPEN: Enable/Disable NTP Server.
AT+NTPTM: Set/Query Ntp Time.
AT+NTPSER: Set/Query Ntp Server.
AT+PLANG=EN/CN: Set/Get the language of WEB page.
AT+WEBU: Set/Get the Login Parameters of WEB page.
AT+OTA: Auto upgrade firmware.
AT+UPURL: Set/Get the path of remote upgrade.
Можно позапускать всякое, но интерес представляет AT+SOCKB, который возвращает "+ok=TCP,10001,dongle.******.ru". Как внезапно…
Попробуем заменить на что-нибудь свое?
"AT+SOCKB=TCP,10001,192.168.0.2". Возвращаем модуль в конвектор и смотрим в wireshark. Теперь железка пытается установить соединение не с сервером в интернете, а с ПК из локальной сети.
Выводы и заключение.
«Буква S в аббревиатуре IoT обозначает Security»
Существует три способа управления конвектором. Через сервер производителя (REST API), напрямую из ЛВС, через UART.
Остался открытый вопрос с модулем для HA - возможно никогда-нибудь, он будет написан =).
aivs
Вот докопался до прибора!!!