Внешний вид закатной лампы
Внутри коробки имеется сама лампа, подставка для неё, пульт дистанционного управления и бумажка с 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
, расположенный по пути папка проекта из JADXappsrcmainlibx86
, оставляем настройки декомпиляции по умолчанию и перед нами появляется список функций, которые есть в библиотеке:
Здесь видим 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 интерфейс или при помощи автоматизаций.
Автор:
equuskk