Reversing the “nokelock” BLE padlock

I’ve been reversing a couple of cheap Chinesium “smart” padlocks that unlock via Bluetooth instead of with a key. My intent is for a talk and a couple of posts on how “smartness” doesn’t stop these things being breakable via simple techniques. For example, the one on the right I can open with just a screwdriver and a pair of pliers due to bad physical design.

But anyway, I’ve been looking at the BLE communication, as are other people, so I thought I’d get my notes up now so people can crib from them if they want. I know how the protocol works, but I’m missing a few bits and can’t actually get it to unlock over BLE, but I’m close.

The device offers the standard BLE services, but two others:

  • 0000fee7-0000-1000-8000-00805f9b34fb – which handles all communication with the lock
  • f000ffc0-0451-4000-b000-000000000000 – which appears to be the TI Over the Aid Download (OAD) service

The 0xffc0 service leaves lots of potential for manipulating the firmware, but I haven’t looked at this yet. It does imply that its built on a TI CC2540 or clone.

The 0xfee7 service, which I’ll call the lock service from now on, appears to handle all communication between the app and the lock. This includes battery life and unlocking it.

The lock service has two characteristics:

  • 0x36f5 – which has write permissions
  • 0x36f6 – which has notify permissions

This implies a two way communication with data sent to 0x36f5 and received via notifications on 0x36f6. This also makes it harder to intercept using, say Frida as tying to the notify method is difficult in the way the Frida maps Java to JavaScript.

The lock requires that an apk is downloaded from the companies site and installed, which is not the safest way of distributing an app, especially as most of the site is in Chinese.

The app has been obfuscated, although I’ve found a few older versions on the web that have lesser levels of obfuscation.

A simple Frida script to hook onto writeCharacteristic shows a random set of data, the below is an exchange of connecting to the lock and unlocking it:

Write: 000036f5-0000-1000-8000-00805f9b34fb
Data: 9e 85 ce c7 9e 63 18 f4 cd a3 25 ec 3f ae fe 98 .....c....%.?...
Write: 000036f5-0000-1000-8000-00805f9b34fb
Data: eb e0 f7 56 e7 7b d8 8f e9 64 94 67 1e 1e 40 82 ...V.{...d.g..@.
Write: 000036f5-0000-1000-8000-00805f9b34fb
Data: 25 85 d3 31 c9 d0 19 74 a4 90 e6 6c 2e ee c6 99 %..1...t...l....

The length and randomness of the packet (16 octets) implies that it is encrypted with something like AES (as it is the right block size).

Some digging into the source shows us what looks like AES encryption being performed in the method com.fitsleep.sunshinelibrary.utils.g.a(‘[B’,'[B’). This takes two byte arrays as parameters, which I suspected was the key and data to be encrypted. So I stuck a hook on this and retried the connection:

Data1: 06 01 01 01 55 63 48 7d 0f 59 7c 3c 2c 6f 4d 2c ....UcH}.Y|<,oM,
Data2: 35 3a 4b 0f 23 21 57 4a 58 4a 21 24 35 33 3c 56 5:K.#!WJXJ!$53<V

Write: 000036f5-0000-1000-8000-00805f9b34fb
Data: a2 d2 b0 c5 2b 3f 06 f0 fe 2c 65 87 88 51 dc ec ....+?...,e..Q..

Data1: 02 01 01 01 f1 07 c3 10 5f 2a 35 6a 5f 3b 23 4f ........_*5j_;#O
Data2: 35 3a 4b 0f 23 21 57 4a 58 4a 21 24 35 33 3c 56 5:K.#!WJXJ!$53<V

Write: 000036f5-0000-1000-8000-00805f9b34fb
Data: 19 38 f6 ab 47 94 5c e6 5f 2c d4 44 b4 63 df 37 .8..G.\._,.D.c.7

Data1: 05 01 06 30 30 30 30 30 30 f1 07 c3 10 63 23 1d ...000000....c#.
Data2: 35 3a 4b 0f 23 21 57 4a 58 4a 21 24 35 33 3c 56 5:K.#!WJXJ!$53<V

Write: 000036f5-0000-1000-8000-00805f9b34fb
Data: 23 80 70 53 24 5e 59 f9 88 54 83 0e 16 c3 6e b3 #.pS$^Y..T....n.

We can see here that the octets are encoded and then passed directly to a writeCharacteristic call. If you look, data2 never changes, implies that it is the encryption key. Let’s try this out with a bit of python:

>>> from Crypto.Cipher import AES
>>> import binascii
>>> key=binascii.unhexlify("353a4b0f2321574a584a212435333c56")
>>> data=binascii.unhexlify("02010101f107c3105f2a356a5f3b234f")
>>> aes=AES.new(key, AES.MODE_ECB)
>>> binascii.hexlify(aes.encrypt(data))
'1938f6ab47945ce65f2cd444b463df37'

That’s a perfect match (look at the red entry above). So we know that they match perfectly, so this is just for some simple obfuscation of the payload and to prevent replay attacks. Both of my locks seemed to use the same key.

After some retries, it looks like this is the magic unlocking command:

05 01 06 30 30 30 30 30 30 f1 07 c3 10 63 23 1d ...000000....c#.

Some interpretation about the protocol:

  • 05 is lock/unlock
  • 01 is unlock
  • 06 is the length of the padlock password
  • 30 30 30 30 30 30 (“000000”) is the padlock password
  • f1 07 c3 10 62 23 1d I have no idea

The last three octets change with every request, but not in a way that suggests a time stamp. The first three alter, but less often. This is the bit I can’t work out.

I noticed that the app logs a lot of stuff logcat as Errors, so running logcat whilst the app is running can show some interesting information:

The first thing I noticed that it logs its calls to the backend API to the console, these map up with stuff I intercepted with burp:

7-11 18:15:59.518 32159 32159 E BaseObserver: onNext:{"result":[{"name":"bob","id":12923,"lockKey":"37,75,66,15,61,42,1,27,49,43,6,8,78,31,29,98","isAdmin":0,"firmwareVersion":"5.0","type":0,"barcode":"GBY040001453","deviceId":"","lockPwd":"000000","mac":"C8:DF:84:2B:9A:D9","account":"xxx@xxxxxxxx.org.uk","gsmVersion":null},{"name":"orange","id":88116,"lockKey":"53,58,75,15,35,33,87,74,88,74,33,36,53,51,60,86","isAdmin":0,"firmwareVersion":"5.0","type":0,"barcode":"GBF040000969","deviceId":"","lockPwd":"000000","mac":"3C:A3:08:C1:AA:A3","account":"xxx@xxxxxxxx.org.uk","gsmVersion":null}],"status":"2000"}
7-11 18:15:59.519 32159 32159 E HomeActivity: [{"name":"bob","id":12923,"lockKey":"37,75,66,15,61,42,1,27,49,43,6,8,78,31,29,98","isAdmin":0,"firmwareVersion":"5.0","type":0,"barcode":"GBY040001453","deviceId":"","lockPwd":"000000","mac":"C8:DF:84:2B:9A:D9","account":"xxx@xxxxxxxx.org.uk","gsmVersion":null},{"name":"orange","id":88116,"lockKey":"53,58,75,15,35,33,87,74,88,74,33,36,53,51,60,86","isAdmin":0,"firmwareVersion":"5.0","type":0,"barcode":"GBF040000969","deviceId":"","lockPwd":"000000","mac":"3C:A3:08:C1:AA:A3","account":"xxx@xxxxxxxx.org.uk","gsmVersion":null}]

Where “Bob” and “orange” are my two padlocks. We can see that encryption key in lockKey and the lock password in lockPwd.

The system log shows more than this though, if we venture down a bit we can see:

07-11 18:16:11.355 32159 32159 E a : 060101011A1B314F421966216B407C2E
07-11 18:16:11.446 32159 32170 E a : 返回:06020725986A6D010500000000000000
07-11 18:16:11.978 32159 32159 E a : 0201010125986A6D7873473C5C5D124C
07-11 18:16:12.374 32159 32170 E a : 返回:02020162986A6D010500000000000000
07-11 18:16:12.892 32159 32159 E a : 05010630303030303025986A6D476152
07-11 18:16:13.498 32159 32170 E a : 返回:05020100986A6D010500000000000000
07-11 18:16:15.191 32159 32170 E a : 返回:050D0100986A6D010500000000000000
07-11 18:16:21.955 32159 32159 E a : 05010630303030303025986A6D60360B
07-11 18:16:23.169 32159 32170 E a : 返回:05020100986A6D010500000000000000
07-11 18:16:25.167 32159 32169 E a : 返回:050D0100986A6D010500000000000000
07-11 18:16:27.447 32159 32159 E a : 05010630303030303025986A6D4F2241
07-11 18:16:29.155 32159 32170 E a : 返回:05020100986A6D010500000000000000
07-11 18:16:31.151 32159 32264 E a : 返回:050D0100986A6D010500000000000000

It’s those magic numbers again, but it looks like we’re getting the response two (the entries with Chinese characters). Mixed with some reversing of the app, here’s a slightly annotated version:

Out: 0201010125986A6D7873473C5C5D124C
In:  02020162986A6D010500000000000000
Out: 05010630303030303025986A6D476152 
In:  05020100986A6D010500000000000000
In:  050D0100986A6D010500000000000000
Out: 05010630303030303025986A6D60360B
In:  05020100986A6D010500000000000000
In:  050D0100986A6D010500000000000000
Out: 05010630303030303025986A6D4F2241

That’s about as far as I can get. I’m pretty much six octets off a full attack.