| Field | Value |
|---|---|
| Product | Energy Curb Home Energy Monitor |
| Company | Curb Inc. (defunct as of ~Feb 2026, brand now under "Powered By Elevation") |
| Serial Number | xxxxxxxx |
| Hardware Version | 1.5 |
| Model Number | 00615 |
| Board S/N | XXXXXXXXXXXXX |
| OS Version | 3.1.0-f8117d6 |
| Software Version | 3.3.0-2975f92 |
| Board Copyright | (c) 2016 Curb Inc. |
| In service since | At least April 2017 (SG7000 update markers on SD card) |
The main compute is a Ka-Ro TX28 System-on-Module plugged into a standard DDR SODIMM socket on the carrier board.
| Component | Detail |
|---|---|
| SoC | Freescale/NXP i.MX28 rev1.2 (ARM926EJ-S) at 454 MHz |
| Module | Ka-Ro TX28-41x0 |
| NAND Flash | Micron MT29F1G08ABAEAWP — 128MB SLC, 2048 byte page, 64 byte OOB, 128KB erase block |
| RAM | 64 MiB DDR2 (Nanya, on back of SODIMM module) |
| Clock | GD24000 CLK01 oscillator |
| Component | Detail |
|---|---|
| Power Supply | RECOM RAC10-05SC/277 — 5V, 100-277V AC input (runs directly off mains) |
| HomePlug AV | ST&T SG7000.4 (QCA7000) — powerline networking to CT sensor hub |
| Analog Frontend MCU | Atmel ATSAM D21 (ARM Cortex-M0+) — handles CT current measurements |
| Ethernet | Single RJ45 jack (eth1, active interface via SPI-connected QCA7000) |
| USB | USB-A host port (USB OTG wired as host — not usable for USB boot without hardware modification) |
| SD Card | Standard slot, ext4 formatted, 8GB |
| Level Shifter | U9 (8-pin, near J6 FTDI header) |
| Component | Detail |
|---|---|
| PLC Chip | QCA7420 |
| MAC | C4:71:54:EF:31:59 |
| Firmware | MAC-QCA7420-1.3.0.2134-00-20151212-CS |
| Role | CCO (Central Coordinator) on the HomePlug AV network |
| Connector | Type | Purpose | Status |
|---|---|---|---|
| J6 | 1x6 pin, 2.54mm pitch | FTDI serial console (labeled V5_FTDI on back) | Header soldered, FTDI cable on order |
| J9 | 2x4 pin, 1.27mm pitch | Debug header (likely JTAG/SWD + UART) | Accessible but needs fine-pitch adapter |
| J3 | Terminal block | CT clamp connector | In use |
| J11 | Terminal block | Phase A / Phase B coil connectors | In use |
| J4 | Unknown | Near Ethernet jack | |
| J14 | Unknown | Near SODIMM socket | |
| USB-A | USB host port | USB host (OTG wired as host) | Cannot be used for USB boot without mod |
Square pad Round pad
● ● ● ● ● ●
Pin 1 Pin 2 Pin 3 Pin 4 Pin 5 Pin 6
GND CTS VCC TX RX RTS
- Pin 1 (square pad) = GND — confirmed with multimeter
- Standard FTDI cable color code: Black=GND, Brown=CTS, Red=VCC, Orange=TXD, Yellow=RXD, Green=RTS
- Serial settings: 115200 baud, 8N1
- Device runs
gettyonttyAMA0at 115200 - FTDI cable ordered: DTECH USB to TTL Serial 5V Adapter with FT232RL (6-pin 0.1" female header)
| Test Point | Label | Function |
|---|---|---|
| TP22 | GND | Ground |
| TP30 | D21_SW0 | BOOTMODE — i.MX28 boot mode select (SODIMM pin 8). Tie to GND for USB boot. |
| TP32 | D21_ID | Chip ID |
| TP40 | D21_RESETN | Reset line |
| TP43 | — | Near FTDI header |
| TP48 | — | Near FTDI header |
| TP86 | — | Near FTDI header |
| TP83 | AD2_REF_OUT | Analog reference |
| TP84 | AD1_REF_OUT1 | Analog reference |
| TP85 | MON_REF_OUT1 | Monitor reference |
Key points:
| Feature | Status |
|---|---|
| NVRAM | None — no battery-backed NVRAM on i.MX28 |
| OTP eFuses | 1,280 bits (40 × 32-bit words) via OCOTP controller |
| Boot encryption | AES-128 via DCP (Data Co-Processor) — separate from GPG firmware encryption |
| HAB (Secure Boot) | HAB4 supported, likely in OPEN mode (default, auth errors ignored) |
| JTAG disable | No eFuse to disable JTAG — controlled only by DEBUG pin state |
| Crypto key | 128-bit OTP key for boot encryption (likely read-locked, but irrelevant to GPG passphrase) |
| USB boot | Supported — ROM enters USB HID mode when BOOTMODE pin pulled LOW |
Critical insight: The GPG passphrase for firmware updates is stored as a plain file (/root/.gnupg/passphrase) on the NAND filesystem, NOT in eFuses. The OTP crypto key is for boot-level encryption only — a completely separate mechanism.
- Linux 3.16.0-karo (built Oct 14, 2020 with Buildroot 2016.02-rc2 / GCC 4.8.5)
- BusyBox v1.24.1 init system
- 64 MiB RAM (40MB available to userspace after kernel/CMA reservation)
- Dual A/B boot scheme — two copies of kernel and rootfs for safe updates
- Active boot slot: B (linux2 + rootfs2)
- UBIFS root filesystem (28MB usable) on UBI volume 0 (mtd5/rootfs2)
- UBIFS user filesystem (43MB usable) on UBI volume 1 (mtd6/userfs) at
/data - ext4 on SD card (7.4GB) at
/data/sd - Kernel command line:
init=/linuxrc console=ttyAMA0,115200 ro debug panic=1 ubi.mtd=rootfs2 ubi.mtd=userfs root=ubi0:rootfs rootfstype=ubifs
| Process | Binary | User | Description |
|---|---|---|---|
| linuxrc (init) | /sbin/init (BusyBox) | root | Init system |
| syslogd | /sbin/syslogd -n -b 1 -s 100 | root | System logging (1 rotation, 100KB max) |
| klogd | /sbin/klogd -n | root | Kernel logging |
| crond | /usr/sbin/crond | root | Cron scheduler |
| dropbear | /usr/sbin/dropbear -s -R | root | SSH server (key-only auth, -s disables passwords) |
| lighttpd | /usr/sbin/lighttpd -f /etc/lighttpd/lighttpd.conf | www-data | Web server (static status page on port 80) |
| avahi-daemon | avahi-daemon | avahi | mDNS (advertises as curb-xxxxxxxx.local) |
| hm | /usr/bin/hm -c /etc/hm.conf | root | Hardware monitor daemon (manages Lua processes) |
| udhcpc | udhcpc -R -b | root | DHCP clients for eth0 and eth1 |
| htpdate | /usr/bin/htpdate -s -t -l -e -D | root | HTTP-based time sync (v1.1.3, uses google.com, linux.org, ntp.org) |
| getty | /sbin/getty -L ttyAMA0 115200 linux | root | Serial console login |
| sampler.lua | luajit /data/lamarr/sampler.lua | curb | Reads energy samples from hardware (1/sec) |
| streamer.lua | luajit /data/lamarr/streamer.lua | curb | Streams data to cloud (POST to border server) |
| load-controller.lua | luajit /data/lamarr/load-controller.lua | curb | Aggregates and controls loads |
The application software lives in /data/lamarr/ and consists of LuaJIT scripts. Named after Hedy Lamarr. Known files from the update log:
sampler.lua— reads raw energy samples from the hardwarestreamer.lua— streams data to the cloud APIload-controller.lua— aggregates samples and manages loadsconfig.lua— configuration managementhub-messaging.lua— communication with CT sensor hubcompressed-message.lua— message compressionade.lua— ADE (Analog Devices Energy) chip interfacedaemonize.lua— process daemonizationdiags.lua— diagnosticsdweet.lua— dweet.io integrationinflux.lua— InfluxDB integration
The device uses a dual A/B partition scheme for safe over-the-air updates:
Offset Size Name Contents
0x000000020000 1MB u-boot U-Boot 2013.07 (Apr 23 2015)
0x000000120000 384KB env U-Boot environment (bootdelay overridden)
0x000000180000 6MB linux1 Kernel slot A
0x000000780000 6MB linux2 Kernel slot B (ACTIVE — Linux 3.16.0-karo)
0x000000D80000 32MB rootfs1 Root filesystem slot A
0x000002D80000 32MB rootfs2 Root filesystem slot B (ACTIVE)
0x000004D80000 49MB userfs User data — /data/lamarr/*.lua, hub-config.json
0x000007F00000 512KB dtb Device tree blob
0x000007F80000 512KB bbt Bad block table (read-only)
- NAND: Micron MT29F1G08ABAEAWP, 128MB SLC
- Page size: 2048 bytes, OOB: 64 bytes
- PEB (Physical Erase Block): 128 KiB, LEB (Logical Erase Block): 124 KiB (126976 bytes)
- UBI overhead: 4096 bytes per PEB (2048 VID header + 2048 data offset)
- Bad PEBs: 0 (both rootfs2 and userfs report 0 bad blocks)
| Field | Value |
|---|---|
| Version | U-Boot 2013.07 (Apr 23 2015 - 13:42:37) |
| Boot source | NAND, 3V3 |
| Board ID | Ka-Ro TX28-41x0 |
| Baseboard | stk5-v3 |
| MAC from fuse | xx:xx:xx:xx:xx:xx |
| Boot delay | Overridden to 0 or -1 — no "Hit any key" prompt visible |
| Active kernel | linux2 (offset 0x780000) — Linux-3.16.0-karo, 3.8MB |
| Active rootfs | rootfs2 (ubi.mtd=rootfs2) |
U-Boot cannot currently be interrupted — Curb overrode bootdelay in the saved environment to prevent users from accessing the bootloader. The compiled-in default is 3 seconds with CONFIG_ZERO_BOOTDELAY_CHECK, but the NAND-stored environment takes precedence.
- PHY: SMSC LAN8710/LAN8720 (on eth0, connected to FEC MAC at 800f0000)
- eth0 gets no DHCP lease (3 discovers, no response — not connected to network)
- eth1: SPI-connected QCA7000 (powerline), gets DHCP lease
- Hub config:
/data/hub-config.json— loaded by load-controller.lua, revision1624952529(June 29, 2021). Updated via SIGUSR1 signal when streamer downloads new config from border server. - Hardware monitor:
/etc/hm.conf - Web server:
/etc/lighttpd/lighttpd.conf
| Schedule | Command |
|---|---|
| Every minute | /etc/wifi_cron.sh |
| Every minute | date +%m%d%H%M%Y > /data/sd/last_timestamp |
| Every 5 minutes | /usr/local/bin/curb_status.sh (regenerates status page) |
| Every 4 minutes | /usr/local/sbin/htpdate_restart.sh |
| Every 10 minutes | /usr/local/sbin/htpdate_monitor.sh |
| Hourly | /bin/run-parts /etc/cron.hourly (includes firmware update check) |
| Interface | Status | IP | MAC | Purpose |
|---|---|---|---|---|
| eth0 | DOWN | none | 00:0C:C6:84:72:B1 | Unused Ethernet (standard i.MX28 MAC) |
| eth1 | UP | /24 | xx:xx:xx:xx:xx:xx | Active network (SPI-connected via QCA7000 powerline) |
| lo | UP | 127.0.0.1 | — | Loopback |
Note: eth1 uses an SPI-connected Ethernet interface (eth1: SPI thread exit in kernel logs), confirming the QCA7000 HomePlug AV chip connects to the i.MX28 via SPI bus.
The device is on subnet 192.168.1.x. Our machine (192.168.8.21) can only be reached by the device outbound (via router bridging), but we cannot initiate connections to the device from our subnet. The device's status page and SSH were accessed from a machine on the same 192.168.1.x subnet.
| Port | Service | Auth |
|---|---|---|
| 22/tcp | SSH (Dropbear) | Public key only (no password auth) |
| 80/tcp | HTTP (lighttpd) | None (static status page) |
| Device | Role | MAC | Chip | Firmware |
|---|---|---|---|---|
| Power Hub (this device) | STA (Station) | xx:xx:xx:xx:xx:xx | QCA7000 | MAC-QCA7000-1.1.0.730-04-20140815-CS |
| CT Sensor Hub | CCO (Coordinator) | C4:71:54:EF:31:59 | QCA7420 | MAC-QCA7420-1.3.0.2134-00-20151212-CS |
PLC Network Keys:
- DAK:
65:F6:D8:C0:9B:8F:9F:FA:44:F7:25:6A:F7:AF:3C:D9 - NMK:
02:2A:C3:67:99:E1:38:99:0B:46:A1:2A:BF:2B:B2:0E - NID:
56:7E:7D:B8:DE:F3:0B
| Server | DNS | IP | Purpose | Status |
|---|---|---|---|---|
| border.prod.energycurb.com | Gone from public DNS | Was AWS | API for samples + config | Dead. ELB returns 404 with correct SNI. User has local DNS pointing to 192.168.8.21. |
| updates.energycurb.com | Still resolves to AWS ELB | 52.41.16.86 / 44.240.63.18 | Firmware updates | Alive. nginx/1.11.3, expired cert (Mar 2019), still serves firmware files. |
| diagnostics.energycurb.com | 35.161.218.8 | 35.161.218.8 | Status reporting | Partially alive. Port 3000 accepts POST /curb/status (returns 200). SSH on 22 (OpenSSH 6.6.1p1, Ubuntu 14.04). |
| www.energycurb.com | 104.198.104.86 (GCP) | — | Website | Redirects to poweredbyelevation.com/curb-energy-monitoring/ |
| app.energycurb.com | S3 bucket IPs | — | Web app | Connection timeout |
| admin.energycurb.com | S3 bucket IPs | — | Admin panel | Connection timeout |
POST https://border.prod.energycurb.com/v3/samples— streamer posts energy samples (30 batched per request, unauthenticated)GET https://border.prod.energycurb.com/...— streamer downloads config revision every 5 minutes (revision 1624952529 = June 29, 2021)GET https://border.prod.energycurb.com/health— health check (from status script via wget)POST http://diagnostics.energycurb.com:3000/curb/status— device status reporting (still alive, returns 200)
- Posts 30 batched samples per request
- Unauthenticated — no API keys, bearer tokens, or auth headers found in any log
- Checks config revision every 5 minutes, signals load-controller via SIGUSR1 on change
- Config stored locally at
/data/hub-config.json - Restarts every ~10 minutes (managed by
hmdaemon) - Uses curl with SSL certificate verification (rejects self-signed certs with
SSL_CACERTerror 60) - Original server cert:
*.energycurb.com(also covers*.legacy.energycurb.com) signed by Amazon CA (expired March 2019)
wget --no-check-certificate— used by status script and update mechanism, does not verify SSLcurl(LuaJIT libcurl) — used by streamer, verifies SSL certificates
- Cron triggers hourly update check
- Downloads MD5 checksum files from
updates.energycurb.com/api/firmware/ - Compares with installed checksums
- If different, downloads GPG-encrypted firmware archive
- Decrypts with
gpg --decrypt --passphrase-file /root/.gnupg/passphrase --no-tty --ignore-time-conflict --no-mdc-warning - Extracts tar.gz and runs
setup.sh - Reboots
https://updates.energycurb.com/api/firmware/{serial}/update.tar.gz.gpghttp://updates.energycurb.com/api/firmware/{serial}/update.tar.gz.gpg(plain HTTP fallback — MITM-able)https://updates.energycurb.com/api/firmware/update.tar.gz.gpg
Uses wget --no-check-certificate (does NOT verify SSL).
- Algorithm: AES256 symmetric (GPG)
- S2K: SHA-1 with iteration count 65536 (very low — modern GPG uses millions)
- S2K salt (update):
D0EBBCE01F1CD084 - S2K salt (os):
0B536A741F855670 - GPG packet version: 4
--no-mdc-warningflag used — indicates older encryption format without Modification Detection Code- Passphrase: stored in
/root/.gnupg/passphraseon device (unknown) - Signing key: RSA key ID E13C6056, signer "Curb Sign"
| File | Size | Status |
|---|---|---|
update-generic.tar.gz.gpg |
35KB | Downloaded, GPG AES256 encrypted, no plaintext leaks |
os-generic.tar.gz.gpg |
23MB | Downloaded, GPG AES256 encrypted, no plaintext leaks |
| Serial-specific URLs | 169B each | Fake — nginx/1.11.3 404 HTML pages, not GPG data |
- Custom Curb-specific wordlist (203 words): Failed
- cracklib-small dictionary (54K words): Failed
- american-english dictionary (104K words): Failed
- The passphrase is not a common English word or obvious company-related term
The SD card (8GB, ext4) was physically removed and mounted at /mnt/ for analysis.
| Database | Size | Rows | Autoincrement | Description |
|---|---|---|---|---|
offline-queue.sqlite3 |
71MB | 52,343 | — | Full-resolution samples the streamer couldn't deliver |
aggregate-queue.sqlite3 |
129KB | 360 | 2,783,943 | Pre-aggregated samples (watt-only, for status page) |
sample-q-1-minute.sqlite3 |
16KB | 4 | 2,485,178 | 1-minute averaged sample queue |
sample-q-1-hour.sqlite3 |
77KB | — | — | Hourly averaged sample queue |
sample-q-5-minute.sqlite3 |
37KB | — | — | 5-minute averaged sample queue |
All databases use schema: CREATE TABLE queue (id INTEGER PRIMARY KEY AUTOINCREMENT, item BLOB)
Items are JSON text stored as BLOBs. The high autoincrement values (~2.5M) indicate ~2.5 million samples have been processed through these queues over the device's lifetime.
No configuration, registration, authentication, or non-energy data exists in any database.
streamer.log/.1/.2— streamer POST activitysampler.log/.1/.2— sample queuing (only "Queuing sample" entries)load-controller.log/.1/.2— aggregated energy data with JSON, config loading eventsmessages/.0— system syslog (cron, update process, SSH probe, shutdown sequence)lighttpd-access.log/lighttpd-error.log— web server logs (only periodic GET / from 192.168.1.1)plc.log— HomePlug AV / powerline communication logs (SG7000 update status)
- SSH probe detected: Connection from 192.168.1.1:56750 disconnected before auth ("Exit before auth: Exited normally")
- No WiFi adapter:
wlan0: error fetching interface information: Device not found - Shutdown sequence: linuxrc → /etc/init.d/rcK → dropbear killed → avahi exits → eth1 SPI thread exit
- Config revision updates: load-controller observed config update from revision 1624952094 to 1624952529 via SIGUSR1
12 circuits across 2 groups (split-phase 240V panel), sampled every second, aggregated to 60-second periods:
{
"t": 1774201980, // Unix timestamp
"p": 60, // Period (seconds) — also seen as 300 (5-min)
"g": [ // Groups (2 = Phase A, Phase B)
{
"t": 3.884, // Apparent power total (kVA)
"f": 59.993, // Line frequency (Hz)
"tg": 3.860, // True power total (kW)
"v": 123.08, // Voltage (V RMS)
"ts": 3.874, // True power sum (kW)
"c": [ // Circuits (6 per group)
{
"i": 11.706, // Current (A RMS)
"w": 0.3684, // Real power (kWh over period)
"var": -0.1101, // Reactive power (kVARh)
"p": 0.9597 // Power factor
}
// ... 5 more circuits
]
}
// ... Group 2 (same structure)
]
}Simplified format with only wattage per circuit:
{
"t": 1774202030,
"g": [
{ "c": [{"w": 22.26}, {"w": 0.14}, {"w": 0.26}, {"w": -0.01}, {"w": -11.78}, {"w": 0.05}] },
{ "c": [{"w": 10.97}, ...] }
]
}- Group 0: 6 circuits on Phase A (circuits 0-5)
- Group 1: 6 circuits on Phase B (circuits 6-11)
- Total: 12 monitored circuits
- Circuit-to-breaker mapping requires manual identification (toggle breakers and observe readings)
- HTTP status page scraping (live energy data every 60 seconds)
- SD card direct access (physical removal, read/write)
- Firmware download from update server (encrypted)
- DNS redirection for border.prod.energycurb.com
- HTTPS capture server receives health check from device (wget, no cert verify)
- SSH access (public key only, no known keys)
- GPG firmware decryption (passphrase unknown, dictionary attacks exhausted)
- Streamer interception (curl verifies SSL certs, rejects self-signed — error 60)
- Web server exploitation (static file serving only, no CGI, no path traversal, no query param injection)
- USB boot mode (USB-A port is wired as host, USBOTG_ID likely tied to GND)
- Cloud API (company infrastructure dead)
- APK analysis (download sources blocked/unavailable — returned Cloudflare challenges and 404s)
- Network-initiated connections to device (different subnet, no route from 192.168.8.x to 192.168.1.x)
- J6 header soldered on carrier board
- FTDI cable connected and working — full boot log captured
- Boot output confirmed: U-Boot 2013.07, Linux 3.16.0-karo, BusyBox login prompt
- BLOCKED: U-Boot boot delay overridden — no "Hit any key" prompt, boots straight to Linux
- BLOCKED: Linux root password unknown —
curb-xxxxxxxx login:prompt requires password - Try holding spacebar during power-on (CONFIG_ZERO_BOOTDELAY_CHECK may catch it if bootdelay=0)
- If that fails: USB boot mode (TP30→GND), JTAG via J9, or root password brute-force
Automated brute-force via curb_crack.py over serial at ~12 attempts/min:
- root user: 255 device-derived passwords (hashes, MACs, serials, company words) — all failed
- curb user: 255 device-derived passwords — all failed
- root user: 250 additional passwords (Caesar shifts, version strings, firmware wordlist) — in progress
- GPG passphrase: 542 candidates tested via fast GPG decrypt (375/s) — all failed
- HomePlug AV DAK (
65:F6:D8:C0:9B:8F:9F:FA:44:F7:AF:3C:D9) reverse-lookup attempted — no match - Total unique passwords tried: ~760+ across both users
- No lockout observed (BusyBox getty has no rate limiting beyond ~3s failure delay)
| File | Purpose |
|---|---|
/root/.gnupg/passphrase |
GPG passphrase — decrypt firmware archives |
/data/hub-config.json |
Device config, circuit labels, API endpoints, config revision |
/data/lamarr/*.lua |
All application source code (sampler, streamer, load-controller, etc.) |
/root/.ssh/authorized_keys |
SSH authorized keys (to add our own for remote access) |
/etc/shadow |
Check if root has a password |
/etc/hm.conf |
Hardware monitor configuration |
/etc/lighttpd/lighttpd.conf |
Web server configuration |
/etc/crontab or /var/spool/cron/ |
Full cron schedule |
/etc/dropbear/ |
SSH host keys and config |
/usr/local/bin/curb_*.sh |
Status, update, post-status scripts |
/etc/init.d/ |
Init scripts (service startup order) |
/data/software_version |
Installed software version string |
/data/update.tar.gz.gpg.md5sum.installed |
Currently installed update checksum |
/data/os.tar.gz.gpg.md5sum.installed |
Currently installed OS checksum |
U-Boot boot delay is overridden and Linux requires a root password. Next options:
- Hold spacebar during power-on — may work if bootdelay=0 (not -1)
- USB boot mode: Tie TP30 (D21_SW0) to GND (TP22), load fresh U-Boot via USB with
imx_usb_loader. Requires soldering to SODIMM USB OTG pins since USB-A port is host-only. - JTAG via J9: Use OpenOCD to dump NAND directly. Needs 1.27mm pitch adapter.
- Root password brute-force: At the serial login prompt. Reddit user u/ElfLogic mentioned root password may be related to serial number.
- Curb-to-MQTT — Node.js script that scrapes the status page and publishes to MQTT with Home Assistant discovery
- Curb-v2 GitHub Org — Official repos (third-party-app-integration, developer-docs)
- CurbBridge — SmartThings integration (no longer working)
- curb_energy Python library — Python API client (requires dead cloud)
- Home Assistant thread — Community discussion on local data access
- Powered By Elevation — Current brand owner
- Serial console — Connect FTDI cable to J6, get root shell at 115200 8N1
- Extract secrets — Read GPG passphrase, SSH keys, Lua scripts, hub-config.json
- Add SSH key — Write our public key to
/root/.ssh/authorized_keysfor persistent remote access - Decrypt firmware — Use passphrase to decrypt downloaded firmware archives
- Analyze Lua scripts — Understand the full data pipeline (sampler → load-controller → streamer)
- Build local data pipeline — Replace dead cloud with local collection (MQTT/InfluxDB/Grafana)
- Reconfigure streamer — Modify config or replace streamer.lua to point at local API server
- Document protocol — Full API documentation for
/v3/samplesendpoint format