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

Под линзой находится три цветовых круга с световыми элементами:
Внешний - синий цвет
Средний - зелёный цвет
Внутренний - красный цвет

Внешний вид официального приложения
Устанавливаем скачанное приложение на телефон - в качестве подопытного используется Samsung A8 2018 года выпуска (SM-A530F). После установки и открытия приложения нас встречает следующий интерфейс:

Возможности приложения:
включить/выключить лампу
группировать несколько ламп в группы для одновременного управления
Поставить цвет из RGB палитры, отрегулировать яркость
Установить один из нескольких предустановленных вариантов свечения ("дыхание", мигание и плавное переливание цветов) и скорость работы эффекта
Установить таймер работы лампы
Функционал свечения в такт музыки - нужно либо выбрать файл с телефона, либо предоставить доступ к микрофону
После подключения лампы к USB разъёму, она становится доступной для соединения с приложением:

Пробуем изменить цвета и установить эффекты - всё работает, значит можно приступать к декомпиляции приложения.
Разбираемся с исходным кодом приложения
Внутри коробки с лампой лежит листок с QR-кодом, который ведёт на страницу скачивания приложения из Google Play или App Store. Чтобы избежать выкачивания приложения из памяти телефона, возьмём APK, который предлагает производитель.

Для декомпиляции приложения воспользуемся JADX - декомпилятор DEX файлов в Java. Скачиваем последний актуальный релиз (1.4.6 на момент написания статьи). Из предложенных в релизе вариантов я выбрал версию со встроенным JRE, дабы не устанавливать лишние зависимости в систему. После запуска открываем ранее скачанный .apk файл и... видим, что исходников практически нет, а те, что есть, не несут какой-либо практической пользы:

Предполагаю, что код приложения обфусцирован и провести обратную операцию либо не получится, либо займёт достаточно много времени. Попробуем пойти более простым путём...
Подготавливаем устройство для сниффинга трафика
Для начала необходимо включить режим разработчика на устройстве - обычно это делается путём 9 нажатий на номер сборки в сведениях об ОС. Далее переходим в настройки режима разработчика, активируем пункты "включить журнал HCI Bluetooth" и "Отладка по USB" и перезапускаем bluetooth.
Заходим в приложение, выбираем из палитры красный, зелёный и синий цвета (чтобы легче было анализировать пакеты), подключаем смартфон через USB к компьютеру и через ADB вытаскиваем дамп:
adb pull /sdcard/btsnoop_hci.log # если не получится с вышеуказанной командой, # то скачиваем полный дамп системы и оттуда вытаскиваем файл по пути # /FS/data/log/bt/btsnoop_hci.log adb bugreport dump
Анализируем протокол общения через bluetooth
Для анализа протокола передачи данных между устройством и лампой воспользуемся Wireshark - программой-анализатором трафика множества различных протоколов. Скачиваем с официального сайта актуальную версию - я выбрал портабельную. Запускаем приложение, открываем bluetoooth dump с устройства, в проставляем фильтр btatt и фильтруем по колонке Info для быстрого поиска отправленных комманд:

Соотносим отправленные цвета по времени и получаем следующую картину:
Цвет | Значение |
Красный |
|
Зелёный |
|
Синий |
|
Никакой закономерности между изменением трёх байт цвета и отправленным значением нет - значит, применяется шифрование на клиенте и в таком виде отправляется на лампу, где происходит обратный процесс и применяются отправленные настройки.
Разбираемся с исходным кодом приложения. Опять
Раз с прошлым приложением у нас ничего не получилось, то скачаем с официального источника. Переходим по ссылке скачивания из Google Play и устанавливаем приложение на телефон. Приложение (на удивление) имеет 100к+ скачиваний и обновлено 27 февраля 2023 года:

Далее необходимо вытащить apk файл приложения при помощи следующих команд:
# Получаем название пакета adb shell "pm list packages | grep strip" # получаем путь до apk файла (из вывода надо выбрать тот путь, что содержит base.apk): adb shell "pm path com.ben.istrips" # забираем приложение на пк adb pull /data/app/com.ben.istrips-JJlXI2S0nofBY-AqpNwOKA==/base.apk ./iStrip.apk
Открываем полученный apk файл через JADX и видим совсем другую картину:

Итак, это успех - у нас теперь есть исходный код приложения, при помощи которого можно узнать, как шифруются данные. Бегло осматриваем исходный код и видим папку ble, в которой содержится файл BleProtocol. Открываем его и видим метод sendColor (комментарии переведены с китайского):
public static void sendColor(DataManager dataManager, int i) { int curColor = dataManager.getCurColor(); byte[] bArr = {84, 82, 0, 87, (byte) 2, (byte) dataManager.getGroupId(), (byte) i, (byte) Color.red(curColor), (byte) Color.green(curColor), (byte) Color.blue(curColor), (byte) dataManager.getLight(), (byte) dataManager.getSpeed(), 0, 0, 0, 0}; LogUtil.d("send data command:" + ByteUtils.BinaryToHexString(bArr)); boolean writeAll = BleManager.getInstance().writeAll(Agreement.getEncryptData(bArr)); LogUtil.d("send data result :" + writeAll); }
Вуаля - у нас есть массив, который шифруется при помощи AES и отправляется на лампу. Давайте подробно рассмотрим структуру данных:
Порядковый номер байта | Значение по умолчанию | Описание |
1 | 84 | Значение по умолчанию. Шапка запроса |
2 | 82 | Значение по умолчанию. Шапка запроса |
3 | 0 | Значение по умолчанию. Шапка запроса |
4 | 87 | Значение по умолчанию. Шапка запроса |
5 | 2 | Тип команды от 1 до 7. |
6 | 1 | ID группы (всегда должно быть больше 1, иначе лампа не примет такой запрос) |
7 | 0 | Неизвестно. В коде именуется как |
8 | Зелёный спектр цвета - от 0 до 255 | |
9 | Красный спектр цвета - от 0 до 255 | |
10 | Синий спектр цвета - от 0 до 255 | |
11 | 100 | Яркость лампы - от 0 до 100 |
12 | 100 | Скорость работы эффекта - от 0 до 100 |
13 | 0 | Используется для команды с типом |
14 | 0 | Используется для команды с типом |
15 | 0 | Используется для команды с типом |
16 | 0 | Используется для команды с типом |
Внимание! Для моего устройства (а может так на всех других) перепутаны местами байты красного и зелёного спектров - поэтому в структуре сначала идёт зелёный, а потом красный, хоть в приложении и наоборот.
Теперь осталось поглядетьgetEncryptDataи дело сделано! Но тут появляется неожиданное обстоятельство:
public static byte[] getEncryptData(byte[] bArr) { aes.cipher(bArr, bArr); return bArr; }
public class aes { public static native void cipher(byte[] bArr, byte[] bArr2); public static native void invCipher(byte[] bArr, byte[] bArr2); public static native void keyExpansion(byte[] bArr); public static native void keyExpansionDefault(); static { System.loadLibrary("AES"); } }
Получается, что приложение использует библиотеку, написанную на C/C++ и ключа шифрования внутри кода нет - метод cipher принимает массив данных и массив, куда необходимо сохранить зашифрованные данные.
Предположим, что ключ шифрования задаётся функцией keyExpansion либо же устанавливается дефолтный ключ функцией keyExpansionDefault - проверим, используются ли эти методы в коде. После поиска по коду было найдено лишь одно использование метода keyExpansionDefault при создании приложения:
public class App extends Application { // ... @Override // android.app.Application public void onCreate() { // .... aes.keyExpansionDefault(); // .... } }
Делаем вывод о том, что ключ всё-таки хранится внутри библиотеки и его необходимо достать оттуда. Для этого в JADX сохраняем проект через меню File -> Save all (или просто жмём CTRL+S) и выбираем папку для сохранения.
Реверсим нативную библиотеку шифрования
Для этого потребуется бесплатная версия IDA - интерактивный дизассемблер, который отличается исключительной гибкостью, наличием встроенного командного языка, поддерживает множество форматов исполняемых файлов для большого числа процессоров и операционных систем.
Устанавливаем приложение с официального сайта, открываем при помощи него файл libAES.so, расположенный по пути папка проекта из JADX\app\src\main\lib\x86, оставляем настройки декомпиляции по умолчанию и перед нами появляется список функций, которые есть в библиотеке:

Здесь видим 4 функции, которые начинаются с Java_ - это и есть те самые нативные функции, описанные внутри aes класса приложения. Переходим в keyExpansionDefault путём двойного нажатия на название в списке и видим первый блок функции, внутри которого есть упоминание key_ptr:

Название переменной говорит само за себя - это указатель на ключ. Поэтому дважды кликаем на key_ptr и переходим в следующий блок:

Переходим в key и... Бинго! Внутри переменной находится массив из 16 байт, который и является ключом шифрования.

Итак, ключ наконец-то найден, теперь можно приступить к генерации собственных шифрованных сообщений для отправки
Пишем сервис для генерации сообщений протокола
Далее будет использоваться .Net Core 6 и язык программирования C#. Весь исходный код опубликован на гитхабе - ссылка на репозиторий.
Проект не представляет из себя чего-то сложного - шифрование AES'ом массива данных при помощи заранее известного ключа.
Создаём класс PayloadGenerator, внутри которого объявляем ранее полученный ключ, шапку запроса, ID группы по умолчанию и создаём экземпляр криптографического объекта для шифрования данных:
public class PayloadGenerator { /// <summary> /// Ключ шифрования данных /// </summary> private static readonly byte[] Key = { 0x34, 0x52, 0x2A, 0x5B, 0x7A, 0x6E, 0x49, 0x2C, 0x08, 0x09, 0x0A, 0x9D, 0x8D, 0x2A, 0x23, 0xF8 }; /// <summary> /// Шапка для запроса - всегда статичная /// </summary> private static readonly byte[] Header = { 0x54, 0x52, 0x0, 0x57 }; private readonly ICryptoTransform _crypt; private const int GroupId = 1; public PayloadGenerator() { var aes = Aes.Create(); aes.Mode = CipherMode.ECB; _crypt = aes.CreateEncryptor(Key, null); } }
Далее опишем метод для генерации payload'a сообщения:
/// <summary> /// Получить payload для установки конкретного цвета лампы /// </summary> /// <param name="red">Красный спектр</param> /// <param name="green">Зелёный спектр</param> /// <param name="blue">Синий спектр</param> /// <param name="brightness">Яркость лампы (от 0 до 100)</param> /// <param name="speed">Скорость смены эффектов (от 0 до 100)</param> /// <returns>payload для установки конкретного цвета лампы</returns> public string GetRgbPayload(byte red, byte green, byte blue, byte brightness = 100, byte speed = 100) { var payload = new byte[16] { Header[0], Header[1], Header[2], Header[3], (byte)CommandType.Rgb, GroupId, 0, green, red, blue, brightness, speed, 0x0, 0x0, 0x0, 0x0 }; var result = new byte[16]; _crypt.TransformBlock(payload, 0, payload.Length, result, 0); return ConvertToHexString(payload); } private static string ConvertToHexString(IEnumerable<byte> payload) { return string.Join("", payload.Select(x => x.ToString("X2").ToLower())); }
И также создадим перечисление доступных команд из приложения:
public enum CommandType : byte { /// <summary> /// Запрос на вступление в группу /// </summary> JoinGroupRequest = 1, /// <summary> /// Установить конкретный цвет лампы /// </summary> Rgb = 2, /// <summary> /// Установить режим свечения в такт музыки /// </summary> Rhythm = 3, /// <summary> /// Установить таймер работы лампы /// </summary> Timer = 4, /// <summary> /// /// </summary> RgbLineSequence = 5, /// <summary> /// Установить скорость работы эффекта /// </summary> Speed = 6, /// <summary> /// Установить яркость лампы /// </summary> Light = 7 }
В Program.cs создаем экземпляр класса нашего генератора и выводим в консоль сгенерированное сообщение:
using IStripLight; var lightController = new PayloadGenerator(); var result = lightController.GetRgbPayload(0, 0, 255, 50); Console.WriteLine(result);
Итак, генератор сообщений у нас теперь есть, проверим созданные сообщения на работоспособность.
Используем gatttool для отправки сообщений лампе
Для отправки сообщений лампе воспользуемся утилитой gatttool - она позволяет считывать и записывать характеристики GATT (Generic Attribute Protocol) для устройств, использующих Bluetooth low energy.
user@pi:~ $ sudo gatttool -I [ ][LE]> connect 43:d0:0c:e6:2b:20 Attempting to connect to 43:d0:0c:e6:2b:20 Connection successful [43:d0:0c:e6:2b:20][LE]> char-write-cmd 0x0009 ae066f229702720ca898a934839235f1
Яркость на лампе убавилась, а цвет поменялся на зелёный!
Вывод
В статье был проанализирован протокола общения приложения и лампы через реверс-инжиниринг android приложения и нативной библиотеки шифрования AES.
В результате было написано приложение для генерации сообщений для изменения цвета/яркости лампы.
В дальнейшем планируется написать кастомную интеграцию Home Assistant для управления лампой через UI интерфейс или при помощи автоматизаций.
