- PVSM.RU - https://www.pvsm.ru -

Разбираем bluetooth протокол RGB лампы

Внешний вид закатной лампы

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

Комплект поставки устройства

Комплект поставки устройства

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

  • Внешний - синий цвет

  • Средний - зелёный цвет

  • Внутренний - красный цвет

Лампа

Лампа

Внешний вид официального приложения

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

Интерфейс приложения

Интерфейс приложения

Возможности приложения:

  • включить/выключить лампу

  • группировать несколько ламп в группы для одновременного управления

  • Поставить цвет из RGB палитры, отрегулировать яркость

  • Установить один из нескольких предустановленных вариантов свечения ("дыхание", мигание и плавное переливание цветов) и скорость работы эффекта

  • Установить таймер работы лампы

  • Функционал свечения в такт музыки - нужно либо выбрать файл с телефона, либо предоставить доступ к микрофону

После подключения лампы к USB разъёму, она становится доступной для соединения с приложением:

Доступное устройство для подключения

Доступное устройство для подключения

Пробуем изменить цвета и установить эффекты - всё работает, значит можно приступать к декомпиляции приложения.

Разбираемся с исходным кодом приложения

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

Страница скачивания приложения

Страница скачивания приложения

Для декомпиляции приложения воспользуемся JADX [2]- декомпилятор 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 [3]- программой-анализатором трафика множества различных протоколов. Скачиваем с официального сайта актуальную версию - я выбрал портабельную. Запускаем приложение, открываем bluetoooth dump с устройства, в проставляем фильтр btatt и фильтруем по колонке Info для быстрого поиска отправленных комманд:

Отправленные устройством команды

Отправленные устройством команды

Соотносим отправленные цвета по времени и получаем следующую картину:

Цвет

Значение

Красный

0b193631a1c203cfadfdbad7820f3856

Зелёный

1273622a87797e5c768211ee59308e5b

Синий

42c9e15436faf27b95fb68d3159c93e2

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

Разбираемся с исходным кодом приложения. Опять

Раз с прошлым приложением у нас ничего не получилось, то скачаем с официального источника. Переходим по ссылке скачивания из Google Play и устанавливаем приложение на телефон. Приложение (на удивление) имеет 100к+ скачиваний и обновлено 27 февраля 2023 года:

Информация о приложении из google play

Информация о приложении из google play

Далее необходимо вытащить 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 [4]и отправляется на лампу. Давайте подробно рассмотрим структуру данных:

Порядковый номер байта

Значение по умолчанию

Описание

1

84

Значение по умолчанию. Шапка запроса

2

82

Значение по умолчанию. Шапка запроса

3

0

Значение по умолчанию. Шапка запроса

4

87

Значение по умолчанию. Шапка запроса

5

2

Тип команды от 1 до 7.

6

1

ID группы (всегда должно быть больше 1, иначе лампа не примет такой запрос)

7

0

Неизвестно. В коде именуется как mode

8

Зелёный спектр цвета - от 0 до 255

9

Красный спектр цвета - от 0 до 255

10

Синий спектр цвета - от 0 до 255

11

100

Яркость лампы - от 0 до 100

12

100

Скорость работы эффекта - от 0 до 100

13

0

Используется для команды с типом 4 (настройка таймер) - минута для включения лампы

14

0

Используется для команды с типом 4 (настройка таймер) - день недели для выключения лампы

15

0

Используется для команды с типом 4 (настройка таймер) - час для выключения лампы

16

0

Используется для команды с типом 4 (настройка таймер) - минута для выключения лампы

Внимание! Для моего устройства (а может так на всех других) перепутаны местами байты красного и зелёного спектров - поэтому в структуре сначала идёт зелёный, а потом красный, хоть в приложении и наоборот.

Теперь осталось поглядеть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 [5]- интерактивный дизассемблер, который отличается исключительной гибкостью, наличием встроенного командного языка, поддерживает множество форматов исполняемых файлов для большого числа процессоров и операционных систем.

Устанавливаем приложение с официального сайта, открываем при помощи него файл libAES.so, расположенный по пути папка проекта из JADXappsrcmainlibx86, оставляем настройки декомпиляции по умолчанию и перед нами появляется список функций, которые есть в библиотеке:

Окно IDA Freeware после декомпиляции

Окно IDA Freeware после декомпиляции

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

Блок метода keyExpansionDefault

Блок метода keyExpansionDefault

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

Указатель key_ptr

Указатель key_ptr

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

Ключ шифрования

Ключ шифрования

Итак, ключ наконец-то найден, теперь можно приступить к генерации собственных шифрованных сообщений для отправки

Пишем сервис для генерации сообщений протокола

Далее будет использоваться .Net Core 6 и язык программирования C#. Весь исходный код опубликован на гитхабе - ссылка на репозиторий [6].

Проект не представляет из себя чего-то сложного - шифрование 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 [7] для управления лампой через UI интерфейс или при помощи автоматизаций.

Автор:
equuskk

Источник [8]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/bluetooth/383443

Ссылки в тексте:

[1] на страницу скачивания приложения : http://api.e-toys.cn/page/app/112

[2] JADX : https://github.com/skylot/jadx

[3] Wireshark : https://www.wireshark.org/

[4] AES : https://en.wikipedia.org/wiki/Advanced_Encryption_Standard

[5] бесплатная версия IDA : https://hex-rays.com/ida-free/

[6] ссылка на репозиторий: https://github.com/equuskk/iStripPayloadGenerator

[7] Home Assistant: https://www.home-assistant.io/

[8] Источник: https://habr.com/ru/post/722412/?utm_source=habrahabr&utm_medium=rss&utm_campaign=722412