Reverse engineering the BM6 BLE battery monitor

Overview

Context

The BM6 battery monitor is an inexpensive BLE battery monitor designed to be installed on a 12v battery such as one commonly found in cars. The intended way to interact with this device is via a smartphone app provided by the manufacturer. This app is invasive and sends data such as phone specifications, GPS location and more to the manufacturer servers.

The previous model (BM2) has strong community support and many open source applications support it. Some applications also claim to support the BM6 but for various reasons, my device does not work with them.

Capturing the BLE traffic between my phone and the device

To figure out an alternate solution to access this device without the app, I unfortunately needed to install and use the app to capture the Bluetooth traffic between my phone and the device. To do this I used Android's built in Bluetooth snooping. There is a great video by Matt Brown showing how to do this.

I first started by enabling it in the developer options menu:

After enabling this setting, you must toggle Bluetooth off/on for it to take effect.

I then opened the BM6 app and interacted with the device. I saw the voltage and temperature readings, the two things I want to capture from the device. I then promptly uninstalled the app.

To get the log from my phone, I went to my computer and ran the "adb bugreport btsnoop" command. This command creates a bug report bundle which will result in a file named btsnoop.zip being downloaded to your computer. Extract the files from this archive and find the file: \FS\data\misc\bluetooth\logs\btsnooz_hci.log. This is the file containing the packets from the interaction with the BM6.

I used Wireshark to open the btsnooz_hci.log:

In order to filter to see more relevant items, I used the 'btatt' filter:

I noticed my phone sent a command "697ea0b5d54cf024e794772355554114" to UUID FFF3:

This command initiated notifications from the BM6 UUID FFF4:

Both the command and notifications appear to be encrypted.

Dumping and decompiling the Android app

To determine how the encryption worked, I went down the rabbit hole of decompiling the BM6 app. To do this I used the tool jadx-gui. When opening the APK in jadx, nothing interesting was showing up as the developer of the BM6 app used an obfuscator called libjiagu. This made things more difficult and required some additional tools:

The frida-server tool runs on the phone itself. You can use adb to push the tool and run it. Note that I ran frida-server from /data/local/tmp as that path does not have the noexec mount option. See below example:

 1> adb push frida-server-arm64 /sdcard
 2> adb shell
 3taimen:/ $ su -
 4taimen:/ #
 5taimen:/ # cp /sdcard/frida-server-arm64 /data/local/tmp
 6taimen:/ # chmod +x /data/local/tmp/frida-server-arm64
 7taimen:/ # pidof com.dc.bm6
 87934
 9taimen:/ # /data/local/tmp/frida-server-arm64
10{"type":"error","description":"Error: Unable to determine Runtime field offsets","stack":"Error: Unable to determine Runtime field offsets\n    at Ve (frida/node_modules/frida-java-bridge/lib/android.js:282:1)\n    at frida/node_modules/frida-java-bridge/lib/memoize.js:4:1\n    at De (frida/node_modules/frida-java-bridge/lib/android.js:191:1)\n    at Oe (frida/node_modules/frida-java-bridge/lib/android.js:16:1)\n    at _tryInitialize (frida/node_modules/frida-java-bridge/index.js:29:1)\n    at new _ (frida/node_modules/frida-java-bridge/index.js:21:1)\n    at Object.4../lib/android (frida/node_modules/frida-java-bridge/index.js:332:1)\n    at o (frida/node_modules/browser-pack/_prelude.js:1:1)\n    at frida/node_modules/browser-pack/_prelude.js:1:1\n    at Object.22.frida-java-bridge (frida/runtime/java.js:1:1)","fileName":"frida/node_modules/frida-java-bridge/lib/android.js","lineNumber":282,"columnNumber":1}

In the above output, I ran the 'pidof com.dc.bm6' command to get the process ID of the currently running BM6 app. It is important to have the app running in the foreground and add the process ID to the '-p' argument before running the frida-dexdump command from your computer. Also note the error above seemed to not matter. See below frida-dexdump example:

 1frida_dexdump\__main__.py -U -d -p 7934
 2
 3-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 4                                                        __      _     _                 _              _
 5                                                       / _|_ __(_) __| | __ _        __| | _____  ____| |_   _ _ __ ___  _ __
 6                                                      | |_| '__| |/ _` |/ _` |_____ / _` |/ _ \ \/ / _` | | | | '_ ` _ \| '_ \
 7                                                      |  _| |  | | (_| | (_| |_____| (_| |  __/>  < (_| | |_| | | | | | | |_) |
 8                                                      |_| |_|  |_|\__,_|\__,_|      \__,_|\___/_/\_\__,_|\__,_|_| |_| |_| .__/
 9                                                                                                                        |_|
10                                                                        https://github.com/hluwa/frida-dexdump
11-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
12
13Attaching...
14INFO:Agent:DexDumpAgent<Connection(pid=Session(pid=7934), connected:True), attached=True>: Attach.
15INFO:frida-dexdump:[+] Searching...
16INFO:frida-dexdump:[*] Successful found 28 dex, used 19 time.
17INFO:frida-dexdump:[+] Starting dump to 'C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed'...
18INFO:frida-dexdump:[+] DexMd5=0a05e5c93919224e5f93946435fb4ca4, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes.dex, DexSize=0x85f8
19INFO:frida-dexdump:[+] DexMd5=aaa48f34c5e91258d9b69756e7a5b818, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes02.dex, DexSize=0xb75fe4
20INFO:frida-dexdump:[+] DexMd5=08960ccb5f426b0e038c0641623113b2, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes03.dex, DexSize=0x671858
21INFO:frida-dexdump:[+] DexMd5=95b7f0cdd91ad906f891691eb5f2f770, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes04.dex, DexSize=0x1662000
22INFO:frida-dexdump:[+] DexMd5=ecfde09f0928271370b618702492a9ae, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes05.dex, DexSize=0x652780
23INFO:frida-dexdump:[+] DexMd5=9b91a021e2e3ba663b69b777c3c55e4c, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes06.dex, DexSize=0xfea000
24INFO:frida-dexdump:[+] DexMd5=a24b7e9d3a5a2aa935dcff82802bbe70, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes07.dex, DexSize=0x3462e8
25INFO:frida-dexdump:[+] DexMd5=a76ec363246b548a814f4b358fbb1f9c, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes08.dex, DexSize=0x991000
26INFO:frida-dexdump:[+] DexMd5=4a1e0a5d05bb3bf68d0d099eadd7fa73, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes09.dex, DexSize=0x63cfa8
27INFO:frida-dexdump:[+] DexMd5=8c96a035ab493c9f8fcf0c51c688c27c, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes10.dex, DexSize=0x644000
28INFO:frida-dexdump:[+] DexMd5=b8799a1564cb405d931585f2dcada4d8, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes11.dex, DexSize=0x85f8
29INFO:frida-dexdump:[+] DexMd5=329ac35fbb1d831319b73bf137f255d0, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes12.dex, DexSize=0xe00000
30INFO:frida-dexdump:[+] DexMd5=f1771b68f5f9b168b79ff59ae2daabe4, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes13.dex, DexSize=0x11c
31INFO:frida-dexdump:[+] DexMd5=516c46d03b35e67d082a1cd6ba103b09, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes14.dex, DexSize=0x2c747
32INFO:frida-dexdump:[+] DexMd5=16dabd97fd9091640bc52903feb6101e, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes15.dex, DexSize=0x677f60
33INFO:frida-dexdump:[+] DexMd5=00119458444ebf1f61da010d249b4d07, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes16.dex, DexSize=0x7c3cf0
34INFO:frida-dexdump:[+] DexMd5=961fa0c8774a917c303009c7260069ee, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes17.dex, DexSize=0x34c6f8
35INFO:frida-dexdump:[+] DexMd5=8547f776b536a69807bad562d36667f0, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes18.dex, DexSize=0x7c3c80
36INFO:frida-dexdump:[+] DexMd5=2bf3f84ac6e3b36488aca98fdd4bc02b, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes19.dex, DexSize=0x17968
37INFO:frida-dexdump:[+] DexMd5=1517d4e9494d4e86ab2d6aaefb211779, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes20.dex, DexSize=0x7c3820
38Set read permission for memory range: 0x779f83be10-0x779fc64000
39ERROR:frida-dexdump:[-] Error: access violation accessing 0x779fc00000
40    at <anonymous> (frida/runtime/core.js:145)
41    at memorydump (src/search.ts:41)
42    at call (native)
43    at <anonymous> (frida/runtime/message-dispatcher.js:11)
44    at o (frida/runtime/message-dispatcher.js:23): {'addr': '0x779f83be10', 'size': 6566880}
45Traceback (most recent call last):
46  File "c:\users\jeff\appdata\local\packages\pythonsoftwarefoundation.python.3.12_qbz5n2kfra8p0\localcache\local-packages\python312\site-packages\frida_dexdump\__main__.py", line 81, in dump
47    bs = self.agent.memory_dump(dex['addr'], dex['size'])
48         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49  File "C:\Users\jeff\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\local-packages\Python312\site-packages\frida_dexdump\agent\__init__.py", line 24, in memory_dump
50    return self._rpc.memorydump(base, size)
51           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52  File "C:\Users\jeff\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\local-packages\Python312\site-packages\frida\core.py", line 180, in method
53    return script._rpc_request(request, data, **kwargs)
54           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
55  File "C:\Users\jeff\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\local-packages\Python312\site-packages\frida\core.py", line 86, in wrapper
56    return f(*args, **kwargs)
57           ^^^^^^^^^^^^^^^^^^
58  File "C:\Users\jeff\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\local-packages\Python312\site-packages\frida\core.py", line 497, in _rpc_request
59    raise result.error
60frida.core.RPCException: Error: access violation accessing 0x779fc00000
61    at <anonymous> (frida/runtime/core.js:145)
62    at memorydump (src/search.ts:41)
63    at call (native)
64    at <anonymous> (frida/runtime/message-dispatcher.js:11)
65    at o (frida/runtime/message-dispatcher.js:23)
66INFO:frida-dexdump:[+] DexMd5=3eb26605dd165cfb88a86f99670b817e, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes21.dex, DexSize=0x3c41f0
67ERROR:frida-dexdump:[-] Error: access violation accessing 0x779fc00000
68    at <anonymous> (frida/runtime/core.js:145)
69    at memorydump (src/search.ts:41)
70    at call (native)
71    at <anonymous> (frida/runtime/message-dispatcher.js:11)
72    at o (frida/runtime/message-dispatcher.js:23): {'addr': '0x779f83c200', 'size': 6655044}
73Traceback (most recent call last):
74  File "c:\users\jeff\appdata\local\packages\pythonsoftwarefoundation.python.3.12_qbz5n2kfra8p0\localcache\local-packages\python312\site-packages\frida_dexdump\__main__.py", line 81, in dump
75    bs = self.agent.memory_dump(dex['addr'], dex['size'])
76         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
77  File "C:\Users\jeff\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\local-packages\Python312\site-packages\frida_dexdump\agent\__init__.py", line 24, in memory_dump
78    return self._rpc.memorydump(base, size)
79           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80  File "C:\Users\jeff\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\local-packages\Python312\site-packages\frida\core.py", line 180, in method
81    return script._rpc_request(request, data, **kwargs)
82           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
83  File "C:\Users\jeff\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\local-packages\Python312\site-packages\frida\core.py", line 86, in wrapper
84    return f(*args, **kwargs)
85           ^^^^^^^^^^^^^^^^^^
86  File "C:\Users\jeff\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\local-packages\Python312\site-packages\frida\core.py", line 497, in _rpc_request
87    raise result.error
88frida.core.RPCException: Error: access violation accessing 0x779fc00000
89    at <anonymous> (frida/runtime/core.js:145)
90    at memorydump (src/search.ts:41)
91    at call (native)
92    at <anonymous> (frida/runtime/message-dispatcher.js:11)
93    at o (frida/runtime/message-dispatcher.js:23)
94INFO:frida-dexdump:[+] DexMd5=1c461e42f485bfda25e2317f43e34792, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes22.dex, DexSize=0x3c3e00
95INFO:frida-dexdump:[+] DexMd5=2dbdf6fb91b6a0b4fbcaee7b72dac195, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes23.dex, DexSize=0x114a4
96INFO:frida-dexdump:[+] DexMd5=1524247b4b8ad3f5cbe5ebcf579c9bfd, SavePath=C:\Users\jeff\Projects\BM6 Battery Monitor\2.0\dexdump.unnamed\classes24.dex, DexSize=0x18000
97INFO:frida-dexdump:[*] All done...

Note: I had to try several older versions of the BM6 app before I found one that I could get the needed information from. The one I used was BM6 Android App APK version 2.0.0.

Now that I had some .dex files to investigate, I opened up jadx-gui. I first had to change the preferences to disable file checksums. That setting is found at Preferences > Plugins > Dex Input > Uncheck "verify dex file checksum before load".

Then I went to File > Open Files and selected all the files from the dexdump.unnamed folder created by frida-dexdump.

The interesting stuff was under the Source Code menu on the left side. The BM6 code showed up under com>p014dc.bm6. After digging around this folder structure a bit, I found a util folder that contained a file named AESUtil. Based on what I read previously about BM2 reverse engineering, things haven't changed much between the BM2 and BM6 as a similar file exists in the source code for the previous model. One significant difference is the AES key has changed slightly.

Encryption

With the info gained from the decompilation, I created the following functions to decrypt/encrypt data:

 1from Crypto.Cipher import AES
 2
 3key=bytearray([108, 101, 97, 103, 101, 110, 100, 255, 254, 48, 49, 48, 48, 48, 48, 57])
 4
 5def decrypt(crypted):
 6  cipher = AES.new(key, AES.MODE_CBC, 16 * b'\0')
 7  decrypted = cipher.decrypt(crypted).hex()
 8  return decrypted
 9
10def encrypt(plaintext):
11  cipher = AES.new(key, AES.MODE_CBC, 16 * b'\0')
12  encrypted = cipher.encrypt(plaintext)
13  return encrypted
14
15print("Message written to FFF3: ", decrypt(bytearray.fromhex("697ea0b5d54cf024e794772355554114")))
16print("Notification from FFF4: ", decrypt(bytearray.fromhex("5a7a41c3a57ca1fa9247f76557c5d618")))

The decrypted messages from earlier are:

1Message written to FFF3:  d1550700000000000000000000000000
2Notification from FFF4:  d155070017010004ab00000000020000

Similar to the BM2, the voltage and temperature can be extracted like this:

1decrypted = decrypt(bytearray.fromhex("5a7a41c3a57ca1fa9247f76557c5d618"))
2print("Temperature: ", int(decrypted[8:10],16))
3print("Voltage: ", int(decrypted[15:18],16) / 100)

Result:

1Temperature:  23
2Voltage:  11.95

Nice.

Connecting using nRF Connect

Now that I knew the encryption method, I attempted to get live data from the device. The Android app "nRF Connect" is a great tool for connecting to BLE devices.

First I ran a scan to find the BM6 and then tapped the "Connect" button to connect:

Then I enabled notifications for the FFF4 UUID by tapping the "three downwards pointing arrows" button next to it.

Then I used the above encryption function to encrypt the command "d1550700000000000000000000000000". This resulted in the encrypted command "697ea0b5d54cf024e794772355554114". I pushed the "upwards pointing arrow" button next to UUID FFF3 and put this encrypted value in the write box and tapped send:

The BM6 then started returning data from the FFF4 UUID:

In this case the decrypted data was:

1d155070018010004ac00000000020000
2Temperature:  24
3Voltage:  11.96

Python Application

With the knowledge gained from all this, I created a simple program in Python to grab the data and output in either plaintext or json formats. You can find it here:
https://github.com/JeffWDH/bm6-battery-monitor/

ESPHome

You can use an ESP32 with ESPHome to poll your BM6 battery monitor. This is useful as the BM6 is quite range limited. I've installed an ESP32 in my garage and it keeps my Home Assistant up to date with current battery status. You can find the code here:
https://github.com/JeffWDH/bm6-battery-monitor/tree/main/ESPHome

References

I wouldn't have been able to create this without the following resources:
https://github.com/KrystianD/bm2-battery-monitor/blob/master/.docs/reverse_engineering.md
https://doubleagent.net/bm2-reversing-the-ble-protocol-of-the-bm2-battery-monitor/
https://www.youtube.com/watch?v=lhLff9VACU4