Reverse Engineering the TP-Link HS110

29.07.2016

The TP-Link HS110 Wi-Fi is a cloud-enabled power plug that can be turned on and off remotely via app and offers energy monitoring and scheduling capabilities. As part of ongoing research into Internet of Things security, we performed a security analysis by reverse engineering the device firmware and Android app, sniffing app-to-device and device-to-app communications and fuzzing the proprietary protocols being used.

While cloud communication was found to be reasonably secure for an IoT device, we discovered two insecure proprietary local configuration protocols: A human-readable JSON protocol "encrypted" with an easily reversible autokey XOR cipher and a binary DES-encrypted configuration and debugging protocol (TDDP – TP-Link Device Debug Protocol). TDDP is in use across most of the TP-Link product line including routers and access points and thus merits further research. We also release a Wireshark dissector and two python clients for the proprietary protocols on GitHub.

Contents

  1. Security Analysis Summary
  2. Device Setup
  3. Reverse Engineering the firmware
  4. Busybox
  5. Portscan
  6. TP-Link Smart Home Protocol
  7. Test Mode
  8. TP-Link Device Debug Protocol

1. Security Analysis Summary

The Good:

  • Cloud functionality can be turned off
  • Cloud communication uses HTTPS and CA pinning
  • Stores energy monitoring data locally
  • Firmware update checks signature against RSA keys

The Bad:

  • Useless encryption for local communication
  • No authentication: Anybody on the local network can turn the Smart Plug on and off, reset it or render it inoperable
  • TLS cloud connection could be intercepted with any valid Symantec EV certificate (only Root CA is checked)
  • Phones home even if set up as local-only
  • Undocumented configuration and debug service (TDDP)

2. Device Setup

The Smart Plug has two physical buttons: An on/off relay switch and a device reset button that resets the device if pushed for five seconds or longer. When plugged in, an unconfigured or freshly reset Smart Plug will start an unsecured open Access Point with the SSID "TP-LINK_Smart Plug_XXXX" where XXXX are four hexadecimal numbers. A quick search on WiGLE reveals several unconfigured TP-Link Smart Plugs in the wild:

WiGLE quick search
WiGLE quick search

TP-Link's Smart Home app "Kasa" makes the smartphone connect to this access point, sends UDP broadcast packets to 255.255.255.255 to find the Smart Plug IP and proceeds to configure it with the SSID and password that the user entered into the app. The Smart Plug then turns off the Access Point and connects to the configured WiFi as a client.

We perform a KARMA attack using the Sensepost MANA Toolkit, forcibly deauthenticating the Smart Plug and trying to get it to connect to a rogue Access Point with the same SSID and no security. The attack is not successful; however repeated deauthentication can be used to perform a temporary Denial of Service attack against the device.

We download the current official firmware for the device (HS110(US)_V1_151016.zip) and use binwalk to extract the contents of the .bin file:

Binwalk
Binwalk

As we can see, the firmware is a typical embedded Linux system and contains three parts:

  • U-Boot Bootloader 1.1.4 (Oct 16 2015 – 11:22:22)
  • Linux Kernel 2.6.31—LSDK-9.2.0_U11.14 ("yt@yangtao.localdomain")
  • Squashfs filesystem

Examining the contents of the filesystem, we find the following interesting files:

  • /bin/busybox v1.01 (2015.10.16-03:17+0000)
  • /etc/newroot2048.crt

This is the certificate used to verify the identity of the cloud server. The file contains the "VeriSign Class 3 Public Primary Certification Authority – G5" root certificate. This means the only check performed when establishing a TLS connection to the cloud is if the provided server certificate has been signed by the Symantec/VeriSign CA for Extended Validation (EV) certificates (CA pinning). A determined attacker could buy his own EV certificate and use it to impersonate a cloud server.

  • /etc/shadow
root:7KBNXuMnKTx6g:15502:0:99999:7:::

The oldschool descrypt password is trivially broken, the password is "media".

  • /usr/bin/shd – the main server application
  • /usr/bin/shdTester – client for energy monitor calibration
  • /usr/bin/calDump – dumps wifi calibration data from /dev/caldata

All proprietary server logic is contained in the shd ("Smart Home Daemon") binary, which is MIPS32 R2 Big Endian:

shd: ELF 32-bit MSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), 
dynamically linked, interpreter /lib/ld-uClibc.so.0, corrupted section header size

The shd binary also contains a copy of OpenSSL 1.0.1j 15 Oct 2014 for establishing TLS connections to the cloud server. We load the shd binary into IDA and start analyzing!

4. Busybox

The Busybox version provided in the firmware is vulnerable to CVE-2011-2716, a command injection vulnerability in the udhcpc DHCP client component of Busybox, which allows to inject shell commands into one of the following DHCP options: (12) Hostname, (15) Domainname, (40) NIS Domain or (66) TFTP Server Name. For this to work, those values have to be actually used by the shell script invoking udhcpc. Analyzing the firmware we find that the shd binary creates a shell script /tmp/udhcpc.script containing:

#!/bin/sh
if[ $1 = renew –o $1 = bound]
then
  ifconfig $interface $ip netmask $subnet
  route del default
  route add default gw $router
  echo "nameserver $dns" > /tmp/resolv.conf
fi

It then executes udhcpc:

/sbin/udhcpc –b –H "HS100(US)" –i br0 –s /tmp/udhcpc.script

As we can see, the hostname is hardcoded and none of the other options are used. Unfortunately, the udhcpc vulnerability is not exploitable in this case.

5. Portscan

An nmap port scan on all TCP and UDP ports reveals the following:

Port Protocol
80/tcp HTTP
9999/tcp TP-Link Smart Home Protocol
1040/udp TP-Link Device Debug Protocol (TDDP)

The Webserver on Port 80 replies with a meaningless ellipsis, no matter what the request is:

HTTP/1.1 200 OK
Server: TP-LINK Smart Plug
Connection: close
Content-Length: 5
Content-Type: text/html

Looking through the shd binary we see that the HTTP Server routine is called "fake_httpd" and will always return this hardcoded reply.

Port 9999 TCP is used for controlling the Smart Plug on the local network via the Kasa app and is described in the TP-Link Smart Home Protocol section. Port 1040 UDP is described in the TP-Link Device Debug Protocol section.

Sniffing the local wireless network traffic reveals that the TP-Link Kasa SmartHome app talks to the HS110 Smart Plug on TCP port 9999 using what looks like encrypted data.

After decompiling the Kasa app for Android, we find the encryption function:

public static byte (] m7377b (byte [] bArr){
  if (bArr != null && bArr.length > 0)
    int i = -85:
    for (int i2 = 0; i2 < bArr.length; i2++){
      byte b = (byte) (i ^ bArr[i2]);
      i = bArr[i2l;
      bArr[i2] = b:
    }
  }
  return bArr:
}

We see the initial key (initialization vector) i has a hardcoded value of -85 (= 171). The first byte of the plaintext is XORed with the key. The key is then set to the plaintext byte. During the next iteration, the next plaintext byte is XORed with the previous plaintext byte. Decryption works the same, with the keystream made out of cyphertext bytes. This is known as an autokey cipher and while it has better statistical properties than simple XOR encryption with a repeating key, it can be easily broken by known plaintext attacks.

Now that we know the algorithm and the key, we implement a Wireshark dissector in LUA which automatically decrypts TP-Link Smart Home packets on port 9999. It turns out that the protocol uses JSON, so we also pass the decrypted contents to the JSON dissector. We can now monitor communications between the Kasa app and the Smart Plug on the local WiFi:

Wireshark Dissector
Wireshark Dissector

The Smart Plug commands are grouped into the following categories:

  • system
  • netif (WLAN interface commands)
  • cnCloud (cloud connection)
  • time
  • emeter (energy meter)
  • schedule (scheduled on/off)
  • count_down (countdown on/off)
  • anti_theft (random scheduled on/off)

We provide a comprehensive list of JSON commands (tplink-smarthome-commands.txt) and a python client to send them with (tplink_smartplug.py).

6.1 System Commands

We can read out information about the system using the get_sysinfo command:

{"system":{"get_sysinfo":{}}}

To send the command using our python client, invoke it with the –c info option:

./tplink_smartplug.py –t 192.168.0.1 –c info

We provide several predefined commands to read out information from the HS110 Smart Plug using –c options. Alternatively, you can use the –j option and provide the full JSON string:

./tplink_smartplug.py –t 192.168.0.1 –j '{"system":{"get_sysinfo":{}}}'

This allows to send any of the commands listed in tplink-smarthome-commands.txt. The get_sysinfo reply will contain the following information:

Command Info
err_code 0 = no error
sw_ver Software Version, currently "1.0.8 Build 151101 Rel.24452"
hw_ver Hardware Version, currently "1.0"
type "smartplug"
model "HS110 (EU)"
mac MAC address of WiFi interface
deviceId Device ID, 40 char hex string
hwID Hardware ID, 32 char hex string
fwID Firmware ID, 32 char hex string
oemID OEM ID, 32 char hex string
alias Text description, e.g. "Basement Lights"
dev_name "Wi-Fi Smart Plug With Energy Monitoring"
icon hash Hash for custom picture
relav state O = Off, 1 = on
on time 0
active mode "schedule" for Schedule Mode
feature "TIM:ENE" (Timer, Energy Monitor)
updating O = not updating
rssi Signal Strength Indicator in dBm (e.g. - 35)
led off O = LED on (default), 1 = LED turned off
latitude Optional geolocation information
logitude Optional geolocation information

We can turn the HS110 Smart Plug on and off using the set_relay_state command, using 1 for on and 0 for off:

{"system":{"set_relay_state":{"state":1}}}

We can reboot the HS110 Smart Plug using the reboot command which requires a delay parameter in seconds:

{"system":{"reboot":{"delay":1}}}

The HS110 Smart Plug can be reset to factory settings, making it act as an open Access Point again:

{"system":{"reset":{"delay":1}}}

Note that since the protocol does not provide authentication, anybody on your network can send this command and force a reset. Here, a prankster would set a high delay value, giving them time to leave the premises.

There are further commands to change the MAC address, change the Device and Hardware IDs, turn off the device LED (night mode) etc.

Of special interest are the firmware flashing commands. You can download a  firmware file from an arbitrary URL using:

{"system":{"download_firmware":{"url":"http://..."}}}

While downloading, you can get the download state using:

{"system":{"get_download_state":{}}}

Once the download is finished, you can flash the firmware using:

{"system":{"flash_firmware":{}}}

Flashing a modified image will not work since the image's signature has to match one of four hardcoded RSA keys (we won’t go into wild speculations why there are four keys here):

HS110 checkfirmware2
HS110 checkfirmware2

6.2 WiFi Commands

You can instruct the HS110 Smart Plug to scan for a list of nearby Access Points:

{"netif":{"get_scaninfo":{"refresh":1}}}

This will return a list of all the available visible APs in the area.

Connecting to an AP with a given SSID and password is done using the command set_stainfo (key_type 3 indicates WPA2):

{"netif":{"set_stainfo":{"ssid":"WiFi","password":"123","key_type":3}}}

Note that all JSON commands are accepted independent of the device state! Connecting to an Access Point would only be necessary in the unconfigured state when the HS110 Smart Plug is acting as an AP. Using the set_stainfo command you can force the Plug to connect to a different WiFi on the fly.

6.3 Cloud Commands

The HS110 Smart Plug regularly tries to connect to the TP-Link cloud server at devs.tplinkcloud.com:50443 using TLS. This behavior continues even if the HS110 Smart Plug is configured as "local only" in the Kasa app. Fortunately for us, there is a command to change the cloud server URL:

{"cnCloud":{"set_server_url":{"server":"devs.tplinkcloud.com"}}}

The shd binary tries to establish a TLS 1.0 connection with the provided URL and checks if the server certificate is signed by Symantec's EV Root CA stored under /etc/newroot2048.crt. An attacker with the necessary funds could purchase a valid EV certificate and perform a Man-in-the-Middle attack on the communication between HS110 Smart Plug and cloud.

To register the device with the cloud we issue the bind command and provide a valid cloud account name and password:

{"cnCloud":{"bind":{"username":alice@home.com, "password":"secret"}}}

It's interesting to note that registration will fail if the Smart Plug's hwID is unknown to the cloud server. This means TP-Link either performs a checksum or other type of plausibility check or checks against a complete list of devices. Either way, this allows TP-Link to track all devices throughout their lifetime.

You can unregister a device from the cloud account using unbind:

{"cnCloud":{"unbind":null}}

This allows an attacker on the local network to unregister the device, forcing the device owner to register again. The attacker can then capture the bind registration command sent by the Kasa app and sees the owner's tplinkcloud.com credentials in cleartext!

7. Test Mode

Analyzing the shd binary, we discovered a hidden "test mode" in the HS110 Smart Plug. It can be invoked remotely or via a command-line argument to the shd binary.

7.1 Command-Line Test Mode

The shd binary can be launched using a –t option which makes it enter test mode:

HS110 testmodecmd
HS110 testmodecmd

This calls a function called wlan_start_art which looks promising because it executes the Atheros Radio Test (ART) client code for performance-testing Atheros wireless adapters.  This piece of debugging code already led to a vulnerability in TP-Link Routers in 2013 where it could be triggered using a hidden URL on the device webserver: TP-Link http/tftp backdoor

The function wlan_start_art runs the following system commands:

LD_LIBRARY_PATH=/tmp
arping –I br0 –c 1 192.168.0.100
tftp –g 192.168.0.100 –r ap_bin/ap121/art.ko –l /tmp/art.ko
insmod /tmp/art.ko
tftp –g 192.168.0.100 –r ap_bin/ap121/nart.out –l /tmp/nart.out
chmod +x /tmp/nart.out
/tmp/nart.out –instance 0 &

The commands connect to a TFTP Server, download a file called nart.out, make it executable and run it. This can be easily used to root the device. All one would need to do is to host a shell script called nart.out on a TFTP server with the IP 192.168.0.100 under the path ap_bin/ap121/. The shell script would start the busybox telnetd daemon:

/bin/busybox telnetd –l/bin/sh &

However, we have no way of invoking the shd binary on the device with the –t option.

7.2 Remote Test Mode

Going through all the JSON commands in the shd binary we find a command called set_test_mode. This means test mode can be enabled remotely!

This is the only JSON command which, as a primitive sort of protection, requires the request to originate from the IP 192.168.0.100. The easiest way to do this is to reset the HS110 Smart Plug and connect to its AP since the DHCP server is configured to issue IPs starting with 192.168.0.100. Next, send the set_test_mode command:

./tplink-smarthome.py -t 192.168.0.1 -j '{"system":{"set_test_mode":{"enable":1}}}'

This writes a special test mode flag into the device's NVRAM. The flag is checked during boot, and if set the HS110 Smart Plug boots into test mode.

Reboot the Smart Plug using:

./tplink-smarthome.py -t 192.168.0.1 -c reboot

The Smart Plug will try to connect to a WPA2-secured Access Point with the SSID "hs_test" and password "12345670" and then turn on the plug's relay:

HS110 testmode4
HS110 testmode4

To set up the hs_test AP, we use hostapd with the following hostapd.conf:

interface=wlan0
driver=nl80211
ssid=hs_test
wpa=2
wpa_passphrase=12345670
channel=1

As expected, the HS110 Smart Plug connects to our hs_test AP. We then observe the Smart Plug going through its regular setup: Request an IP via DHCP, synchronize the time using a server from the NTP-Pool (cn.pool.ntp.org) and connect to the TP-Link Cloud server at devs.tplinkcloud.com:50443.

Unfortunately, the Smart Plug's behavior was no different in test mode than after a regular boot. While there is a reference to the wlan_start_art function in the remote test mode code, it is in a part without any references leading to it. This likely means that the function call to that part was commented out.

A portscan of the HS110 Smart Plug revealed an open UDP port of 1040. Analyzing the shd binary for setsockopt() calls, we could see the port was being bound by a component called "TDDP". The protocol seemed to be binary and designed in a very stealthy manner so that no reply is given whatsoever unless a fully valid packet was sent to the port. Reverse-engineering the protocol would have been a major new undertaking and fortunately, we didn't have to.

Hacked by Patent

After quite a bit of googling for "TP-Link" and "TDDP", we discovered the protocol had been patented in China as patents "CN 102096654 A" and "CN 102123140 B" which Google handily auto-translates into English:

http://www.google.com/patents/CN102096654A?cl=en

A complete protocol specification is included in the patent description and shows you how to construct a TDDP packet.

TDDP can be used to ping or discover a TP-Link Device on the network through broadcasts, read and set configuration options and execute special device-specific commands. TDDP provides integrity through an MD5 digest of the whole packet included in the packet header:

Block 1 Block 2 Block 3 Block 4
Ver Type Code ReplyInfo
PktLength PktLength PktLength PktLength
PktID PktID SubType Reserve
Digest 0 Digest 1 Digest 2 Digest 3
Digest 4 Digest 5 Digest 6 Digest 7
Digest 8 Digest 9 Digest 10 Digest 11
Digest 12 Digest 13 Digest 14 Digest 15

It also encrypts the packet payload using DES. This means that any configuration data we read out will be encrypted in the reply, and any configuration we want to write needs to be sent encrypted.

The DES key is determined by concatenating the device's username and password, building an MD5 hash of that string, and then using the first half of the MD5 (16-digit hex number or 8 bytes) or as the DES key:

md5(username + password)[:16]

Since the HS110 Smart Plug does not provide any authentication, the username and password are hardcoded into the shd binary as admin/admin:

tddpadmin
tddpadmin

For other TP-Link devices, this means an attacker has a highly efficient way to check for default passwords and perform an offline brute-force attack on the login. With a single UDP packet, they can request information that is already known (we found a "Heartbeat"/ping request in the SmartPlug that always returns "ABCD0110) or can be easily determined (e.g. MAC address). The encrypted reply can then be brute-forced offline trying different combinations for username (likely "admin") and password.

We provide a very basic proof-of-concept TDDP client which tries to read out one of three values from a TP-Link device by using a packet type of 0x03 and specific hex-values in the "SubType" header field:

HS110 tddp
HS110 tddp

0x0A returns the test string "ABCD0110", 0x012 return the deviceID and 0x14 returns the hardwareID. They might return different types of information on other types of TP-Link devices. We also found additional values which changed the deviceID (0x13, 0x15), the hardwareID (0x12) and the MAC address (0x06).

The binary TDDP protocol merits further research since it is available on a broad range of TP-Link devices. The possibility for device-specific special commands (CMD_SPE_OPR) has potential to uncover undocumented functions, vulnerabilities or even backdoors. As a next step, different TP-Link devices should be fuzzed with the full range of hex values in the TDDP header and their behavior monitored and analyzed.

UPDATE 19.06.2018:

TP-Link has updated their firmware. If you install this firmware upgrade, tplink_smartplug.py does not work anymore.

UPDATE 04.07.2018:

We updated our tool. It now supports TP-Link HS100, HS105 and HS110 with the latest firmware. Hostnames instead of IP addresses are also supported and the script can now be imported as a Python module. A big thank you to the persons who opened issues and pull requests on GitHub.

Read about other interesting topics on our blog.