



Executive Summary
Bluetooth Low Energy is how a phone talks to a lock, a watch, a glucose sensor, a tractor ECU, a conference badge. The radio is cheap and the firmware is often older than the intern who last touched it. The testing stack is worse: deprecated hcitool, a full-fat bettercap install just to print a GATT tree, gatttool tutorials from 2016, and a Bleak script you swear you will tidy up next quarter. Praetorian’s IoT engineers got tired of being the glue. On 10 July 2026 they released Caeruleus — Latin for the deep blue in Bluetooth — one Go binary on Linux/BlueZ that scans, enumerates, reads, writes, listens, fuzzes, and runs structured assess workflows with JSON an agent can eat.
This draft keeps every command, table, and walkthrough from the Praetorian post, then adds the kitchen picture of GATT as a shared notebook, a stack diagram, the GitHub command map (including assess pairing the blog barely names), and a defender checklist for unpaired reads, DFU doors, and write-without-response smash. Schema.org on the original page lists 2,167 words. This one is longer on purpose. It will not sniff, MITM, or clone a peripheral: Caeruleus cannot, and we will not pretend it can.
Caeruleus — Latin, deep blue
Praetorian, 10 July 2026
BLE for People Who Do Not Own a Sniffer


Classic Bluetooth is a cable replacement. BLE is a noticeboard. A peripheral advertises “I am a heart-rate strap.” A central connects and sees a tree of services and characteristics. Each characteristic has a handle (a small integer like 0x0015), a UUID (standard 0x2A37 or a vendor 128-bit), and properties: Read, Write, Write-without-response, Notify, Indicate. Pairing and encryption are optional. Many medical and industrial gadgets ship with every interesting drawer marked world-readable. That is not a crypto break. That is a missing checkbox in the GATT database.
The BLE Testing Tax

Pick a random BLE guide. You will be told to use hcitool lescan (deprecated, gone from many distros), bettercap ble.recon (a network-attack framework with libpcap/libusb/Ruby-era baggage, just to browse GATT), gatttool (deprecated for years, still in every tutorial), and a custom Bleak or pygatt script for anything real. BlueZ’s supported frontends are bluetoothctl and btmgmt: they manage the adapter and pair a headset. They are the wrong abstraction for an assessment. None of them emit JSON the next step can parse. You become the bus.
Caeruleus keeps abstractions at the level a human or an agent actually works: scan, enumerate, read, write, listen, assess, fuzz. Secrets in characteristic values are run through Praetorian’s open-source Titus rule set. Results can be JSON for scripts.
| Use case | Previous method | Using Caeruleus |
|---|---|---|
| Discovering nearby devices | hcitool lescan / bettercap ble.recon / bluetoothctl scan on | caeruleus scan |
| Listing (and reading) services/characteristics | bettercap ble.enum | caeruleus enumerate -b <mac> --values |
| An interactive session | gatttool -I / bluetoothctl menu gatt | caeruleus shell -b <mac> |
| Reading a handle | gatttool --char-read-hnd 0x0013 | caeruleus read -b <mac> -a 0x0013 |
| Writing to a handle | gatttool -b de:ad:be:ef:be:f1 --char-write-req -a 0x002c -n $(echo -n "some value"|xxd -ps) | caeruleus write -b de:ad:be:ef:be:f1 -a 0x002c --req -s "some value" |
| Capturing notifications | custom Bleak notification logger | caeruleus listen -b <mac> -a <handle> |
| Checking unauth data exposure | custom Bleak audit scripts | caeruleus recon / caeruleus assess ... |
| Fuzzing a characteristic | custom write fuzzers / boofuzz | caeruleus fuzz write -b <mac> -a <handle> |
| Connection params, MTU | hcitool con / btmgmt con-info | caeruleus conn-params -b <mac> |
| Adapter power and recovery | btmgmt power / hciconfig reset / rfkill | caeruleus doctor / caeruleus adapter power cycle |
Agents Welcome Here
-o json and -o jsonl are marked authoritative, not a CSV afterthought. The repo ships an agentskills.io Skill at skills/caeruleus/SKILL.md that teaches any compatible assistant the command surface and a recommended methodology. Praetorian’s benchmark: Opus 4.8 xHigh with Caeruleus and the skill used 62% of the time and 70% of the tokens versus the same model with free tool choice (it picked hcitool and Bleak).
Ergonomics That Sound Small Until You Live in a Faraday Closet
BLE health
caeruleus doctor walks BlueZ and kernel state: bluetoothd running, adapter powered, no leaked discovery session, BlueZ-cached address matches MGMT chip-live address, a 2-second LE scan probe, ExchangeMTU in a safe band. Failures come with a fix command, e.g. caeruleus adapter power cycle. Exit 0 = all-clear, 2 = a check failed (grep/diff convention).
Clean teardown: SIGTERM/SIGHUP run the disconnect path and poll until Connected=false hits BlueZ, so the peripheral re-advertises instead of sitting in a 10–30 second ghost connection after gatttool dies. Anyone who has fuzzed a lock and then waited for the supervision timeout knows this tax.
Output formats
- text — borderless tables so MACs and UUIDs are double-click selectable.
- json — indented, canonical.
- jsonl — one object per event for
listenandscan --live. enumerate --compact— one key=value line per characteristic for grep or a tight LLM window.
Scripting
- Daemon:
caeruleus serveholds one GATT connection on a Unix socket.caeruleus send "read 0x0029"avoids the ~1.5 s reconnect per CLI spawn. - Batch:
caeruleus batch -b $ADDRreads commands from stdin, ignores blanks and # comments, no readline, no async pump.-bconnects up front. - Atomic trigger-then-capture:
listen --trigger-handle/--trigger-valueset up notifications, write, capture. Separate commands miss the notify.
Convenience
- Device type from GAP Appearance + advertised UUIDs (0x00C2 + Heart Rate Service → “Smartwatch”).
- Handles:
0x002a,0X2A, or42. Hex:deadbeef,de:ad:be:ef,de ad be ef,0xdeadbeef. caeruleus recipes— 16 workflows in the blog, 19 in the current README;caeruleus recipes triggersearches by keyword.conn-params— interval, peripheral latency, supervision timeout, whether the peripheral renegotiated. gatttool and bettercap do not show this.
Opinionated: What It Will Not Do
Central only, never peripheral. No sniffing, no active AITM/MITM, no cloning. If you need Ubertooth/nRF sniffer, a GATTacker-style clone, or a pairing-mode downgrade over the air, bring another tool. Caeruleus is the GATT clerk, not the radio tap.
conn-params, monitor, fuzz write --raw, cccd --raw, raw-write, mtu --set, bdaddr --set. Older BlueZ (e.g. 5.55) hides remote GAP/GATT from D-Bus; enumerate --raw (root) walks ATT directly for true handles.A Quick Walkthrough
Sanity-check the adapter, then see what is yelling in the 2.4 GHz ISM band:
$ caeruleus doctor
adapter: hci0
[ OK] bluetoothd active
[ OK] Powered true
[ OK] Discovering false
[ OK] Address agreement 00:1A:7D:DA:71:13
[ OK] LE scan healthy saw advertisements
[ OK] ExchangeMTU 247
$ caeruleus scan
ADDRESS TYPE RSSI DEVICE NAME
------------------------------------------------------------
9C:9C:1F:F2:88:86 public -32 BLECTF_JohnsonSpace
C9:F0:81:C6:3B:52 random -50 Govee_H61B5_3B52
C4:C0:B0:3B:5B:EF random -59 Aranet4 06D4B
DC:FD:B4:CA:B4:E9 random -62 JBL Boombox 2
Browse the GATT tree and read every characteristic in one shot:
$ caeruleus enumerate -b 9C:9C:1F:F2:88:86 --values
SVC HND CHR HND CHR UUID PROPS HEX ASCII
0x0014 0x0015 0x2A00 R 424c457b46344c... BLE{F4K3_1D3NT1TY}
0x0028 0x002b 0xFF02 R,W 577269746520... Write Flags Here
0x0028 0x002d 0xFF03 R 424c457b503447... BLE{T0K3N_S4V3R}
0x0028 0x002f 0xFF05 R,W 577269746520... Write anything here
0x0028 0x004f 0xFF16 B,R,W,N,ext 536f206d616e79... So many properties!
Poke individual handles. The write-then-read on 0x002f is the CTF-shaped demo from the post:
$ caeruleus read -b 9C:9C:1F:F2:88:86 -a 0x002d
0x002d hex=424c457b503447335f5455524e5e33527d ascii="BLE{T0K3N_S4V3R}"
$ caeruleus write -b 9C:9C:1F:F2:88:86 -a 0x002f -s "praetorian"
0x002f written=10 type=request
$ caeruleus read -b 9C:9C:1F:F2:88:86 -a 0x002f
0x002f hex=424c457b43304c30535333554d7d ascii="BLE{C0L0SS3UM}"
Interactive shell (gatttool-style verbs), stdin batch, and serve/send hold one connection across many operations. BLECTF_JohnsonSpace is a lab/CTF target in the screenshot, not a production lock. Do not fuzz a stranger’s insulin pump on the train.
Repeatable Assessment Workflows
Handle I/O is the easy part. recon fingerprints and audits the tree. Each assess probes one weakness class. The blog lists four; the GitHub README adds pairing:
assess check-auth— what an unpaired attacker can read (or write with--probe-writein the README).assess encryption— whether pairing/encryption requirements are actually enforced.assess pairing— SMP feature exchange and downgrade resistance (README; not in the original comparison table).assess dfu— exposed, unauthenticated firmware-update entry points.assess wwr— write-without-response overflow and rapid-fire resilience.
Every assessment emits {address, test, summary, findings[]} with per-finding severity, handle, uuid, evidence. Titus escalates a hardcoded key on an unpaired link to high:
$ caeruleus assess check-auth -b CC:B6:0E:3C:97:0B -o json | jq '.findings[] | select(.severity=="high")'
{
"title": "Secret readable without authentication",
"severity": "high",
"handle": 19,
"uuid": "00ca0001-bede-ad43-4145-52554c455500",
"evidence": "6177735f...774a61 (\"aws_access_key_id=AKIADEADBEEFDEADBEEF aws_secret_access_key=wJa\") +37 bytes",
"detail": "Titus matched rule \"AWS API Credentials\"; value read over an unpaired, unencrypted link."
}
Writable characteristic, throwaway Bleak fuzzer replaced:
$ sudo caeruleus fuzz write -b CC:B6:0E:3C:97:0B -a 0x0015 --raw --max-iter 20 --max-time 15s
[+] opening session to CC:B6:0E:3C:97:0B
[+] target=0x0002 liveness=0x0002 seeds=13 out=fuzz-out-20260701T144803Z
[+] done iter=2 states=2 queue=2 crashes=1 hangs=0 elapsed=1m2s
[+] results in fuzz-out-20260701T144803Z
$ jq '{mut, op, resp_op, crash}' fuzz-out-20260701T144803Z/crashes/*.json
{
"mut": "swap",
"op": "write_req",
"resp_op": "disconnect",
"crash": true
}
fuzz replay reproduces a crash. Corpus on disk, crash/hang triage built in. Root is required for --raw.
Command Map from the Repo (Apache-2.0)
The blog is the story. The README is the map. Global flags on every command: -b/--bdaddr, -i/--adapter (default hci0), -t/--timeout, -o/--format, -q/--quiet, -v/--verbose.
| Group | Commands |
|---|---|
| Discovery | scan, enumerate, primary, characteristics |
| GATT operations | read, write, raw-write, listen, cccd, mtu |
| Connection management | pair/unpair, conn-params, disconnect, forget |
| Scripting | shell, batch, serve/send |
| Security assessment | recon, assess check-auth, encryption, pairing, wwr, dfu |
| Fuzzing | fuzz write (incl. --raw), fuzz replay |
| Adapter and diagnostics | doctor, adapter, bdaddr, monitor |
Extensible by design
Each assess is one Go file plus one line in assess.go. Shared primitives: session management, findings schema, Titus, structured output. Repeat a manual check twice, upstream it, everyone gets JSON and agent-drivability in the next build.
Part of the Praetorian Toolkit
Same philosophy as Nerva (service fingerprinting), Vespasian (API discovery), Hadrian (API authorization), Brutus (credential testing): one binary, structured output, no dependency hell, now at the RF layer. Titus is the shared secrets engine. Caeruleus is not a full-device assessment. A connected product is also the mobile app, the cloud API, the DFU pipeline, and identity. Praetorian’s close is a sales paragraph; the engineering claim still stands without it.
Getting Started
go install github.com/praetorian-inc/caeruleus/cmd/caeruleus@latest
Or a prebuilt x86_64/arm64 archive from Releases. From source:
git clone https://github.com/praetorian-inc/caeruleus
cd caeruleus
make build # -> ./caeruleus
make test # unit tests + shell tests
Needs Linux, BlueZ (bluetoothd), a standard BLE adapter. Run doctor first, then scan. Issues and new assess workflows welcome. License: Apache-2.0.
A Methodology You Can Run on a Lab Peripheral
caeruleus doctoruntil exit 0. If LE scan fails, power-cycle the adapter before blaming the lock.caeruleus scan -o jsonland pick by RSSI and name. Random addresses rotate; do not key your notes on the MAC alone if it is RPA.enumerate -b $ADDR --values -o jsonunpaired. That is the check-auth baseline.assess check-auth,encryption,pairing,dfu,wwr. Keep the JSON. Titus hits on AWS/GitHub/private keys are report-ready.- For each writable handle you care about,
fuzz writewith a short--max-timefirst. Replay crashes. Do not fuzz a device you cannot reflash. - If you need sniffing or a fake peripheral, stop. That is nRF Sniffer / GATTacker territory, not this binary.
Defender View: What a Vendor Should Fear from This Tool
- Characteristics with Read and no encryption/authentication that hold tokens, Wi-Fi PSKs, serials, session keys.
- DFU characteristics writable without pairing. That is firmware replace from the parking lot.
- Write-without-response with no length check. Disconnect-on-swap in the fuzz JSON is a crash, sometimes a useful one.
- Encryption flags in the GATT database that SMP does not actually enforce (
assess encryption). - Just-works pairing with no MITM protection if you later add a sniffer (out of Caeruleus, still in the threat model).
Mitigations are boring and sufficient for most gadgets: require bonding before sensitive characteristics, LE Secure Connections, reject WWR on bounded fields, DFU behind a signed bootloader and a physical confirm, no AWS keys in firmware. Caeruleus will still enumerate. It should fail check-auth on anything that matters.
FAQ from the Original Page
What is Caeruleus?
A free, open-source BLE security testing tool from Praetorian: one Go binary on BlueZ covering scan, enumerate, read/write/notify, fuzz, and structured assessment.
How is it different from bettercap, gatttool, and hcitool?
Those tools were never designed to work together and do not emit machine-readable output. Caeruleus is one command surface, JSON/JSONL, no dependency hell.
Can AI agents use it?
Yes: -o json/jsonl plus an Agent Skill. Benchmark: 62% time, 70% tokens versus free tool choice.
Limitations?
Central only. No sniff, no AITM, no clone. Linux/BlueZ only.
Install?
go install github.com/praetorian-inc/caeruleus/cmd/caeruleus@latest or a GitHub release binary. doctor then scan.
What Pairing, DFU and WWR Actually Mean on a Bench
Pairing is a handshake, not a vault
BLE pairing (SMP) negotiates whether the link will be encrypted and whether a human has to confirm a passkey. Just Works is the default on gadgets without a display: both sides agree on a key with no MITM protection. Legacy pairing is still in the wild and is breakable with a sniffer. assess pairing in Caeruleus looks at the feature exchange and whether a downgrade is accepted. That is not the same as assess encryption, which asks: the characteristic said it needed encryption, did the stack actually refuse the unpaired read? Firmware authors set the GATT flag and forget the stack config. Testers have been filing that bug since 2015. The tool just makes the file JSON.
DFU is the firmware front door
Nordic, Dialog, TI, and a dozen module vendors ship a Device Firmware Update service. In development it is wide open so the intern can flash from nRF Connect. In production it is supposed to demand a signed image and often a button. assess dfu looks for the development posture still enabled: an unauthenticated write that takes you to bootloader. If that finding is real, the rest of the product security is a suggestion. An attacker in radio range replaces the firmware. Caeruleus will not write a malicious image for you. It will tell you the door is unlocked. Do not walk through it on a customer device without RoE that explicitly allows bricking.
Write-without-response is a firehose
A Write Request waits for an ATT acknowledgement. Write Command (WWR) does not. Stacks that copy WWR payloads into a fixed buffer without a length check are a gift. assess wwr and fuzz write --raw are how you find the disconnect-on-swap crash in the blog’s JSON. A disconnect is not always a security bug. A watchdog reset that leaves DFU open is. Triage the crash, do not file “fuzz made it drop” as Critical.
BlueZ, D-Bus, and Why gatttool Will Not Come Back
Linux BLE is BlueZ. Userland talks to bluetoothd over D-Bus. The kernel speaks HCI to the dongle. Old tutorials call hcitool and gatttool, which spoke HCI/ATT more directly and bit-rotted when BlueZ moved the brain into bluetoothd. Caeruleus uses D-Bus for the polite path and raw L2CAP/HCI when you pass --raw or when D-Bus lies (old BlueZ hiding GAP/GATT). That is why some commands need root and why doctor checks that the address BlueZ cached matches the chip. A mismatch is how you scan on hci0 while the packets leave hci1.
Ghost connections: when a central dies without a proper terminate, the peripheral keeps the link until supervision timeout (often 10–30 s) and will not advertise. Fuzzers that SIGKILL themselves poison the next run. Caeruleus intercepts SIGTERM/SIGHUP and waits for Connected=false. That single behavior is worth the binary if you have ever sat in a closet waiting for a lock to remember it is a lock.
# typical first five minutes on a lab dongle
sudo systemctl status bluetooth
caeruleus doctor || caeruleus adapter power cycle
caeruleus scan --live -o jsonl | tee /tmp/ble-scan.jsonl
ADDR=9C:9C:1F:F2:88:86
caeruleus enumerate -b "$ADDR" --values -o json > /tmp/gatt.json
caeruleus assess check-auth -b "$ADDR" -o json > /tmp/check-auth.json
How This Fits a Full IoT Engagement
Praetorian’s close is correct even if you never hire them. BLE is one face. Also test: the companion app (hardcoded keys, backup BLE commands), the cloud API (device identity, BOLA on serial numbers), the update server, UART/JTAG if the case opens. Caeruleus replaces the BLE scavenger hunt, not the rest of the week. Pipe JSON into the same report factory you use for Burp. A high from Titus on handle 19 is the same shape as a TruffleHog hit in an APK.
- If check-auth is clean but the app pairs Just Works and then reads secrets, the bug is in the app’s trust of the link, not in GATT flags. Still a finding. Caeruleus pairing/encryption assess is the evidence.
- If DFU is closed over BLE but open over USB-CDC, say so. Radio tools will not see the USB door.
- If the device uses a random address, log the IRK only if you have it from a paired dump; otherwise identify by advertised name + manufacturer data + RSSI room.
Authors and Housekeeping
Aaron Wasserman (lead OSE, IoT/hardware, medical and semiconductor, Georgia Tech M.S., OSCP, ACIP, DEF CON / CypherCon / HackSpaceCon). Hunter Ver Helst (OSE, web/mobile/code review, UF CS, BSCP). Siddhant Kalgutkar (application and hardware, CTFs). Will McCardell (listed; bio on the original page is a stub). Built with Praetorian’s 5th Legion. Published 10 July 2026 under IoT Security, Labs, Offensive Security, Open Source Tools. Word count on the page schema: 2,167. This draft is the longer cut.
Key Takeaways
- BLE testing was a scavenger hunt across deprecated binaries. Caeruleus is one Go binary that speaks BlueZ and JSON.
- GATT is a notebook of slots. Unpaired readable slots with secrets are high findings without any crypto break.
doctortells you if the adapter is the problem. Ghost-connection teardown saves the 30-second sulk after a crash.assessturns “I poked a few handles” into {findings[]} with Titus on the values.- Fuzz write + replay replaces the Bleak fuzzer you were ashamed of. Root for –raw.
- It will not sniff or MITM. Do not file a finding that required a sniffer and cite this tool.
- Apache-2.0. Linux only. Write a new assess as one Go file when you repeat a check.
Defensive Recommendations
- On every BLE product: require LE Secure Connections + bonding before any characteristic that holds secrets or DFU.
- Mark unused write/WWR properties off. Fuzzers love leftover vendor characteristics from the SDK sample.
- Never store cloud credentials in GATT. Titus will find them in one unpaired enumerate.
- DFU must demand a signed image and a local confirm.
assess dfuis looking for the opposite. - Test with Caeruleus (or equivalent) in CI against a lab unit: check-auth JSON should be empty of highs.
- If you need sniffing coverage, add a sniffer to the lab; do not pretend the clerk tool is a tap.
- Document GAP Appearance and advertised UUIDs. Device-type inference is how an agent decides “this is a lock” in 200 ms.
Conclusion
Wasserman, McCardell, Kalgutkar, Ver Helst and Praetorian’s 5th Legion did not invent BLE security. They stopped paying the tax of five terminals and a deprecated man page. Caeruleus is the clerk who lists the drawers, reads the unlocked ones, shakes the writable ones, and files JSON. The radio is still 2.4 GHz and the firmware is still late. The difference is you can now tell a human or a model: run doctor, scan, enumerate –values, assess check-auth, and do not copy a MAC by hand again.
Original text: “Bluetooth Low Energy Security Testing, Consolidated: Introducing Caeruleus” by Aaron Wasserman, Hunter Ver Helst, Siddhant Kalgutkar, and Will McCardell at Praetorian. Tool: https://github.com/praetorian-inc/caeruleus (Apache-2.0).


