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:

Schematic

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.

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

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://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

Sharing

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:

user-a to server
{
    "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
    }
}
server to user-b
{
    "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:

user-b to server
{
    "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"
}
server to user-a
{
    "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.

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), 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

Further reading