Реверс-інжиніринг, Bluetooth та електроживлення: пишемо Android-застосунок для акумулятора

Усі статті, обговорення, новини про Mobile — в одному місці. Підписуйтеся на телеграм-канал!

Вітаю! Мене звати Олег, я розробляю Android-застосунки в Uptech Product Studio. Цієї зими всі ми зіштовхнулися з потребою забезпечити себе електроживленням на час планових відключень. Чекати новий Ecoflow місяцями або переплачувати спекулянтам не хотілося, тому я вирішив зібрати його самостійно. Забрав у одному з інтернет-магазинів останній LiFePO4 акумулятор від польського виробника VoltPolska, прикупив до нього зарядку та інвертор.

Коли компоненти приїхали, на моє здивування, на акумуляторі було нанесено Bluetooth-імʼя. Зрадівши, я почав читати інструкцію в пошуках посилання на застосунок. І воно там справді було, однак після переходу я отримав... помилку 404. Виробник чомусь видалив свій застосунок. Після додаткового пошуку, я таки знайшов .apk чи-то офіційної, чи ні, апки десь у глухих закутках інтернету. Поставив на старий телефон, перевірив — працює.

Як Android-розробник і як простий користувач, я знаю, що ставити рандомні .apk з закапелків інтернету на свій телефон — це погана ідея. Мені хотілося стежити за акумулятором зі свого основного девайсу, і при цьому не наражати себе на небезпеку. Тому рішення (чи проблема) намалювалося самостійно: треба зареверсінжинірити застосунок, зрозуміти, як працює Bluetooth-модуль акумулятора і написати власний аналог.

Що з цього вийшло, читайте далі.

Disclaimer: усі дії у статті проводилися на основі відкритих ресурсів з метою дослідження, розваги та покращення власного комфорту. Повторювати на свій страх і ризик.

Bluetooth Low Energy

Головний герой історії

Для початку потрібно зрозуміти, з чим я взагалі маю справу. Зазвичай IoT-пристрої використовують особливу специфікацію Bluetooth, що називається Bluetooth Low Energy (BLE). Як можна здогадатися, вона дозволяє заощадити заряд пристрою, що особливо актуально для таких штук як акумулятори. Робиться це за рахунок обмежень пропускної здатності, радіусу та швидкості взаємодії.

BLE працює за стандартною клієнт-серверною моделлю, але має кілька особливостей. Для передачі даних використовується Attribute Protocol (ATT), що визначає так звані атрибути. Кожен атрибут має своє унікальне ім’я-UUID і двійкове значення, яке може бути зчитане клієнтом.

У свою чергу, на ньому будується протокол GATT (Generic Attribute Profile), що використовує ATT-атрибути для визначення таких абстракцій, як профілі, сервіси та характеристики.

Працює це так: у профілі може бути декілька сервісів, у кожному з яких може бути декілька характеристик.

На практиці це нагадує стандартне HTTP API, де профіль — це саме API, сервіси — це згруповані за певною фічею чи сутністю роути, а характеристики — окремі ендпоінти.

У сервісів і характеристик можуть бути права доступу: читання, запис, встановлення сповіщень (девайс самостійно відправлятиме вам повідомлення, коли значення характеристики змінюється) тощо.

Також можна встановлювати людиночитабельні назви та описи, одиниці вимірювання, проміжки можливих значень та інші метадані, що передаються за допомогою дескрипторів.

Знаючи це все, можна спробувати аналізувати, які саме сервіси і характеристики підтримує мій акумулятор.

Реверс-інжиніринг

Спочатку я спробував просканувати девайс за допомогою застосунку nRF Connect. Це класна штука, що показує, які атрибути доступні в девайсу. Вона навіть може відправляти запити і зчитувати відповіді. Однак, вивід застосунку був не дуже красномовним:

Вочевидь, розробник вирішив не додавати людиночитабельні дескриптори з міркувань чи-то безпеки, чи-то нестачі ресурсів. На додачу, при зчитуванні всі характеристики повертали нулі.

Загалом це глухий кут, але тут є кілька підказок, що стануть очевидними пізніше.

Мій наступний крок був просканувати трафік між оригінальним застосунком і акумулятором. На щастя, Android має функцію логування Bluetooth-пакетів у меню розробника. Я увімкнув її і почав гратися. Скинув отриманий btsnoop_hci.log файл на комп’ютер і відкрив його у Wireshark:

Усе ще нічого не ясно, але дуже цікаво. Видно write-команди, за якими слідують сповіщення про зміну якоїсь характеристики. Вочевидь, застосунок відправляє запит надати йому дані у write-характеристику і отримує відповідь через notify-характеристику. Ці дві характеристики також видно у nRF Connect на скриншоті вище.

Також я спробував проаналізувати значення, які відправляються та повертаються. Виглядають вони якось так:

Приклад write-запиту

Приклад notify-відповіді

Звичайно, це зашифровані в двійковий код і склеєні докупи числа, рядки і флаги. Просто так, не знаючи принципу формування, їх не розшифрувати. Але є щось дивне. Абсолютно всі вони починаються із байту 7b і закінчуються байтом 7d. Навмання звертаємося до ASCII-таблиці:

Бінго! Це фігурні дужки і, схоже, це не збіг. Якийсь JSON чи щось таке? Навряд, бо далі б мали йти лапки, а це не так. Але це вже старт, щоб поритися в оригінальній .apk. Відкриваємо її у Android Studio, знаходимо файл app-service.js. Вочевидь, він відповідає за обмін даними, тоді як app-view.js — за UI. Код мініфікований і обфускований, але не повністю (і це стане фатальним фактором).

Виконуємо пошук за 7b, і тут справді є багато роботи з бінарними значеннями (чомусь вони подаються як рядки, а не гексові числа). Зрештою, знаходиться ще один ключ до розгадки. Мініфікований код викликає купу якихось функцій, що називаються за принципом new7b**ToInfo.

Шукаємо декларацію.

Знову бінго. Назви функцій не мініфіковані, тому що з якоїсь причини розробники склали їх у JS-масив у вигляді об’єктів:

[{ key: “new7b**ToInfo”, value: function(...) { … }]

Потім вони присвоїли їх як поля для іншого об’єкта, який, вочевидь, діє як репозиторій даних чи щось таке. Тепер у нас є код для дешифрування двійкових результатів. Функція вище відкушує по кілька байтів від рядка, що представляє відповідь акумулятора (у шістнадцятковому записі), і парсить їх у int.

І заодно останній пазл головоломки: 7b та 7d — це справді фігурні дужки для розрізнення коректних значень, а наступний байт — це код команди. Наприклад, команда 01 змушує акумулятор повідомити загальну інформацію про свій стан, 02 повертає кількість окремих батарейних блоків усередині та дані кожного з них. І так далі.

Загальний принцип роботи виглядає так: надсилаємо у write-характеристику запит, наприклад, 7b01007d, і отримуємо відповідь по типу 7b0103010007d. Як бачимо, перші байти запиту і відповіді співпадають. Таким чином можна зрозуміти, на яку саме команду прийшла відповідь, і розпарсити її відповідно. Далі йдуть власне дані. Для чисел, де дробові значення (сила струму, напруга), вони діляться на 100 перед виводом на екран. От і все.

Android-застосунок

Тепер можна писати власний застосунок. Я накидав орієнтовний дизайн у Figma, а для розробки обрав Jetpack Compose. У деталі вдаватися не буду, оскільки на Android Developers робота з Bluetooth описана доволі добре. Згадаю лише незвичайні моменти.

В останніх версіях Android Bluetooth SDK отримало чимало покращень, серед яких і нові permissions. Я вирішив поставити нижню планку на 27 SDK (Android 8.1 Oreo). Тому мені потрібно підтримувати і новий, і старий підходи. Пермішни для старих SDK виглядали так:

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

Дозволи на локацію потрібні, тому що деякі блютуз-девайси можуть викрити місцезнаходження користувача під час під’єднання (наприклад, поштомат «Нової Пошти», скоріше за все, знає свою локацію, і підключення означатиме, що ви перебуваєте в радіусі ~10 метрів від нього). Тому Google вимагає від розробників явно повідомляти юзерів про таку можливість. Експериментально я встановив, що без дозволу на FINE_LOCATION сканування взагалі не запуститься.

Утім, починаючи зі SDK 30 (Android 11 R), старі блютуз-пермішни (з локацією все так само) задепрекейтили і тепер потрібно використовувати більш спеціалізовані:

<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>

Роботу з пермішнами я роблю в Compose-коді за допомогою гуглівської бібліотеки Accompanist. Також я дописав на рівні актівіті логіку, що стежить за тим, щоб Bluetooth і геолокація були увімкнені:

private val broadcastReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if (intent == null) return

        when (intent.action) {
            BluetoothAdapter.ACTION_STATE_CHANGED -> {
                when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)) {
                    BluetoothAdapter.STATE_OFF -> bluetoothEnabled = false
                    BluetoothAdapter.STATE_ON -> bluetoothEnabled = true
                }
            }

            LocationManager.PROVIDERS_CHANGED_ACTION -> {
                val locationManager = getSystemService<LocationManager>()
                val isGpsEnabled = locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER) ?: false
                val isNetworkEnabled = locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER) ?: false

                locationEnabled = isGpsEnabled || isNetworkEnabled
            }
        }
    }
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    registerReceiver(broadcastReceiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
    registerReceiver(broadcastReceiver, IntentFilter(LocationManager.PROVIDERS_CHANGED_ACTION))
    …
}

Пошук девайсів відбувається прозаїчно, згідно з гайдами з Android Developers. У кожного знайденого девайсу я перевіряю наявність сервісу та характеристик з потрібними UUID, щоб підтвердити, що це VoltPolska.

Після встановлення повноцінного підключення кожну секунду відправляю запит на оновлення характеристики. Наразі я працюю лише з командою 01, оскільки там є всі дані, що мене цікавлять — ємність, заряд, напруга тощо.

Знайти вихідний код і збілдити застосунок, якщо у вас теж акумулятор від VoltPolska, можна на GitHub. Також ось скриншоти того, що вийшло в кінці:

Реалізація всіх інших команд уже прозаїчна. Якщо розвивати застосунок далі, то треба було б винести з’єднання із в’юмоделі в окремий сервіс і прив’язати його до sticky-нотифікації. Тоді з’єднання не перериватиметься, коли застосунок йде у фон.

Післямова

Чого можна навчитися з цієї історії? Маючи достатньо бажання, можна обійти будь-яку систему безпеки. У моєму випадку спрацював ефект швейцарського сиру. Якби хтось не опублікував у мережі робочий застосунок чи якби розробники мініфікували код повністю, реверс-інжинірити було б у рази важче, але все ще можливо (аналізуючи вхідні/вихідні значення побайтово).

Із цього напрошується висновок про незахищеність і вразливість блютуз-девайсів. У них часто немає ніякого контролю над тим, хто і як підключається чи хоча б над тим, увімкнений блютуз чи ні. Умовний сусід теоретично міг би провести той же експеримент і підключитися до мого девайсу. З акумулятором це не страшно, але те саме працює з розумними вагами, датчиками, лампами, камерами тощо. Особисто мене це трохи лякає.

Це був цікавий експеримент, який захопив мене на кілька вечорів. Акумулятор і свій застосунок я потім використовував за призначенням, і це відчувалося справді класно — як щось, зроблене своїми руками.

👍ПодобаєтьсяСподобалось22
До обраногоВ обраному5
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

В розробці мобільних додатків не тямлю, але прочитати було цікаво. Дякую!

Загальний принцип роботи виглядає так: надсилаємо у write-характеристику запит, наприклад, 7b01007d, і отримуємо відповідь по типу 7b0103010007d

1. У запиті 00 перед 7d це якісь вхідні, так би мовити, дані для конкретної характеристики?
2. Скажи, будь ласка, чому у відповіді не парна кількість символів? Як зрозуміти що це таке? 7b і 7d зрозуміло, 01 також, а як зрозуміти інше?

Кількість символів має бути парна, то одруківка. Два символи — один байт. Це зразок відповіді для схематичного розуміння. Справжня відповідь виглядає якось так:

7b0120005c0577000000b500b503fc243b271027101f40000200040000000d000000647d

Щодо нулів у запиті, то це цікаве питання. Для чого вони там, я так і не зрозумів. Коди запитів в оригінальній апці були захардкоджені, після коду завжди йде нульовий байт. Можливо, на випадок розширення списку команд до двох байт. 🤷‍♂️

Схожим чином, перший байт відповіді (після байту 7d) у більшості команд теж ігнорується, хоча має і ненульове значення. На гітхабі можна подивитися побайтовий розбір команди 01.

Для чого вони там, я так і не зрозумів.

Можливо uk.wikipedia.org/wiki/Вирівнювання_даних

Мені хотілося стежити за акумулятором зі свого основного девайсу, і при цьому не наражати себе на небезпеку. Тому рішення (чи проблема) намалювалося самостійно: треба зареверсінжинірити застосунок, зрозуміти, як працює Bluetooth-модуль акумулятора і написати власний аналог.

У мене ця задача самовирішилась за хвилину — встановлена з Google Play апка для Xiaoxiang BMS, що стояла на іншій збірці, несподівано задетектила і змогла приконектитись до вольт польскої.

Цікаво, ця апка окремо реалізовувала підтримку для вольт польска, чи це якийсь загальний стандарт для BMS? 🤔

Виробників BMS не так багато, конкретно ця jiabaidabms.com ставиться в вольт польскій, топу апка і підійшла.
А ось у зворотній бік чомусь не спрацювало — вольт польска бачить тільки вольт польску, мабуть тупо прив’язується до формату імені.

Цікаво, але не зовсім зрозумів, воно тільки транслює базові параметри, чи дає можилвість чимось керувати?

В оригінальній апці була фіча оновлення BMS. Інших способів керування акумулятором не пригадую.

Дякую, наступний акум буду обирати без блютусу, бо після цієї інфи, щось не дуже є впевненість у захищеності від шкідливих дій щодо високоенергетичного предмету )

В продвинутих BMS там ніби паролем захищене. Але як воно працює не певен.

Тут теж є, але сумніваюся в надійності такого захисту.

Круто. Але якщо хочете не турбуватись про безпеку — просто поставте щось на кшталт prom.ua/...​r-voltmetr-ampermetr.html Потреби в постійному моніторингу батареї з мобільного девайсу немаж, та й сам девайс із включенним Bluetooth розряджається досить швидко.

девайс із включенним Bluetooth розряджається досить швидко.

en.wikipedia.org/...​uetooth_Low_Energy_beacon
Battery life can range between 1–48 months.
У топикстартера конечно не маячок, но я думаю что расход батарейки будет примерно такой же

Я про «приймач», тобто смартфон. Передавачу із аккумом в 100 А*год, думаю, глибоко пофіг на те, лоу енерджі там чи не лоу.

Смартфон скорее будет терять батарейку на процессор и включенный экран, чем на связь....

Почитав декілька статей про споживання Bluetooth і дуже здивувався — дійсно, сучасні версії Bluetooth надзвичайно енергоощадливі. Тим краще.

Та, думав про такі штуки, але поки надумав, то відключення вже закінчилися)

Поки є можливість (і немає ажіотажу не усе, що хоч трохи пов’язано з автономним живленням) — купіть і поставте. Зима близько.

Такий акум зазвичай має власний bms, що відображає і напругу, і струм і ємність. Та й 10 А пристрій дуже слабий для таких акумів.

Так, 10А тут ні про що, згоден. Щодо BMS — через мене пройшла купа «голих» плат, без будь-якої індикації, навіть без світлодіодів у балансирному блоці. Та й такий модуль можна причепити до будь-якого акума, генератора, сонячної панелі, ядерного реактора, стеларатора чи що там в кого є.

Та що ж таке, га?! Скільки потрібно часу, щоб нарешті второпати, що харчуються у їдальні, а електроприлади — живлять? Хіба з очей кров не йде від такої української? Це знущання якесь...

Нє. «Електрохарчування» — це вже частина фольклору, разом із бавовною та пекельними борошнами.

Це ж слово-мем тепер, ну камон. Може тепер ще і камон не писати бо це калька з англійської? Та камон! :D

Стосовно Ecoflow, раз вже згадали, щоб зламати її захист, потрібно щоб він там для початку був. 😃

Для повної картини не вистачає лише чату gpt, який написав би аплікуху.

гексові числа

😊😍

Цікаво чи зміг би він розкласти по поличкам мініфікований код?Десь була стаття в котрій chat GPT непогано розбирав обфускованйи код.

...Маючи достатньо бажання, можна обійти будь-яку систему безпеки...

— при всьому бажанні обійти AES256 треба буде мільярди років N-ГВт елктроенергії.

..але те саме працює з розумними ... датчиками, лампами, камерами...

в дійсно «розумних сенсорах» викоритосвується екрипшин.

Робота подібних систем за допомогою «JS-онів» — це свідчить про те що систему розробляли:
1. Розробники які не знають, що таке секюріті || НЕ інженери, а швидше ІТ-шніки
2. Спеціально — щоб експоузнути «АПІ» з можливістю використати в інших застосунках

при всьому бажанні обійти AES256 треба буде мільярди років N-ГВт елктроенергії.

таким способом (в лоб) вже давно ніхто не робить. Є більш швидкі способи отримати ключ.

в дійсно «розумних сенсорах» викоритосвується екрипшин.

Там ключ теж лежить не сильно в захищеному місці і не сильно захищеним способом. При великому бажанні його теж можна з легкістю отримати. Питання — навіщо?

1. я в загальному процитував твердження про «достатнє бажання». Інколи лише бажання не достатньо.
2. якщо щось десь лежить не в не сильно захищеному місці — то це погане секюріті.

Робота подібних систем за допомогою «JS-онів» — це свідчить про те що систему розробляли:

Там не JSON, ви не уважно читали статтю.

Так, Ви маєте рацію, по хорошому має бути саме шифрування. Але з мого особистого досвіду роботи в ІоТ і використання розумних девайсів, справжній захист це скоріше рідкість. Починаючи від лампочок, які можна увімкнути просто пославши їм код кольору, і до камер, що комунікують у корпоративній мережі взагалі по чистому HTTP. Тому я загалом з настороженням ставлюся до всіх «розумних» пристроїв.

«-Дядя Петя, ты дурак?» www.youtube.com/watch?v=N-8jkDqR58U

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

Розпові вчительке!

Розпові редакції як робити клікбейт :-P

Електрохарчування?

З категорії «пекельні борошна».

Немає вже сечі терпіти електрохарчування

Відразу видно хто не користується тві :)

Підлога країни терпить пекельні борошна, і ми потерпимо)

Мабуть знову редактори підказали :)

Та ні, не ми) але дійно, не всі в курсі слова, тому замінили.

Дякую за уважність, дійсно моя помилка, вже виправлено)

Підписатись на коментарі