Norec Attack: Stripping BLE encryption from Nordic’s Library (CVE-2020–15509)
How I was able to disable BLE encryption without a blink
This article talks about a vulnerability i have found in a library that almost every BLE android application is based on, in a combination with an android bug which helped for that vulnerability to be formed.
[Article resurrected from our previous blog]
Bluetooth Low Energy: The Bond
Nowadays, a lot of applications wish to encrypt their traffic in order to protect the confidentiality of the transferred data. Therefore, on Bluetooth Low Energy standard, a procedure has to be precede encryption which is called bonding. When two devices are bonded it means they have exchanged LTK keys and thus the communication is encrypted. How secure that communication is shall be analyzed in a separate article but let’s assume the protection given by BLE is just good. Also, keep in mind that the bond is not required and has to be initiated by one of the two paired devices.
Using Android API to Bond as a Central Device: The Confusion
An Android developer can use the function createBond() in order to bond with a BLE device. Ideally, this function should return true if a bond is created. Unfortunately, there is a confusion in the Android API (which i have reported) that when both parties have the key stored and when those keys are used in future bonding events, the function returns false even though the bonding happened and the traffic is encrypted.
Standing on the shoulders of vulnerable giants
The Nordic Semiconductors created a very handful, and easy to use, Android library that helps developers to handle Bluetooth Low Energy connections easily. I have found a vulnerability in this library in which an adversary can strip the BLE encryption, yet the user believes that the traffic is encrypted due to incorrect handling.
Two libraries of Nordic’s Semiconductors are affected:
The Android-BLE-Library is used by developers to handle BLE Connections.
The Android-DFU-Library is used by developers to upgrade their BLE firmware over the air.
I have tested just a few random BLE Applications that they do make use of BLE Bond, and they make use of the interested bond function of the Nordic’s Library. The applications that i have checked are listed below:
LINKA (An application for a ~$200 Smart Bike Lock) — com.linka.lockapp.aos
Smart Lock — services.singularity.smartlock
Noke (A ~$60 smart lock) — com.fuzdesigns.noke
MiLocks BLE — tw.auther.milocks_ble
nRF Connect (nordic’s product)
Mi Home — com.xiaomi.smarthome
An application that does not rely on the nordic’s library is the phantom lock, yet the Phantom Lock (com.plantraco.coolapps.phantomlock) creates a bond without having a check on the result of the bonding procedure.
The vulnerability is the same in both nordic’s libraries. So we will examine just one of the two libraries: Android-DFU-Library
The vulnerability can be found in class no.nordicsemi.android.dfu/BaseDfuImpl
/**
* Creates bond to the device. Works on all APIs since 18th (Android 4.3).
*
* @return true if it's already bonded or the bonding has started
*/
@SuppressWarnings("UnusedReturnValue")
boolean createBond() {
final BluetoothDevice device = mGatt.getDevice();
if (device.getBondState() == BluetoothDevice.BOND_BONDED)
return true;
boolean result;
mRequestCompleted = false;
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Starting pairing...");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG, "gatt.getDevice().createBond()");
result = device.createBond();
} else {
result = createBondApi18(device);
}
// We have to wait until device is bounded
try {
synchronized (mLock) {
while (!mRequestCompleted && !mAborted)
mLock.wait();
}
} catch (final InterruptedException e) {
loge("Sleeping interrupted", e);
}
return result;
}
The nordic’s function createBond is called by developers to secure the communication between the mobile device and a peripheral device (could be for example a heart rate monitor device).
The developers instead of initiating and forcing an encryption procedure to start, developers invoke the getBondState() to get the current bond state. One could think this is the right thing to do as in the android documentation, at the first glance, everything looks good:
BluetoothDevice | Android Developers
AccessibilityService.MagnificationController.OnMagnificationChangedListener
By inspecting the method we don’t see anything that could go wrong. The function returns the bond state of the remote device, and this can be BOND_NONE, BOND_BONDING or BOND_BONDED.
I find this misleading because it misleads the developers that the getBondState() it will return the bonding state as it just happened. This is not the case though. Let’s check the BOND_BONDED state:
The getBondState() will return the state (constant) BOND_BONDED in case the key is stored on the device. That does not necessarily mean that the device is currently bonded with the paired device. That means communication could be in plain-text. Keep in mind that this state (BOND_BONDED) could also be returned even when the mobile device is not paired at all, with any device. This is because the state BOND_BONDED is just an indication which states that the Bluetooth Device examined have stored keys on the android system and could be used.
The developers obviously got mislead by the android’s documentation that the getStateBond() method would return the current bonding state and thus return true if the state was BOND_BONDED. That can be translated to: return true (encryption is on) if the key exists on the device despite the fact that bond may have failed.
The attack
There are many vectors to attack. The most obvious one is to attack the peripheral in order to eliminate the slots available for keys. Most BLE chipsets found on every IoT device have limited memory size and thus the old keys (of different devices) are replaced by newer keys. Evicting all previous keys is possible by masquerading the BDADDR and then bond a few hundred times to the targeted peripheral. That way all previous LTK keys are evicted and dump keys are stored on the device. Finally, this will help us to achieve our goal because the bug in the library will not create a bond by default and the user is notified that the connection is secure (we don’t need to do any further steps, the traffic will be unencrypted without any further action).
Another attack vector is to hijack the communication before the two paired devices become paired. At the time of a connection request, an adversary could hijack the connection and respond as the peripheral indicating that no keys are stored in the peripheral’s memory. It is not a very difficult attack to implement as the connection request is initiated on the advertisement channels and those are static (no FHSS).
Confusion = Bug = Vulnerability
Below i present some findings i have regarding the state of each device, the result of the native createBond() method of android, and the expected result.
As we can observe in the last row, when the keys exist on both devices the createBond() at first it seems to be buggy and returns false. This makes things harder for the developer to develop a robust wrapper and which will often lead developers to create bugs such as the bug of nordic’s library which, unfortunately, is a security bug.
Please note that even though the createBond() returns false the communication is still encrypted and the bond took place. This is because the call is asynchronous and digging in the Bluetooth service it is shown that if the current state is not BOND_NONE then return false. That’s pretty confusing.
With a communication i had with the nordic’s PSIRT team:
…Our Team confirmed a problem, being able to connect to a device with erased bond information despite the Android showing bond status as BONDED.
However, the issue seems to appear from Android side. Method createBond() on Android returns false every time the bond information is present on client (Android) side, also when it’s present on peripheral side. Therefore, the two situations (valid bond on both sides and bond info on client side, thus unencrypted link) are indistinguishable. …
[Part of Communication with Nordic’s PSIRT Team]
What nordic told me is partly true. When the client has the key but PE does not, its safe and SHOULD return false because returning true would be a major security issue. This is because the user should be warned if the peripheral has no keys (an attack could have been in place otherwise). This could be avoided if a re-paring could be enforced but android does not support such mid-level operations. The answer is partially true as the android indeed provides a confusing return output, as I mentioned before.
The first insecure patch
…To check if the device is paired (which is not 100% reliable), we do check if CCCD are still enabled after reconnection. That assumes that CCCD state is preserved for bonded devices, which usually is true, and can be faked on a remote device pretending being the one (the same address, CCCD enabled by default).
Therefore, it seems that on Android it is not possible to check if you are truly bonded without using some 3rd party encryption mechanism to get this info from the device using GATT.
Not checking bond state before calling createBond() will cause an error even if the devices are bonded correctly.
We would recommend you to contact Google about this issue…
[Part of Communication with Nordic’s PSIRT Team]
They have already patched the library with a solution that is far away from a secure solution.
Their change is displayed below (they have changed only Android-BLE-Library as the Android-DFU-Library is still un-patched).
Patched Version of vulnerable function createBond():
private boolean internalCreateBond() {
final BluetoothDevice device = bluetoothDevice;
if (device == null)
return false;
log(Log.VERBOSE, "Starting bonding...");
// Warning: The check below only ensures that the bond information is present on the
// Android side, not on both. If the bond information has been remove from the
// peripheral side, the code below will notify bonding as success, but in fact the
// link will not be encrypted! Currently there is no way to ensure that the link
// is secure.
// Android, despite reporting bond state as BONDED, creates an unencrypted link
// and does not report this as a problem. Calling createBond() on a valid,
// encrypted link, to ensure that the link is encrypted, returns false (error).
// The same result is returned if only the Android side has bond information,
// making both cases indistinguishable.
//
// Solution: To make sure that sensitive data are sent only on encrypted link make sure
// the characteristic/descriptor is protected and reading/writing to it will
// initiate bonding request.
if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
log(Log.WARN, "Bond information present on client, skipping bonding");
request.notifySuccess(device);
nextRequest(true);
return true;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
log(Log.DEBUG, "device.createBond()");
return device.createBond();
} else {
/*
* There is a createBond() method in BluetoothDevice class but for now it's hidden.
* We will call it using reflections. It has been revealed in KitKat (Api19).
*/
try {
final Method createBond = device.getClass().getMethod("createBond");
log(Log.DEBUG, "device.createBond() (hidden)");
//noinspection ConstantConditions
return (Boolean) createBond.invoke(device);
} catch (final Exception e) {
Log.w(TAG, "An exception occurred while creating bond", e);
}
}
return false;
}
I find their solution a bit naive. It does not solve the problem and assumes the peripheral has a client characteristic configuration descriptor (this could also be faked). This is insane and insecure.
Attack Mitigation and a recommended patch
I have suggested a mitigation technique that is easy to implement and will secure the application until google fixes their framework. My recommended mitigation is to invoke android’s native createBond() each time the user wishes to have encrypted communication and then check the result. Then, check the current bond state.
If the result is false and the bond state is BOND_BONDED, just remove the key and try again.
If it fails and the bond state is BOND_NONE, then it just failed and now you may terminate the connection to protect the user’s privacy.
To understand how this can be solved, let’s examine the recalled table below:
Before erasing the bond key
After erasing the bond key
The two corner cases where a developer must watch-out is the 2nd Row Case, and 4th Row Case. Those are the two cases where the createBond() fails. The fact that we don’t know if the key exists on the other side, and the fact that there is a confusing method in android, it makes us unable to distinguish those two cases. We can however, to delete the key, which will transfer the state of the phone to 1st and 3rd Row cases. That shifted our state, and createBond will return true in either case. That way our communication is secure and the solution rocks!
Attack Limitations
The attack is limited when the central wish to connect to an authenticated characteristic and thus bonding has to be headed. In this situation, the createBond won’t matter and bonding will occur normally without any problem.
Coordinated Vulnerability Disclosure Time Table
23/06/2020: Vulnerability Found
24/06/2020: Vulnerability Report Sent to Nordic’s PSIRT
01/07/2020: Nordic’s First Patch (only on Android-BLE-Library)
02/07/2020: Nordic Confirmed the security bug
02/07/2020: Nordic Notified about the insecure patch
02/07/2020: CVE Request
02/07/2020: CVE Received (CVE-2020–15509)
03/07/2020: Published
Implementation
The attack is implemented with a custom tool as shown (now named blebit.io):
The attack using a custom SDK and custom Hardware. The attack implemented in under 5 lines of code. The whole program is under 100 lines of code.
private static void startNoricAttack(CEController ce) throws Exception
{
for(int i=0; i<100; i++)
{
selectRandomMac(); ce.connect(target, ConnectionTypesCommon.AddressType.RANDOM_STATIC_ADDR);
ce.bondNow(true);
try {Thread.sleep(100);}catch(InterruptedException iex) {}
ce.disconnect(19);
int peer_id = getPeerId();
ce.deletePeerBond(peer_id);
}
}