Classroom Glossary Public page

WIR-101 Lab 10 — BLE Sniffing, GATT Exploitation, and Protocol Reverse Engineering

676 words

Prerequisites: Week 10 lecture; bleak installed; nRF52840 dongle or BLE PCAP; authorized lab BLE target device Duration: ~90 min Points: 100


Authorization

  • Lab Authorization Form signed
  • BLE target: instructor-designated lab device only (address provided in class)
  • Read-only enumeration first; write operations only to characteristics explicitly marked as lab targets
  • No pairing attempts against devices not designated as the lab target

Objective

Perform a complete BLE security assessment: passive advertising capture, device fingerprinting, GATT enumeration, unauthenticated write testing, and passive pairing exchange analysis. Document findings in a GATT security profile that matches the capstone report format.


Part A — BLE Advertising Capture and Device Fingerprinting (20 min)

Option 1 — nRF52840 with Sniffle

python3 -m sniffle.sniff_receiver -e -a --rssi -50 2>&1 | tee ble_advertising.log

Run for 2 minutes. Ctrl+C when done.

Parse the output:

# parse_ble_adv.py
import re

with open('ble_advertising.log') as f:
    content = f.read()

# Extract device addresses
addresses = re.findall(r'([0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2})', content)
unique_addresses = list(set(addresses))
print(f"Unique BLE devices seen: {len(unique_addresses)}")
for addr in unique_addresses[:20]:
    print(addr)

Option 2 — Provided PCAP (virtual path)

Open lab10_ble_advertising.pcap in Wireshark. Filter: btle.advertising_header. List all unique advertiser addresses.

Device Fingerprinting

For each unique address, attempt to identify the device type from its AD structures:

# Using bleak BleakScanner for richer AD data
import asyncio
from bleak import BleakScanner

async def scan_devices():
    print("Scanning for 10 seconds...")
    devices = await BleakScanner.discover(timeout=10.0, return_adv=True)
    for addr, (device, adv) in devices.items():
        print(f"\nAddress: {addr}")
        print(f"  Name: {device.name or 'Unknown'}")
        print(f"  RSSI: {adv.rssi} dBm")
        print(f"  Service UUIDs: {adv.service_uuids}")
        if adv.manufacturer_data:
            for company_id, data in adv.manufacturer_data.items():
                print(f"  Manufacturer ({company_id:#06x}): {data.hex()}")

asyncio.run(scan_devices())

Match manufacturer IDs to companies (Bluetooth SIG Assigned Numbers list -- linked from course portal). Apple is 0x004C; Nordic Semiconductor is 0x0059.

Deliverable A

Table: device address, inferred name/type, RSSI, service UUIDs from advertising, manufacturer data (decoded if possible).


Part B — GATT Enumeration (25 min)

Connect to the instructor's designated lab BLE device and enumerate its complete GATT profile:

# gatt_enum.py
import asyncio, json
from bleak import BleakClient

TARGET = "XX:XX:XX:XX:XX:XX"  # instructor-provided

async def full_gatt_enum(addr):
    profile = {}
    async with BleakClient(addr, timeout=20.0) as client:
        for service in client.services:
            svc_entry = {
                'uuid': service.uuid,
                'description': service.description,
                'characteristics': {}
            }
            for char in service.characteristics:
                char_entry = {
                    'uuid': char.uuid,
                    'description': char.description,
                    'properties': list(char.properties),
                    'value': None,
                    'read_error': None
                }
                if 'read' in char.properties:
                    try:
                        val = await client.read_gatt_char(char.uuid)
                        char_entry['value'] = val.hex()
                    except Exception as e:
                        char_entry['read_error'] = str(e)
                svc_entry['characteristics'][char.uuid] = char_entry
            profile[service.uuid] = svc_entry
    return profile

profile = asyncio.run(full_gatt_enum(TARGET))
print(json.dumps(profile, indent=2))
with open('gatt_profile.json', 'w') as f:
    json.dump(profile, f, indent=2)

Security analysis from GATT profile

For each characteristic, classify:

UUID Description Properties Security Risk
... ... ... Critical / High / Med / Low / None

Security risk criteria:

  • Critical: write-without-response to an actuator (lock, relay, motor) with no authentication
  • High: write (with response) to actuator; or cleartext PII/health data in notify characteristic
  • Medium: writable characteristic with unauthenticated access to config (could affect device behavior)
  • Low: readable characteristic exposes device metadata (firmware version, serial) useful for fingerprinting
  • None: read-only non-sensitive characteristic

Deliverable B

gatt_profile.json + the security analysis table.


Part C — Unauthenticated Write Test (15 min)

The instructor has designated one specific characteristic UUID as the write test target. This characteristic controls an LED on the lab device and has no access restrictions.

# write_test.py
import asyncio
from bleak import BleakClient

TARGET = "XX:XX:XX:XX:XX:XX"
WRITE_CHAR_UUID = "XXXX-XXXX"  # instructor provides this

async def test_write():
    async with BleakClient(TARGET, timeout=20.0) as client:
        # Turn LED on
        await client.write_gatt_char(WRITE_CHAR_UUID, bytes([0x01]))
        print("Wrote 0x01 (LED ON) -- observe the LED")
        
        import asyncio; await asyncio.sleep(2)
        
        # Turn LED off
        await client.write_gatt_char(WRITE_CHAR_UUID, bytes([0x00]))
        print("Wrote 0x00 (LED OFF)")

asyncio.run(test_write())

Observe the LED state changing on the lab device. This demonstrates unauthenticated write-to-actuator access.

Deliverable C

Screenshot or video still of LED state change + written answer: in a real IoT pentest, what would this class of vulnerability enable? Provide a realistic attack scenario (not hypothetical -- describe an actual IoT deployment category where this is present).


Part D — Pairing Exchange Analysis (15 min)

The instructor will re-pair a phone to the lab BLE device while you observe with btmon (on a machine participating in the pairing) or via the Sniffle sniffer.

# btmon to observe HCI-level pairing
sudo btmon 2>&1 | grep -A5 "Pairing"

Identify from the output:

  1. Which pairing method was used (Just Works, Passkey Entry, Numeric Comparison, OOB)?
  2. What is the IO Capability reported by each device?
  3. Is Secure Connections enabled (look for "Secure Connections" in the HCI events)?

Virtual Path Alternative

Analyze the provided lab10_pairing_btmon.log (a btmon log from a Just Works pairing). Answer the same three questions from the log.

Deliverable D

Answers to the three questions + 1 paragraph: for the pairing method observed, what is the attack surface? Specifically: is the derived LTK vulnerable to passive eavesdropping + offline recovery? Explain why or why not based on the pairing method.


Write-up Questions

  1. You found a write-without-response characteristic with no authentication requirement on a smart home thermostat. The characteristic controls the target temperature setpoint. CVSS v3.1 score this finding. Write the full vector string.
  2. The lab device uses notify to stream temperature sensor data over BLE. The pairing method was Just Works. An attacker sits in the cafe next to the user with an nRF52840 sniffer. Can they read the temperature data? Explain the attack path.
  3. MAC address randomization is enabled on the lab device (address changes every ~15 minutes). Does this prevent the GATT enumeration you performed in Part B? Why or why not?
  4. The KNOB attack (CVE-2019-9506) forces Bluetooth to negotiate 1-byte entropy. This applies to Classic Bluetooth. Does a BLE Secure Connections pairing using ECDH P-256 share the same vulnerability? Explain the difference.

Submission

Zip into lab10_YOURNAME.zip:

  • ble_advertising.log or Wireshark screenshot (Part A)
  • deliverable_A_table.md
  • gatt_profile.json + deliverable_B_security_table.md
  • deliverable_C.md + screenshot
  • deliverable_D.md
  • gatt_enum.py, write_test.py, parse_ble_adv.py
  • writeup.md