This is a step by step guide into how I hacked my Bluetooth Low Energy (BLE) fitness tracker so I could control it from Linux.
This story started from a Facebook post I made talking about a problem of the absence of an API for fitness trackers, and why that’s not helping data scientists make cool stuff for them.
The post generated quite a lot of discussion and after caught the attention of my friend Volodymyr Shymanskyy who tried to help me and found some code on Github from Leo Soares for my fitness tracker MiBand 2. He tried to run it, but there were some problems with the connection. He fixed the problem in a few hours, commited the code and sent me a link.
That got me off to a head start. The code I had allowed me to connect to the MiBand 2 device, run notifications, and get a heart measurement. But that was not enough for what I wanted to do. I needed to get real-time raw data from the sensors to use it in my data science experiments. I wanted to make a gym exercise predictor.
So I decided to hack the hell out of my fitness tracker.
The fun begins.
I didn’t have any experience on working with BLE devices, so first I tried to understand how the technology worked and how the different pieces fitted together. As it turned out, it was pretty simple and here is a summary.
- Each BLE device has a few services
- Each service has some characteristics
- Some of the characteristics have descriptors (if the characteristic has more then one parameter or is of type read or notification).
- Some of the characteristics have only read/write access (For example, current time, battery status or revision info)
- Some of the characteristics are more complicated and work using a request/notification loop (For example real-time heart rate monitor or authorization)
This is basically all you need to know to start working with the fitness tracker.
You’ll also need two apps that will help you debug the BLE device - Wireshark protocol analyzer and BLE debugger. And you will need to get access to your Android phone developer options (Sorry iOS folks but you’ll have to find the equivalent options for the iOS platform).
To start, you will need to unpair your MiBand2 device from the phone app.
So let’s see now what services and characteristics our band has. Open BLE debugger and run a scan. You’ll see something like this.
Save your device MAC address somewhere, we will need it later. Now let’s connect to it and see what services and characteristics does it have.
From these two simple operations we’ve already got some useful information about our device. Another way of doing this is to use command line tools such as
To run a BLE scan from the command line:
sudo hcitool lescan
Connecting to your BLE device’s Mac address and retrieving services and descriptors:
sudo gatttool -b YOUR_MAC -I -t random > connect > primary > char-desc
In some cases BLE stack can glitch and you can turn your bluetooth device On/Off or run this command:
sudo hciconfig hci0 reset
Preparing to sniff data
To sniff data from our phone when it is communicating with the BLE device, we’ll need to enable Bluetooth logs in dev settings. To do this you need to turn on developer settings on your Android device first. To do that follow the steps below.
On Android 4.1 and lower, the Developer options screen is available by default. On Android 4.2 and higher, you must enable this screen as follows:
- Open the Settings app on the Android device.
- Select System. (This only needs to be done on Android 8.0 devices or higher)
- Scroll to the bottom and select About phone.
- Scroll to the bottom and tap Build number 7 times.
- Return to the previous screen to find Developer options near the bottom.
Now open dev settings and find “Enable Bleutooth HCI snoop log” and enable it. After this all Bluetooth communication from the device with any external Bluetooth device will be logged. The logs can be viewed by accessing a file called btsnoop_hci.log (in the case of my Android 7.0 device, they’re located at /mtklog/btlog/btsnoop_hci.log).
So now we will need to perform the following steps to get some info about how authentication (pairing) works.
- Turn on Bluetooth and HCI log.
- Pair your device to the Xiaomi Android App.
- Turn off Bluetooth.
- Download your btsnoop_hci.log to your PC.
- Open it with Wireshark.
- Find the first ATT protocol request that go to the handle 0x0055 (which represents Anhui Huami Information Technology Co, the company that makes wearable devices and owns the Xiaomi brand)
You’ll see something like this:
As can be seen from the figure above, the handle UUID values are:
- Pairing device
Main service UUID
- Auth Characteristic (Char) UUID
- Notification descriptor (Des) handle
0x2902 (it same for all stuff)
And here are the Authentication steps:
- Setting on auth notifications (to get a response) by sending 2 bytes request \x01\x00 to the Des.
- Send 16 bytes encryption key to the Char with a command and appending to it 2 bytes \x01\x00 + KEY.
- Requesting random key from the device with a command by sending 2 bytes \x02\x00 to the Char.
- Getting random key from the device response (last 16 bytes).
- Encrypting this random number with our 16 bytes key using the AES/ECB/NoPadding encryption algorithm (from Crypto.Cipher import AES) and send it back to the Char (\x03\x00 + encoded data)
Real Time Data
The Auth process was a little complicated and to add to the problems, the heart rate monitor was turning off after 15 sec. Here are the UUID values that were obtained from the device:
- Hardware service (HRDW) UUID
- Heart Monitor Service (HMS) UUID
- Heart Rate Measure Characteristic (HRM) UUID
- Heart Monitor Control Characteristic (HMC) UUID
- Sensor Characteristic (SENS) UUID
- Notification descriptor (DES) handle
0x2902 (it’s the same always)
And here is how a few common actions are carried out:
- To turn off the current hear monitor measurement.
Send a request to HMC \x15\x02\x00 for one-shot measurements.
Send a request to HMC \x15\x01\x00 for continous measurements.
- Enabling Gyroscope and Heart raw data by sending a command to SENS \x01\x03\x19
- Enabling notification for HRM by writing a request to DES \x01\x00
- Start continuous heart measurements by sending a request to HMC \x15\x01\x01
- Sending command to SENS \x02 (don’t know why this is need)
- Then while getting notifications, every 12 seconds we need to send a ping with the value \x16 to the HCM
Understanding how data is parsed was not very interesting , as you needed to find out how to unpack data that comes from the device.
Some part of it can be parsed from logs, and some cannot.
Finding the right packets and encoding can take some time. In my case I tried to find similar byte occurrences in packets next to each other, some repeating within a packet.
Raw heart: 02102d8c348c448c458c3d8c428c488c 16 Raw heart: 0218468c418c3d8c468c3f8c398c418c 16 Realtime heart: 93 Raw heart: 0220408c448c3f8c428c498c3c8c3d8c 16 Raw heart: 02283d8c398c488c3e8c468c488c328c 16 Realtime heart: 99 Raw heart: 0230438c408c378c3a8c318c458c388c 16 Realtime heart: 102 Raw heart: 02404f8c408c458c428c4d8c558c4d8c 16 Raw heart: 02483e8c3b8c3f8c348c398c318c428c 16 Realtime heart: 98 Raw heart: 02504c8c428c5e8c4f8c588c498c558c 16 Raw heart: 0258478c458c3c8c4e8c3f8c468c4d8c 16 Realtime heart: 100 Raw heart: 0260518c4d8c4f8c4b8c4f8c528c458c 16 Raw heart: 0268408c3f8c538c4d8c408c548c598c 16 Realtime heart: 102 Raw heart: 0278418c508c4e8c548c588c468c498c 16 Raw heart: 0280368c328c2e8c3c8c338c308c3f8c 16 Realtime heart: 101
He we can see a clear pattern with the string 368c 328c 2e8c 3c8c 338c 308c 3f8c repeating and the packet being 16 bytes in length. If we unpack this with 2 bytes unsigned short then we get 7 raw measurements of the heart sensor.
Also we see that second byte just iterating, and I think it’s just time difference between measurements (responses)
Raw gyro: 01de49ffd9ff3c004cffd8ff3b004dffdcff4400 Raw gyro: 01df4cffd6ff44004dffd8ff40004cffd1ff4700 Raw gyro: 02e1103231323d3274328e329632af32c732cf32 Raw gyro: 01e34fffd7ff56004bffc7ff590049ffccff4c00 Raw gyro: 01e443ffccff43004effcdff40005bffd4ff4c00 Raw gyro: 01e558ffc9ff5f005effbfff66005fffb0ff5900 Raw gyro: 01e64cffacff60005cffa7ff410066ffc9ff4600 Raw gyro: 01e760ffdcff4b0051ffe4ff4f0034ffdeff5300 Raw gyro: 02e903365c36813663361036543688374139fe3a Raw gyro: 01eb4bffc3ff50004fffc1ff430047ffbbff4100 Raw gyro: 01ec3effb2ff3c0050ffbfff560047ffccff7300 Raw gyro: 01ed4fffe0ff78005cffebff8e0056fff6ff8300 Raw gyro: 01ee7efffbffa1008bff0f00bc00b1ff1900b800 Raw gyro: 01ef9bff0c00d10095fff3ffd600b7ff0800df00 Raw gyro: 02f12445314600479e473348aa481c499749244a Raw gyro: 01f3c3ff1600fe00beff1800f200a6ff0800e700 Raw gyro: 01f4a9fff8ffd300a7fff3ffd700a9fff1ffdf00 Raw gyro: 01f5b1fff8ffe800b4fff1fff700acfffcffef00 Raw gyro: 01f67ffff7ffc0006bfff4ffb00078ffe9ffb600 Raw gyro: 01f786ffecffc0006ffff0ffbc0060fff1ffc000 Raw gyro: 02f9ca4cbb4c784c964ca84c784c854c444c1b4c Raw gyro: 01fb7cff0f00bb007eff2700ae0083ff30009800 Raw gyro: 01fc79ff1800b00076ff0f00bc0068ff0900d900 Raw gyro: 01fd78ff07000c01f6fffbff19011c000b00f600 Raw gyro: 01fe4b001100d30054000700c3004300efffeb00 Raw gyro: 01ff1f00d0ff1701fbffe8ff1b01e3ffffff1101 Raw gyro: 0201214b014bec4ad04aba4acb4abe4aba4abd4a Raw gyro: 0103efffecfffc00e3fff3fff300defff3fffc00 Raw gyro: 0104e3fff0fff400e6ffefff0301dbffe9ff0c01 Raw gyro: 0105e3fff0ff0301e6ffe6fffc00dcffecfffc00 Raw gyro: 0106dffff0fff700dbffeefff600d6fff0fff400 Raw gyro: 0107dfffecffff00e1fff0ff0301defff3fffc00
As for the Gyroscope, it was a bit harder. But my thoughts were that it should be packed in a similar way as the heart data, but in this case we have 3 measurements for each gyro axis which should be signed and the packet length is 20 bytes. So 12 x,y,z measurements will not cover all packages, but 3 will, leaving the first 2 bytes (the same as in the previous packet). So i tried this and it worked. 😃
You can find the code on Github as always, with an example of usage provided. It’s pretty simple, so I don’t need to elaborate on it further in this article.