Reversing Treadmill Bluetooth - Analyzing Writes
This is part of a few posts on trying to understand my treadmill’s Bluetooth.
Finding a pattern
Now that we have some data, let’s look for sequences that contain commands (e.g. increase the speed) and ignore everything else. First, there are two repetitive lines (which usually alternated); these may be a heartbeat/keep-alive, but they definitely don’t contain our data:
fe021403 001202040210041002000a1b9430000040500080 ff02182700000000000000000000000000000000
fe021903 001202040215041502000f800a41000000000000 ff07000000810010860000000000000000000000
Removing those cuts out a lot of noise:
0100
fe020802 ff08020402040204818700000000000000000000
fe020802 ff08020402040404808800000000000000000000
fe020802 ff08020402040404889000000000000000000000
fe020a02 ff0a0204020602068200008a0000000000000000
fe020a02 ff0a0204020602068400008c0000000000000000
fe020802 ff08020402040204959b00000000000000000000
fe022c04 0012020402280428900701cec4b0aaa2a8949696 0112aca8a2bad0dccefe14003a52786486a6fc18 ff08324aa0880200004400000000000000000000
fe021903 001202040215041502000f001000d81c480000e0 ff070000001000086e0000000000000000000000
fe021903 0012020402150415020e00000000000000000000 ff070000001001003a0000000000000000000000
fe021703 0012020402130413020c00000000000000000000 ff0500800000a500000000000000000000000000
fe021703 0012020402130413020c00000000000000000000 ff0500800000a500000000000000000000000000
fe021703 0012020402130413020c00000000000000000000 ff0500800000a500000000000000000000000000
fe021703 0012020402130413020c00000000000000000000 ff0500800000a500000000000000000000000000
fe022c04 0012020402280428900701cec4b0aaa2a8949696 0112aca8a2bad0dccefe14003a52786486a6fc18 ff08324aa0880200004400000000000000000000
fe022003 00120204021c041c020900004002184000008030 ff0e2a0000c720580200b400580200ee00000000
fe021102 ff110204020d040d02020310a00000000a00d200
fe021102 ff110204020d040d02020310a00000000200ca00
fe020d02 ff0d020402090409020101b10000c20000000000
fe020d02 ff0d020402090409020101c10000d20000000000
fe020d02 ff0d020402090409020101d10000e20000000000
fe020d02 ff0d020402090409020101c10000d20000000000
fe020d02 ff0d020402090409020101b10000c20000000000
fe020d02 ff0d020402090409020101a00000b10000000000
fe020d02 ff0d020402090409020102320000440000000000
fe020d02 ff0d020402090409020102640000760000000000
fe020d02 ff0d020402090409020102960000a80000000000
fe020d02 ff0d020402090409020102c80000da0000000000
fe020d02 ff0d020402090409020102fa00000c0000000000
fe020d02 ff0d0204020904090201022c01003f0000000000
fe020d02 ff0d020402090409020200100400250000000000
fe020f02 ff0f0204020b040b020202100000010026000000
fe021703 0012020402130413020c00000000000000000000 ff0500800100a600000000000000000000000000
fe021703 0012020402130413020c00000000000000000000 ff0500800100a600000000000000000000000000
The data doesn’t make sense yet, but we do know that we issued ~10 commands.
That makes the chunk in green interesting - the majority of the lines (13)
start with fe020d02, followed by data that looks roughly similar. Here it is
broken into byte-sized chunks:
fe020d02 ff 0d 02 04 02 09 04 09 02 01 01 b1 00 00 c2 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 01 01 c1 00 00 d2 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 01 01 d1 00 00 e2 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 01 01 c1 00 00 d2 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 01 01 b1 00 00 c2 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 01 01 a0 00 00 b1 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 01 02 32 00 00 44 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 01 02 64 00 00 76 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 01 02 96 00 00 a8 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 01 02 c8 00 00 da 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 01 02 fa 00 00 0c 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 01 02 2c 01 00 3f 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 02 00 10 04 00 25 0000000000
Randomly staring at those numbers surfaces the pattern in green - two of the values increase and then decrease. This must be ‘the data’, so let’s try to figure out what it means.
Unpacking the packets
Speed
Looking at just the first value:
In [1]: for b in 'b1', 'c1', 'd1', 'c1', 'b1', 'a0':
...: print(int(b, 16))
...:
177
193
209
193
177
160
These numbers aren’t immediately familiar. However thinking a bit, the numbers on the treadmill have decimals, so multiplying by 100 would make it easier to store as integers. Let’s try dividing by 100:
In [2]: for b in 'b1', 'c1', 'd1', 'c1', 'b1', 'a0':
...: print(int(b, 16) / 100)
...:
1.77
1.93
2.09
1.93
1.77
1.6
This looks a little more in the ballpark but still not right. However in any
domain with Metric vs. Imperial units, why not try the other? Multiplying by
0.621 converts kilometers to miles, and with some rounding:
In [3]: for b in 'b1', 'c1', 'd1', 'c1', 'b1', 'a0':
...: print(round((int(b, 16) / 100) * 0.621, 1))
...:
1.1
1.2
1.3
1.2
1.1
1.0
Alright, granted our sample is not great, but that exactly matches what we did (in increments of 0.1 MPH we went from 1.0 -> 1.3 -> 1.0).
As for the other values, unclear - they mirror the pattern but are always
0x10 larger. Trying to convert them to/from Imperial/Metric doesn’t yield
anything interesting. Definitely seems related, but rather than spin our wheels
let’s keep going - we seem to have found the speed and that other value is
always 0x10 higher, so we can just blindly include that when we make our own
command.
Incline
Let’s try the incline. We increased the incline from 0.0 to 3.0 in increments of 0.5, so let’s look for that in the next sequence:
fe020d02 ff 0d 02 04 02 09 04 09 02 01 02 32 00 00 44 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 01 02 64 00 00 76 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 01 02 96 00 00 a8 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 01 02 c8 00 00 da 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 01 02 fa 00 00 0c 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 01 02 2c 01 00 3f 0000000000
The highlighted bytes are in the same position as those we found for the speed, so let’s try parsing in the same way:
In [10]: for b in '32', '64', '96', 'c8', 'fa', '2c':
...: print(int(b, 16))
...:
50
100
150
200
250
44
Close! After dividing by 100 that would match the 0.5 increases, except for
the last one. Thinking about it, a byte can only represent 0-255, so how would
you represent an incline of 3.0? One way is to use two bytes - if we assume
the 01 in red above is also part of the value (little endian):
In [11]: for b in '32', '64', '96', 'c8', 'fa', '012c':
...: print(int(b, 16))
...:
50
100
150
200
250
300
Splendid - it looks like we can now identify increases/decreases in
speed/incline. We can also assume that the byte just before the value indicates
the command (0x01 == SPEED, 0x02 == INCLINE, etc.):
fe020d02 ff 0d 02 04 02 09 04 09 02 01 01 a0 00 00 b1 0000000000
fe020d02 ff 0d 02 04 02 09 04 09 02 01 02 32 00 00 44 0000000000
Naturally we have no way of knowing if this is right, but based on what we’ve seen it is hopefully not wrong? Who knows, let’s keep going.
Understanding what’s left
Now…the rest. We now know how to identify some of the data within a packet, so let’s see if we can figure out the rest using the advanced ‘make random guesses’ approach. Let’s go back to one of the commands we just deciphered and mark the bytes we know:
fe020d02 ff 0d 02 04 02 09 04 09 02 01 01 a0 00 00 b1 0000000000-------- -- -- -- -- -- -- -- -- -- -- CC DD DD DD DD DDDDDDDDDD
Where:
CC: command DD: data
For the others, we can start at the beginning. First thing that pops out - the
first two bytes (fe02) are always the same for every packet in the file. Let’s
mark this as the Beginning of a sequence (and
while we’re at it, we can use the same color to indicate the beginning of the
End packet):
fe020d02 ff 0d 02 04 02 09 04 09 02 01 01 a0 00 00 b1 0000000000BBBB---- EE -- -- -- -- -- -- -- -- -- CC DD DD DD DD DDDDDDDDDDBB/EE: beginning/end of a packet CC: command DD: data
Next we can start to think about the protocol. Let’s make a few assumptions:
- Serial communication: only one command/sequence at a time is sent, and the packets are always in order; in other words, the treadmill would probably be befuddled if you sent two commands in parallel and interwove their packets
- Commands require no previous knowledge: a command can be issued at any time, without any knowledge of the existing state (maybe not 100% true but makes starting the analysis easier)
- The first packet contains info about what follows: the first packet must contain some basic info like ‘how many packets follow this one’
Armed with these assumptions, let’s go spelunking for the number of packets. Fortunately, the first packet is pretty small and we already identified half of the bytes. Grabbing a few samples of varying length and just looking at the unknown bytes of the first packet, we may have a winner:
fe020802 ff08020402040204818700000000000000000000
fe020a02 ff0a0204020602068200008a0000000000000000
fe022c04 0012020402280428900701cec4b0aaa2a8949696 0112aca8a2bad0dccefe14003a52786486a6fc18 ff08324aa0880200004400000000000000000000
fe021903 001202040215041502000f001000d81c480000e0 ff070000001000086e0000000000000000000000
Scanning the rest of the file, this byte always matches the number of packets in the sequence, so let add the Number of packets to our collection:
fe020d02 ff 0d 02 04 02 09 04 09 02 01 01 a0 00 00 b1 0000000000BBBB--NN EE -- -- -- -- -- -- -- -- -- CC DD DD DD DD DDDDDDDDDDBB/EE: beginning/end of a packet NN: number of packets CC: command DD: data
Now for that last byte. Thinking about things from the treadmill side, how does it know which bytes contain the data to process? It knows how many packets to expect, but within those packets which data is useful? For example, here is the command to set the speed to 1.0:
fe020d02 ff0d020402090409020101a00000b10000000000
Look at the end - those 0x00’s are very likely just padding used to make the
packet length consistent. Why? One reason is it makes processing simpler - you
can allocate (or reuse) a fixed amount of memory for a packet since you know
they will all be the same length. This means the treadmill can receive a packet
and dump the entire thing into fixed-length buffer for further processing. You
may reasonably be thinking ‘But why can’t it just stop at the first NULL byte?
There are plenty of them!’. However note the packet above - there are multiple
NULL bytes between a0 and b1, which means NULL bytes can be present in the
actual data.
Therefore there must be a way for the treadmill firmware to know exactly how many ‘useful’ bytes are in a sequence/packet. Looking at a few sequences, that remaining unknown byte in the first packet looks promising:
fe020802 ff 08 020402040204818700000000000000000000
fe020802 ff 08 0204020602068200008a0000000000000000
fe021903 00 12 02040215041502000f001000d81c480000e0
ff 07 0000001000086e0000000000000000000000
It also looks like the number reappears in the last packet, which makes sense - packets arrive independently, so each one needs to tell the firmware how many bytes it contains.
Note the last packets - the number in the first packet (0x19) is the sum of
the sizes of the following packets (0x12 and 0x07). This pattern holds for
the rest of the sequences in the file, so we can now identify the Size of a sequence/its packets:
fe020d02 ff 0d 02 04 02 09 04 09 02 01 01 a0 00 00 b1 0000000000BBBBSSNN EE SS -- -- -- -- -- -- -- -- CC DD DD DD DD DDDDDDDDDDBB/EE: beginning/end of a packet SS: size of a sequence/packet NN: number of packets CC: command DD: data
Moving on! There is another pattern that appears in every single second packet in a sequence, regardless of how many packets there are:
fe020802 ff08020402040204959b00000000000000000000
fe022c04 0012020402280428900701cec4b0aaa2a8949696 0112aca8a2bad0dccefe14003a52786486a6fc18 ff08324aa0880200004400000000000000000000
...
fe021903 001202040215041502000f001000d81c480000e0 ff070000001000086e0000000000000000000000
fe022003 00120204021c041c020900004002184000008030 ff0e2a0000c720580200b400580200ee00000000
fe021102 ff110204020d040d02020310a00000000a00d200
...
fe021703 0012020402130413020c00000000000000000000 ff0500800100a600000000000000000000000000
fe021703 0012020402130413020c00000000000000000000 ff0500800100a600000000000000000000000000
Not too sure about that one, so let’s just consider it a Constant for now:
fe020d02 ff 0d 02 04 02 09 04 09 02 01 01 a0 00 00 b1 0000000000BBBBSSNN EE SS CC CC CC -- -- -- -- -- CC DD DD DD DD DDDDDDDDDDBB/EE: beginning/end of a packet SS: size of a sequence/packet CC: some constant value NN: number of packets CC: command DD: data
At this point we’re still in ‘Look for simple patterns without thinking too deeply’ mode. Fortunately, some of the remaining five bytes appear to have such a pattern:
fe022c04 0012 020402 28 04 28 900701cec4b0aaa2a8949696 0112aca8a2bad0dccefe14003a52786486a6fc18 ff08324aa0880200004400000000000000000000
fe022003 0012 020402 1c 04 1c 020900004002184000008030 ff0e2a0000c720580200b400580200ee00000000
fe021102 ff11 020402 0d 04 0d 02020310a00000000a00d200
With little squinting (and applying some logic/guessing from earlier), this looks like it describes the size of the ‘data’. For example, look at these two commands:
fe021102 ff11 020402 0d 04 0d 02 02 03 10 a0 00 00 00 0a 00 d2 00
fe020d02 ff0d 020402 09 04 09 02 01 01 c1 00 00 d2 00 00 00 00 00
Notice how the highlighted value matches the number of bytes that follow. This seems to apply to three-request sequences as well (note the header bytes of the third packet are ignored - they already have a byte count):
fe021903 0012 020402 15 04 15 02 00 0f 00 10 00 d8 1c 48 00 00 e0
ff07 00 00 00 10 00 08 6e 00 00 00 00 00 00 00 00 00 00 00
For a four-request sequence, looks to be the same:
fe022c04 0012 020402 28 04 28 900701cec4b0aaa2a8949696
0112aca8a2bad0dccefe14003a52786486a6fc18
ff08 324aa0880200004400000000000000000000
So let’s call that the data siZe (running out of letters):
fe020d02 ff 0d 02 04 02 09 04 09 02 01 01 a0 00 00 b1 0000000000BBBBSSNN EE SS CC CC CC ZZ -- ZZ -- -- CC DD DD DD DD DDDDDDDDDDBB/EE: beginning/end of a packet SS: size of a sequence/packet CC: some constant value ZZ: size of data NN: number of packets CC: command DD: data
Now for the last three. The 0x04 is interesting because it is constant
throuought the capture, except for a few packets at the beginning of the
conversation:
fe020802 ff08 020402 040204 818700000000000000000000
fe020802 ff08 020402 040404 808800000000000000000000
fe020802 ff08 020402 040404 889000000000000000000000
fe020a02 ff0a 020402 060206 8200008a0000000000000000
fe020a02 ff0a 020402 060206 8400008c0000000000000000
fe020802 ff08 020402 040204 959b00000000000000000000
Since those packets happen at the beginning, maybe it is part of the initial
handshake protocol, and afterwards everything happens with the value 0x04?
Unclear, but let’s just mark it as a constant for now - when we try to create a
session later we’ll just replay the initial packets we’ve captured and hope it
all works.
fe020d02 ff 0d 02 04 02 09 04 09 02 01 01 a0 00 00 b1 0000000000BBBBSSNN EE SS CC CC CC ZZ CC ZZ -- -- CC DD DD DD DD DDDDDDDDDDBB/EE: beginning/end of a packet SS: size of a sequence/packet CC: some constant value ZZ: size of data NN: number of packets CC: command DD: data
And now the last two. Scanning the other packets, maybe this is some sort of high-level indicator about what kind of data to expect? For example, the two bytes are:
0201when changing the speed/incline0200for the bulk of the capture (best guess: heartbeat/keep-alives)020cfor a few random packets- A mixture of values at the beginning (
8187,8088,8890, etc.)
Although we could try to determine exactly what these packets meant, it may not
be worth our time - we know that we can send commands with 0201 and a
keep-alive/something with 0200 (the bytes of the whole sequence never
change). For 020c, let’s just skip it and sort it out later - it may not even
be important (famous last words…).
Finally, the initial bytes (8187, etc.) are probably important, but if we
assume those are some sort of handshake, perhaps we can just replay them at the
beginning of a session? There are multiple reasons this may not work (unique
session IDs, some kind of SYN, SYN-ACK, ACK flow that requires a
specific/changing response, etc.), but trying to sort it out now is a little
pointless since we can’t yet verify anything; plus, it would risk stymied
progress and a reduction in interest, which we can’t have! So let’s mark those
bytes as Operation and move on:
fe020d02 ff 0d 02 04 02 09 04 09 02 01 01 a0 00 00 b1 0000000000BBBBSSNN EE SS CC CC CC ZZ CC ZZ OO OO CC DD DD DD DD DDDDDDDDDDBB/EE: beginning/end of a packet SS: size of a sequence/packet CC: some constant value ZZ: size of data NN: number of packets OO: operation CC: command DD: data
Note this conveniently ignores all of the three- and four-byte sequences, but hopefully this will get us started. Remember, we’re not trying to fully reverse engineer everything - all we want to do is make a dirt-simple program that can send some basic commands to the treadmill and read the current state.
So assuming we’re not totally off-base, let’s put it to the test!