BLE Packet Capture and Decryption with Nordic nRF Sniffer

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.

  1. 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:

  1. Open Wireshark, go to "Help" → "About Wireshark" → "Folders"
  2. Find the "Plugins" directory (usually ~/.local/lib/wireshark/plugins or %APPDATA%\Wireshark\plugins)
  3. 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 Dongle
  • sniffer_nrf52dk_nrf52832_*.hex - For nRF52 DK Development Board
  • sniffer_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:

  1. Install nRF Connect for Desktop (available from Nordic's official website)
  2. Open the Programmer tool
  3. Insert the Dongle into your computer and click "Select device"
  4. Select the corresponding Dongle and click the "Write" button
  5. 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:

  1. In the capture interface list, you should see "nRF Sniffer for Bluetooth LE COMx"
  2. Double-click the interface to start capturing
  3. 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

  1. 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:

  1. Connection Parameter Negotiation: The connection interval, slave latency, and supervision timeout parameters directly impact power consumption and response speed
  2. Channel Selection Algorithm: BLE uses adaptive frequency hopping to avoid interference
  3. 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

Tags: BLE nRF52840 wireshark LESC Bluetooth Low Energy

Posted on Sun, 25 Oct 2026 16:11:29 +0000 by scarabee