Wiki / Protocols

BLE Advertising

The connectionless broadcast layer of BLE: short length-prefixed packets a device repeats on three channels, and everything a scanner can learn about it before any connection exists.

An ADV_IND payload, byte for byte
AdvA, the advertiser address (6 bytes) D5 1F 8A 27 4B C0
AD: Flags (type 0x01) 02 01 06
AD: Complete Local Name (type 0x09) 09 09 44 45 4D 4F 2D 54 41 47
AD: Manufacturer Specific Data (type 0xFF) 07 FF FF FF 11 22 33 44
The payload is the 6-byte advertiser address followed by AD structures placed back to back. Each structure is [length][AD type][data], and the length byte counts the AD type byte as well as the data, so the Flags structure above spends 3 bytes on the air to carry 1 byte of content. Legacy advertising gives you 31 bytes total for the AD structures, which is why a vendor packs its identity into Manufacturer Specific Data and then has almost nothing left.

BLE advertising is the connectionless half of Bluetooth Low Energy. A peripheral repeats a small packet into the air on a fixed schedule, anyone in range can receive it, nobody has to connect, and nothing is authenticated. It is the first thing you see on a target and the last thing most people bother to read properly.

Advertising, ATT, GATT: where this sits

The three fit together in one order, and mixing them up is the usual source of confusion.

Advertising is before the connection. It is a broadcast on the link layer. There is no client, no server, no session, no handshake. A device shouts, scanners listen.

ATT and GATT are after the connection. Only once a central sends CONNECT_IND and the link exists do the attribute table and the service tree mean anything. See Wiki: ble-att and Wiki: ble-gatt.

So a scanner sees advertising data; a connected client sees a GATT table. They carry different information, they are protected differently (advertising: not at all), and a device can expose plenty in one and nothing in the other. A beacon is simply a device that never leaves the first half: it advertises and does not accept connections at all.

Where it happens

BLE splits the 2.4 GHz ISM band into 40 channels of 2 MHz. Three of them are the primary advertising channels: 37, 38, and 39, at 2402, 2426, and 2480 MHz. They are deliberately spaced to sit in the gaps between the common Wi-Fi channels, which is why BLE discovery still works in a crowded office. A device sends the same advertising PDU on all three in quick succession, which is one advertising event, then waits out its advertising interval and does it again.

Every advertising PDU uses the same fixed link-layer access address, 0x8E89BED6. That constant is what lets a sniffer recognise an advertising packet with no prior knowledge of the device, and it is also why capturing advertising is easy while capturing an established connection is not: a connection uses its own random access address and hops across 37 data channels.

The packet

An advertising PDU carries a header (PDU type, address types, length) and a payload. For the ordinary ADV_IND the payload is the 6-byte advertiser address, then up to 31 bytes of advertising data. Those 31 bytes are not a blob: they are a chain of AD structures, each one length-prefixed.

[ length ][ AD type ][ data ... ][ length ][ AD type ][ data ... ] ...

Walking it is a three-line loop, and the two traps are both in the diagram above:

  • The length byte covers the AD type byte, not just the data. length - 1 is how many data bytes follow. Off by one and you truncate the structure.
  • Multi-byte fields are little-endian on the air. A company identifier of 0xE591 rides as 91 E5. Reverse it and you have attributed the packet to a different vendor.

The AD types you meet constantly:

Type Name Contents
0x01 Flags Discoverability and BR/EDR support bits
0x02 / 0x03 Incomplete / Complete list of 16-bit service UUIDs Which services the device claims
0x08 / 0x09 Shortened / Complete Local Name The name a scanner displays
0x0A Tx Power Level The transmit power, in dBm
0x16 Service Data, 16-bit UUID Payload attached to a service UUID
0xFF Manufacturer Specific Data 2-byte company identifier, then whatever the vendor wants

Manufacturer Specific Data is where the interesting bytes live. The first two bytes are a company identifier assigned by the Bluetooth SIG, little-endian; everything after them is vendor-defined and unspecified. Sensor readings, device state, serial numbers, session tokens, and occasionally key material all ship in there in the clear because the vendor treated a broadcast as a private channel. iBeacon is Apple's convention inside this structure (their company identifier, then a 16-byte UUID plus a 2-byte major and 2-byte minor and a measured-power byte). Eddystone is Google's, and uses Service Data under UUID 0xFEAA instead.

PDU types, and what "scan" actually does

The advertising PDU type says what the advertiser will accept, and it is the first thing to read:

PDU Connectable Scannable Use
ADV_IND yes yes the normal case: a device that wants a central
ADV_DIRECT_IND yes, to one address no reconnecting to a known bonded peer
ADV_SCAN_IND no yes broadcast plus a scan response
ADV_NONCONN_IND no no a pure beacon, it will never talk to you

A passive scan only receives. An active scan transmits a SCAN_REQ to each advertiser and collects a SCAN_RSP, which is a second, separate payload of up to 31 more bytes, very often carrying the full name that did not fit in the advert. That extra data is genuinely useful, but understand the trade: an active scan is not passive. You are transmitting, from your own address, and a target that logs scan requests has seen you.

Extended advertising (Bluetooth 5.0) changes the shape: a small ADV_EXT_IND on the primary channels points at an AUX_ADV_IND on a secondary channel, and the auxiliary PDU carries up to 254 bytes, chained across further PDUs if the device needs more. If a scan shows a device with a tiny or empty advert whose real data never appears, you are probably looking at legacy-only scanning against an extended advertiser. Ubertooth One does not handle extended advertising; an nRF52840 running Sniffle does.

Addresses, and why the MAC lies

The 6-byte advertiser address is not a reliable identifier.

  • A public address is a real IEEE-assigned 48-bit address, and its top 24 bits are a vendor OUI, so it fingerprints the chipset maker.
  • A random static address is picked by the device and stays fixed until it reboots. It may change on reboot.
  • A resolvable private address (RPA) is regenerated on a timer from an Identity Resolving Key. Only a peer that bonded and holds the IRK can tell that two RPAs are the same device. Rotation periods vary widely between vendors.
  • A non-resolvable private address is simply random, resolvable by nobody.

Practically: never key your tooling, or your conclusions, on an address you saw once. A device that seems to have vanished has usually just rotated. Conversely, a device that never rotates has told you something about how much its designers thought about privacy.

How to look at it

Modern BlueZ, no extra hardware:

bluetoothctl
#  > scan on              # active scan by default
#  > devices
#  > info AA:BB:CC:DD:EE:FF

# Raw HCI, including the full AD bytes of every LE Advertising Report.
sudo btmon

btmon is the one to reach for when you want the actual bytes rather than a stack's summary of them. Note that hcitool lescan still appears in every old write-up and is deprecated upstream in BlueZ; on current distributions it is often absent or built only behind a deprecated-tools flag. Use bluetoothctl, btmgmt, or bleak instead.

In Python, bleak gives you the parsed structures directly:

import asyncio
from bleak import BleakScanner

async def main():
    devices = await BleakScanner.discover(timeout=10.0, return_adv=True)
    for addr, (device, adv) in devices.items():
        print(addr, adv.rssi, adv.local_name)
        print("  manufacturer:", adv.manufacturer_data)   # {company_id: bytes}
        print("  service data:", adv.service_data)

asyncio.run(main())

adv.manufacturer_data is keyed by the company identifier already decoded from little-endian, so what you get back is the vendor payload alone.

To read advertising off the air rather than through your own host stack, an nRF52840 dongle running Sniffle writes a pcap you open in Wireshark, where the dissector is btle. In the Espilon labs the same two steps are probe scan and probe sniff.

Pitfalls

  • Advertising is not encrypted. Legacy advertising has no link-layer encryption at all, and pairing does not retroactively protect a broadcast. Bluetooth 5.4 added Encrypted Advertising Data, but you will almost never meet it in a shipping product. Anything a device advertises, it has published.
  • A well-formed advert authenticates nothing. There is no signature and no counter, so any receiver that trusts a broadcast trusts anyone who can transmit. See Wiki: ble-replay.
  • Your host stack hides bytes from you. Phones and desktop stacks de-duplicate advertisements, merge the scan response into the advert, and drop AD types they do not understand. If the byte you want is missing, look at btmon or a sniffer capture, not at a scanner app.
  • RSSI is not distance. It moves with orientation, body blocking, and transmit power, and the Tx Power AD type is whatever the vendor wrote there. Treat it as "nearer or further than a moment ago", nothing more.
  • A captured frame is not a bare advert. A pcap frame carries a pseudo-header, the access address, and the link-layer header before the payload starts, so an offset that works on a hand-typed advert is wrong on a capture. Search the frame for the structure you want instead of hardcoding an offset.
  • Cloning the payload is not cloning the device. Re-emitting the AD bytes is easy; reproducing the advertiser address is a controller-level operation that stock BlueZ often cannot do for a public address. If the receiver checks the address, the clone fails. See Wiki: ble-replay.

Further reading