208 lines
7.0 KiB
Python
208 lines
7.0 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:]
|
|
|
|
# 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
|
|
sock_receive = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
sock_receive.bind(('', port_receive))
|
|
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))
|
|
|