Using a static passkey for Bluetooth Low Energy pairing is insecure. Recent versions of the Bluetooth specification contain an explicit warning about this. However, in practice, we often see static passkeys being used. Moreover, there are no public implementations of proofs-of-concept that can practically show why using a static passkey is an issue. This is why we implemented one.

In a recent assessment, we were testing a device that offered a Bluetooth interface for data export and configuration. This device uses Bluetooth Low Energy (BLE), and a static passkey (or PIN) is required to pair with it. This passkey is displayed for a few seconds when the device is booted and stays the same on each reboot. In fact, it is derived from static, device-specific data.

Using a static BLE passkey severely weakens the authentication process. The Bluetooth specification even mentions this. However, there is probably only a handful of people that read the specification in detail. With almost 3000 pages (for Bluetooth 5.1), it’s even a challenge for some PDF readers. Interestingly, the warning about static passkeys has only been added in specification version 5.1 which was released in January 2019. It can be found in Part H Section 2.3.5.6.3 on page 2450 (this is, by the way, the greatest section reference I ever put anywhere). It says “the Passkey should be generated randomly during each pairing procedure and not be reused from a previous procedure. Static passkeys should not be used since they can compromise the security of the link”. Version 5.0 does not contain these two sentences. I’m not sure if there is a similar note somewhere else in the specification, but I do think that this is quite an important piece of information for developers.

Alright, now we know that it’s not a good idea to use a static BLE passkey. But why? In short, it’s because a passive Machine-in-the-Middle (MitM) can record a successful pairing process and derive the passkey from the recorded messages. Additionally, the authentication algorithm allows an attacker to efficiently brute-force the passkey.

In theory, it’s easy to explain and reason why this is a security issue and why it should be mitigated. In general, such a theoretical analysis and explanation might suffice for a security assessment. However, having an actual proof-of-concept is always better. And more fun! Unfortunately, we did not find any proofs-of-concept or other implementations for these attacks. So we decided to build them ourselves and publish them here.

In the following, I will explain, in theory, two of the consequences of a static BLE passkey. Then I will show our proofs-of-concept and discuss why implementing them can be a pain due to the architecture of a Bluetooth stack.

Bluetooth Low Energy Pairing

BLE offers two kinds of pairing. LE Legacy Pairing and LE Secure Connections (LESC). Here we’re concentrating on LESC — just because the target in our case used that. In principle, the attacks should work against Legacy Pairing as well.

LE Secure Connections use Elliptic Curve Diffie-Hellman during the pairing process to safely establish a shared key. In LESC, this is the Long-Term Key (LTK). The pairing process consists of three phases, where the third one is optional. In the first phase, the devices exchange their supported pairing features to decide which pairing method should be used. There are three options: Just Works, Numeric Comparison (only in LESC), passkey Entry, and OOB. In the second phase, authentication and key material are established. In the third phase, optional additional key material is distributed, such as the IRK that identifies private random BLE addresses.

Here we’re concentrating on the authentication part of the pairing process — the passkey entry. BLE uses an interesting approach for comparing the parties’ passkeys. It uses a bitwise commit-based approach where each bit of the passkey is committed individually by each party. After committing to the bit, the parties reveal the committed bits to each other to verify that the bit has the expected value. As soon as there is a discrepancy, the authentication procedure is canceled.

The Bluetooth specification specifies the confirm value generation function f4 that derives such a commitment. In short, it’s a function that incorporates part of the parties’ public keys, a random nonce, and the bit value of the passkey at the current position. The function concatenates the two public keys and the bit value and calculates an AES-CMAC by using the nonce as a key. A pseudocode representation of this function is found below:

        f4(U, V, X, Z) = AES-CMACX(U || V || Z)

where U and V are the X-coordinate of the respective parties’ public keys. X denotes the nonce for each bit position and Z the bit value XORed with 0x80 (i.e., 0x80 for an unset bit and 0x81 for a set bit).

Each party sends their result of the f4 function. Once both values are received, the parties disclose their committed value by providing the nonce  used as a key for AES-CMAC. The other party can then verify that the bit has the expected value.

The whole pairing process is implemented in a protocol which will be explained in the following section.

Security Manager Protocol (SMP)

The protocol used for BLE pairing is the Security Manager Protocol (SMP). It’s built on top of L2CAP, which means that the host system, not the Bluetooth controller itself, is responsible for the pairing process. This is different from how pairing in Classic Bluetooth works. Classic Bluetooth relies on the Link Manager Protocol that runs on the Bluetooth chip. We will later see why this makes implementing the attack slightly inconvenient.

SMP is a relatively simple protocol. The first byte determines the opcode, the rest of the payload is specific for each of the commands and the fields within the payload mostly have a static size. The table below shows the opcodes that are important for this post:

Opcode Description
0x01 Pairing Request
0x02 Pairing Response
0x03 Pairing Confirm
0x04 Pairing Random
0x05 Pairing Failed
0x0c Pairing Public Key

The initiating device starts by sending a Pairing Request (0x01). This message starts the pairing process with the first phase by submitting pairing feature information. This includes a flag that describes the device’s capabilities, such as whether it has a display or input methods. It also contains a field called “encryption key size”, maybe you remember the KNOB attack. The other device responds with a Pairing Response (0x02) which acknowledges the pairing process and submits the responding device’s capabilities. The matrix of which capability combinations lead to which pairing method is rather complex. For passkey authentication, there needs to be at least one display and one input method. The Bluetooth specification describes this in a large matrix that shows the different configuration possibilities and the resulting pairing mechanism.

If the devices agree on a certain pairing mechanism from their capabilities and requirements, the initiating device continues by sending its public key using the Pairing Public Key (0x0c) message. The message contains the X and Y coordinate of the ECDH public key. While we won’t need Diffie-Hellman crypto for this attack, the values are required later in the authentication part. The responding device then responds with its public key using the same Pairing Public Key (0x0c) command.

Now that the public keys have been exchanged, the verification of the passkey is started. The initiating device begins by sending a Pairing Confirm Command (0x03). This message contains the first confirm value, which is the result of executing the f4 function for the first bit of the passkey. The responding device does the same. This way, both devices have committed to the bit value. Now the disclosure of the value starts. The initiating device begins by sending the Pairing Random (0x04) command. This message contains the random value (X) used in the f4 function. Now the responding device has all the parameters to derive the confirm value the initiating device has sent in the previous message.

The flowchart below shows the first part of the SMP authentication where the passkey is verified.

Once every bit has successfully been committed and verified by both parties, the part of the authentication process we’re interested in has been completed. The pairing process has not finished yet and requires a few additional steps, but we will ignore them in this blog post as they are not required for the attacks. If one of the bits during the confirmation phase turns out wrong, the party responds with a Pairing Failed (0x05) message, indicating a Confirm Value Failed error code 0x04. The pairing process is then immediately aborted, and the Bluetooth connection is closed.

In the following sections, I’ll go over how this approach can be attacked.

Decoding The Passkey From A Captured Connection

Let’s assume we have a packet trace of an SMP pairing. The commit mechanism and its disclosure allows us to derive each of the passkey’s bits by calculating the f4 value for each passkey bit and a random value. To do that, an attacker needs to find the Pairing Public Key messages of both parties and extract the X-coordinates. Then they need to extract the confirm value and a random value of each of the Pairing Confirm and Pairing Random commands from either the sender or the receiver. For each bit value, a guess needs to be made. If the result of the f4 function is wrong, the bit value needs to be inverted.

We implemented a proof-of-concept for passkey extraction in BTSnoop log files. Such log files are created by, e.g., hcidump, Android’s Bluetooth logging, or using Apple’s PacketLogger.

In the screenshot below, you can see a BTSnoop log opened in Wireshark. This log file was created using hcidump on a Raspberry Pi. While the log was captured, I paired the Pi with a device using LE Secure Connections with a passkey. The target was an nRF52-based board which was configured to have the static passkey 123456.

Wireshark screenshot showing a recorded BTSnoop log

Running the extraction script that decodes the passkey from the log looks as follows. The script can be found in this repo. The script parses BTSnoop log files pretty naively. Given that there are different formats for BTSnoop logs, it might very well be possible that it does not work with all of them. I tested it on log files that are generated by hcidump and Apple’s PacketLogger. Other than that, it only extracts the confirm and random values and does what I’ve described above. The BLE L2CAP maximum transmission unit (MTU) is usually only 23 bytes, which means that the Pairing Public Key command does not fit a single L2CAP message. This is why the script needs to implement L2CAP/ACL reassembly.

Screenshot of the SMP Passkey Extraction Script

Of course, the real challenge here is being able to sniff BLE over the air. And it’s questionable whether this is useful. Remember that the outcome of this “attack” is the passkey. This does not lead to a MitM setup where the traffic between the two participants can be decrypted or manipulated. Therefore, the next attack is probably more relevant in practice.

Brute-Forcing The Passkey

The more interesting attack enabled by this authentication scheme is an easy method to brute-force the static passkey. As each bit in the passkey is verified individually, we can effectively brute-force the passkey bit-by-bit. The design of the protocol makes this very effective. We need to brute-force only 20 bits, and each correctly guessed bit does not introduce any overhead. For every incorrectly guessed bit, we need to reconnect. However, as soon as the remote device signals that the bit was incorrect, we immediately know the correct bit value — the inverse of the value we sent. So in the worst case, if every bit is guessed wrong, this requires 19 reconnection attempts.

An abstract algorithm looks as follows:

known_bits = [0] * 20
known_index = 0
pubkey = generate_pubkey()
random = generate_random()
while known_index < 19:
        send_pairing_request()
        send_pairing_pubkey()
        for i in 0...19:
                known_index = 0
                confirm_val = f4(known_bits[i])
                send_confirm_value()
                resp = send_rand_value()

                if resp == SMP_PAIRING_FAILED:
                        known_bits[i] = !known_bits[i]
                        break
                else:
                        known_index += 1

At the end, the known_bits denote the passkey the device expects. We can now successfully pair with the device.

In theory, this is pretty easy to implement. In practice, it’s not. This mostly comes from the architecture of a Bluetooth stack and the fact that SMP, as a higher-level protocol, manages very low-level connection properties.

Implementing The Attack

It’s not possible to implement this attack using APIs intended for developers. The problem is that BLE APIs often abstract as high as the GATT protocol, which is implemented on top of L2CAP. Everything below GATT is usually hidden, which is fine for developing BLE applications. For security research, this is not helpful. To implement this attack, we need to be able to send arbitrary SMP messages.

For reference, an image of a how a generic Bluetooth stack usually looks like is shown below:

The next problem is that SMP is an L2CAP protocol that manages very low-level properties, namely, the encryption of the BLE transport. As such, the protocol itself is usually implemented in the kernel or very privileged system services. It’s not like you can implement and plug in your own SMP implementation. Even if you manage to send arbitrary L2CAP messages, it might not be possible, using the system’s API, to send data to the fixed L2CAP channel with SMP‘s channel ID 6. And even then, the system’s own SMP implementation will most likely interfere with the manually sent messages.

Luckily, we can take control over the Bluetooth chip on many devices. That means that we avoid the connection between the operating system’s protocol implementation and directly attach to the HCI, ACL, SCO part of the Bluetooth chip. The proofs-of-concept we implemented here are tailored to Linux and Broadcom Bluetooth chips, but in theory, they should work with any Bluetooth chip where an HCI socket is available. Note that HCI, ACL, and SCO strictly speaking are three different protocols. Usually, they are wrapped and multiplexed in another protocol called H4, where the first byte determines which of the three protocols follow. This allows speaking all three protocols over one transport. To connect to other Bluetooth devices, the HCI protocol is required, and L2CAP is implemented on top of ACL.

I used the Internalblue framework to implement the proof-of-concept. Internalblue is a Bluetooth research framework for Broadcom Bluetooth controllers. It implements the H4 protocols and enables writing Python scripts to interact with the Bluetooth chip. In the past, I used it for implementing proof-of-concepts against Apple’s Bluetooth stack. For these proofs-of-concept I had to implement a few layers of a Bluetooth stack, a connection manager, and a basic L2CAP layer. As this is exactly what is required as a baseline for implementing the SMP brute-force attack, I chose to build it on top of that.

The proof-of-concept can be found here. It should work on any device supported by Internalblue (i.e., Broadcom and Cypress Bluetooth chips on Linux/Android and iOS). I tried it with a Realtek-based USB dongle, but that did not work. A Raspberry Pi (3 or newer) works really well with Internalblue. It’s what we used to develop the proof-of-concept.

As mentioned before, we need to take care of the operating system’s SMP implementation before sending arbitrary SMP messages. Luckily, a special HCI socket type has been integrated into the Linux kernel. The HCI_CHANNEL_USER. This allows us to get exclusive access to the HCI interface of the Bluetooth chip without the kernel, or in this case BlueZ, interfering with our communication. During the creation of our proofs-of-concept I added this socket type to Internalblue. Using this socket type requires the device to be down. You can do this by simply stopping the Bluetooth service (e.g., systemctl stop bluetooth). It also requires CAP_NET_RAW.

The recording below shows the brute-force script running against the previously mentioned nRF52-based target with the static passkey 123456. With an initial guess of all bits set to 0 and the passkey 123456 the script took about 40 seconds.

Conclusion

Don’t use a static Bluetooth Low Energy passkey. Change it for every pairing attempt. As we’ve seen, it’s possible to brute-force the passkey in under a minute. With an optimized implementation, it’s probably possible to do it even faster. As a defense, implementing pairing lockout is difficult. It’s very easy for an attacker to change their Bluetooth address. In most cases it’s easier to implement passkeys as it was intended by changing them for every pairing attempt. This is not really obvious, and I think the Bluetooth specification should have been more explicit about this before v5.1.

What we also found is that for security testing, implementing the Bluetooth stack components that are required from scratch is often better than relying on APIs. BlueZ’ dbus API, for example, does not give us a lot of control over the stack, and I think implementing SMP on top of it is nearly impossible. Having such a “hackable”, easily scriptable Bluetooth stack is beneficial, and it could certainly make sense to improve and integrate this into Internalblue.

The code can be found here: github.com/ttdennis/bluetooth_smp_pocs.