Adventures in Bluetooth: Part 1

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”

  • Side Note: I’m _mattata on Twitter, you should give me a follow. I do stuff like this often.

So it Begins…

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 should hack this with custom firmware!

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.

firmware version

So a plan begins to form:

  1. Reset watch by pressing and holding activity button while on the charger
  2. Start the pairing process using the Official Fitbit app
  3. Intentionally submit an incorrect pairing code so that I can repeat this as many times as I want
  4. Record this bluetooth traffic somehow

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.

Capturing Bluetooth Logs on Android

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:

  • System –> Developer Options

…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.

android snoop log

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).

Accessing Bluetooth Logs on Android without Root

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!

  • Insert ~3 hours of reading and trial/error attempts here.

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)

Reverse engineering the Fitbit protocol

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).

wireshark btsnoop

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:

  1. Charge 2 –> moto g(7): Client Characteristic Configuration
  2. moto g(7) – Charge 2: 9 Write Commands to Handle 0x0010 with values:
    • c00a0a
    • c00400
    • c001
    • c00a0a
    • c00401
    • c001
    • c00a0a
    • c0100d
    • c001
  1. Charge 2 –> moto g(7): Client Characteristic Configuration
  2. moto g(7) – Charge 2: 6 Write Commands to Handle 0x0010 with values:
    • c00a0a
    • c00400
    • c001
    • c00a0a
    • c00401
    • c001

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).

Attempting to replay BTLE Packets

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).

Writing a tool to replay BTLE packets

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.

tinygo support

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!

Writing a BTSNOOP Parser

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.

btsnoop ksy

Fin

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!