▲ 15 r/BCI+1 crossposts

My Attempts at Reverse Engineering the Dreem 2 Headband to Pull Full EEG Data

Disclaimer: All documentation, including this post, was written by Codex. AI was used extensively, though not exclusively, throughout the project.

Repository: https://github.com/jatrou/dreem

I have been trying to recover the original full-fidelity overnight .h5 recording—or read-only filesystem access—from my own Dreem 2 EEG headband. I have not recovered root or a verified raw .h5, but I have published the complete working archive so other owners and embedded researchers can inspect the evidence, avoid repeating the same attempts, and continue from a better starting point.

The repository contains board photographs, electrical measurements, BLE/GATT mappings, Android helpers, backend and APK findings, network/TLS observations, i.MX6ULL SDP tooling, U-Boot UMS experiments, HDF5 carving tools, and tests.

Current Status

Target or path Observed result
Root/filesystem access Not obtained
Raw overnight HDF5 Not recovered
BLE report Small ZIP containing reporting_v2.data, not identifiable raw EEG
Bluetooth Classic report Same compact report surface as BLE
Live BLE EEG Low-rate preview data, not the stored overnight recording
Wi-Fi Provisioning worked
LAN SSH on TCP/22; no other useful open service found
Legacy backend Some routes still respond; tested routes did not expose raw files
i.MX6ULL SDP Real 15a2:0080 enumeration observed, but not yet reproduced reliably
U-Boot USB mass storage Payloads prepared; no UMS/block device obtained
Direct storage No eMMC dump; eMCP is BGA and not clip-accessible

These results describe the device, firmware, account states, and test conditions I had. They do not prove that every possible software or hardware route is closed.

Device and Hardware

Item Observation
Device Dreem 2 / Dreem Two / Beacon
FCC ID 2AH2Q-DREEM2
Firmware observed 4.6.9; later 4.7.11+PRODUCTION
Hardware version v2plus_medical
Processor board Femto MP-V2 / Femto MP-V2+
SoC NXP i.MX6ULL
SoC marking MCIMX6Y1DVK05AB 1N70S
ROM recovery USB ID 15a2:0080
Storage/RAM Kingston 04EMCP04-NL2DM627 eMCP
Likely capacity About 4 GB eMMC + 512 MB LPDDR2
PMIC NXP/Freescale MC32PF3000A6
EEG front end ADS1294-class TI 24-bit biopotential AFE
Audio WM8960 codec; 19.2 MHz oscillator on sensor/audio side
Storage package 162-ball BGA; no clip-accessible pins

The processor/eMCP/USB board and acquisition/analog board reuse some TP numbers. The following readings refer to the processor/storage board. High-resolution photographs and focused crops are in the repository.

Test-Point Measurements

Pad Resistance to ground USB-powered voltage
TP2 Varies about 60–250 kOhm 4.33 V
TP3 Starts near 300 kOhm and rises 3.11 V
TP7 Starts near 300 kOhm and rises 4.28 V
TP8 Starts near 100 kOhm and rises 3.38 V
TP9 Starts near 300 kOhm and rises 3.28 V
TP10 Starts near 400 kOhm and rises 1.81 V
TP11 Starts near 400 kOhm and rises 0.003 V
TP32 About 0.6 Ohm Ground candidate
TP38 Starts near 300 kOhm and rises 3.14 V
TP39 About 0.5 Ohm Ground candidate

TP10 remains the suspected recovery/boot-control pad. Earlier direct TP10-to-ground timing was associated with ROM SDP behavior; a 1 kOhm connection was reportedly too weak. I have not yet reproduced that result reliably enough to call the timing solved.

BLE/GATT Findings

BLE was the most useful software-visible interface. I built an Android helper because BlueZ connections and service discovery were inconsistent on the bench host.

Characteristic Observed role
D003 Diagnostic JSON-like data
D102 / D103 Wi-Fi configuration / status
D203 / D204 Battery / plugged status
D208 Health/error status
D301 Live preview notifications
D302 Record command: 3 starts nap; 0 finalizes
D304 / D309 Record status / current record UUID
D30A Nap configuration JSON
D401 Latest report UUID
D402 Latest report bytes
D601 Set time
D701 / D705 Firmware version reads
D704 Firmware-status notification
D706 / D707 Firmware-flow writes/start trigger
D708 / D709 Version/hardware reads
D901 User ID
D902 Server URL configuration
D903 Headband address read
D904 / D905 Server-password state / write

Historical value handles included D905 near 0x003d, D902 near 0x0043, D901 near 0x0045, D402 near 0x0075, D401 near 0x0077, D309 near 0x0085, and D301 near 0x0095 with its CCCD near 0x0096.

Recording and Report Result

The working short-recording sequence was:

write D30A nap configuration
write D302 = 3 to start
write D302 = 0 to finalize
read D401 report UUID
read D402 report bytes

One fresh nap produced:

report size: 402 bytes
container: ZIP
entry: reporting_v2.data
HDF5 signature: absent

Bluetooth Classic/RFCOMM IDs 601/602 reached the same report surface. APK analysis showed the companion app reading these compact bytes, parsing reporting_v2.data, storing them locally, and uploading the same body. I found no app-side conversion into raw HDF5.

Live Preview Result

D301 produced 484 notifications totaling 13,552 bytes in about 20 seconds. The payload was consistent with four little-endian floats per notification at a low display/preview rate. It is useful for live experiments but did not resemble a stored full-rate overnight recording.

Passive reads of D007 and D008 returned status 2 with no payload. I did not blindly write to unknown characteristics.

Wi-Fi, TLS, and Upload Behavior

BLE Wi-Fi provisioning worked, but device power state mattered:

  • charger off and headset awake worked best for BLE/server/Wi-Fi configuration;
  • a successful state read as wifiStatusLE=0;
  • writes while charging could report BLE success while internal status became wifiStatusLE=9 and the SSID cleared; and
  • turning the charger on after provisioning was more useful for provoking a network/upload wake.

D902 uses a four-byte little-endian JSON length followed by JSON containing user_api_url and user_auth_url. Production Rythm HTTPS hosts were accepted. Local IPs, arbitrary domains, plain HTTP, user@host tricks, and suffix-hostname tricks returned status 3 or otherwise failed.

No capture showed a transfer large enough to resemble raw HDF5. One transparent production TLS observation produced:

Field Observation
SNI login.rythm.co
ClientHello 517 bytes
Server response About 4,528 bytes
TLS 1.2
Cipher TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
Client after certificate No Finished, key exchange, or application request observed
Connection end Headset closed after about 60 seconds

The server chain was a current Let's Encrypt/ISRG chain. One plausible explanation is that the old headset trust store rejected it, but that remains a hypothesis.

Legacy Backend Results

Parts of the legacy backend still responded with disposable/current test accounts:

  • guest creation and some token issuance worked;
  • the headband resolver accepted the Wi-Fi address in uppercase underscore form;
  • configuration returned firmware, content IDs, storage counters, and nickname;
  • content routes mostly described audio/UI/media packages;
  • unsigned object fetches returned access denied; and
  • tested dataupload, raw, HDF5, record, and report guesses returned combinations of 401, 403, 404, empty data, or compact reports.

The strongest controlled backend test was reportv2:

  1. Upload the 402-byte BLE report.
  2. Backend returns HTTP 201.
  3. List and download the stored report.
  4. Download has the same SHA-256 as the upload.
  5. Contents still consist only of reporting_v2.data.

This shows that reportv2 stored the compact report unchanged in the account state tested. It did not reveal a hidden raw recording.

A backend update_password call returned a 20-character server password after the required association state. Writing it to D905 cleared D904 to 00 00 00 00, but it did not unlock SSH or cause a visible raw upload.

Android/APK Findings

Test setup included a rooted Pixel 7, patched/debuggable and production app variants, static APK inspection, custom BLE/auth helpers, and app sandbox/database inspection.

Versions examined:

  • Alfin 1.12.10;
  • Dreem Connect 1.0.4; and
  • Dreem 2 2.15.1, build 478.

The APKs are not redistributed in the repository; researchers must source them lawfully.

The patched app database contained Dreemer/HeadbandConfig rows, but NightReport/NightScore-related tables were empty in the inspected state. No .h5 or other large recording artifact was found. Hawk/Conceal decoding recovered 26 preference entries and identifiers, but CURRENT_AUTH_TOKEN, CURRENT_JWT_TOKEN, and CURRENT_GOOGLE_TOKEN were absent; REFRESH_TOKEN was a Boolean-like flag rather than a reusable token.

The inspected apps contained report logic, BLE configuration formats, and firmware triggers, but no embedded firmware/rootfs image, HDF5 file, CA bundle, obvious trust override, or raw-file endpoint. Google-auth experiments did not produce a usable legacy Rythm session.

The repository includes the Android BLE helper, auth probes, a root-context pairing helper, and a Magisk privileged-app overlay.

LAN and SSH

When connected to Wi-Fi, the device exposed only TCP/22 in the useful scans. Observed banners were:

SSH-2.0-dropbear_2016.74
SSH-2.0-dropbear_2018.76
authentication: publickey,password

None-auth was rejected. Passive version research, saved/backend-derived candidates, and bounded authentication tests did not produce access. Timing and malformed-public-key user probes did not provide a useful signal. No practical unauthenticated route was identified for the observed configuration.

Historical SSH recovery scripts remain in the repository to document what was attempted, but brute force, broad password guessing, and user enumeration are stopped paths unless someone first obtains firmware, /etc/shadow, or a strong credential clue.

UART and Logic-Analyzer Results

The bench setup included a Tigard V1.1/FT2232H, LA1010 logic analyzer, multimeter, sigrok/PulseView, Bus Pirate 6, Linux bench host, and rooted Pixel 7.

A scan across 34 saved sigrok captures found no credible U-Boot, Linux, Freescale, i.MX, login, or shell strings. Some inverted high-baud decodes around 1.5–2.0 Mbaud produced noise, not repeatable text.

Capture Observation
TP3 5 MS/s, 15 s, all high, zero transitions
TP11 5 MS/s, 15 s, only 1–18 transitions on one channel
TP2 5 MS/s, 15 s, 14,458 transitions on one channel, no valid UART text
TP8 About 13.6 s, one transition
TP9 15 s, three transitions
TP10 15 s, two transitions, about 34% high
TP38 15 s, one transition

These captures looked more like power/control-state changes than a boot console. A console could still be disabled, elsewhere, missed by timing, or using an unidentified interface.

i.MX6ULL SDP and U-Boot Work

A genuine ROM Serial Downloader Protocol window was observed:

USB ID: 15a2:0080
description: SP Blank 6ULL / i.MX 6ULL recovery mode
HAB security state: production mode (0x12343412)

IOMUX, USDHC, fuse, and other register regions were readable in at least some SDP sessions. This confirms partial ROM communication, not arbitrary code execution.

Observed payload behavior:

  • one OCRAM load test failed with report 2 out err=-7;
  • a UUU OCRAM attempt saw a fresh SDP device but hit a local script/path problem before execution was proven;
  • one SPL appeared to load from the host tool's perspective;
  • SPL size was 39,936 bytes;
  • IVT header was at 0x00907400;
  • entry point was 0x00908000; and
  • the tool jumped to 0x00907400.

After the jump there was no UART output, U-Boot prompt, USB re-enumeration, UMS gadget, or block device.

Possible blockers include HAB rejecting unsigned code, wrong TP10 timing, a mismatched i.MX6UL/6ULL payload, failed LPDDR2 initialization, or code starting without the expected console/USB path.

The repository includes:

  • a watcher for USB 15a2:0080;
  • bounded, non-persistent SDP triage and register probes;
  • operator-cued TP10-to-ground recovery orchestration;
  • experimental 9x9/LPDDR2 and 14x14/DDR3 U-Boot UMS payload families;
  • a watcher that distinguishes newly exposed USB storage from existing disks;
  • read-only imaging support; and
  • HDF5 signature carving with optional h5ls validation.

Neither U-Boot family is proven for this board. If SDP cannot expose storage, the remaining hardware paths are board-level eMMC mapping/tapping with the SoC held inactive or professional eMCP removal and reading.

Repository Map

Path Contents
README.md Status, starting points, and project map
docs/evidence-summary.md Short evidence summary
docs/dreem-2-recovery-reference.md Full technical handoff and attempt history
hardware/ Original board photos, closeups, and FCC exhibit
tools/ SDP, UMS, HDF5, ATT, and Bluetooth utilities
recovery/ UUU, imx_usb, OCRAM, OpenOCD, and U-Boot assets
apps/ Android BLE/auth/pairing helpers and Magisk module
experiments/ Backend, capture, preference, and stopped security probes
tests/ Unit tests for maintained recovery tools
docs/references.md Public teardowns, research, prior art, and boot tools

Project-authored code and text are Apache-2.0. Original board photographs are CC BY 4.0. U-Boot-derived and other third-party material retains separate provenance documented in the repository.

What I Would Not Repeat Without New Evidence

  • Pulling D402 or Classic 602 again expecting raw HDF5.
  • Re-downloading the same reportv2 object.
  • Repeating D905 writes and identical upload captures.
  • Trying more arbitrary D902 URL variants without a new validation or trust-store clue.
  • Blind writes to unknown BLE characteristics.
  • Firmware-trigger loops without a real package and recovery plan.
  • Plain button/charger/Fingerbot SDP attempts without the physical TP10 condition.
  • Searching the same APKs for embedded firmware that was not present.
  • Broad SSH password guessing or user enumeration.

Different firmware, account state, protocol evidence, or hardware timing could justify revisiting one of these.

Repository: https://github.com/jatrou/dreem

u/AndroidAssistant — 12 days ago