Testing a mobile app using a device you don’t have
We will go through a BLE Bulb Application and try to make the BLE communication happen without having the actual bulb in our hands
How many times have you tested IoT-related apps but without testing their BLE communication? Have you ever wonder what extra endpoints would be triggered if you just enabled the BLE functionality?
There are myriad of applications that have been tested either through penetration testing or through bug bounties. However few people have tested their BLE functionality, because many people aren’t familiar with this technology. We would like to bring BLE security to the front-line by enabling everyone to learn how BLE works. However, this article combines reverse engineering and BLE and helps our mission: to make BLE Security aware to everyone.
TL;DR:
In this article we will go through a BLE Bulb Application and try to make the BLE communication happen without having the actual bulb in our hands. The same concepts apply to all apps. We will need BLE-related tools though, which help us to talk to the mobile application. We make use of BLE:Bit tool, a tool designed by Cybervelia.
The end-goal is to reverse engineer the application, understand how it works and make it talk with a fake device we’ re gonna build. If we would be able to make the BLE communication happen that may trigger requests towards to back-end endpoints and that would enable us to test them too (i.e. via Burp). However, I can assure you the app we are testing in this example won’t trigger any requests to the back-end. More advanced applications would trigger such requests, so nothing changes in the methodology we use.
Taking the application apart
First of all we have to decompile the application and get the decompiled java code – at least for Android.
We are going to delve into the decompiled code of the application. If this is not for you, no hurt feelings you may close this page. However, if you like application digging and you often find yourself trying to understand the application’ internals, then this is gold for you.
We will dissect the decompiled application to find three core components which will define the behavior of the device and application:
Device name
Each BLE device often advertise certain data to the air. Such data are received by the Android OS and are forwarded to the application. The device may, or may not advertise its name, but often it does. Among other things it can advertise custom data which are used to identify the type of the device.
The application does not wish to recognize any BLE as its potential device – instead the developer of the app tries to recognize and show to the user only the relevant devices. Therefore the vendor places certain special data into the advertisement data. When the application receives such data, knows which MAC address, hence which device, is the one must connect to.
To make the application connect to our fake device, we must find out what exactly is looking for in the advertisement data. This will enable us to start advertising with the same data and offering the same services, hence making the application connect to our fake peripheral, as it pretends to be the potential X device.
Service discovery
Each BLE device provides certain services and characteristics (read the BLE basics here).
When connecting to the BLE you may start enumerating its services and characteristics. This is exactly what the BLE mobile application does after the connection is established. This is done to receive the characteristic object. A characteristic in BLE is just like an endpoint in Web 2.0.
You can write or read data from a characteristic, and each characteristic is uniquely identified by its UUID. The applications have the device’s characteristic and service UUIDs hardcoded.
Therefore in this phase we must recognize and retrieve such UUIDs. We need those as we will clone the application’s behavior and transfer that functionality in our rogue device we’ re gonna build later-on.
Behavioral analysis
This is split into two other subcategories: Locating the write functions and locating the notification handling functions.
The notifications happen when the device is sending out messages to the device.
The write methods are what is being used by the application to send data to the device.
Device Name
Let’s find the name of the BLE target and any other advertised data. We don’t have the BLE device, so we have to dig into the decompiled code and find the name the application is looking for – if looking at all, for any name.
Our reverse engineering efforts are based on finding the methods provided by the standard Android API. Even when the code-base has the symbols being stripped-out, or when the code is completely obfuscated, we can still locate the functions handling the BLE connection, as the Android API must be used – and this cannot be obfuscated.
When we are looking for any advertisement data, we can lookup for the following standard functions:
ScanResult Class
ScanResult is a class and the instances of this class must be used to extract the advertisement data. It provides several methods for that. The developer may receive the raw bytes (this parser may contain vulnerability) or letting the android to properly parse the data and get the required values. Let’s see a few examples.
private static BluetoothScanResult newBluetoothScanResult(ScanResult scanResult) {
ScanRecord scanRecord = scanResult.getScanRecord();
byte[] scanRecordBytes = scanRecord.getBytes();
if (isBeacon(scanRecordBytes)) {
... SNIP ...
In the above decompiled code, the scanResult object contains a method called getScanRecord() which can be used to extract the raw bytes.
List<ParcelUuid> uuids = scanResult.getScanRecord().getServiceUuids();
The above snippet can be used to extract the services offered by the device.
The snippet following shows how the scanRecord object can be used to extract the device’s name.
String deviceName = scanResult.getDeviceName();
onLeScan method
In older Android versions (up to kitkat) the method onLeScan was used instead of ScanResult. The scanRecord was used to provide the raw data and the developer had to parse and extract the advertisement values manually from the raw bytes. Some things can be extracted but generally speaking, not much functionality is given.
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
LeRecord leRecord = parseData(scanRecord);
String name = device.getName();
Let’s search the code base using those two methods to locate the device-specific checks in our targeted application. We need to find where the application recognizes the device to be it’s “own” device type (i.e. this is my own M15X SmartWatch).
$ grep -Rni "ScanResult"
$ grep -Rni "onLeScan"
consmart/ble/BleController.java:21: public void onLeScan(BluetoothDevice bluetoothDevice, int i, byte[] bArr) {
consmart/ble/BleController.java:25: BleController.this.mMyLeScanCallback.onLeScan(bluetoothDevice, i);
consmart/ble/BleController.java:28: BleController.this.mMyLeScanCallback.onLeScan(bluetoothDevice, i);
consmart/ble/BleController.java:43: void onLeScan(BluetoothDevice bluetoothDevice, int i);
consmart/ble/MainActivity.java:15: public void onLeScan(BluetoothDevice bluetoothDevice, int i) {
qh/blelight/BluetoothLeService.java:72: public synchronized void onLeScan(final BluetoothDevice bluetoothDevice, int i, byte[] bArr) {
We can see three classes to make sense below
BleController (package consmart)
MainActivity (package consmart)
BluetoothLeService (package qh)
Navigating to the BleController:
private String name;
public void setScanLeDeviceType(UUID[] uuidArr, String str) {
this.serviceUuids = uuidArr;
this.name = str;
}
private LeScanCallback mLeScanCallback = new LeScanCallback() {
public void onLeScan(BluetoothDevice bluetoothDevice, int i, byte[] bArr) {
String name = bluetoothDevice.getName();
if (BleController.this.name != null || "".equals(BleController.this.name)) {
if (BleController.this.name.equals(name.substring(0, BleController.this.name.length())) && BleController.this.mMyLeScanCallback != null) {
BleController.this.mMyLeScanCallback.onLeScan(bluetoothDevice, i);
}
} else if (BleController.this.mMyLeScanCallback != null) {
BleController.this.mMyLeScanCallback.onLeScan(bluetoothDevice, i);
}
}
};
The device name seems to be “123456”, as shown below.
$ grep -Rni "setScanLeDeviceType"
consmart/ble/BleController.java:66: public void setScanLeDeviceType(UUID[] uuidArr, String str) {
consmart/ble/MainActivity.java:25: this.mBleController.setScanLeDeviceType(null, "123456");
Not very sensible right?
Navigating to MainActivity:
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.color.color1);
this.mContext = getApplicationContext();
this.mBleController = BleController.initialization(this.mContext);
new UUID[1][0] = UUID.fromString("0000180d-0000-1000-8000-00805f9b34fb");
this.mBleController.setScanLeDeviceType(null, "123456");
... SNIP ...
It seems that when the MainActivity is loaded, the BLE controller is initialized, the device name is set, and the UUID service is set to be “0000180d-0000-1000-8000-00805f9b34fb”.
Before moving further, if we use what we have found so far to build a peripheral, and then try to connect using the mobile application, we would see that the application cannot find our custom-build device. Let’s continue our research, you will soon understand why.
Navigating to BluetoothLeService:
... SNIP ...
public static final String COMPANY_NAME = "^Triones-|^BRGlight|^Triones\\+|^Dream|^Light-|^Triones~|^Triones";
public static final UUID UUID_HEART_RATE_MEASUREMENT = UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);
... SNIP ...
public synchronized void onLeScan(final BluetoothDevice bluetoothDevice, int i, byte[] bArr) {
CharSequence name = bluetoothDevice.getName();
if (name != null) {
if (Pattern.compile(BluetoothLeService.COMPANY_NAME).matcher(name).find()) {
if (!BluetoothLeService.this.mDevices.containsKey(bluetoothDevice.getAddress())) {
BluetoothLeService.this.mDeviceAddr = bluetoothDevice.getAddress();
BluetoothLeService.this.mDevices.put(bluetoothDevice.getAddress(), bluetoothDevice);
... SNIP ...
public int connBLE(final String str) { ... }
public boolean scanLeDevice() { ... }
... SNIP ...
We can see several interesting methods defined by this class. For now, the most important is the method onLeScan.
In that particular method, the device’s name is retrieved and matched against a regular expression. It checks the device’s name against several names including the following:
Triones
BRGlight
Dream
Light
if any of the above is included in the device’s name will match and will be added to the application’s list.
But what list are we referring to?
Service Discovery
We almost have what we need to create a rogue peripheral. What is missing here is the UUID of services used by this application.
We have seen one to be defined in the BluetoothLeService:
public static final UUID UUID_HEART_RATE_MEASUREMENT = UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);
Let’s see where HEART_RATE_MEASUREMENT has been defined (same symbol could be used in more than one classes):
$ grep -Rni "HEART_RATE_MEASUREMENT"
.idea/workspace.xml:41: <find>SampleGattAttributes.HEART_RATE_MEASUREMENT</find>
.idea/workspace.xml:42: <find>HEART_RATE_MEASUREMENT</find>
com/qh/blelight/BluetoothLeService.java:37: public static final UUID UUID_HEART_RATE_MEASUREMENT = UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);
com/qh/tools/SampleGattAttributes.java:7: public static String HEART_RATE_MEASUREMENT = "0000ffe1-0000-1000-8000-00805f9b34fb";
com/qh/tools/SampleGattAttributes.java:8: public static String HEART_RATE_MEASUREMENT2 = "0000ffe2-0000-1000-8000-00805f9b34fb";
com/qh/tools/SampleGattAttributes.java:14: attributes.put(HEART_RATE_MEASUREMENT, "Heart Rate Measurement");
We have found two UUIDs:
0000ffe1-0000-1000-8000-00805f9b34fb
0000ffe2-0000-1000-8000-00805f9b34fb
We also found another UUID earlier – This is a known service UUID which stands for heart rate service:
0000180d-0000-1000-8000-00805f9b34fb
Are those all? Are UUID numbers 0xffe1 and 0xffe2 a type of characteristic or a service?
Such UUIDs are defined in class SampleGattAttributes. So let’s go in reverse and search globally where these calls are used for.
$ grep -Rni "SampleGattAttributes"
com/qh/blelight/BluetoothLeService.java:21:import com.qh.tools.SampleGattAttributes;
com/qh/blelight/BluetoothLeService.java:37: public static final UUID UUID_HEART_RATE_MEASUREMENT = UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);
com/qh/blelight/MyBluetoothGatt.java:27:import com.qh.tools.SampleGattAttributes;
com/qh/blelight/MyBluetoothGatt.java:619: BluetoothGattDescriptor descriptor = this.photoCharacteristic.getDescriptor(UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
com/qh/tools/SampleGattAttributes.java:5:public class SampleGattAttributes {
The class MyBluetoothGatt seems to be very interesting as it is using the CCCD UUID to define a characteristic descriptor. Let’s visit it.
This is where it is used:
public void setNotify() {
UUID fromString = UUID.fromString("0000ffd0-0000-1000-8000-00805f9b34fb");
UUID fromString2 = UUID.fromString(DeviceUUID.CONSMART_BLE_NOTIFICATION_CHARACTERISTICS_DATA_UUID);
if (this.mBluetoothGatt != null) {
BluetoothGattService service = this.mBluetoothGatt.getService(fromString);
if (service != null) {
this.photoCharacteristic = service.getCharacteristic(fromString2);
this.mBluetoothGatt.setCharacteristicNotification(this.photoCharacteristic, true);
BluetoothGattDescriptor descriptor = this.photoCharacteristic.getDescriptor(UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
if (descriptor != null) {
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
this.mBluetoothGatt.writeDescriptor(descriptor);
}
}
}
}
We gained one more UUID. However, we can see that UUIDs are retrieved from somewhere else: from the class DeviceUUID.
Before moving into this class, I can ensure you this is the class we will need for the next phase. The class MyBluetoothGatt contains all BLE handling methods.
Let’s visit the class DeviceUUID:
public class DeviceUUID {
public static final String CONSMART_BLE_180a_UUID = "0000180a-0000-1000-8000-00805f9b34fb";
public static final String CONSMART_BLE_2a25_UUID = "00002a25-0000-1000-8000-00805f9b34fb";
public static final String CONSMART_BLE_NOTIFICATION_CHARACTERISTICS_DATA_UUID = "0000ffd4-0000-1000-8000-00805f9b34fb";
public static final String CONSMART_BLE_NOTIFICATION_CHARACTERISTICS_MUSICCHECK_UUID = "0000fff4-0000-1000-8000-00805f9b34fb";
public static final String CONSMART_BLE_NOTIFICATION_CHARACTERISTICS_MUSICMOD_UUID = "0000fff9-0000-1000-8000-00805f9b34fb";
public static final String CONSMART_BLE_NOTIFICATION_CHARACTERISTICS_TIME_UUID = "0000fff7-0000-1000-8000-00805f9b34fb";
public static final String CONSMART_BLE_NOTIFICATION_CHARACTERISTICS_WRGB_UUID = "0000ffd9-0000-1000-8000-00805f9b34fb";
public static final String CONSMART_BLE_NOTIFICATION_SERVICE_DATA_UUID = "0000ffd0-0000-1000-8000-00805f9b34fb";
public static final String CONSMART_BLE_NOTIFICATION_SERVICE_WRGB_UUID = "0000ffd5-0000-1000-8000-00805f9b34fb";
public static final String CONSMART_BLE_WRITE_CHARACTERISTICS_MUSICCHECK_UUID = "0000fff3-0000-1000-8000-00805f9b34fb";
public static final int SLIC_BLE_MANUFACTURER_DATA_LEN = 4;
public static final String SLIC_BLE_NOTIFICATION_CHARACTERISTICS_SIGNAL_UUID = "00002a06-0000-1000-8000-00805f9b34fb";
public static final String SLIC_BLE_NOTIFICATION_SERVICE_SIGNAL_UUID = "00001803-0000-1000-8000-00805f9b34fb";
public static final String SLIC_BLE_READ_CHARACTERISTICS_BATTERY_UUID = "00002a19-0000-1000-8000-00805f9b34fb";
public static final String SLIC_BLE_READ_CHARACTERISTICS_DEVICE_INFO_MANUFACTURER_NAME_UUID = "00002a29-0000-1000-8000-00805f9b34fb";
public static final String SLIC_BLE_READ_CHARACTERISTICS_INFO_ADDRESS_UUID = "00002a03-0000-1000-8000-00805f9b34fb";
public static final String SLIC_BLE_READ_CHARACTERISTICS_INFO_APPEARANCE_UUID = "00002a01-0000-1000-8000-00805f9b34fb";
public static final String SLIC_BLE_READ_CHARACTERISTICS_INFO_DEVICE_NAME_UUID = "00002a00-0000-1000-8000-00805f9b34fb";
public static final String SLIC_BLE_READ_CHARACTERISTICS_TX_POWER_LEVEL_UUID = "00002a07-0000-1000-8000-00805f9b34fb";
public static final String SLIC_BLE_READ_SERVICE_BATTERY_UUID = "0000180f-0000-1000-8000-00805f9b34fb";
public static final String SLIC_BLE_READ_SERVICE_DEVICE_INFO_UUID = "0000180a-0000-1000-8000-00805f9b34fb";
public static final String SLIC_BLE_READ_SERVICE_INFO_UUID = "00001800-0000-1000-8000-00805f9b34fb";
public static final String SLIC_BLE_READ_SERVICE_TX_POWER_LEVEL_UUID = "00001804-0000-1000-8000-00805f9b34fb";
public static final String SLIC_BLE_WRITE_CHARACTERISTICS_SOUND_ALERT_UUID = "00002a06-0000-1000-8000-00805f9b34fb";
public static final String SLIC_BLE_WRITE_SERVICE_SOUND_ALERT_HIGH_UUID = "00001802-0000-1000-8000-00805f9b34fb";
public static final String SWITCH_CAMERA_FIND_CHARA = "0000ffd1-0000-1000-8000-00805f9b34fb";
public static final String SWITCH_CAMERA_FIND_SERVICE = "0000ffd0-0000-1000-8000-00805f9b34fb";
public static final String SWITCH_FLIGHTMODE = "0000ffd3-0000-1000-8000-00805f9b34fb";
}
I guess that’s all the UUIDs we are going to need. Here the UUIDs are defined along and their explanation. But are all of those UUIDs really used by this app?
Let’s return to class MyBluetoothGatt.
We note down all the UUIDs used in this class, and that’s how we gain all UUIDs have been used from this application.
Behavioral Analysis
Since we have what we need in class MyBluetoothGatt what is missing is to understand the various methods used in this class (when the developers use the class BluetoothGatt often it contains most of the BLE implementation).
Let’s take a method from this class.
public void openLight(boolean z) {
if (this.datas != null && this.datas.length >= 4) {
byte[] bArr = new byte[]{(byte) -52, (byte) 35, (byte) 51};
if (z) {
this.datas[2] = (byte) 35;
} else {
bArr = new byte[]{(byte) -52, (byte) 36, (byte) 51};
this.datas[2] = (byte) 36;
if (this.mHandler != null) {
this.mHandler.postDelayed(new Runnable() {
public void run() {
MyBluetoothGatt.this.writeCharacteristic(DeviceUUID.CONSMART_BLE_NOTIFICATION_SERVICE_WRGB_UUID, DeviceUUID.CONSMART_BLE_NOTIFICATION_CHARACTERISTICS_WRGB_UUID, new byte[]{(byte) -52, (byte) 36, (byte) 51}, true);
}
}, 300);
}
}
writeCharacteristic(DeviceUUID.CONSMART_BLE_NOTIFICATION_SERVICE_WRGB_UUID, DeviceUUID.CONSMART_BLE_NOTIFICATION_CHARACTERISTICS_WRGB_UUID, bArr, true);
}
}
This is the service UUID: 0000ffd5-0000-1000-8000-00805f9b34fb
This is the characteristic UUID: 0000ffd9-0000-1000-8000-00805f9b34fb
Note: We ignore the UUIDs we have found before, as the new ones are probably the ones the application uses to talk to the bulb.
Why did i chose this particular methods over all the methods of the class? Because the target is a bulb and from all the other methods in the class, the method’s name looks exactly what I wanted, to open the light.
Let’s rapidly build a peripheral to test our assumptions: Make the application 1) make the app connect to our device, 2) Make it discover our strategically placed services and characteristics and 3) communicate and read or write data from/into them.
// Configure peripheral using BLE:bit tool
PEController pe = BLEHelper.getPeripheralController(getPEBLEDeviceCallbackHandler());
if (pe == null) {
System.err.println("PE tool Not found");
System.exit(1);
}
pe.configurePairing(PairingMethods.NO_IO);
pe.sendBluetoothDeviceAddress(BLEHelper.generateRandomDeviceAddress(), BITAddressType.PUBLIC);
pe.sendConnectionParameters(new PEConnectionParameters());
// Set device name
pe.sendDeviceName("Triones-1");
AdvertisementData advdata = new AdvertisementData();
advdata.setFlags(AdvertisementData.FLAG_LE_GENERAL_DISCOVERABLE_MODE | AdvertisementData.FLAG_ER_BDR_NOT_SUPPORTED);
advdata.includeDeviceName();
pe.sendAdvertisementData(advdata);
// Define service
BLEService ble_service = new BLEService("0000ffd5-0000-1000-8000-00805f9b34fb");
// Define characteristic
BLECharacteristic chr = new BLECharacteristic("0000ffd9-0000-1000-8000-00805f9b34fb", new byte[] {0});
chr.enableRead();
chr.enableWrite();
chr.enableNotification();
chr.setAttributePermissions(BLEAttributePermission.OPEN, BLEAttributePermission.OPEN);
chr.setMaxValueLength(27);
// Add service and characteristic
ble_service.addCharacteristic(chr);
pe.sendBLEService(ble_service);
// Start advertisement
pe.finishSetup();
Our device has started advertising using the device name “Triones-1”. As shown below, the application has found the device and connected automatically to the device.
This is the output of the tool without touching anything in the app:
Connected 65:da:23:e7:31:4b
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: ef0177
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 242a2b42
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 1014160c07171033030001
Playing with the app and changing the color on the application we inspect the packets exchanged:
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 5600bf0f00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 5600bf1a00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 5600bf7400f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 5600bf7b00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 5600bfb400f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 5600bfba00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 56008dbf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 560087bf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 56007fbf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 560078bf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 560073bf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 56006bbf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 560067bf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 560060bf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 56005cbf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 560056bf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 560024bf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 56001fbf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 561300bf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 561800bf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 563900bf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 563c00bf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 567700bf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 568100bf00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 56bf00ad00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 56bf00a400f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 56bf009c00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 56bf009300f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 56bf008900f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 56bf007f00f0aa
Characteristic write 0000ffd9-0000-1000-8000-00805f9b34fb data: 56bf007500f0aa
So, we have mapped our targeted functionality in the mobile application to the targeted functionality on the device (light color changing).
Closing remarks
This is the tip of the iceberg, and more interesting things can be done.
We could continue our exploration of the class by understanding each and every method of the class. However, we won’t delve more into this article. A future article shall delve deeper into the third phase and cover the behavior analysis.
Would you like to learn more?
Subscribe to stay ahead of competition!