tplink-smartplug/tddp-client/tddp-client.py
Xiaolan.Lee c5eaa41b64 Binding reply receive socket before send request.
Reply maybe comes fast while we are busy updating the screen or closing the
request socket, in this case replay is missed.
2016-09-23 15:45:08 +08:00

210 lines
7.1 KiB
Python

#!/usr/bin/env python
#
# TP-Link Device Debug Protocol (TDDP) v2 Client
# Based on https://www.google.com/patents/CN102096654A?cl=en
#
# HIGHLY EXPERIMENTAL and untested!
# The protocol is available on all kinds of TP-Link devices such as routers, cameras, smart plugs etc.
#
# 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.
#
#
from pyDes import *
import hashlib
import argparse
import socket
import struct
import binascii
version = 0.1
# Default username and password
username = "admin"
password = "admin"
# Check if IP is valid
def validIP(ip):
try:
socket.inet_pton(socket.AF_INET, ip)
except socket.error:
parser.error("Invalid IP Address.")
return ip
# Parse commandline arguments
parser = argparse.ArgumentParser(description="Experimental TP-Link TDDPv2 Client v" + str(version))
parser.add_argument("-v", "--verbose", help="Verbose mode", action="store_true")
parser.add_argument("-t", "--target", metavar="<ip>", required=True, help="Target IP Address", type=validIP)
parser.add_argument("-u", "--username", metavar="<username>", help="Username (default: admin)")
parser.add_argument("-p", "--password", metavar="<password>", help="Password (default: admin)")
# We only allow three different CMD_SPE_OPR commands that read out values from the target device
# Other values can potentially be write commands and modify the device
commands = {'test1':"0A", 'test2':"12", 'test3':"14"}
parser.add_argument("-c", "--command", metavar="<command>", help="Preset command to send. Choices are: "+", ".join(commands), choices=commands)
args = parser.parse_args()
# Set Target IP, username and password to calculate DES decryption key for data and command to execute
ip = args.target
if args.username:
username = args.username
if args.password:
password = args.password
if args.command is None:
cmd = "12"
else:
cmd = commands[args.command]
# TDDP runs on UDP Port 1040
# Response is sent to UDP Port 61000
port_send = 1040
port_receive = 61000
# TDDP DES Key = MD5 of username and password concatenated
# Key is first 8 bytes only
tddp_key = hashlib.md5(username + password).hexdigest()[:16]
if args.verbose:
print "TDDP Key:\t", tddp_key, "(" + username + password + ")"
## TDDP Header
# 0 1 2 3
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | Ver | Type | Code | ReplyInfo |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | PktLength |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | PktID | SubType | Reserve |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | MD5 Digest[0-3] |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | MD5 Digest[4-7] |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | MD5 Digest[8-11] |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | MD5 Digest[12-15] |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
## TDDP Protocol Version
tddp_ver = "02"
## Packet Type
# 0x01 SET_USR_CFG - set configuration information
# 0x02 GET_SYS_INF - get configuration information
# 0x03 CMD_SPE_OPR - special configuration commands
# 0x04 HEART_BEAT - the heartbeat package
tddp_type = "03"
## Code Request Type
# 0x01 TDDP_REQUEST
# 0x02 TDDP_REPLY
tddp_code = "01"
## Reply Info Status
# 0x00 REPLY_OK
# 0x02 ?
# 0x03 ?
# 0x09 REPLY_ERROR
# 0xFF ?
tddp_reply = "00"
## Packet Length (not including header)
# 4 bytes
tddp_length = "00000000"
## Packet ID
# 2 bytes
# supposed to be incremented +1 for each packet
tddp_id = "0001"
# Subtype for CMD_SPE_OPR (Special Operations Command)
# Set to 0x00 for SET_USR_CFG and GET_SYS_INF
#
# Subtypes described in patent application, hex value unknown:
# CMD_SYS_OPR Router system operation, including: init, save, reboot, reset, clr dos
# CMD_AUTO_TEST MAC for writing operation, the user replies CMD_SYS_INIT broadcast packet
# CMD_CONFIG_MAC Factory settings MAC operations
# CMD_CANCEL_TEST Cancel automatic test, stop receiving broadcast packets
# CMD_GET_PROD_ID Get product ID
# CMD_SYS_INIT Initialize a router
# CMD_CONFIG_PIN Router PIN code
#
# Subtypes that seem to work for a HS-110 Smart Plug:
# 0x0A returns "ABCD0110"
# 0x12 returns the deviceID
# 0x14 returns the hwID
# 0x06 changes MAC
# 0x13 changes deviceID
# 0x15 changes deviceID
tddp_subtype = cmd
# Reserved
tddp_reserved = "00"
# Digest 0-15 (32char/128bit/16byte)
# MD5 digest of entire packet
# Set to 0 initially for building the digest, then overwrite with result
tddp_digest = "%0.32X" % 00
# TDDP Data
# Always pad with 0x00 to a length divisible by 8
# We're not sending any data since we're only sending read commands
tddp_data = ""
# Recalculate length if sending data
tddp_length = len(tddp_data)/2
tddp_length = "%0.8X" % tddp_length
## Encrypt data with key
key = des(binascii.unhexlify(tddp_key), ECB)
data = key.encrypt(binascii.unhexlify(tddp_data))
## Assemble packet
tddp_packet = "".join([tddp_ver, tddp_type, tddp_code, tddp_reply, tddp_length, tddp_id, tddp_subtype, tddp_reserved, tddp_digest, data.encode('hex')])
# Calculate MD5
tddp_digest = hashlib.md5(binascii.unhexlify(tddp_packet)).hexdigest()
tddp_packet = tddp_packet[:24] + tddp_digest + tddp_packet[56:]
# Binding receive socket in advance in case reply comes fast.
sock_receive = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock_receive.bind(('', port_receive))
# Send a request
sock_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock_send.sendto(binascii.unhexlify(tddp_packet), (ip, port_send))
if args.verbose:
print "Raw Request:\t", tddp_packet
t = tddp_packet
print "Request Data:\tVersion", t[0:2], "Type", t[2:4], "Status", t[6:8], "Length", t[8:16], "ID", t[16:20], "Subtype", t[20:22]
sock_send.close()
# Receive the reply
response, addr = sock_receive.recvfrom(1024)
r = response.encode('hex')
if args.verbose:
print "Raw Reply:\t", r
sock_receive.close()
print "Reply Data:\tVersion", r[0:2], "Type", r[2:4], "Status", r[6:8], "Length", r[8:16], "ID", r[16:20], "Subtype", r[20:22]
# Take payload and decrypt using key
recv_data = r[56:]
if recv_data:
print "Decrypted:\t" + key.decrypt(binascii.unhexlify(recv_data))