Skip to content

Protocol

Live capture·

How the vendor app's own traffic was captured on a Pixel and matched byte-for-byte to the APK read and the driver.

  • hackers capturing their own ring
  • 1 min read
  • 222 words

In 10 seconds

Turn on the Pixel's HCI snoop socket, let LoraFit talk to a ring, mark every tap, decode the ATT layer with tools/btsnoop_att.py — that is how the APK read was confirmed byte for byte (66 frames in the first capture) and how 0x19 and 0x44 were found. tools/capture.sh start lorafit · mark "tap Start HR" · stop

Recipe (Pixel, Android 14+)·

step do
1 Developer optionsEnable Bluetooth HCI snoop logEnabled and the snoop log socket toggle on (settings put secure bluetooth_hci_log 1 alone is not enough)
2 adb forward tcp:8872 tcp:8872tools/capture.sh start lorafit (background reader → captures/btsnoop_lorafit_<ts>.log)
3 drive the app: connect, every screen, start/stop every measurement — tools/capture.sh mark "tap Start HR" per action → captures/markers.jsonl
4 tools/capture.sh stop → decodes to .att.jsonl + .txt via tools/btsnoop_att.py (below)

The stream is standard btsnoop v1 — the same framing as btsnoop_hci.log inside a bugreport, which works. 99 % of the bytes are advertising reports; the ATT traffic is tiny.

The socket dies with the stack

Any Bluetooth restart on the phone (svc bluetooth disable, airplane mode) ends the stream — start a new file. Two lanes sharing one phone radio killed each other's captures twice.

What the captures proved — link, connect burst, HR, SpO₂ 0x19, what is not on the wire
link handles 0x0017 write / 0x0019 notify, CCCD 0x001a ← 01 00; MTU 512 (ring) / 517 (phone); LE-encrypted by the phone's Just-Works pairing — the protocol has no auth
connect burst 37 → 13-byte headerless → 68 → 55 → 29 → 6, then the app's handshake (0x2F, 0x3E every 2 s, 0x3D, 0x21 × 5 days, 0x01, 0x1A, 0x08, 0x22, 0x24) — identical across two sessions
HR 0x07 then 0x1E 0000 173b 01 05; off-finger → no 0x0B frames, no error, the UI just says Measuring…
SpO₂ 0x17 01 → echo → 0x19 empty = ended without value (APK case 25setBloodStatus(false))
not on the wire the interpreter screen uses the phone mic (only 0x2F/0x3D queries); LoraFit v89 has no find-ring, alarm or temperature UI for this ring
never captured the 4-tap memo transfer (0x3D → 0x40 → 0x34 → 0x36) — needs a human tapping during a window; proven [LIVE] from the driver instead (Audio)

The full ledger in the order things were found: Capture ledger.

tools/btsnoop_att.py — the dependency-free btsnoop → ATT decoder

tools/btsnoop_att.py turns a btsnoop (HCI H4) log — from the Android snoop socket or a bugreport — into the ATT conversation: every write, notification and read with direction, handle and payload hex. No dependencies beyond Python 3.

python3 tools/btsnoop_att.py capture.log                 # writes + notifies, human-readable
python3 tools/btsnoop_att.py capture.log --all           # every ATT PDU (MTU, discovery, reads)
python3 tools/btsnoop_att.py capture.log --handles       # the recovered handle → UUID map
python3 tools/btsnoop_att.py capture.log --json out.att.jsonl

Output line:

15:05:23.412 TX WRITE_REQ h=0017  fe fc 01 00 01 00 01 00 07 00 b7 7e ae 6a 00 00 00
15:05:23.501 RX NOTIFY    h=0019  fe fc 01 00 01 00 01 00 07 00 ea 07 09 13 0b 05 19

What it does

  • Walks btsnoop records (timestamps are µs since year 0; the epoch constant is 0x00E03AB44A676000), keeps HCI ACL packets, reassembles L2CAP fragments, and decodes CID 4 (ATT).
  • Recovers the handle → UUID map from Find-Information / Read-By-Type / Read-By-Group responses when the phone discovered services in-band. Android caches GATT for bonded peers, so a capture may have no discovery at all — then handles stay numeric (the JY handles are constant anyway).
  • --json emits one object per PDU: {ts, dir, op, handle, uuid?, hex} — the captures/*.att.jsonl files in the repo.

Reading JY frames in the output

Every JY frame starts fe fc; the cmd is the next byte (LE u16, high byte 0). Payload begins at offset 10. The 13-byte headerless block after device info (0c 98 00 <mac> 3a 00 48 00) is not a JY frame — it is the advertising manufacturer data echoed on the link.

Pair the decoded lines with captures/markers.jsonl (tools/capture.sh mark) to attribute a frame to a UI action.