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

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

Внутри коробки имеется сама лампа, подставка для неё, пульт дистанционного управления и бумажка с 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 для быстрого поиска отправленных комманд:

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

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

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

Цвет

Значение

Красный

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

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

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

Описание

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

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

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

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

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

Блок метода keyExpansionDefault

Блок метода keyExpansionDefault

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

Указатель key_ptr

Указатель key_ptr

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

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

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

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

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

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

Проект не представляет из себя чего-то сложного — шифрование AES’ом массива данных при помощи заранее известного ключа.

Создаём класс PayloadGenerator, внутри которого объявляем ранее полученный ключ, шапку запроса, ID группы по умолчанию и создаём экземпляр криптографического объекта для шифрования данных:

public class PayloadGenerator
{
    /// 
    /// Ключ шифрования данных
    /// 
    private static readonly byte[] Key =
    {
        0x34,
        0x52,
        0x2A,
        0x5B,
        0x7A,
        0x6E,
        0x49,
        0x2C,
        0x08,
        0x09,
        0x0A,
        0x9D,
        0x8D,
        0x2A,
        0x23,
        0xF8
    };

    /// 
    /// Шапка для запроса - всегда статичная
    /// 
    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 сообщения:

/// 
/// Получить payload для установки конкретного цвета лампы
/// 
/// Красный спектр
/// Зелёный спектр
/// Синий спектр
/// Яркость лампы (от 0 до 100)
/// Скорость смены эффектов (от 0 до 100)
/// payload для установки конкретного цвета лампы
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 payload)
{
	return string.Join("", payload.Select(x => x.ToString("X2").ToLower()));
}

И также создадим перечисление доступных команд из приложения:

public enum CommandType : byte
{
    /// 
    /// Запрос на вступление в группу
    /// 
    JoinGroupRequest = 1,

    /// 
    /// Установить конкретный цвет лампы
    /// 
    Rgb = 2,

    /// 
    /// Установить режим свечения в такт музыки
    /// 
    Rhythm = 3,

    /// 
    /// Установить таймер работы лампы
    /// 
    Timer = 4,

    /// 
    /// 
    /// 
    RgbLineSequence = 5,

    /// 
    /// Установить скорость работы эффекта
    /// 
    Speed = 6,

    /// 
    /// Установить яркость лампы
    /// 
    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 интерфейс или при помощи автоматизаций.

© Habrahabr.ru