Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: Did you backwards engineer the UDP methods? #155

Open
JLannoo opened this issue Nov 18, 2022 · 9 comments
Open

Question: Did you backwards engineer the UDP methods? #155

JLannoo opened this issue Nov 18, 2022 · 9 comments
Labels
enhancement New feature or request

Comments

@JLannoo
Copy link

JLannoo commented Nov 18, 2022

If you did, how did you do it?

I'm asking because I'm working on a project for interfacing with WiZ Lights and basing most of my functionality on your documentation of the UDP methods.

While investigating I've found that some of the methods don't work with my bulbs (ESP24_SHRGBC_01) and some that you are missing.

For example, {"method":"getDevInfo","params":{}} returns:

{
  "method": "getFavs",
  "env": "pro",
  "result": {
    "favs": [
      [4,0,0,0,0,0,0],
      [14,0,0,0,0,0,0],
      [6,0,0,0,0,0,0],
      [2,0,0,0,0,0,0]
    ],
    "opts": [[],[],[],[]]
  }
}

The first number in each array is the scene code for my selected favorite scenes, but I don't know about the rest.

Also I've been trying to change the fade time with {"method":"setUserConfig","params":{"fadeIn": 0, "fadeOut": 0}} but get back "error": {"code": -32600, "message": "Invalid Request"}

I would love to collaborate in expanding the documentation and knowledge about the inner functioning of the API , but I don't know how I could go about helping you.

@nmoschkin
Copy link

nmoschkin commented Dec 28, 2022

I was noticing that some of my bulbs became unresponsive after a software update. I wrote a library distantly based on this one, in C#, and I'm in the middle of updating the project, so I came here looking for some answers to interesting questions I had. This is definitely in the vicinity of the kind of questions I had been having.

Actually, I've managed to solve a lot of those issues. I'm in the middle of working on a new version of my own library (it's C#, things can get hairy), and so, I've got some relatively recent familiar experience with all of this.

Plus, my lib does have some features they haven't incorporated into pywizlight, but I've talked about it with them, like how the type of bulb is knowable (Lighting strip, A19, candelabra, flood light, etc.), but they don't want to program that information because they don't trust. However, in 3 years, I've found these specific markers to be reliable indicators of bulb type. Also, there are newer and older versions of the software, the reporting JSON is slightly different between the two.

@sbidy
Copy link
Owner

sbidy commented Jan 6, 2023

Hey, there can be an increasing variation in the bulb API (because Wiz is now part of Signify / Philips Hue).
ESP24 seems to me something new (new chip or hardware platform). Potentially, they also change the API.

For having an overview of the API calls you can make and the returns, you can have a look into https://docs.pro.wizconnected.com/#introduction - this is the MQTT API which is partly also available via UDP.

@sbidy sbidy added the enhancement New feature or request label Jan 6, 2023
@s0ullight
Copy link

ESP24_SHRGBC_01 is reported as the moduleName if the module is an ESP32-C3-WIZ20212. The chip is an ESP32-C3FH4.

@dreamlayers
Copy link

dreamlayers commented Dec 16, 2023

With ESP24_SHRGBW_01 firmware version 1.28.0, the bulb is willing to connect via MQTT to a self-signed certificate. That means it can connect to another MQTT server, like mosquitto, with TLS enabled, or its conversation with its own servers can be intercepted and decrypted. Firmware updates might break this capability.

I set up my router to give my PC as the DNS server and default gateway to the bulb via DHCP. Then I set up bind9 on my PC to resolve names to my PC's IP address. I set up mosquitto on my PC, generating a self-signed certificate. I also downloaded the latest mitmproxy to intercept communications between the bulb and the Wiz MQTT server. This all seemed excessively complicated and annoying, but eventually it worked.

The bulb subscribes to a specific MQTT topic, with its unique identifiers. Then one can publish JSON to that topic to change its configuration. That JSON looks similar to what is sent via the UDP interface, but I guess some methods are only accepted via MQTT. I sent the exact same string via UDP and MQTT. It failed via UDP but worked via MQTT. Here is that example, changing the power on and off fade times. Note the userConfigTs below. I had to increment it to get the change to happen. Probably if it matches the current configuration, the configuration is not updated.

~$ echo '{"method":"setUserConfig","params":{"fadeIn":700,"fadeOut":700,"dftDim":100,"startupMode":"wizclick","pwmRange":[0,100],"whiteRange":[2700,6500],"extRange":[2200,6500],"po":0,"poRhy":1,"userConfigTs":4}}' | nc -u -w 1 x.x.x.x 38899  ; echo
{"method":"setUserConfig","env":"pro","error":{"code":-32600,"message":"Invalid Request"}}
~$ mosquitto_pub -t 'OP/pro/redacted/devices/redacted' -m '{"method":"setUserConfig","params":{"fadeIn":700,"fadeOut":700,"dftDim":100,"startupMode":"wizclick","pwmRange":[0,100],"whiteRange":[2700,6500],"extRange":[2200,6500],"po":0,"poRhy":1,"userConfigTs":4}}' -p 8884
~$ echo '{"method":"getUserConfig","params":{}}' | nc -u -w 1 x.x.x.x 38899  ; echo
{"method":"getUserConfig","env":"pro","result":{"fadeIn":700,"fadeOut":700,"dftDim":100,"opMode":0,"po":false,"minDimming":0,"tapSensor":1}}

Edit: One can trigger an OTA firmware update attempt by publishing something like {"method":"updateOta","params":{"fw":"1.30.1","s1":"477d2106ca3ce6f08bf96b07afc4100d6729eeee8a0820f381d1f8f69a3fe230","s2":"477d2106ca3ce6f08bf96b07afc4100d6729eeee8a0820f381d1f8f69a3fe230"}} to the MQTT topic. The firmware downloads themselves are via plain http, so you can simply use nc to grab what the bulb sends:

~$ sudo nc -l 80
GET /firmwares/ESP24_SHRGBW_01/1.30.1/wizlight.bin?mac=redacted&fw=1.28.0 HTTP/1.1
User-Agent: WiZ
Host: firmware.wiz.world
Content-Length: 0


I was also able to download http://firmware.wiz.world/firmwares/ESP24_SHRGBW_01/1.28.0/wizlight.bin , which is hopefully the firmware that is currently present in the bulb. Presumably the long hexadecimal string in the updateOta method is some kind of hash or something that is used to verify the download.

Edit: These are standard ESP32 images and they may be examined via esptool.py --chip esp32-c3 image_info -v 2 wizlight.bin

Edit: I changed the favourites by sending this via MQTT: '{"method":"setFavs","params":{"favs":[[12,0,0,0,0,0,0],[11,0,0,0,0,0,0],[6,0,0,0,0,0,0],[2,0,0,0,0,0,0]],"opts":[{},{},{},{}],"favConfigTs":2}}'. Once again, note the favConfigTs, which needs to be incremented. With WiZclick, the first favourite is the power on configuration, and the second favourite is what you get when you turn it off for a few seconds to change it. These are scene numbers, as documented at https://docs.pro.wizconnected.com/#light-modes . I thought the default warm white is ugly and I don't want the bulb to turn on in that mode, so this exchanged the first two favourites, making daylight the turn on state.

The ESP32-C3 contains a RISC-V core, and the firmware can be disassembled via Ghidra. But the parser seems complicated, and use of favourites to change the turn on state was only a guess.

Edit: Ghidra now works really well for this. I had two problems to fix. The file offsets printed by esptool are actually offsets of the header for that segment. The actual data for the segment starts 8 bytes after those offsets. Also, the ESP32-C3 chip is RISC-V RV32IMC. The parsing code is now fairly easy to understand.

Edit: https://raw.githubusercontent.com/owntracks/tools/master/TLS/generate-CA.sh is a script to easily generate self signed certificates for mosquitto MQTT server.

The favourites JSON arrays are sceneId, red, green, blue, warm white, cool white, colour temperature. For example: red PWM 255: [0,255,0,0,0,0,0] and colour temperature 3000K [0,0,0,0,0,0,3000]. You can also do options dim, spd and ratio. For example, sending this makes the 2nd favourite 3000K, dimmed to 50%: '{"method":"setFavs","params":{"favs":[[12,0,0,0,0,0,0],[0,0,0,0,0,0,3000],[6,0,0,0,0,0,0],[2,0,0,0,0,0,0]],"opts":[{},{"dim":50},{},{}],"favConfigTs":8}}' . This is all via MQTT. Still no progress on using it via UDP.

All the get methods work via UDP, though getSensor is weird, needing a pos parameter with weird values, like:

~$ echo '{"method":"getSensor","params":{"pos":11}}' | nc -u -w 1 x.x.x.x 38899  ; echo
{"method":"getSensor","env":"pro","result":{"id":0,"pos":0,"time":[0,0,0],"days":0,"delay":[0,0],"motion":[0,0,"0000000000",0,0],"noMotion":[0,0,"0000000000",0,0],"lux":0,"confTs":0}}

Few of the set methods seem available via UDP. Each method in an array in DROM consists of a pointer to a name string, a pointer to the function that handles it, and 4 bytes, with the first two used for flags. I think if the second flag byte is zero the method is unavailable via UDP.

Via UDP you can use setState like {"method":"setState","params":{"r":255}} but that is a lot like setPilot.

The most interesting one is setEffect as in {"method":"setEffect","params":{"preview":{"state":1,"duration":10,"cct":4000}}} That shows the 4000K state for 10 seconds, and then reverts to the previous state.. You can also use pulse instead of preview to pulse for the specified duration. It also accepts an r, g, b, cool white, warm white PWM array as "color":[0,0,0,0,255]. You can also use something like "sceneID":33,"speed":20.

@s0ullight
Copy link

The newest firmware for both the GU10's and G.E27's branded as Philips Wiz is 1.30.1

I've attempted to point them to my own mqtt broker without success, but that might've been me doing something wrong.

The firmware dumps on device seem encrypted, but that doesn't necessarily mean that the OTA updates are as well. Did you check the OTA update wether or not it is encrypted? We might be able to trigger the update, pointing to a tasmota firmware image for example.

@dreamlayers
Copy link

Yes, their MQTT server was telling my bulb to download firmware 1.30.1. That is at http://firmware.wiz.world/firmwares/ESP24_SHRGBW_01/1.30.1/wizlight.bin and there is also http://firmware.wiz.world/firmwares/ESP24_SHRGBW_01/1.28.0/wizlight.bin . When the bulb makes the request, it appends its mac address and current firmware to it, like ?mac=redacted&fw=1.28.0.

At sbidy/wiz_light#85 (comment) someone found that firmware 1.23 for their bulb stopped such insecure MQTT access. Maybe later firmware for my bulb would also, but I did not allow it to upgrade firmware.

The OTA updates are certainly not encrypted, and can be easily disassembled via Ghidra. The file format is documented at https://docs.espressif.com/projects/esptool/en/latest/esp32/advanced-topics/firmware-image-format.html . Note that the file consists of multiple segments that are loaded to different locations in memory. I used this simple Python script to transform the segment data output by esptool.py into dd commands for splitting the file:

import sys
for l in sys.stdin:
    s = l.split()
    print('dd if=wizlight-1.28.0.bin bs=1 count=' + str(int(s[1],16)) + \
          ' skip=' + str(int(s[3],16) + 0x8) + \
          ' of=' + s[2] + '_' + '_'.join(s[4:]))

Then I manually loaded the segments, using "Add to program" to load additional ones after the first one.
https://github.com/tenable/esp32_image_parser should transform ESP32 flash dumps to ELF files which can be easily loaded into a disassembler, but it didn't work for me with the OTA file.

I thought about loading Tasmota, but was still enjoying reverse exploring their firmware, and I don't want to brick the bulb because recovery may require physically breaking it to open it. Tasmota does support the ESP32-C3FH4 chip in the ESP32-C3-WIZ2012 module that is inside according to FCC ID info. The Tasmota release at https://ota.tasmota.com/tasmota32/tasmota32c3.bin is the same file format as the OTA file from Wiz.

I don't know if the bulb makes some checks when installing firmware that would block Tasmota. The {"method":"updateOta","params":{"fw":"1.30.1","s1":"477d2106ca3ce6f08bf96b07afc4100d6729eeee8a0820f381d1f8f69a3fe230","s2":"477d2106ca3ce6f08bf96b07afc4100d6729eeee8a0820f381d1f8f69a3fe230"}} sent from their MQTT server seems to be sending some signature or hash there that the bulb may use to verify the firmware, but I don't know if my firmware verifies that. In any case, with insecure MQTT, it may be possible to send the right codes there to load other firmware. I still don't know what those numbers are. They're not SHA256 of the file or the SHA256 output by esptool.py.

@s0ullight
Copy link

s0ullight commented Dec 29, 2023

Meanwhile I've managed to get my Wiz devices to connect to my mosquitto instance.
I have 2 GU10's and 1 G.E27, they're all on firmware version 1.30.1

Steps to reproduce:
On my DNS server I assigned the mqtt domain (in my case: eu.mqtt.wiz.world) to my mosquitto instance.
I configured mosquitto with the per_listener_settings since I already had a listener on port 1883 with authentication.
I added a second listener on port 8883. For this one I set up a CA, cert and key. Finally I disabled authentication.

The devices connect as <macAddress>_<homeId>
They subscribe to OP/pro/<homeId>/devices/<macAddress>
They publish to DEV/pro/<macAddress>

I found out the first identifier is the homeId by publishing {"method":"getSystemConfig","params":{}} to the topic it listens on. The mac address explains itself.

@dreamlayers thank you for bringing this back to my attention. I might seem like I'm repeating you, but just wanted to write down my findings for future reference. I have a spare GU10 with the encrypted firmware dumped. I will experiment with that one to see if I can trick it into installing tasmota.

@dreamlayers
Copy link

dreamlayers commented Jan 20, 2024

@s0ullight any luck flashing Tasmota? I'm not sure that would succeed even if the original firmware permitted it. The first installation of Tasmota involves flashing the bootloader and partition table. That is normally done by flashing the appropriate *.factory.bin via esptool over serial. If only the application partition is flashed, that might not work with the bootloader and partition table from original firmware.

Edit: I was able to flash the 1.30.1 WiZ firmware for my bulb locally, using BIND 9, Mosquitto and sudo python3 -m http.server 80. The bulb did not reboot and a power cycle was needed to see getSystemConfig report the new firmware. It still connects to my Mosquitto like before. I was hoping the upgrade would fix the discontinuity between 4200K and 4201K. but no change there.

@dreamlayers
Copy link

I bought another bulb. This one came with firmware 1.22.59. The WiZ MQTT server wanted it to upgrade to 1.30.1, using the same {"method":"updateOta","params":{"fw":"1.30.1","s1":"477d2106ca3ce6f08bf96b07afc4100d6729eeee8a0820f381d1f8f69a3fe230","s2":"477d2106ca3ce6f08bf96b07afc4100d6729eeee8a0820f381d1f8f69a3fe230"}} MQTT message mentioned earlier. Later I sent that message, but on my web server /firmwares/ESP24_SHRGBW_01/1.30.1/wizlight.bin was actually firmware version 1.28.0. The bulb successfully installed that firmware. So, the s1 and s2 values in the MQTT message are either not specific to one firmware version or not checked at all.

BTW. The old style Android WiZ app couldn't talk to firmware 1.22.59 using Bluetooth, and had to use the manual configuration method, via the bulb's unsecured WiFi network. 1.22.59 also didn't have the white temperature discontinuity between 4200 K and 4201 K. It seems to still be available at http://firmware.wiz.world/firmwares/ESP24_SHRGBW_01/1.22.59/wizlight.bin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

5 participants