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.
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.
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:
#!/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 -T -p 8079 --host --ignore 52.29.48.55:8081
mitmdump -T -p 8079 -s "modify_response_body.py" --ignore 52.29.48.55:8081 "~u http://52.28.103.75/api/app"
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 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
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
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:
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.
Hostname | IP-Address | DNS | Port | Protocol | Usage | Description |
---|---|---|---|---|---|---|
eu-disp.coolkit.cc | 52.29.61.50 | TRUE | 8081 | HTTP | device == 1.5.0 | dispatch/register device online, receive WS host |
eu-disp.coolkit.cc | 52.29.61.50 | TRUE | 443 | HTTPS | device >= 1.5.2 | dispatch/register device online, receive WS host |
iotgo.iteadstudio.com | 52.29.48.55 | FALSE | 8081 | WS | device == 1.5.0 | WebSocket to control the device from apps |
iot.itead.cn | 52.29.48.55 | FALSE | 443 | WSS | device >= 1.5.2 | sec. WebSocket to control the device from apps |
dl.itead.cn | 52.28.103.75 | FALSE | 8088 | HTTP | device >= 1.5.0 | OTA firmware download |
eu-ota.coolkit.cc | 52.28.103.75 | TRUE | 80 | HTTP | app >= 2.1.19 | Check for updated OTA firmware |
eu-ota.coolkit.cc | 52.28.103.75 | TRUE | 8080 | HTTPS | app >= ? | OTA other / scene |
eu-api.coolkit.cc | 52.29.61.50 | TRUE | 8080 | HTTPS | app >= ? | http://iotgo.iteadstudio.com/api |
alog.umeng.com | 110.173.196.36 | TRUE | 80 | HTTP | app >= 2.1.19 | Used for user tracking |
eu-long.coolkit.cc | 52.29.48.55 | TRUE | 8080 | WSS | app >= 2.1.19 | sec. WebSocket to control devices from app |
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
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.
curl http://10.10.7.1/device
{ "deviceid":"10000xxxxx", "apikey":"9a4251ce-e828-4f3a-8180-XXXXXXXXXXXX", "accept":"post" }
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
{ "version": 4, "ssid": "Pi3-AP", "password": "XXXXXXXXXXXX", "serverName": "eu-disp.coolkit.cc", "port": 443 }
{ "error": 0 }
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.
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
{ "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:
{ "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
{ "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" }
{ "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:
{ "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
{ "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" }
{ "rtnCode": 0, "upgradeInfoList": [ { "bizRtnCode": 10001, "deviceid": "10000xxxxx", "model": "ITA-GZ1-GL", "version": "1.5.0", "binList": [ { "downloadUrl": "http://sonoff-142817.appspot.com/ota.php?filename=user1.1024.new.2", "digest": "a0046523521134e2d7eeaa243f29d8bd4748d0647788e097b4fdf75502a8ffba", "name": "user1.bin" }, { "downloadUrl": "http://sonoff-142817.appspot.com/ota.php?filename=user2.1024.new.2", "digest": "a4986bd17a5c2a884c4860c79a1d58d5c9546da232d68ce888fa8c4ad4389a19", "name": "user2.bin" } ] }, { "bizRtnCode": 10002, "deviceid": "10000xxxxx" } ] }
To calculate the digest use the following command:
openssl dgst -sha256 /path/to/user1.bin
Once you have registered a device in your account you have the option to share it to an other account. To share a device both users need to be online at the same time. User-A initiates the process and User-B has to accept it. The whole process is carried out via WebSocket (secure/unsecure depending on your firmware).
User-A sends an invitation message to User-B:
{ "action": "share", "apikey": "6ffb86ec-2190-4ade-a6a9-7ead37a660fc", # user api key from sender "deviceid": "10000xxxxx", # device id "sequence": "1474273634539", # milliseconds "userAgent": "app", "params": { "userName": "recipient@example.com", # recipient "uid": "sender@example.com", # sender "deviceName": "Device0xxxxx" # device name } }
{ "action": "share", "apikey": "6ffb86ec-2190-4ade-a6a9-7ead37a660fc", # user api key from sender "deviceid": "10000xxxxx", # device id "sequence": "1474273634539", # milliseconds "shareUser": "f9f168bf-675a-4d88-91c6-4c5fe9f0be57",# user api key from recipient "userAgent": "app", "params": { "uid": "sender@example.com", # recipient "userName": "recipient@example.com", # sender "deviceName": "Device0xxxxx" # device name } }
Notice: The server used the provided email address of the recipient to lookup his user api key.
User-b accepts the invitation:
{ "apikey": "6ffb86ec-2190-4ade-a6a9-7ead37a660fc", # user api key from sender "deviceid": "10000xxxxx", # device id "error": 0, # no error "result": 2, # success "sequence": "1474273634539", # milliseconds "userAgent": "app" }
{ "apikey": "6ffb86ec-2190-4ade-a6a9-7ead37a660fc", # user api key from sender "deviceid": "10000xxxxx", # device id "error": 0, # no error "result": 2, # success "sequence": "1474273634539", # milliseconds }
Notice: The sharing process is dealt out between two users and the server. The server is the router here. If a device is not shared with your account, the server will not route through messages to the device. On the other hand, the device does not know anything about it's owner or who is allowed to control it. Nothing holds you back from directly talking to a Sonoff device. It will accept any command from anyone.
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.
If you ever messed around and maybe even bricked your device (like I did), you will need to solder and 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