Nov 30, 2021
13 mins read
Symbian OS, Android, Radio Frequencies, BTSNOOZ, BTSNOOP, and getting kicked in the teeth. Below follows a chronicling of deciding to explore Bluetooth by hacking the firmware for a Fitbit Smartwatch and realizing I was in way over my head and slowly trying to regain any hope of understanding.
Things I knew about bluetooth at the start of this generally consisted of “it’s like wifi, but if you didn’t brush your teeth after eating a blue raspberry snowcone”
I’ve had a Fitbit Charge 2 smartwatch for a while. It used to be used as a means to gently wake me up when I’d get alerts while working an OnCall week so that my wife didn’t make me sleep on the couch. It had remain un-charged for almost a year now and I dug it out while cleaning out my stuff.
I mean seriously, how hard can it be? It’s just a small microcontroller that connects to a smartphone app via Bluetooth Low Energy and pushes an update.
I know there’s almost certainly a firmware update available not only because it’s been turned off for over a year, but also because on boot it shows the firmware version which is at least a major version behind everything shown in screenshots on the website.
So a plan begins to form:
This would ideally give me infinite tries to understand how the fitbit protocol works in a general sense while not allowing the watch to grab updated firmware until I am ready.
I decided to use the Android fitbit app since Android is a bit more flexible for debugging stuff so I pull out a phone I picked up brand new a while back specifically for messing with Android stuff. It’s a Moto g(7) Play running Android 10. I open settings and navigate to:
…and turn on the developer toggle. This allows me ADB access to the device. Essentially a shell.
Conveniently, there’s an option right below Developer mode that says “Enbale Bluetooth HCI snoop log” which sounds like exactly what I want.
I flip the toggle and do some Googling. It turns out that in order to access the location the log is stored on my phone via adb, I need to root my phone. While this is literally a throwaway device, it seems like overkill for the task at hand (I regret this later).
Surely there has to be a way to view the developer logs without having a rooted android device, I mean the option is right there in the settings menu!
I got it! Beautiful isn’t it?
adb devices && \
adb bugreport bugreport && \
unzip "bugreport.zip" -d "./bugreport" && \
./btsnooz ./bugreport/bugreport*.txt > BTSNOOP.log && \
rm -rf ./bugreport && \
rm bugreport.zip && \
btmon -r BTSNOOP.log
Explanation –>
adb devices
This enumerates the Android devices connected via USB on a system and starts the adb server needed for interaction. If the device is already connected and the servers started, this just logs to the console.
adb bugreport bugreport
This uses adb’s bugreport feature to export all debug logs as a zip file. The first instance of “bugreport” is the command itself, while the second “bugreport” instructs adb to write the file as “bugreport.zip” on the host OS (my computer). This contains all logs, not just the HCI logs.
unzip "bugreport.zip" -d "./bugreport"
This unzips the contents of the .zip file to a directory named bugreport
./btsnooz ./bugreport/bugreport*.txt > BTSNOOP.log
This uses a helper file called “btsnooz” to parse the bugreport{Phone_Name}.txt and export a btsnoop log file. As simple as the previus sentence seems, I assure you it is far more chaotic than it seems.
btsnooz is a custom format designed to be included in bugreports.
It can be described as:
base64 {
file_header
deflate {
repeated {
record_header
record_data
}
}
}
where the file_header and record_header are modified versions of
the btsnoop headers.
BTSNOOZ is a custom file format for HCI logs. It is effectively a re-encoding of the BTSNOOP file format in base64 with some gzip compression mixed in. Oh also, there’s 2 different version of BTSNOOZ in use in the Android ecosystem. BTSNOOP is at least semi-well-defined as it has an RFC associated (RFC1761) that defines the “snoop” format, but not the “bt” part of BTSNOOP.
So anyways, there’s a tool called “btsnooz” that is provided as part of the Android ecosystem that converts from BTSNOOZ (v1/2) –> BTSNOOP which can be found here:
rm -rf ./bugreport && rm bugreport.zip
Generic cleanup
btmon -r BTSNOOP.log
Use the btmon tool provided by the linux BlueZ package to read the btsnoop file
< HCI Command: Read BD ADDR (0x04|0x0009) plen 0 #14 0.322992
> HCI Event: Command Complete (0x0e) plen 10 #15 0.323401
Read BD ADDR (0x04|0x0009) ncmd 1
Status: Success (0x00)
Address: **:**:**:**:**:** (Motorola Mobility LLC, a Lenovo Company)
After figuring out that you need to disable/enable the bluetooth stack on the Android device for HCI logging to actually start which took me way more time to figure out than it should have, I’m in action!
I use the Fitbit android app to connect to the watch and and submit an incorrect pairing pin of “0000” 4 times then export the HCI log to my computer for dissecting.
I have a btsnoop.log file and I can parse it’s contents in btmon as well as Wireshark. I open it in Wireshark and there is… a lot of shit going on of which I understand nearly nothing. Also it’s using Bluetooth Low Energy (BLE/BTLE).
After doing some reading with the objective of identifying only packets/data that is being sent to the watch I come across some useful Wireshark filters
btatt.opcode in { 0x12 0x13 0x52 0xD2 0x16 0x17 0x18 0x19}
Opcodes were identified in part by reading a header (.h) file from a BlueZ tool for ATT and looking for anything that had “write” in it.
#define BT_ATT_OP_WRITE_REQ 0x12
#define BT_ATT_OP_WRITE_RSP 0x13
#define BT_ATT_OP_WRITE_CMD 0x52
#define BT_ATT_OP_SIGNED_WRITE_CMD 0xD2
#define BT_ATT_OP_PREP_WRITE_REQ 0x16
#define BT_ATT_OP_PREP_WRITE_RSP 0x17
#define BT_ATT_OP_EXEC_WRITE_REQ 0x18
#define BT_ATT_OP_EXEC_WRITE_RSP 0x19
This Wireshark filter made it far more readable and the result was a fairly logical stream of data consisting of:
There is clearly some semblance of a pattern here! What does it mean? I have no idea.
Let’s orchestrate software to yeet these values at the watch and see what happens! Theorhetically it should cause the watch to buzz and rotate the 4 digit code shown on the screen that happens when an incorrect pin is submitted in the pairing process. That seems like a way easier way to confirm that I’m on the right track than trying to reverse engineer the protocol blind when I have no idea what these opcodes actually do (I regret this later).
First I need hardware that supports BTLE on a computer. I tried to use a Raspi 4, but couldn’t get it to work with the on-board bluetooth. Then I plugged in a USB Bluetooth 4 (LE) adapter I had laying around. lsusb
output provided below for replication:
Bus 001 Device 003: ID 0a5c:21e8 Broadcom Corp. BCM20702A0 Bluetooth 4.0
Running the following command on linux to perform a BTLE scan with the USB adapter worked!
sudo hcitool -i hci1 lescan
However, as you can see above I had to specify the HCI device since the primary (hci0) is the on-board bluetooth adapter. This caused some problems when trying to use the adaper programatically.
Editing /boot/config.txt
and adding the below config and rebooting successfully disables the on-board bluetooth and causes the USB adapter to be enumerated as the primary/hci0 interface.
# Disable Bluetooth
dtoverlay=disable-bt
I shortly find some software that should allow me to replay btsnoop files! The tool is BLE-Replay from the BLESuite tools and according to the documentation si should be able to replay a btsnoop.log file to a device!
python ble-replay.py -p btsnoop.log -r
Unfortunately after resolving all of the broken dependencies the script crashes on attempting to parse the btsnoop.log
file. I fiddled with a bit, but eventually just moved on and decided to write my own tool (I regret this later).
I choose to use Golang to write the tool because it’s the language I’m trying to improve my skill with and at this point I’m so far in the deep end that I’m basically going backwards, difficulty doesn’t matter.
I decide to use tinygo-org/bluetooth because I’ve used TinyGo libraries in the past (See: META Gameboy Advance Blog) and it’s support chart for Linux+BlueZ has a lot of green checkmarks ✅.
I’m a sucker for green checkmarks. Just look at em! Consider me sold.
After slopping together some code from the provided examples and making some adjustments to display values in the same format as an app I’d been using for exploration (nRF Connect for iOS/Android), I have the following functional code that can connect and discover services.
// This example scans and then connects to a specific Bluetooth peripheral
// and then displays all of the services and characteristics.
//
// To run this on a desktop system:
//
// go run ./asdf.go EE:74:7D:C9:2A:68
//
package main
import (
"strconv"
"tinygo.org/x/bluetooth"
"tinygo.org/x/bluetooth/rawterm"
)
var adapter = bluetooth.DefaultAdapter
func main() {
//wait()
println("Enabling BTLE Stack...")
// Enable BLE interface.
must("enable BLE stack", adapter.Enable())
ch := make(chan bluetooth.ScanResult, 1)
// Start scanning.
println("scanning...")
err := adapter.Scan(func(adapter *bluetooth.Adapter, result bluetooth.ScanResult) {
if result.Address.String() == connectAddress() {
println("found device:", result.Address.String(), result.RSSI, result.LocalName())
adapter.StopScan()
ch <- result
}
})
var device *bluetooth.Device
select {
case result := <-ch:
println("Address:", result.Address)
device, err = adapter.Connect(result.Address, bluetooth.ConnectionParams{})
if err != nil {
println(err.Error())
return
}
println("connected to ", result.Address.String())
}
// get services
println("discovering services/characteristics")
srvcs, err := device.DiscoverServices(nil)
must("discover services", err)
var indicator bluetooth.DeviceCharacteristic
for _, srvc := range srvcs {
println("- service", srvc.UUID().String())
println("- service", "0x"+strconv.FormatInt(int64(srvc.UUID().Get16Bit()), 16))
chars, err := srvc.DiscoverCharacteristics(nil)
if err != nil {
println(err)
}
for _, char := range chars {
println("-- characteristic", char.UUID().String())
println("-- characteristic", "0x"+strconv.FormatInt(int64(char.UUID().Get16Bit()), 16))
if char.UUID().String() == "00002a05-0000-1000-8000-00805f9b34fb" {
indicator = char
}
}
}
// Enable notifications to receive incoming data.
err = indicator.EnableNotifications(func(value []byte) {
for _, c := range value {
rawterm.Putchar(c)
}
})
if err != nil {
println("Failed to enable TX notifications:", err.Error())
return
}
done()
}
func must(action string, err error) {
if err != nil {
panic("failed to " + action + ": " + err.Error())
}
}
Now I just need to be able to read in a btsnoop.log
file and write the necessary commands!
I’ve been eying Kaitai Struct for a while now. At a high level it allows you to write a KSY
format description file and use their compiler to produce a library for a variety of code languages that is capable of parsing a file format, Golang included!
After stumbling a bit, I revert back to writing the parsing proof-of-concept in Python so that I can easily confirm the results using the Scapy library.
I start writing the KSY
file and get familiar with the syntax (it’s a bit like YAML) regualrly stealing constants from the reliable (.c/.h) source files from the monitor
(btmon) tool from the BlueZ package
The resulting btsnoop.ksy
file is shown below:
meta:
id: btsnoop
file-extension: .log
endian: be
seq:
- id: header
type: header
- id: packet
type: packet
repeat: eos
types:
header:
seq:
- id: magic
contents: [0x62, 0x74, 0x73, 0x6e, 0x6f, 0x6f, 0x70, 0x00]
- id: version
type: u4
- id: datalink_type
type: u4
enum: datalink_types
packet:
seq:
- id: original_length
type: u4
- id: included_length
type: u4
- id: packet_flag
type: u4
enum: packet_flags
- id: cumulative_drops
type: u4
- id: timestamp_ms
type: u8
- id: packet_type
type: u1
enum: packet_types
- id: data
size: included_length - 1
enums:
datalink_types:
0: invalid
1001: proto_hci
1002: proto_uart
1003: proto_bcsp
1004: proto_3wire
2001: proto_monitor
2002: proto_simulator
packet_flags:
0: host_to_controller_data
1: controller_to_host_data
2: host_to_controller_command
3: controller_to_host_event
packet_types:
1: hci_cmd
2: acl_data
3: sco_data
4: hci_evt
When compiled to a python library it can be imported into a python script along with scapy to validate that correct parsing has occurred
import btsnoop
from pprint import pprint
from scapy.all import *
from scapy.layers.bluetooth4LE import BTLE_DATA
g = btsnoop.Btsnoop.from_file("0000_2.log")
print("magic:", g.header.magic)
print("version:", g.header.version)
print("datalink_type:", g.header.datalink_type)
for p in g.packet:
pprint(vars(p))
sp = BTLE_DATA(p.data)
sp.show()
Running the above script allows us to confirm that we can successfully parse the btsnoop
file format and identify the needed attributes for replaying the traffic. I simply need to compile the ksy file for Golang and hook up all the needed fiddly-bits.
What started as “I’m going to hack a smartwatch’s firmware” has resulted in a tremendous backslide in progress, but I’ve learned a ton. I also may have created the only documented way to convert BTSNOOZ(v1/2) into a BTSNOOP file that can be parsed in C++/STL, C#, Go, Java, JavaScript, Nim, Perl, PHP, Python, Ruby
through use of Kaitai Struct!
I’ll count that as a win even if I’m further from my goal than when I started. Stay tuned for “Adventures in Bluetooth Part 2 –> X”
-remy
Sharing is caring!