From 1b2633431aaa17b23f76eaeba1af402f5d0df310 Mon Sep 17 00:00:00 2001 From: GammaRay360 Date: Sat, 28 Oct 2017 15:44:37 +0300 Subject: [PATCH 01/10] md5 field initialization before encryption. before the encryption the md5 field should be initialized with the device's token (not with zeros). --- doc/PROTOCOL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/PROTOCOL.md b/doc/PROTOCOL.md index 07faa6b..6c08e7a 100644 --- a/doc/PROTOCOL.md +++ b/doc/PROTOCOL.md @@ -45,7 +45,7 @@ It is an encrypted, binary protocol, based on UDP. The designated port is 54321. MD5 checksum: calculated for the whole packet including the MD5 field itself, - which must be initialized with 0. + which must be initialized with device's token. In the special case of the response to the "Hello" packet, this field contains the 128-bit device token instead. From e8860d297575b0c467131395c4391b0d359e0d34 Mon Sep 17 00:00:00 2001 From: GammaRay360 Date: Sat, 28 Oct 2017 06:13:14 -0700 Subject: [PATCH 02/10] added an optional use of a 'master token' --- pcap-decrypt.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/pcap-decrypt.py b/pcap-decrypt.py index e88a8ce..965e599 100755 --- a/pcap-decrypt.py +++ b/pcap-decrypt.py @@ -67,13 +67,18 @@ def get_macs(packet): args.pcapfile, display_filter=("udp.port == 54321")) device_token = {} # type: Dict[str, bytes] +master_token = -1 +# if you want to use hardcodded token, you can use the master_token var, for example: +# master_token = bytes.fromhex("0123456789abcdef0123456789abcdef") for packet in cap: if "data" not in packet: continue - if (not ipaddress.ip_address(packet.ip.src).is_private - or not ipaddress.ip_address(packet.ip.dst).is_private): - print("NOT IMPLEMENTED: packet to/from Xiaomi Cloud") + if (not ipaddress.ip_address(packet.ip.src).is_private): + print("packet from Xiaomi Cloud") + continue + elif(not ipaddress.ip_address(packet.ip.dst).is_private): + print("packet to Xiaomi Cloud") continue mac_src, mac_dst = get_macs(packet) @@ -94,6 +99,7 @@ def get_macs(packet): decrypted = None if incoming: + print("incoming:") if len(mp.data) == 0: token = mp.md5 device_token[mac_src] = token @@ -101,12 +107,31 @@ def get_macs(packet): mac_src, token.hex())) elif mac_src in device_token: decrypted = miio.decrypt(device_token[mac_src], data) + elif master_token != -1: + print("using master token") + try: + decrypted = miio.decrypt(master_token, data) + except Exception as e: + print(str(e)) + else: + print("Error: can't decrypt. no device token or master token found") + elif outgoing: + print("outgoing:") if mp.md5 == b"\xff" * 16: print("META: Hello") elif mac_dst in device_token: decrypted = miio.decrypt(device_token[mac_dst], data) - if decrypted: + elif master_token != -1: + print("using master token") + try: + decrypted = miio.decrypt(master_token, data) + except Exception as e: + print(str(e)) + else: + print("Error: can't decrypt. no device token or master token found") + + if decrypted is not None: print(decrypted.decode('UTF-8')) # vim:set expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap: From d91aa8857bff5a07d25c8bb137b3016350c6319c Mon Sep 17 00:00:00 2001 From: GammaRay360 Date: Sat, 28 Oct 2017 06:16:52 -0700 Subject: [PATCH 03/10] fixed some bugs. changed unknown2 to did. and added the use of a did. md5 field initialized with the device's token (insted of zeros). --- miio.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/miio.py b/miio.py index 33251ab..7766d04 100644 --- a/miio.py +++ b/miio.py @@ -69,17 +69,17 @@ def AES_cbc_decrypt(token: bytes, ciphertext: bytes) -> bytes: def print_head(raw_packet: bytes): """Print the header fields of a MiHome packet.""" head = raw_packet[:32] - magic, packet_len, unknown1, unknown2, stamp, md5 = \ + magic, packet_len, unknown, did, stamp, md5 = \ struct.unpack('!2sHIII16s', head) print(" magic: %8s" % magic.hex()) print(" packet_len: %8x" % packet_len) - print(" unknown1: %8x" % unknown1) - print(" unknown2: %8x" % unknown2) + print(" unknown field: %8x" % unknown1) + print(" device ID: %8x" % did) print(" stamp: %8x" % stamp) print(" md5 checksum: %s" % md5.hex()) -def encrypt(stamp: int, token: bytes, plaindata: bytes) -> bytes: +def encrypt(stamp: int, did: int, token: bytes, plaindata: bytes) -> bytes: """Generate an encrypted packet from plain data. Args: @@ -87,13 +87,13 @@ def encrypt(stamp: int, token: bytes, plaindata: bytes) -> bytes: token: 128 bit device token plaindata: plain data """ - def init_msg_head(stamp: int, token: bytes, packet_len: int) -> bytes: + def init_msg_head(stamp: int, did: int, token: bytes, packet_len: int) -> bytes: head = struct.pack( '!BBHIII16s', 0x21, 0x31, # const magic value packet_len, 0, # unknown const - 0x02af3988, # unknown const + did, # device id stamp, token # overwritten by the MD5 checksum later ) @@ -101,7 +101,7 @@ def init_msg_head(stamp: int, token: bytes, packet_len: int) -> bytes: payload = AES_cbc_encrypt(token, plaindata) packet_len = len(payload) + 32 - packet = bytearray(init_msg_head(stamp, token, packet_len) + payload) + packet = bytearray(init_msg_head(stamp, did, token, packet_len) + payload) checksum = md5(packet) for i in range(0, 16): packet[i+16] = checksum[i] @@ -125,7 +125,7 @@ def __init__(self): self.magic = (0x21, 0x31) self.length = None self.unknown1 = 0 - self.unknown2 = 0x02af3988 + self.did = 0 self.stamp = 0 self.data = None self.md5 = None @@ -134,7 +134,7 @@ def read(self, raw: bytes): """Parse the payload of a UDP packet.""" head = raw[:32] self.magic, self.length, self.unknown1, \ - self.unknown2, self.stamp, self.md5 = \ + self.did, self.stamp, self.md5 = \ struct.unpack('!2sHIII16s', head) self.data = raw[32:] From 82f132d879303a1176285a12f8920046c18b1afe Mon Sep 17 00:00:00 2001 From: GammaRay360 Date: Sat, 28 Oct 2017 06:20:07 -0700 Subject: [PATCH 04/10] added script: yee-toggle. this will script will toggle any uninitialized yeelight bulb using the binary protocol. --- yee-toggle.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 yee-toggle.py diff --git a/yee-toggle.py b/yee-toggle.py new file mode 100644 index 0000000..9617cf0 --- /dev/null +++ b/yee-toggle.py @@ -0,0 +1,49 @@ +import miio +import socket +import struct + +def main(): + TARGET_IP = "192.168.13.1" + MY_IP = "192.168.13.3" + UDP_PORT = 54321 + pkt_hello = bytes.fromhex("21 31 00 20 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff") + payload_toggle = bytes.fromhex("7b 22 69 64 22 3a 31 2c 22 6d 65 74 68 6f 64 22 3a 22 74 6f 67 67 6c 65 22 2c 22 70 61 72 61 6d 73 22 3a 5b 5d 7d") + stamp = 0 + + sock = socket.socket(socket.AF_INET, # Internet + socket.SOCK_DGRAM) # UDP + sock.bind((MY_IP, UDP_PORT)) + print("open socket") + + sock.sendto(pkt_hello, (TARGET_IP, UDP_PORT)) + print("sending hello") + + + while True: + data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes + break + + print("received message:", data) + miio.print_head(data) + head = data[:32] + magic, packet_len, unknown1, did, stamp, token = \ + struct.unpack('!2sHIII16s', head) + + pkt_toggle = miio.encrypt(stamp+10,did,token,payload_toggle) + print("sending message:", data) + miio.print_head(pkt_toggle) + sock.sendto(pkt_toggle, (TARGET_IP, UDP_PORT)) + print("message sent") + + while True: + data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes + break + + print("received message:", data) + miio.print_head(data) + +if __name__ == "__main__": + main() + + + From 2bf3791ecf24d5c2098ba7a8dffad27e63a9b7fa Mon Sep 17 00:00:00 2001 From: GammaRay360 Date: Sat, 28 Oct 2017 06:24:25 -0700 Subject: [PATCH 05/10] updated the .gitignore not to include tcpdump files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 72364f9..516b784 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# tcpdump files +dump/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] From 8dbc49a47cc335e1d6dd0c3037acc6cdf83a66bb Mon Sep 17 00:00:00 2001 From: GammaRay360 Date: Sat, 4 Nov 2017 03:22:19 -0700 Subject: [PATCH 06/10] fix small bug: changed unknown to unknown1 --- miio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio.py b/miio.py index 7766d04..09577f2 100644 --- a/miio.py +++ b/miio.py @@ -69,7 +69,7 @@ def AES_cbc_decrypt(token: bytes, ciphertext: bytes) -> bytes: def print_head(raw_packet: bytes): """Print the header fields of a MiHome packet.""" head = raw_packet[:32] - magic, packet_len, unknown, did, stamp, md5 = \ + magic, packet_len, unknown1, did, stamp, md5 = \ struct.unpack('!2sHIII16s', head) print(" magic: %8s" % magic.hex()) print(" packet_len: %8x" % packet_len) From 1b267f543c8bbafa0568df43420bcdcdf90b9c39 Mon Sep 17 00:00:00 2001 From: GammaRay360 Date: Sat, 4 Nov 2017 04:40:00 -0700 Subject: [PATCH 07/10] using broadcast ip to send 'hello' packet, then take the target ip from the response to the broadcast --- yee-toggle.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/yee-toggle.py b/yee-toggle.py index 9617cf0..3acaf8c 100644 --- a/yee-toggle.py +++ b/yee-toggle.py @@ -3,8 +3,9 @@ import struct def main(): - TARGET_IP = "192.168.13.1" - MY_IP = "192.168.13.3" + TARGET_IP = "" + BROADCAST_IP = "192.168.13.255" + #MY_IP = "192.168.13.3" UDP_PORT = 54321 pkt_hello = bytes.fromhex("21 31 00 20 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff") payload_toggle = bytes.fromhex("7b 22 69 64 22 3a 31 2c 22 6d 65 74 68 6f 64 22 3a 22 74 6f 67 67 6c 65 22 2c 22 70 61 72 61 6d 73 22 3a 5b 5d 7d") @@ -12,10 +13,11 @@ def main(): sock = socket.socket(socket.AF_INET, # Internet socket.SOCK_DGRAM) # UDP - sock.bind((MY_IP, UDP_PORT)) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + #sock.bind((MY_IP, UDP_PORT)) print("open socket") - sock.sendto(pkt_hello, (TARGET_IP, UDP_PORT)) + sock.sendto(pkt_hello, (BROADCAST_IP, UDP_PORT)) print("sending hello") @@ -23,7 +25,13 @@ def main(): data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes break - print("received message:", data) + TARGET_IP = addr[0] + + if(data == pkt_hello): + print("received hello back from:", TARGET_IP) + return + + print("received message from ip " + TARGET_IP + ":\n", data) miio.print_head(data) head = data[:32] magic, packet_len, unknown1, did, stamp, token = \ From 85a8d39d41cf0adb40a0d494351dd9fe97f0060e Mon Sep 17 00:00:00 2001 From: GammaRay360 Date: Sun, 5 Nov 2017 15:27:27 +0200 Subject: [PATCH 08/10] Update PROTOCOL.md --- doc/PROTOCOL.md | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/doc/PROTOCOL.md b/doc/PROTOCOL.md index 6c08e7a..1bf7457 100644 --- a/doc/PROTOCOL.md +++ b/doc/PROTOCOL.md @@ -6,22 +6,26 @@ It is an encrypted, binary protocol, based on UDP. The designated port is 54321. ## Packet format - 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 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Magic number = 0x2131 | Packet Length (incl. header) | - |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| - | Unknown1 | - |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| - | Device ID ("did") | - |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| - | Stamp | - |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| - | MD5 checksum | - | ... or Device Token in response to the "Hello" packet | - |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| - | optional variable-sized data (encrypted) | - |...............................................................| + 0 1 2 3 + 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+------- + 0 | Magic number = 0x2131 | Packet Length (incl. header) | ↑ + |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| | + 1 | Unknown1 | | + |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| | + 2 | Device ID ("did") | | + |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| | + 3 | Time Stamp | Header + |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| | + 4 | MD5 checksum. | | + 5 | OR | | + 6 | Device Token in response to the "Hello" packet. | | + 7 | size = 16 bytes. | ↓ + |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|------- + 8 | optional, variable-sized data | ↑ + ⋮ ⋮ max size = 65503 bytes (0xffff - header size). ⋮ Payload +n-1 | encrypted with AES-128 (see below). | ↓ + n |...............................................................|------- Mi Home Binary Protocol header Note that one tick mark represents one bit position. From 5bcf499986c8c6fb1339a31f11cd0fcc98953160 Mon Sep 17 00:00:00 2001 From: GammaRay360 Date: Sun, 5 Nov 2017 15:32:07 +0200 Subject: [PATCH 09/10] Update PROTOCOL.md --- doc/PROTOCOL.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/doc/PROTOCOL.md b/doc/PROTOCOL.md index 1bf7457..5b0686d 100644 --- a/doc/PROTOCOL.md +++ b/doc/PROTOCOL.md @@ -6,26 +6,26 @@ It is an encrypted, binary protocol, based on UDP. The designated port is 54321. ## Packet format - 0 1 2 3 - 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+------- - 0 | Magic number = 0x2131 | Packet Length (incl. header) | ↑ - |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| | - 1 | Unknown1 | | - |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| | - 2 | Device ID ("did") | | - |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| | - 3 | Time Stamp | Header - |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| | - 4 | MD5 checksum. | | - 5 | OR | | - 6 | Device Token in response to the "Hello" packet. | | - 7 | size = 16 bytes. | ↓ - |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|------- - 8 | optional, variable-sized data | ↑ - ⋮ ⋮ max size = 65503 bytes (0xffff - header size). ⋮ Payload -n-1 | encrypted with AES-128 (see below). | ↓ - n |...............................................................|------- + 0 1 2 3 + 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+------ + 0 | Magic number = 0x2131 | Packet Length (incl. header) | ↑ + |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| | + 1 | Unknown1 | | + |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| | + 2 | Device ID ("did") | | + |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| | + 3 | Time Stamp | Header + |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| | + 4 | MD5 checksum. | | + 5 | OR | | + 6 | Device Token in response to the "Hello" packet. | | + 7 | size = 16 bytes. | ↓ + |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|------ + 8 | optional, variable-sized data | ↑ + ⋮ ⋮ max size = 65503 bytes (0xffff - header size). ⋮ Payload + n-1 | encrypted with AES-128 (see below). | ↓ + n |...............................................................|------ Mi Home Binary Protocol header Note that one tick mark represents one bit position. From dd0b3852ef5d8e0a4d3f8f385ef5a6ddb52eb657 Mon Sep 17 00:00:00 2001 From: GammaRay360 Date: Sun, 5 Nov 2017 15:38:41 +0200 Subject: [PATCH 10/10] Update PROTOCOL.md --- doc/PROTOCOL.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/PROTOCOL.md b/doc/PROTOCOL.md index 5b0686d..d0674cf 100644 --- a/doc/PROTOCOL.md +++ b/doc/PROTOCOL.md @@ -18,13 +18,13 @@ It is an encrypted, binary protocol, based on UDP. The designated port is 54321. 3 | Time Stamp | Header |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| | 4 | MD5 checksum. | | - 5 | OR | | - 6 | Device Token in response to the "Hello" packet. | | - 7 | size = 16 bytes. | ↓ + 5 | OR | | + 6 | Device Token in response to the "Hello" packet. | | + 7 | size = 16 bytes. | ↓ |-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|------ - 8 | optional, variable-sized data | ↑ + 8 | optional, variable-sized data | ↑ ⋮ ⋮ max size = 65503 bytes (0xffff - header size). ⋮ Payload - n-1 | encrypted with AES-128 (see below). | ↓ + n-1 | encrypted with AES-128 (see below). | ↓ n |...............................................................|------ Mi Home Binary Protocol header