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 describes a vulnerability I discovered in a widely used Android library, one that forms the foundation of many Android applications. The issue becomes exploitable because of the way this library interacts with an existing Android bug, and together they create a security weakness that would not exist independently.
[Article resurrected from our previous blog]
Bluetooth Low Energy: The Bond
Many modern applications want to encrypt their communication in order to protect the confidentiality of the data being exchanged. In Bluetooth Low Energy, encryption requires a preceding process known as bonding. When two devices are bonded, they exchange Long Term Keys, and from that point on their communication is encrypted.
The strength of this protection will be discussed in a separate article, but for now we assume that BLE provides a reasonable level of security. It is also important to note that bonding is optional, and it must be initiated by one of the two paired devices. Without bonding, no long-term encryption keys are stored and the communication remains unprotected.
Using Android API to Bond as a Central Device: The Confusion
An Android developer can call the function createBond() to establish a bond with a BLE device. In theory, this function should return true when bonding succeeds. Unfortunately, there is a long-standing confusion in the Android API, which I have already reported to Google. When both devices already have the bonding keys stored and reuse those keys in a future bonding event, the function returns false, even though the bonding process completes successfully and the communication is encrypted.
This behavior misleads developers into believing that bonding has failed, while in reality, the devices have securely bonded and are communicating with encryption enabled.
Standing on the shoulders of vulnerable giants
Nordic Semiconductor has created a very useful and easy to use Android library that simplifies Bluetooth Low Energy communication for developers. However, I discovered a vulnerability in this library that allows an attacker to strip BLE encryption, while the user is led to believe that the communication is still protected. This happens because the library handles certain bonding and encryption states incorrectly, creating a false sense of security.
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 tested several random BLE applications that use BLE bonding, and that rely on the specific bonding function provided by the Nordic library. The applications I examined 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 Nordic’s library is Phantom Lock (com.plantraco.coolapps.phantomlock). However, Phantom Lock creates a BLE bond without checking the result of the bonding process. This means the application assumes bonding is successful even when the system reports a failure, which leads to the same type of security issue.
The vulnerability appears in both of Nordic’s libraries, so for clarity we will focus on only one of them, the Android DFU Library. The problematic behavior can be found in the 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 createBond function is used by developers to secure communication between the mobile device and a BLE peripheral, such as a heart rate monitor. Instead of explicitly starting and enforcing an encryption procedure, developers often rely on getBondState() to determine the current bonding status. At first glance, this seems correct, because the Android documentation makes the process appear straightforward and reliable.
However, the issue arises when the bond state does not accurately reflect the real security state of the connection, which leads to incorrect assumptions about whether encryption is actually enabled.
BluetoothDevice | Android Developers
AccessibilityService.MagnificationController.OnMagnificationChangedListener
By inspecting the method, nothing appears to be wrong. The function simply returns the bond state of the remote device, and this value can be BOND_NONE, BOND_BONDING, or BOND_BONDED. The problem is that this behavior is misleading. Developers often assume that getBondState() will report the bonding state as it occurred during the most recent bonding attempt. In reality, this is not true.
To understand why, we need to look more closely at the meaning of the BOND_BONDED state.
The getBondState() function returns the constant BOND_BONDED when a bonding key is stored on the device. However, this does not guarantee that the device is currently bonded with the peripheral. The communication may still be happening in plain text. The presence of the BOND_BONDED state only indicates that the Android system has a stored key for that Bluetooth device, and that this key could be used if bonding is successfully re-established.
In fact, getBondState() can return BOND_BONDED even when the mobile device is not paired with any device at that moment. This is because the state does not reflect an active secure connection. It only reflects that a key exists somewhere in the system.
Developers were understandably misled by the Android documentation into believing that getBondState() reports the current bonding state. As a result, many applications assume that BOND_BONDED means “encryption is active,” when in reality, it only means “a key exists,” even if the recent bonding attempt failed and no encryption is actually being used.
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 several findings about the state of each device, the outcome of Android’s native createBond() method, and the result that developers would reasonably expect. These observations help clarify how the bonding process behaves in practice, and how the API responses can differ from what most developers assume.
As we can see in the final row of the table, when both devices already have bonding keys stored, the createBond() method appears to behave incorrectly by returning false. This makes it difficult for developers to build a reliable and secure bonding workflow, and it often leads to incorrect assumptions in application logic. This is exactly how the bug in the Nordic library was introduced, and unfortunately, it results in a security issue.
It is important to understand that even though createBond() returns false, the communication is still encrypted and the bond has been successfully re-established. The misleading return value is caused by the asynchronous nature of the bonding process. When examining the internal Bluetooth service, we can see that if the device’s current state is not BOND_NONE, the method simply returns false. This behavior is confusing for developers, who expect the method to indicate whether bonding has succeeded in the current session, not simply whether a key already exists.
The communication 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 already patched the library with a solution that is far away from a secure solution.
Their change is displayed below (they 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 insecure.
Attack Mitigation and a recommended patch
I 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 tables below:
Before erasing the bond key
After erasing the bond key
The two corner cases developers must pay attention to are the second row and the fourth row. These are the situations in which createBond() returns false. Because the Android API does not clearly indicate whether the key exists on the peripheral side, and because the bonding method behaves in a confusing way, it becomes impossible to reliably distinguish between these two conditions based on the API alone.
However, there is a practical workaround. By deleting the stored key on the phone, the bonding state shifts into the first or third row conditions. In both of these states, createBond() will return true, which means the bonding process will be retried properly. This ensures that encryption is correctly established, and the communication becomes secure. With this approach, the bonding logic becomes predictable, and the solution works reliably.
I forwarded my recommendation to the PSIRT team, they implemented my approach and patched the vulnerability successfully.
Attack Limitations
The effectiveness of the attack becomes limited when the central device attempts to access an authenticated characteristic, because bonding must occur in order to proceed. In such cases, the behavior of createBond() does not affect the outcome. The bonding process will complete as required, and the connection will be properly secured without any issues.
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 developed tool.
This is a prototype of a product on which I will launch in the following months.
The Java SDK is not yet published.
The attack using the custom SDK and custom Hardware is done in under 5 lines of code. The total program is under 100 lines of code (that little program is based on my own SDK which is around 10k lines of code and a firmware of around 20k LOC).
Update: I created a slim version of this tool called BLE:bit. It’s open-source and open-hardware. You can find it here: https://blebit.io
Update: I closed-sourced the tool and is no longer a public project.
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);
}
}






