Wiki / Protocols

BLE MITM

Sitting between a BLE central and a peripheral by advertising a clone of the target and forwarding every GATT operation, which requires winning the victim's connection and is defeated only by a pairing model somebody actually verifies.

One MITM rig, two halves
Your host, two BLE adapters
Adapter A: the cloned peripheral
Advertises the target's name, UUIDs and manufacturer data
Serves a copy of the GATT table to the victim
Adapter B: your central
Holds the connection to the real peripheral
Forwards reads, writes and notifications both ways
A BLE MITM is not one program on one radio. Half of it is a peripheral facing the victim and half of it is a central facing the real device, and they have to run at the same time, which is why every tool in this family wants two adapters. The GATT table on the left half is a copy dumped from the right half, so the victim's app finds the UUIDs it expects and never notices the detour.

A BLE MITM puts you between a central and a peripheral so that every read, write, and notification passes through you before it reaches the other side. Unlike Wiki: ble-replay, which only repeats what already happened, a MITM sees traffic live and can change it in flight. It is also considerably more work than most write-ups admit, and the reasons it fails in the field are more interesting than the diagram.

The shape of it

A BLE MITM is always two roles running at once:

  1. A peripheral facing the victim. It advertises a convincing clone of the target and serves a copy of the target's GATT table.
  2. A central facing the real device. It holds the genuine connection.

Your code sits between them and forwards operations. The victim connects to your peripheral, writes what it intended to write, and your central issues that same write to the real device; responses and notifications come back the other way.

victim central  --GATT-->  [ your peripheral | your central ]  --GATT-->  real peripheral

That is the whole idea. Everything difficult about it is in the word "convincing" and in getting the victim to pick you.

What it actually requires

Two radios. A BLE controller can advertise as a peripheral and connect as a central, but running both roles simultaneously with independent GATT databases is fragile on the stacks you will use. Every tool in this family (GATTacker, BtleJuice) assumes two adapters, sometimes on two hosts. Plan for two.

A cloned GATT table, not just a name. The victim's app usually looks for specific service and characteristic UUIDs and gives up if they are not there. So the first step of a MITM is a plain enumeration of the real device (see Wiki: ble-gatt) and the second is re-serving that structure. On Linux the peripheral half is BlueZ's D-Bus GATT API (org.bluez.GattManager1, RegisterApplication), or the bless Python library which wraps it. Note that bleak cannot do this half: it is a central-only library, and expecting it to act as a peripheral is a common dead end.

A way to win the connection. The victim connects to one advertiser, and it has to be yours. Options, roughly in order of how well they work:

  • Connect to the real device first. Most peripherals stop advertising while connected, so once your central holds the link, the real advertiser disappears and only your clone remains visible. This is the trick the classic tools rely on, and it is by far the most reliable one.
  • Out-advertise it. Higher transmit power, a better antenna, closer proximity. Workable but not deterministic: the victim's scan result depends on its own stack's timing as much as on RSSI.
  • Take the real device off the air. Physical distance, shielding, or jamming its advertising. Jamming is loud, frequently unlawful outside a controlled environment, and rarely what you want on an engagement.

A window before bonding. If the victim already bonded with the real device it holds an LTK and will attempt to encrypt. Your clone does not have that key, encryption fails, and the app errors out instead of talking to you. In practice MITM is clean during first pairing, or against a device whose control path is never encrypted at all, and awkward everywhere else.

The right address, sometimes. Some apps pin the peripheral's address. Reproducing an arbitrary public address is a controller-level operation that stock BlueZ generally cannot do, the same limit described in Wiki: ble-replay. If the app pins the address, your Linux box is not going to be the tool.

What LE Secure Connections changes, and what it does not

This is where the marketing and the reality part company, so take it in two halves.

LESC fixes passive eavesdropping. LE legacy pairing derives its keys in a way that a listener who caught the handshake can brute force offline; crackle does exactly that and hands you the LTK. LE Secure Connections replaces that step with an ECDH key agreement on P-256, so a purely passive attacker who recorded the whole pairing still cannot derive the LTK. Against a listener, LESC is a real fix.

LESC does not, by itself, fix MITM. Resistance to an active attacker comes from the association model, chosen from the two sides' declared IO capabilities and the MITM flag in the pairing request:

Association model MITM protection Why
Just Works none Nothing is compared, so an attacker in the middle pairs with both sides
Numeric Comparison (LESC only) yes, if a human looks Both screens show a six-digit value derived from the exchange; with an attacker in the middle the two values differ
Passkey Entry yes The passkey is a shared secret an attacker did not get
Out of Band depends on the OOB channel As strong as the channel that carried the OOB data

So Just Works under LESC gives you an encrypted link with an unauthenticated key. That is a genuine improvement against a passive listener and no improvement at all against someone standing in the middle. Since most products ship with no display and no keyboard, most products negotiate Just Works, and most products are therefore in exactly this state.

Two more things people miss:

  • Numeric Comparison is enforced by the user, not the radio. If nobody looks at the two numbers, or the app auto-confirms, the mechanism has done nothing.
  • The MITM flag is a request, not an enforcement. A peripheral can ask for an authenticated link and still leave the characteristic that matters readable and writable with no permission on it. Pairing strength and per-attribute permissions are separate settings, and it is the permissions that decide what an attacker can touch (see Wiki: ble-att).

MITM is not a relay attack

The two get used interchangeably and they are different attacks with different defences.

A relay attack simply extends the radio range between a genuine central and a genuine peripheral. Nothing is cloned, nothing is decrypted, nothing is modified: two radios and a link between them carry the traffic further than it was meant to go, so a phone or fob that is far away appears to be nearby. This defeats proximity-based unlocking however strong the crypto is, because the crypto is being carried out correctly by the real parties. The mitigation is not better encryption, it is a distance bound: round-trip timing, an ultra-wideband ranging channel, or requiring a user action.

A MITM terminates both links and reconstructs the traffic, so it can read and modify. It needs the cloning and the connection-winning described above, and pairing models can defeat it. Different attack, different fix.

Tooling, honestly

  • GATTacker and BtleJuice are the two reference implementations of the pattern, and reading either one is the fastest way to understand it. Both are old Node.js projects built on bluetooth-hci-socket, both are effectively unmaintained, and both are likely to fight you over Node versions and native builds before they run. Budget time for that or do not start.
  • btlejack targets a different problem: it sniffs, jams, and hijacks existing connections rather than proxying new ones. Useful for taking the real link down, not for the proxy itself.
  • Mirage is a more current framework with BLE modules including man-in-the-middle scenarios, and is usually a better starting point than reviving a 2016 Node project.
  • Rolling your own is genuinely practical: bleak for the central half, bless or the BlueZ D-Bus GATT API for the peripheral half, and a dictionary that maps cloned characteristic UUIDs to real ones. A hundred lines gets you a working proxy for one device, and you will understand every failure it produces.

Pitfalls

  • Cloning the name is not enough. Apps match on service UUIDs, on manufacturer data, and sometimes on the address. Dump everything the real device advertises (see Wiki: ble-advertising) and reproduce all of it.
  • Notifications need the CCCD wired through. If your proxy forwards writes but drops the client's CCCD subscription, the victim's app hangs waiting for updates and the operator blames the radio.
  • MTU is negotiated per link. Your two links can end up with different MTUs, and a value that fits on one side gets fragmented on the other. Negotiate the smaller of the two on both.
  • A bonded victim will not talk to your clone. Test on an unbonded pairing, or expect to have to make the victim forget the device first, which is a very visible action.
  • The real device may still be advertising. If your central has not connected to it, both advertisers are on the air and the victim may pick either one. Verify which one it connected to before you conclude the proxy is broken.
  • "We use LESC" is not an answer. Ask which association model actually gets negotiated on the deployed hardware, and whether the characteristics that matter require an authenticated link. Those two answers, not the version number, decide whether a MITM is worth your time.

Further reading