TP-Link HS110

For issues with communication ports, protocols, etc.
Dinosaur
Posts: 1357
Joined: Jul 24, 2005 1:13
Location: Hervey Bay
Contact:

TP-Link HS110

Postby Dinosaur » Sep 17, 2019 15:44

Hi All

Don't know if anyone has done anything with these "Smart Plugs" , but I am doing a little private project
for a Museum in Australia where it takes an hour to walk around and turn all the lights on/off.
It is a large acreage property with heritage equipment and displays.

The HS110 seems very popular in all parts of the world.
Someone has reverse engineered the software in it (it runs on Linux) to verify
the ability to withstand hacking.
That resulted in a .py script that allows you to talk to it from a PC and control all the functions.
So obviously it failed the hacking test.
https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/

Code: Select all

#!/usr/bin/env python2
#
# TP-Link Wi-Fi Smart Plug Protocol Client
# For use with TP-Link HS-100 or HS-110
#
# by Lubomir Stroetmann
# Copyright 2016 softScheck GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import socket
import argparse
from struct import pack

version = 0.2

# Check if hostname is valid
def validHostname(hostname):
   try:
      socket.gethostbyname(hostname)
   except socket.error:
      parser.error("Invalid hostname.")
   return hostname

# Predefined Smart Plug Commands
# For a full list of commands, consult tplink_commands.txt
commands = {'info'     : '{"system":{"get_sysinfo":{}}}',
         'on'       : '{"system":{"set_relay_state":{"state":1}}}',
         'off'      : '{"system":{"set_relay_state":{"state":0}}}',
         'cloudinfo': '{"cnCloud":{"get_info":{}}}',
         'wlanscan' : '{"netif":{"get_scaninfo":{"refresh":0}}}',
         'time'     : '{"time":{"get_time":{}}}',
         'schedule' : '{"schedule":{"get_rules":{}}}',
         'countdown': '{"count_down":{"get_rules":{}}}',
         'antitheft': '{"anti_theft":{"get_rules":{}}}',
         'reboot'   : '{"system":{"reboot":{"delay":1}}}',
         'reset'    : '{"system":{"reset":{"delay":1}}}',
         'energy'   : '{"emeter":{"get_realtime":{}}}'
}

# Encryption and Decryption of TP-Link Smart Home Protocol
# XOR Autokey Cipher with starting key = 171
def encrypt(string):
   key = 171
   result = pack('>I', len(string))
   for i in string:
      a = key ^ ord(i)
      key = a
      result += chr(a)
   return result

def decrypt(string):
   key = 171
   result = ""
   for i in string:
      a = key ^ ord(i)
      key = ord(i)
      result += chr(a)
   return result

# Parse commandline arguments
parser = argparse.ArgumentParser(description="TP-Link Wi-Fi Smart Plug Client v" + str(version))
parser.add_argument("-t", "--target", metavar="<hostname>", required=True, help="Target hostname or IP address", type=validHostname)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("-c", "--command", metavar="<command>", help="Preset command to send. Choices are: "+", ".join(commands), choices=commands)
group.add_argument("-j", "--json", metavar="<JSON string>", help="Full JSON string of command to send")
args = parser.parse_args()


# Set target IP, port and command to send
ip = args.target
port = 9999
if args.command is None:
   cmd = args.json
else:
   cmd = commands[args.command]



# Send command and receive reply
try:
   sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   sock_tcp.connect((ip, port))
   sock_tcp.send(encrypt(cmd))
   data = sock_tcp.recv(2048)
   sock_tcp.close()

   print "Sent:     ", cmd
   print "Received: ", decrypt(data[4:])
except socket.error:
   quit("Cound not connect to host " + ip + ":" + str(port))


The key to using this range (HS100, HS110) is the ability to disconnect it from the Cloud
and it can still be controlled from a Wifi network. I have tested that.

I am interested in writing a small program that the Museum staff can use to control the lights,
so that it is not a number of people with the "Kasa" app on their phone doing conflicting functions.

Initially I looked at Joshy's SNC but I am not knowledgeable enough on the Ethernet & Wifi differences to convert that.
The other option is to call the script from a shell , but really don't like that.
Because I have never used python, the conversion of the script is beyond me, unless I get some good pointers.
The script itself does not look to complex, but the syntax gets me befuddled.
Additionally the functions it calls may not be available to a FB program or have to be called in a different way.

Using sncSmallNetworkPrinter seems the easiest to try out, but
the command for turning On is {"system":{"set_relay_state":{"state":0}}}
and putting that in Quotes wont compile. Additionally the command has to be encrypted with Key=171
and I don’t understand the syntax of that section either.

Have tried putting that in different format where it will compile, but the HS110 does not respond.
Also I will have to check the License before I proceed with this.

Any Suggestions ??

Regards
badidea
Posts: 2122
Joined: May 24, 2007 22:10
Location: The Netherlands

Re: TP-Link HS110

Postby badidea » Sep 17, 2019 21:24

Not sure if such an easy to hack device is wise to use in a museum, but it looks like a fun project.

The socket part should be possible in freeBASIC, but will take some time to get familiar with.
There are some network libraries on this forum that might work, but you can also implement the socket API calls yourself. See example in c: https://en.wikipedia.org/wiki/Berkeley_ ... _using_TCP
Ethernet or Wifi should not make a difference (as far as I know).

About the encrypt function: ^ is xor, ord() is asc().
The pack('>I', len(string)) is needed because python sucks when working with binary data.
Your link includes a c version as well:
Image

Without owning such a HS110, it is difficult to help. They are widely available here as well I see. Maybe I buy one, just for fun, if I don't forget.

Code: Select all

dim as string commandStr = !"{\"system\":{\"set_relay_state\":{\"state\":0}}}"
print commandStr
Dinosaur
Posts: 1357
Joined: Jul 24, 2005 1:13
Location: Hervey Bay
Contact:

Re: TP-Link HS110

Postby Dinosaur » Sep 17, 2019 23:37

Hi All

Thanks for your response badidea.
Not sure if such an easy to hack device is wise to use in a museum, but it looks like a fun project.
That's why I would disconnect the Wifi from the internet. Then the only danger is someone breaking into the Wifi,
(if they are within this Wifi range) and then being motivated enough to hack the Smart plugs "To play with Lights".

I now understand the encrypting part, except for the Pack statement.
No amount of browsing clarifies it for me.
The C example is like going from Swahili to Chinese for me. The Python is more readable when I understand the commands.

Thanks for the print example, that means I can call the script from FB.

Regards

EDIT: PS there is an app on a Dutch forum (Athom) but it was unreliable and to get the app onto your phone
is not very well explained. (even in Dutch, which I can read but not speak or write any more)

EDIT2: It appears that the statement: result = pack('>I', len(string)) packs a blank string (result) with '>I' ,
but can't resolve what '>I' actually represents.
D.J.Peters
Posts: 8132
Joined: May 28, 2005 3:28
Contact:

Re: TP-Link HS110

Postby D.J.Peters » Sep 18, 2019 4:25

I don*t use Python but looks like you send and receive XOR*ed strings via TCP thats all isn't it ?

tplink-smarthome-commands.txt

Joshy
Last edited by D.J.Peters on Sep 18, 2019 7:23, edited 1 time in total.
badidea
Posts: 2122
Joined: May 24, 2007 22:10
Location: The Netherlands

Re: TP-Link HS110

Postby badidea » Sep 18, 2019 7:09

> & I speficy byte order and byte alignment.

https://docs.python.org/2/library/struct.html

edit: not "l" but "I"

Also not exactly sure what python is doing, but my best guess for this freebasic version of encrypt / decrypt is this:

Code: Select all

function encrypt(byref cmd as string) as string
   if len(cmd) > 0 then
      dim as ubyte key = 171 'same as -85
      for i as integer = 0 to len(cmd) - 1
         dim as ubyte a = key xor cmd[i]
         key = a
         cmd[i] = a
      next
   end if
   return cmd
end function

function decrypt(byref cmd as string) as string
   if len(cmd) > 0 then
      dim as ubyte key = 171 'same as -85
      for i as integer = 0 to len(cmd) - 1
         dim as ubyte a = key xor cmd[i]
         key = cmd[i]
         cmd[i] = a
      next
   end if
   return cmd
end function

dim as string cmdStr = !"{\"system\":{\"get_sysinfo\":{}}}"
print cmdStr
encrypt(cmdStr)

for i as integer = 0 to len(cmdStr) - 1
   print hex(cmdStr[i], 2) & " ";
next
print

decrypt(cmdStr)
print cmdStr

Maybe encrypt & decrypt need to be exchanged. There is some discrepancy between the softscheck website and the python code.

python test code:

Code: Select all

#!/usr/bin/env python2

from struct import pack

def encrypt(string):
   key = 171
   result = pack('>I', len(string))
   for i in string:
      a = key ^ ord(i)
      key = a
      result += chr(a)
   return result

def decrypt(string):
   key = 171
   result = ""
   for i in string:
      a = key ^ ord(i)
      key = ord(i)
      result += chr(a)
   return result

cmd = '{"system":{"get_sysinfo":{}}}'

print cmd

encryptCmd = encrypt(cmd)
print len(encryptCmd)
for a in encryptCmd:
   print hex(ord(a))[2:].upper(),

Freebasic:
D0 F2 81 F8 8B FF 9A F7 D5 EF 94 B6 D1 B4 C0 9F EC 95 E6 8F E1 87 E8 CA F0 8B F6 8B F6
Python:
0 0 0 1D D0 F2 81 F8 8B FF 9A F7 D5 EF 94 B6 D1 B4 C0 9F EC 95 E6 8F E1 87 E8 CA F0 8B F6 8B F6

1D = 29 = actual length of string, why python adds this is unclear to me.
I don't think python is meant for people raised transistors, ttl-logic and assembly code.
Dinosaur
Posts: 1357
Joined: Jul 24, 2005 1:13
Location: Hervey Bay
Contact:

Re: TP-Link HS110

Postby Dinosaur » Sep 19, 2019 2:13

Hi All

Thanks for the replies, will spend some time testing this weekend and then respond.
My paying job is pulling my attention.

Regards
Dinosaur
Posts: 1357
Joined: Jul 24, 2005 1:13
Location: Hervey Bay
Contact:

Re: TP-Link HS110

Postby Dinosaur » Sep 21, 2019 21:46

Hi All

Using the information provided here, I wrote a small test program that outputs
the same data as the python script.
It uses (or tries to) Joshy's sncNetworkPrinter.

The HS110 does not respond, although I have not worked out yet how to get the response.
For the moment I am just trying to send a command.

Code: Select all

#include once "snc.bi"

function encrypt(byref cmd as string) as string
   if len(cmd) > 0 then
      dim as ubyte key = 171 'same as -85
      for i as integer = 0 to len(cmd) - 1
         dim as ubyte a = key xor cmd[i]
         key = a
         cmd[i] = a
      next
   end if
   return cmd
end function

function decrypt(byref cmd as string) as string
   if len(cmd) > 0 then
      dim as ubyte key = 171 'same as -85
      for i as integer = 0 to len(cmd) - 1
         dim as ubyte a = key xor cmd[i]
         key = cmd[i]
         cmd[i] = a
      next
   end if
   return cmd
end function
'==========================================================================
dim as string cmdStr = !"{\"system\":{\"set_relay_state\":{\"state\":0}}}"
dim as string newcmdStr = Chr(0) + Chr(0) + Chr(0) + Chr(42)
encrypt(cmdStr)
newCmdStr = newCmdStr + cmdStr
for i as integer = 0 to len(newcmdStr) - 1
   print hex(newcmdStr[i], 2) & " ";
next
print
print "Len    cmdStr=";len(cmdStr)
print "Len newcmdStr=";len(newcmdStr)

'' This works for on & off
'Shell "./tplink_smartplug.py -t 192.168.1.145 -c off"
'end



const as string ServerIP   = "192.168.1.145"
const as ushort ServerPort = 9999
var client = NetworkClient(ServerIP,ServerPort)
var connection = client.GetConnection()
Print "Connection   = ";Connection
while connection->CanPut()<>1: sleep 100 : wend
Print "Connection Opened"
connection->PutData(@newcmdStr,Len(newcmdStr))
Print "Sleeping"
sleep

FB Output
00 00 00 2A D0 F2 81 F8 8B FF 9A F7 D5 EF 94 B6 C5 A0 D4 8B F9 9C F0 91 E8 B7 C4 B0 D1 A5 C0 E2 D8 A3 81 F2 86 E7 93 F6 D4 EE DE A3 DE A3
Len cmdStr= 42
Len newcmdStr= 46
Connection =23354032
Connection Opened
Sleeping

Python output.
0 0 0 2A D0 F2 81 F8 8B FF 9A F7 D5 EF 94 B6 C5 A0 D4 8B F9 9C F0 91 E8 B7 C4 B0 D1 A5 C0 E2 D8 A3 81 F2 86 E7 93 F6 D4 EE DE A3 DE A3
Refreshing myself on Wireshark so I can see what is happening, but
any suggestions on how to read the port using SNC greatly appreciated

Regards
D.J.Peters
Posts: 8132
Joined: May 28, 2005 3:28
Contact:

Re: TP-Link HS110

Postby D.J.Peters » Sep 21, 2019 22:20

The most of all examples comes with SNC receive data as well.
Why not looking at this examples ?

Joshy

Code: Select all

' ready to receive ?
while connection->CanGet()<>1 : sleep 100 : wend
dim as zstring ptr buffer
var nBytes = connection->GetData(buffer)
print "number of received bytes " & nBytes
if nBytes>0 then
  dim as string aString =*buffer
  ...
end if
NOTE: the received "buffer" pointer are allocated by SNC so if you done with the received data you have to deallocate it:

Code: Select all

' ready to receive ?
while connection->CanGet()<>1 : sleep 100 : wend
dim as zstring ptr buffer
var nBytes = connection->GetData(buffer)
print "number of received bytes " & nBytes
if nBytes>0 then
  dim as string aString =*buffer
  ...
  deallocate buffer ' <--- important
end if
badidea
Posts: 2122
Joined: May 24, 2007 22:10
Location: The Netherlands

Re: TP-Link HS110

Postby badidea » Sep 21, 2019 22:54

Did you try the command without "00 00 00 2A" in front of it?
Dinosaur
Posts: 1357
Joined: Jul 24, 2005 1:13
Location: Hervey Bay
Contact:

Re: TP-Link HS110

Postby Dinosaur » Sep 21, 2019 23:28

Hi All

@badidea
Yes I did, and also tried encrypting WITH the file len bytes in front.
It boils down to working blind.

Interestingly I have written snc into one of my applications where reporting is sent
from one to another pc. BUT that was 3 years ago, and being a Dinosaur, I have re-learn all this.

Thank you Joshy, I will spend the time to get the response with your suggestions.

Regards
badidea
Posts: 2122
Joined: May 24, 2007 22:10
Location: The Netherlands

Re: TP-Link HS110

Postby badidea » Sep 22, 2019 0:55

From what I read, the 4-byte padding with message length is needed for TCP connection, not for UDP messages.
If you search GitHub for "HS110" you will find lots of code in various languages, but no freeBASIC (yet).
One c-code example: https://github.com/JustinZhou300/TP-Lin ... 10Client.c
C is very similar to freeBASIC.
Dinosaur
Posts: 1357
Joined: Jul 24, 2005 1:13
Location: Hervey Bay
Contact:

Re: TP-Link HS110

Postby Dinosaur » Sep 22, 2019 1:58

Hi All

@badidea
I figured there must be something wrong as the following test all failed.

By printing the cmd sent by python and comparing it to the encrypted cmd in FB, they are identical.
So, I have confirmed that I am sending an identical string as python. But you were right
the file len chr's are not included.

I could not get a response with SNC either.

Code: Select all

#include once "snc.bi"
'here using encrypted chr's
dim as string cmdStr = Chr(208)+Chr(242)+Chr(129)+Chr(248)+Chr(139)+Chr(255)+Chr(154)+Chr(247)+Chr(213)+Chr(239)_
+Chr(148)+Chr(182)+Chr(197)+Chr(160)+Chr(212)+Chr(139)+Chr(249)+Chr(156)+Chr(240)+Chr(145)+Chr(232)+Chr(183)_
+Chr(196)+Chr(176)+Chr(209)+Chr(165)+Chr(192)+Chr(226)+Chr(216)+Chr(163)+Chr(129)+Chr(242)+Chr(134)+Chr(231)_
+Chr(147)+Chr(246)+Chr(212)+Chr(238)+Chr(222)+Chr(163)+Chr(222)+Chr(163)

for i as integer = 0 to len(cmdStr) - 1
   print hex(cmdStr[i], 2) & " ";
next
print
'prints as below which = Python print
'D0 F2 81 F8 8B FF 9A F7 D5 EF 94 B6 C5 A0 D4 8B F9 9C F0 91 E8 B7 C4 B0 D1 A5 C0 E2 D8 A3 81 F2 86 E7 93 F6 D4 EE DE A3 DE A3

print "Len cmdStr=";len(cmdStr)
'prints 42

const as string ServerIP   = "192.168.1.145"
const as ushort ServerPort = 9999
var client = NetworkClient(ServerIP,ServerPort)
var connection = client.GetConnection()
Print "Connection   = ";Connection
'prints Connection   = 19105984

while connection->CanPut()<>1: sleep 100 : wend
Print "Connection Opened"
connection->PutData(@cmdStr,Len(cmdStr))
Print "Wait for CanGet"

while connection->CanGet()<>1 : sleep 100 : wend
dim as zstring ptr buffer
sleep 5000

var nBytes = connection->GetData(buffer)
print "number of received bytes " & nBytes  'prints 0
if nBytes>0 then
  dim as string aString =*buffer
  Print aString
end if
deallocate buffer ' <--- important
end


So I copied the cmd into a file and used the following command:

Code: Select all

shell "nc 192.168.1.145 9999 < tplink.txt"
No errors, and about a 5 sec sleep when sent.
Still no response. So the mystery may lay with the port opening method. (TCP or UDP)

More research needed.

Regards
D.J.Peters
Posts: 8132
Joined: May 28, 2005 3:28
Contact:

Re: TP-Link HS110

Postby D.J.Peters » Sep 22, 2019 10:26

Funny you will do low level hardware I/O but you don't know the difference of @ vs strptr() ;-)

connection->PutData( @cmdStr, len(cmdStr) )
Is wrong you don't send the chars of the string you send the string descriptor !

it must be:
connection->PutData( strptr(cmdStr), len(cmdStr) )

If you don't get any response try to send the string terminator "0" also

connection->PutData( strptr(cmdStr), len(cmdStr) + 1)

Joshy
Dinosaur
Posts: 1357
Joined: Jul 24, 2005 1:13
Location: Hervey Bay
Contact:

Re: TP-Link HS110

Postby Dinosaur » Sep 22, 2019 13:14

Hi All

Funny you will do low level hardware I/O but you don't know the difference of @ vs strptr() ;-)

I am surprised I even learned to drive on the wrong side of the road here in the US.:)
Even at 73 I am impatient to do things.

Thanks for picking up that error,although it did not immediately solve the problem I will persevere.

Regards
Dinosaur
Posts: 1357
Joined: Jul 24, 2005 1:13
Location: Hervey Bay
Contact:

Re: TP-Link HS110

Postby Dinosaur » Sep 22, 2019 14:38

Hi All

Managed to achieve the Tx of the command and the HS110 reponded by turning off.
Got 49 Chr's in return. (not decrypted that yet)
The main errors: (besides the one that Joshy pointed out)
I printed every Chr that the encrypt routine added to the Result.
This I thought was what was being sent, however the actual transmission has the string len at the beginning. (as badidea pointed out)
Now I can see where that is being added in the python script.(I know, that was pointed out before as well)
Even though I don't understand how that Pack statement works.

Code: Select all

result = pack('>I', len(string))
So now I can progress and use the encrypt & decrypt routines to translate the commands.

So, the string to be sent is: 4 unencrypted chr's representing the String Len + the encrypted command.
Adding the String terminator didn't seem to make any difference, although the 49 chr's returned may nominate an error as a result.
Will post again when I have completed that into a usable routine.
Many thanks for the help sofar.

Regards

Return to “Hardware Interfaces / Communication”

Who is online

Users browsing this forum: No registered users and 3 guests