BLE Packet Capture and Decryption with Nordic nRF Sniffer
When developing IoT devices based on BLE or performing security analysis on Bluetooth communications, the ability to capture and decrypt encrypted packets is an essential skill. Many developers encounter difficulties when attempting to capture BLE communications for the first time—after device pairing, they find Wireshark filled with "Encrypted packet decrypted incorrectly" warnings, making encrypted packets completely unanalyzable. This is due to the BLE security mechanisms, particularly the LE Secure Connections (LESC) pairing introduced in BLE 4.2, which renders traditional packet capture tools ineffective.
However, with the solution presented here, you can set up a complete BLE packet capture environment within 5 minutes, easily decrypt even LESC-encrypted communications. This solution is based on Nordic's official nRF Sniffer firmware and Wireshark, requiring minimal investment (just an nRF52840 Dongle costing tens of dollars) while providing results comparable to professional protocol analyzers. More importantly, I'll detail how to obtain private keys by enabling Debug mode in the SDK, allowing you not only to capture packets but also to gain a deep understanding of every detail in BLE encrypted communications.
- Environment Setup: Building a Packet Capture Platform from Scratch
1.1 Hardware Selecsion and Preparation
To set up a BLE packet capture environment, you first need to select appropriate hardware. There are multiple BLE Sniffer solutions available, but Nordic's nRF Sniffer is highly favored due to its open-source nature, ease of use, and low cost.
Hardware Comparison:
| Hardware Device | Price Range | Advantages | Disadvantages | Recommended Scenarios |
|---|---|---|---|---|
| nRF52840 Dongle | $7-15 | Very low cost, excellent official support, powerful performance | Requires Sniffer firmware flashing | Individual developers, educational research |
| nRF52 DK Development Board | $45-75 | Built-in debugger, comprehensive features | Higher price, larger size | Professional development teams |
| Ellisys Bluetooth Analyzer | $30,000-45,000 | Professional grade, powerful features, supports multiple protocols | Expensive price | Enterprise product development, deep security analysis |
| Ubertooth One | $150-300 | Open source hardware, active community | Complex configuration, limited performance | Security researchers |
For most developers, the nRF52840 Dongle is the optimal choice. Based on Nordic's nRF52840 chip, it supports BLE 5.0 with sufficient performance for the vast majority of scenarios.
Note: When purchasing, ensure you select genuine products, as some counterfeits may have firmware compatibility issues. It's recommended to buy from official authorized channels or reputable distributors.
1.2 Software Environment Configuration
Software environment configuration is relatively straightforward, but there are several key steps to note:
Step 1: Install Wireshark
# Ubuntu/Debian systems
sudo apt update
sudo apt install wireshark
# macOS systems
brew install wireshark
# Windows systems
# Download installer from official website: https://www.wireshark.org/download.html
After installation, configure Wireshark to support the nRF Sniffer plugin:
- Open Wireshark, go to "Help" → "About Wireshark" → "Folders"
- Find the "Plugins" directory (usually
~/.local/lib/wireshark/pluginsor%APPDATA%\Wireshark\plugins) - Download the nRF Sniffer plugin and place it in that directory
Step 2: Obtain and Flash Sniffer Firmware
Nordic provides precompiled Sniffer firmware available from the GitHub repository:
# Clone nRF Sniffer for Bluetooth LE repository
git clone https://github.com/nordicsemiconductor/nRF-Sniffer-for-Bluetooth-LE.git
# Enter firmware directory
cd nRF-Sniffer-for-Bluetooth-LE/hex
Firmware files are categorized by hardware model:
sniffer_nrf52840dongle_nrf52840_*.hex- For nRF52840 Donglesniffer_nrf52dk_nrf52832_*.hex- For nRF52 DK Development Boardsniffer_nrf5340_dk_nrf5340_*.hex- For nRF5340 DK Development Board
There are multiple methods to flash firmware. Here's an example using the nRF52840 Dongle:
Using nRF Connect for Desktop to flash:
- Install nRF Connect for Desktop (available from Nordic's official website)
- Open the Programmer tool
- Insert the Dongle into your computer and click "Select device"
- Select the corresponding Dongle and click the "Write" button
- Choose the downloaded hex file and start flashing
Using command line tools to flash:
# Install nrfutil
pip install nrfutil
# Enter DFU mode: Hold the button on the Dongle while inserting USB, wait for LED to start breathing
nrfutil dfu usb-serial -pkg nrf_sniffer.zip -p COMx # Windows
nrfutil dfu usb-serial -pkg nrf_sniffer.zip -p /dev/ttyACM0 # Linux/macOS
Step 3: Configure Wireshark for Packet Capture
After flashing, reinsert the Dongle and open Wireshark:
- In the capture interface list, you should see "nRF Sniffer for Bluetooth LE COMx"
- Double-click the interface to start capturing
- In the toolbar, click "nRF Sniffer" → "Device" and select the BLE device address to monitor
Tip: If you don't see the nRF Sniffer interface, you may need to manually install USB drivers. On Windows, you can use the Zadig tool to install WinUSB drivers for the Dongle.
1.3 Basic Packet Capture Practice
Let's start with a simple example. Suppose we want to monitor a heart rate monitor device (such as Nordic's HRM example):
# Simulating BLE device advertising (for understanding the advertising process only)
import time
def simulate_ble_advertisement():
# BLE device powers on and starts advertising
print("Device starting advertising...")
# Advertisement data includes:
# 1. Device name
# 2. Service UUIDs (heart rate service, battery service, etc.)
# 3. Transmit power
# 4. Connection parameter suggestions
advertisement_data = {
"device_name": "Nordic_HRM",
"services": ["180D", "180F", "180A"], # Heart rate, battery, device info services
"tx_power": -20, # dBm
"connectable": True
}
return advertisement_data
# In Wireshark, you would see similar advertisement packets:
# ADV_IND type with complete advertising data
The advertisement packets captured in Wireshark will display detailed information:
Key Field Analysis:
- PDU Type:
ADV_IND(Connectable undirected advertising) - AdvA: Advertising device address (usually a random address)
- AdvData: Advertising data containing:
- Flags: LE General Discovery Mode
- List of complete 16-bit service UUIDs
- Device name
- Transmit power level
Wireshark Filtering Tips:
# Show only advertisement packets from a specific device
btle.advertising_address == aa:bb:cc:dd:ee:ff
# Show only advertisements containing specific services
btle.advertising_data.service_uuid16 == 0x180D
# Exclude empty packets
btle.length != 0
# Combine filter conditions
btle.advertising_address == aa:bb:cc:dd:ee:ff && btle.advertising_data.service_uuid16 == 0x180D
- In-depth Analysis of BLE Protocol Layers: From GAP to GATT
2.1 GAP Layer: The Foundation of Connection Establishment
GAP (Generic Access Profile) is responsible for BLE device discovery, connection establishment, and security initialization. Understanding the GAP process is crucial for debugging connection issues.
Detailed Connection Establishment Process:
When a Central device (such as a smartphone) scans the advertisement from a Peripheral device (such as a heart rate monitor), it initiates a connection request:
# Key parameters of connection request packet (CONNECT_IND)
connect_ind_packet = {
"initiator_address": "11:22:33:44:55:66", # Initiator address
"advertiser_address": "aa:bb:cc:dd:ee:ff", # Advertiser address
"access_address": 0x8E89BED6, # Access address (randomly generated)
"crc_init": 0x555555, # CRC initial value
"connection_parameters": {
"conn_interval": 30, # Connection interval: 30 * 1.25ms = 37.5ms
"conn_latency": 0, # Slave latency
"supervision_timeout": 600 # Supervision timeout: 600 * 10ms = 6s
},
"channel_map": 0x1FFFFFFFFF, # Channel map (using all 37 data channels)
"hop_increment": 11 # Hop increment
}
When analyzing the connection establishment process in Wireshark, pay special attention to the following key points:
- Connection Parameter Negotiation: The connection interval, slave latency, and supervision timeout parameters directly impact power consumption and response speed
- Channel Selection Algorithm: BLE uses adaptive frequency hopping to avoid interference
- Access Address: Each connection has a unique access address used to distinguish different connections
Common Connection Issue Troubleshooting:
| Problem Phenomenon | Possible Causes | Troubleshooting Methods |
|---|---|---|
| Frequent disconnections | Supervision timeout set too short | Check if supervision\_timeout is sufficiently large |
| Large data transmission delay | Connection interval too long | Check if conn\_interval is reasonable |
| High power consumption | Connection interval too short | Appropriately increase conn\_interval |
| Unstable data transmission | Poor channel quality | Check channel\_map and actual environmental interference |
2.2 Link Layer Control Protocol: The Core of Connection Maintenance
After connection establishment, devices exchange capabilities and update parameters through the Link Layer Control Protocol (LLCP). This is the most easily overlooked but crucial part of BLE communication.
Capability Exchange:
# LL_FEATURE_REQ/RSP packet structure
feature_exchange = {
"supported_features": {
"le_encryption": True, # Encryption support
"conn_param_req": True, # Connection parameter request support
"extended_reject": True, # Extended reject indication support
"slave_initiated_feature_exchange": True, # Slave-initiated feature exchange support
"le_ping": True, # Ping support
"le_data_packet_length_extension": True, # Data packet length extension support
"ll_privacy": False, # Privacy support
"extended_scanner_filter_policies": False, # Extended scanner filter policies support
"le_2m_phy": True, # 2M PHY support
"le_coded_phy": False, # Coded PHY support
"le_extended_advertising": False, # Extended advertising support
"le_periodic_advertising": False # Periodic advertising support
}
}
Key Parameter Update Process:
Data Length Update:
# Filter LLCP packets in Wireshark
btl2cap.cid == 0x0004 # LLCP channel
# View data length update process
btle.ll.opcode == 0x22 # LL_LENGTH_REQ
btle.ll.opcode == 0x23 # LL_LENGTH_RSP
Data length update is an important feature introduced in BLE 4.2, expanding the maximum packet size from 27 bytes to 251 bytes, significantly improving throughput.
Connection Parameter Update:
# Connection parameter update request
conn_param_update = {
"conn_interval_min": 15, # Minimum connection interval: 15 * 1.25ms = 18.75ms
"conn_interval_max": 30, # Maximum connection interval: 30 * 1.25ms = 37.5ms
"conn_latency": 0, # Slave latency
"supervision_timeout": 600, # Supervision timeout
"preferred_periodicity": 0, # Preferred periodicity
"reference_conn_event_count": 0, # Reference connection event count
"offset0": 0,
"offset1": 0,
"offset2": 0,
"offset3": 0,
"offset4": 0
}
PHY Update: BLE 5.0 introduced 2M PHY and Coded PHY. The PHY update process allows devices to negotiate using higher rate or longer range physical layers.
Note: PHY updates may affect communication range and power consumption, so careful selection based on actual application scenarios is necessary.
2.3 GATT Service Discovery and Data Exchange
GATT (Generic Attribute Profile) is the core of BLE application layer communication, defining the discovery and access mechanisms for Services, Characteristics, and Descriptors.
MTU Exchange Process: Before starting service discovery, devices need to exchange Maximum Transmission Unit (MTU) size:
# Exchange MTU Request/Response
mtu_exchange = {
"client_rx_mtu": 247, # Client receive MTU
"server_rx_mtu": 247 # Server receive MTU
}
MTU size directly impacts data transmission efficiency. A larger MTU reduces protocol overhead but requires devices to support the data length extension feature.
Service Discovery Process:
def discover_services(connection_handle):
# 1. Discover all primary services
services = []
start_handle = 0x0001
end_handle = 0xFFFF
while True:
# Send Read By Group Type Request
request = {
"opcode": 0x10, # Read By Group Type Request
"starting_handle": start_handle,
"ending_handle": end_handle,
"attribute_type": 0x2800 # Primary Service
}
# Receive response
response = send_request(request)
if response["opcode"] == 0x11: # Read By Group Type Response
for service in response["services"]:
services.append({
"handle": service["handle"],
"group_end_handle": service["group_end_handle"],
"uuid": service["uuid"]
})
# Update start handle
start_handle = services[-1]["group_end_handle"] + 1
if start_handle > end_handle:
break
else:
# Error response, end discovery
break
return services
Characteristic Discovery and Read/Write Operations: After discovering services, you need to further discover the characteristics within each service:
def discover_characteristics(service):
characteristics = []
start_handle = service["handle"] + 1
end_handle = service["group_end_handle"]
while start_handle <= end_handle:
# Send Read By Type Request
request = {
"opcode": 0x08, # Read By Type Request
"starting_handle": start_handle,
"ending_handle": end_handle,
"attribute_type": 0x2803 # Characteristic
}
# Receive response
response = send_request(request)
if response["opcode"] == 0x09: # Read By Type Response
for characteristic in response["characteristics"]:
characteristics.append({
"handle": characteristic["handle"],
"value_handle": characteristic["value_handle"],
"properties": characteristic["properties"],
"uuid": characteristic["uuid"]
})
# Update start handle
start_handle = characteristic["value_handle"] + 1
else:
# Error response, end discovery
break
return characteristics