## DOCUMENT 0 - INITIAL INFORMATION GATHERING --- ### Installation of Custom Software TODO: - Check if CFW is installable --- ## 0.1 Services Running on the Device The system consists of four parts that expose services: the lock (inner/motor module), the outer touch module, the bridge, and the cloud backends used by the LOQED and Shelly apps. ### Lock Its only wireless interface is BLE. | Property | Value | | --- | --- | | Advertising name | `Loqed Inside` | | Address | `FF:EC:EE:E7:32:E5`, type PUBLIC (fixed, does not rotate) | | Advertising | Undirected, connectable | | Manufacturer data | Company ID `0xFFFF` (reserved/test ID), payload `0000` | The GATT table could be enumerated without pairing and contains three services: | Service | Characteristic | Properties | Notes | | --- | --- | --- | --- | | Generic Access (`0x1800`) | Device Name (`0x2A00`) | READ, WRITE | Value `Loqed Inside`. Declared writable. | | | Appearance (`0x2A01`) | READ | `0x0000` (unknown) | | | Peripheral Preferred Connection Parameters (`0x2A04`) | READ | `ffffffff0000ffff` (no preference) | | | Central Address Resolution (`0x2AA6`) | READ | `0x01` (privacy supported, although a public address is used) | | Generic Attribute (`0x1801`) | Service Changed (`0x2A05`) | INDICATE | | | Custom (`bdce0001-e90d-4685-b89d-5578cd199a9f`) | `bdce0101-…` | WRITE | Accepted unauthenticated writes (ToothFairy WRLS-GATT-004) | | | `bdce0102-…` | READ, INDICATE | 33 bytes: `0x01` + 32 high-entropy bytes | | | `bdce0103-…` | WRITE | Accepted unauthenticated writes (ToothFairy WRLS-GATT-004) | | | `bdce0104-…` | READ, INDICATE | 97 bytes: `0x01` + 96 high-entropy bytes | The custom service appears to consist of two request/response channels. The readable values look random, which suggests they are encrypted or cryptographic material rather than plaintext state. > This is an assumption based on the data, not yet verified. According to the bridge firmware analysis, the lock receives commands (lock state changes, time sync, firmware updates in parts) from the bridge over this BLE link. #### Wired interfaces - UART (115200 8N1): internal link to the touch module carrying a framed binary protocol with display text - SWD: debug port of the nRF52840; no connection could be established ### Outer Module (Touch) No wireless services. Connected to the motor module by cable and receives display content over the UART link described above. ### Bridge (ESP32) The bridge connects to WiFi as a client and relays between the cloud and the lock. | Service / Connection | Direction | Details | | --- | --- | --- | | BLE central (GATT client) | Bridge → Lock | Scans for, pairs/bonds with and controls the lock | | MQTT over TLS | Bridge → Cloud | `mqtts://mqtt.production.loqed.com:8883` / `production.loqed.com:8883`, topics `locks//to_lock`, `locks//to_backend`, `…/state`, `…/status` | | HTTPS | Bridge → Cloud | `gateway.production.loqed.com:443`, `production.loqed.com:8080` | | HTTPS OTA | Bridge → Cloud | `firmware.loqed.com` (firmware for bridge and lock) | | Core dump upload | Bridge → Cloud | `https://app.loqed.com/Bridge-core-dump/bridge-core-dump2.php` | | HTTP (port 80, clear text) | LAN → Bridge | Likely: `10.12.2.17` (MAC `8c:4b:14:6a:1a:cc`, Espressif OUI) answered `GET /shelly` from the app in the init capture, matches the Shelly local device API and the app's mDNS queries for `_shelly._tcp.local` | | UART console (115200) | Physical | Boot log, firmware contains format strings that print tokens and WiFi credentials in clear text | Endpoints are taken from the firmware analysis unless stated otherwise. ### Cloud Backends > Observed during initial setup. From the PreTester report of the init capture and the nmap scans: | Host | IP | Ports observed in capture | Notes | | --- | --- | --- | --- | | `control.shelly.cloud` | `34.95.90.149` | 443 (TLS 1.3) | | | `shelly-101-eu.shelly.cloud` | `34.79.5.199` | 31234 (TLS 1.3) | [nmap](network/nmap/shelly-101-eu-shelly-cloud.nmap): 22 (OpenSSH 9.6p1 Ubuntu), 80 + 443 (nginx 1.24.0, Ubuntu); 443 returns `401` without `WWW-Authenticate` header; 31234 filtered | | `shelly-120-eu.shelly.cloud` | `104.155.32.247` | 443, 6113 (TLS 1.3) | Rescan pending (see 2.6) | | `shelly-infra1-eu.shelly.cloud`, `app-ratings.shelly.cloud`, `paypal.shelly.cloud` | `35.205.173.252` | 443, 9443 (TLS 1.2) | | | `changelog-backend.shelly.cloud` | `34.62.47.252` | 443 (TLS 1.2) | | | `repo.shelly.cloud` | `35.241.201.25` | 443 | | | `integrations.loqed.com` | `34.155.200.196` | 443, 8883 (MQTTS) | | All cloud connections in the capture were encrypted (TLS 1.2/1.3 with AEAD cipher suites) The only clear-text HTTP flows were captive-portal checks and the local request to `10.12.2.17:80`. ### Apps | App | Package | | --- | --- | | LOQED | `com.loqed.keychain` | | Shelly | `cloud.shelly.smartcontrol` | ## 0.2 Hardware and Software Specifications | Component | Main chip | Firmware | Interfaces | | --- | --- | --- | --- | | Lock (inner/motor module) | Nordic nRF52840 | Unknown (not extracted) | BLE, UART, SWD, I²C | | Outer module (touch) | Unknown (not opened, glued) | Unknown | Cable to motor module (UART) | | Bridge | ESP32-WROVER-B | `LoqedBridge`, ESP-IDF v4.4.1 | WiFi, BLE, UART | Power supply: 8× AA batteries (lock). The bridge has its own mains power supply beneath the PCB. ### Lock (Inner/Motor Module) | Item | Details | | --- | --- | | SoC | Nordic nRF52840 | | CPU / memory | Arm Cortex-M4F @ 64 MHz, 1 MB internal flash, 256 KB RAM, BLE 5 radio (datasheet values) | | External flash | Macronix MX25R3235F, 4 MB SPI NOR, ultra-low-power, WSON-8 package (U6) | | PCB markings | `LOQED IM31201218 PT2256A4 13Jul2022`, manufacturer marking `HT-M E327182`, `94V0` (flammability rating) | | U7 (`H451`) | SOP-8 next to a 0.22 Ω shunt resistor (`R220`) and the motor/power connectors, likely part of the motor driver or power stage (unverified) | | U4 | Sensor on I²C (SDA/SCL pads), function unverified | | Other components | Buzzer, push button (S1), LED, motor terminals (M1/M2), battery terminals (+/−), connectors for touch module, power supply and motor | | Debug / test points | SWD (`CLK`, `DIO`, `3V3`), 2×5 pad header (likely factory programming), UART (`Rx`, `Tx`, `VCC`, J2: `GND`, `3V3`, `RX`, `TX`, `VEXT`) | | Mechanics | DC gear motor driving a brass worm wheel with a hexagonal shaft for the cylinder | | Firmware | Not extracted, per the bridge firmware, lock updates are downloaded by the bridge from `firmware.loqed.com` and streamed to the lock over BLE in parts | | Lock states (from bridge firmware) | Positions: open / latch / night lock, Triggers: twist assist, touch-to-lock, outside module button/PIN, BLE, remote | ### Outer Module (Touch) | Item | Details | | --- | --- | | Construction | Plastic shell, three screws, additionally glued, not opened to avoid damage | | Function | Touch sensor and display (shows e.g.: "Touch to Lock", "Locking") | | Connection | Cable to the motor module, display content is sent over UART | ### Bridge | Item | Details | | --- | --- | | Module | Espressif ESP32-WROVER-B (ESP32-D0WD, 64 Mbit PSRAM), WiFi 2.4 GHz + Bluetooth/BLE | | Flash | 4 MB, MD5 `847d4795ee64714d3b02e2bdb3d20266` | | Framework | ESP-IDF v4.4.1, project `LoqedBridge` | | Build | Git commit `33beadfc1ba5d4d3b0cb6a6ee673535a1d12972c`, version string `33beadf-dirty` (built from an uncommitted working tree) | | Security features | `secure_boot_v1` (per boot log), NVS encryption (`nvs_key` partition) | | Partitions | `nvs` (16K), `nvs_key` (4K), `otadata` (8K), `phy_init` (4K), `factory` / `ota_0` / `ota_1` (1280K each), `coredump` (64K) | | Update mechanism | HTTPS OTA with A/B partitions and rollback, for both bridge and lock | | Crypto library | mbedTLS (AES, RSA, EC, SHA-1/SHA-256), standard CA trust store bundled, no embedded private keys | | Debug interface | UART (115200), used for boot log and firmware read-out via `esptool` | | Other | Reset button, developer paths leaked in strings (`/home/pierre/bridge/…`, `/root/bridge/…`) | ## 0.3 Known Vulnerabilities / Problems / Hacks No known vulnerabilities or hacks were found during research or on CVE. Common issues for consumers include: - Touch display glitches/freezing - Fast battery depletion - Insufficient motor power/jams - No response from lock/bridge disconnections ## 0.4 Collecting APKs Using the rooted Google Pixel and `adb`, I was able to obtain the [Shelly app](app_apks/shelly.apk).: ```bash adb pull $(adb shell pm path cloud.shelly.smartcontrol | tr -d '\r') ./shelly.apk ``` Additionally, I found out that there is a specific [LOQED app](app_apks/loqed.apk) as well, which I also obtained: ```bash adb pull $(adb shell pm path com.loqed.keychain | grep base.apk | tr -d '\r') ./loqed.apk ``` ## 1.1 && 2.1 Unpacking The packaging contains: - A user and safety guide - The guide explains installation without any pictures but uses pictures to explain the app setup - why? - [Video installation guide](https://www.youtube.com/watch?v=ajNjStpTIlk&source_ve_path=OTY3MTQ&embeds_referring_euri=https%3A%2F%2Fembed.jotform.io%2F) - 8 AA batteries for power supply - An outdoor module (touch) - Mounting HW (screws, rings, screwdrivers, ...) - A lock cylinder - A bridge - A motor module - An indoor module (the lock) - The shell The lock is surrounded by a plastic packaging with molds for eah component. Within the box it is kept in place with some cardboard. The box itself is held shut by a small magnet. Contents of the box ## 1.2 || 2.2 Outer Inspection ### Outer Module The outer module is a touch sensor surrounded by a plastic shell with two screws and one cable inside. Back of the touch module During installation, the cable is supposed to connect to the motor module. Cable position of the touch module ### Inner Module The inner module includes the motor module, which is connected using cables to: - the power supply - the touch module The inner module including the motor module Unopened, it is surrounded by a metal shell which can easily be removed by pulling a plastic strap, as this is necessary during installation. ### Bridge The bridge is to be placed within 3 meters of the lock. It is used to connect the lock to the app. It features a reset button. The bridge ## 1.3 && 1.4 Open Casing / HW Inspection ### Bridge The plastic enclosure of the bridge can be opened by unscrewing two screws, revealingthe PCB. What you can spot immediately is: - UART - ESP32 WROVER B - WiFi & Bluetooth/BLE capable - [Datasheet](docs/esp32_datasheet.pdf) PCB inside the bridge Under the PCB is HW for the power supply. Power supply beneath PCB ### Outer Module There are three screws on the back of the touch module, however, it's additonally glued together. It can't be opened without being (probably) permanently damaged. Back of the touch module with screws removed ### Inner Module As I'm working with an already opened device, I'm not interiley sure hw many screws were intially needed to open the motor module, but I'm assuming it were four. After unscrewing these the plastic shell can be removed by gently wiggling it until it comes loose. Inside of the motor module Once again you can immedatiely spot UART. After unplugging all cables, the PCB can be leveraged out of the shell with a flat screwdriver, without causing any damage. Other side of the motor module PCB Here you can see: - UART - $I^2C$ - Most likely an nRF52840 - A buzzer and a button ## 1.5 Software Components from Firmware Analysis Only the bridge firmware could be extracted, so this section covers the bridge The components below were identified through static analysis. Apart from ESP-IDF, no component versions are embedded as strings, the versions of the bundled libraries follow from the ESP-IDF release. ### Bridge | Component | Version / Source | Evidence | Purpose | | --- | --- | --- | --- | | ESP-IDF | v4.4.1 | Version string in image | Espressif's SDK and build framework | | FreeRTOS | Bundled with ESP-IDF v4.4.1 | Part of every ESP-IDF application | Real-time operating system | | Bluedroid BLE stack | Bundled with ESP-IDF | `esp_ble_gattc_*`, `ESP_GAP_BLE_AUTH_CMPL_EVT` | BLE central: scanning, pairing/bonding, GATT client to the lock | | WiFi driver | Bundled with ESP-IDF | `ap.authmode`, `ap.passwd`, "Set WiFi credentials" | WiFi station; credentials provisioned and stored in NVS | | mbedTLS | Bundled with ESP-IDF | mbedTLS label tables, SHA-256 constants, AES/RSA/EC code, TLS-PSK paths | TLS and cryptography | | CA certificate store | Standard ESP/mbedTLS bundle | Public CA certificates (Entrust, COMODO, Starfield, …) | TLS server certificate validation | | MQTT client | ESP-IDF MQTT component (likely `esp-mqtt`) | `mqtts://` URLs, MQTT auth error strings | Cloud connection to `mqtt.production.loqed.com:8883` | | HTTP client / HTTPS OTA | ESP-IDF `esp_http_client`, `esp_https_ota` | `esp_https_ota_*` | Firmware downloads for bridge and lock | | OTA / app update | ESP-IDF `app_update` | `otadata`, `ota_0` / `ota_1` partitions, rollback, image magic `0xE9` | A/B updates with rollback | | NVS with encryption | ESP-IDF `nvs_flash` | `nvs` and `nvs_key` (subtype 0x04) partitions | Encrypted storage of device identity, tokens and WiFi credentials | | Secure boot | ESP-IDF secure boot v1 | `secure_boot_v1` in boot log | Bootloader/app signature verification | | Core dump | ESP-IDF `espcoredump` | `coredump` partition, upload URL | Crash dumps, uploaded to `app.loqed.com` | | Button driver | `iot_button` from Espressif's esp-iot-solution | Path `components/button_driver/iot_button.c` | Reset button handling | | `LoqedBridge` application | Custom, commit `33beadf` (`-dirty`) | Project name, git hash | BLE ↔ MQTT relay, command protocol, lock/bridge OTA logic | The custom application implements: - a binary command protocol between backend, bridge and lock (`REQUEST_STATUS_INFO_BRIDGE`, `COMMAND_ELECTRONIC_DOOR`, `COMMAND_LOCK_FIRMWARE_UPGRADE`, `REQUEST_TIME_FROM_BRIDGE`, …) - the lock state machine (`GO_TO_STATE_*`: open / latch / night lock via twist assist, touch-to-lock, outside module, BLE or remote) - lock firmware delivery: the bridge downloads lock firmware from `firmware.loqed.com` and sends it to the lock over BLE in parts #### Security-Relevant Observations - The firmware prints tokens and WiFi credentials to the UART console (`token: %s`, `auth_token: %s`, `Setting wifi credentials to:`) - It was built from an uncommitted working tree (`-dirty`), and developer paths are left in the binary (`/home/pierre/bridge/…`, `/root/bridge/…`) - ESP-IDF v4.4.1 is from 2022, the bundled components (Bluedroid, mbedTLS, WiFi driver) should be checked against known CVEs for that release ### Lock (Inner Module) The lock firmware could not be extracted, so its components are unknown. What can be said from the outside: - It runs on the nRF52840, so it is most likely built on Nordic's nRF5 SDK with a SoftDevice or on the nRF Connect SDK (Zephyr) - No standard Nordic DFU service is exposed over BLE (ToothFairy WRLS-GATT-002), updates use the bridge's custom transfer mechanism instead - It implements a custom GATT service (`bdce0001-…`) and the UART display protocol to the touch module ## 1.6 Collect "Fresh" Firmware Dump The device has been tampered with before, therefore there is no "fresh" dump available. ## 1.7 && 2.3 Initial setup The initial setup is done using the LOQED app, the lock can then be integrateed into the Shelly app. The capture can be found [here](network/captures/init.pcapng). Additionally, the Shelly app offers to download the data stored on the device, which you can find [here](lock_data/after_init/README.md). Unsure which IP address is most relevant, here are all Shelly-like or LOQED-like addresses: - `34.62.47.252`: changelog-backend.shelly.cloud0 - `34.95.90.149`: control.shelly.cloud - `104.155.32.247`: shelly-120-eu.shelly.cloud - `34.155.200.196`: integrations.loqed.com - `35.205.173.252`: app-ratings.shelly.cloud, paypal.shelly.cloud, shelly-infra1-eu.shelly.cloud - `34.79.5.199`: shelly-101-eu.shelly.cloud - `35.241.201.25`: repo.shelly.cloud For the PreTester analysis, I chose control.shelly.cloud, as I suspect the lock to communicate to a cloud via the bridge. As the bridge has a built in ESP32 supporting WiFi, I suspect the communication looks as follows: PlantUML sequence diagram of suspected communication flow The [PreTester report](network/pretester_reports/init_report.md) didn't find anything unusual. It reported no critical or high findings. ### Old Initial Setup Capture You can also find an older initial setup capture made by Manu [here](network/captures/old_init.pcapng). I tested this in the PreTester as well using integrations.loqed.com, as this seemed to be the most present IP in the capture. The [report](network/pretester_reports/old_init_report.md) can be found here. Once again, it reported no critical or high findings. > The old init capture supposedly was the first real fresh setup. As mentioned, the lock has been tempered with before. ## 2.6 Port Scan I used the PreTester to probe all URLs and endpoints, here are some (interesting) findings: While most of ports are common for HTTPS and SSH, there are a few interesting ones: - 31234 onshelly-101-eu.shelly.cloud: [Nmap scan](network/nmap/shelly-101-eu-shelly-cloud.nmap) revealed no more information - 6113 on shelly-120-eu.shelly.cloud: [Nmap scan](network/nmap/shelly-120-eu-shelly-cloud.nmap) found ports 6115, 6022, 8007, 6114 - It may be important to note that scan took significantly more time (almost 1h) and was eventually aborted - **#TODO: RESCAN** - 9443 on shelly-infra1-eu.shelly.cloud: widely recognized as an alternative or **administrative** HTTPS port - 8883 on mqtt.production.loqed.com: official standard port designated for MQTT over TLS (MQTTS) Some playing around with Metasploit's `check` on these 2 cloud endpoints couldn't find anything either. ## 2.7 BLE I ran ToothFairy on the lock. It's advertising name is "Loqed Inside" and it uses a fixed MAC address `FF:EC:EE:E7:32:E5`. I obtained the [GATT table](ble/gatt_FF-EC-EE-E7-32-E5_20261023_085131.json) and a very incomplete [test report](ble/toothfairy_report_FFECEEE732E5_20261023_085351.json), due to a lack of HW. However, ToothFairy labelled `WRLS-FUZZ-002` as `FAIL`, due to the target not recovering after malformed packets were sent. ### Sniffing After obtaining the needed HW, I tried snifing the app connection process and sent a lock/unlock command, none of which showed ATT protocol packets. This could be due to: - Encryption on the link layer - The sniffer struggling with frequency hopping (nRF nordic) ## 1.8 Collect Firmware Dump of Firmware after initial Setup ### Bridge Hooking up the bridge using UART with a baud rate of 15200 revealed its [boot process](firmware/bridge/boot.txt). Using an older version of esptool, I was able to obtain the [firmware](firmware/bridge/firmware.bin): ```bash pipx run --spec esptool==3.3.2 esptool.py -p /dev/ttyUSB1 -b 115200 read_flash 0 0x400000 full_firmware.bin ``` > Writing this down in case I need it again. #### FW Analysis I found an [IoT FW analysis tool](https://github.com/ericyoc/iot_device_firmware_analysis_poc/tree/main) on GitHub, which I used for generating a quick [report](firmware/bridge/report.txt). ##### Reverse Engineer Anything Using [REA](https://github.com/morluto/rea), we ran the firmware through Claude Code and received a short [report](firmware/bridge/claude_firmware_analysis.md). ### Inner Module Using UART with a baud rate of 115200, I was able to log the boot process & runtime traffic. It seems that the UART link is used for displaying text onto the touch module's display, as you can see it writing whatever the touch module is currently showing. E.g.: "Touch To Lock" or "Locking". Inspect the data [here](firmware/inner_module/inner_module_uart.txt). In picocom, the device didn't react to any text-based commands. I also wasn't able to obtain the FW using the test clip from the H451 chip (unknown by mini programmer) or the NOR flash (couldn't grab it). I tried SWD using the j-link mini, but it kept resulting in an error. It seems the extraction of the motor module FW exceeds this test. ## 1.9 && 2.4 Firmware Update / 1.10 Collect Updated Firmware Dump There is no FW update available. ## 1.11 Collect Firmware Dump after Factory Reset ### Bridge You can find the firmware after a full reset [here](firmware/bridge/reset_fw.bin). I generated a [report](firmware/bridge/reset_fw_report.txt) using the same IoT FW analysis tool. These are the differences: - After reset: MD5 a3ae4e32c155426e5a163429fd35ce74 - Before: MD5 847d4795ee64714d3b02e2bdb3d20266 - Not byte-for-byte identical, firmware itself is unchanged - Only differing string is a single value (`MMNHB<84` vs `LLNHB<84`) > Further & deeper analysis of the FW will be done later on as it exceeds a "simple" informartion gathering. ## 2.5 Inspection of Device During Normal Usage The lock works as expected, the only issue seems to be the touch module which is often delayed or doesn't properly react to input. Nothing interesting other than that.