feat: mr_go_cube blog
This commit is contained in:
401
mr_go_led_cube/README.md
Normal file
401
mr_go_led_cube/README.md
Normal file
@@ -0,0 +1,401 @@
|
||||
# Mr. Go with ESP reverse engineering
|
||||
|
||||
We received a bunch of IR-controlled LED cubes which had been ... touched:
|
||||
|
||||

|
||||
|
||||
An ESP8266 board had been wired into the on-board IR receiver; said
|
||||
receiver was hooked up to U1, an unlabeled PWM driver. The thinking
|
||||
probably was that if the board can be remotely controlled by some IR
|
||||
protocol, it can be remotely controlled by a bitbanged simulacra of
|
||||
the same.
|
||||
|
||||
## Reverse engineering, with a dump
|
||||
I didn't have a remote that can drive the unlabeled infrared-directed
|
||||
LED PWM controller U1, so I tried the next best thing: dumping the
|
||||
firmware of the ESP8266 board attached to thea IR sensor's output
|
||||
pins.
|
||||
|
||||
## `esptool` is espcool
|
||||
|
||||
https://cyberblogspot.com/how-to-save-and-restore-esp8266-and-esp32-firmware/
|
||||
|
||||
[`esptool`](https://docs.espressif.com/projects/esptool/en/latest/esp32/esptool/flasher-stub.html)
|
||||
allows you to manipulate Espressif ESP boards connected over serial;
|
||||
as far as I can tell, it resets the board, and chats with the ESP
|
||||
BootROM to copy and execute a utility stub with which the host-side
|
||||
`esptool` can interact. In this case I gathered details about the
|
||||
board using the `flash_id` command:
|
||||
|
||||
```console
|
||||
mkennedy@jafar:~/blog$ esptool flash_id
|
||||
esptool.py v4.7.0
|
||||
Found 33 serial ports
|
||||
Serial port /dev/ttyUSB0
|
||||
Connecting....
|
||||
Detecting chip type... Unsupported detection protocol, switching and trying again...
|
||||
Connecting....
|
||||
Detecting chip type... ESP8266
|
||||
Chip is ESP8266EX
|
||||
Features: WiFi
|
||||
Crystal is 26MHz
|
||||
MAC: dc:4f:22:10:94:00
|
||||
Uploading stub...
|
||||
Running stub...
|
||||
Stub running...
|
||||
Manufacturer: 20
|
||||
Device: 4016
|
||||
Detected flash size: 4MB
|
||||
Hard resetting via RTS pin...
|
||||
```
|
||||
|
||||
Knowing my ESP8266 had 4MB (0x400000 B) of integrated flash, I dumped it as such:
|
||||
```sh
|
||||
esptool --baud 115200 --port /dev/ttyUSB0 read_flash 0x0 0x400000 fw-backup-4M.bin
|
||||
```
|
||||
|
||||
And, now `strings`! I saw `tasmota` in the output, and got excited,
|
||||
before realizing that, oops, this is one I'd already flashed with
|
||||
Tasmota:
|
||||
|
||||
```console
|
||||
mkennedy@jafar:~/blog/mr_go_led_cube$ strings fw-backup-4M.bin
|
||||
:
|
||||
Laboratory B # our SSID
|
||||
<our WiFi password>
|
||||
:
|
||||
tasmota_%06X
|
||||
:
|
||||
```
|
||||
|
||||
I switch for another cube and try the same procedure again.
|
||||
|
||||
## Dumping again
|
||||
Most serial adapters are capable of higher baud rates -- I tested 921600 baud to speed up the second dump:
|
||||
|
||||
```console
|
||||
mkennedy@jafar:~/blog/mr_go_led_cube$ esptool --baud 921600 --port /dev/ttyUSB0 read_flash 0x0 0x400000 fw-backup-4M.bin
|
||||
esptool.py v4.7.0
|
||||
:
|
||||
Changing baud rate to 921600
|
||||
Changed.
|
||||
4194304 (100 %)
|
||||
4194304 (100 %)
|
||||
Read 4194304 bytes at 0x00000000 in 51.9 seconds (646.0 kbit/s)...
|
||||
Hard resetting via RTS pin...
|
||||
```
|
||||
|
||||
Success! I performed this twice and took a diff, and the two dumps were identical, so we're in business.
|
||||
|
||||
## `strings` ...
|
||||
|
||||
I ran `strings` on the new firmware dump, and found a whole Charlie board worth of crumb trails ...
|
||||
|
||||
IMG: Charlie red string meme, with impact font. Reuse a quote, replacing "Pepe Silvia" with "`PETdemo`"
|
||||
|
||||
```console
|
||||
ap": {
|
||||
"enable": true,
|
||||
"ssid": "cube-00",
|
||||
"pass": "cubesAPpass",
|
||||
"channel": 6,
|
||||
"max_connections": 10,
|
||||
"ip": "192.168.4.1",
|
||||
"netmask": "255.255.255.0",
|
||||
"gw": "192.168.4.1",
|
||||
"dhcp_start": "192.168.4.2",
|
||||
"dhcp_end": "192.168
|
||||
.4.100",
|
||||
"trigger_on_gpio": -1,
|
||||
"keep_enabled": true
|
||||
},
|
||||
"sta": {
|
||||
"enable": true,
|
||||
"ssid": "PETdemo",
|
||||
"pass": "packetnrg"
|
||||
},
|
||||
```
|
||||
|
||||
This seems relevant. On my laptop, I scanned for networks, and connected to the SSID `cube-16` (matching the paper label on this board), and the password `cubesAPpass` got me in.
|
||||
|
||||
From here, I opened Firefox to 192.168.4.1, and:
|
||||
|
||||
IMG: 2024-09-07T13:56:32-04:00 screenshot of webpage
|
||||
|
||||
## Networking
|
||||
|
||||
Using the board as an AP to my workstation's STA makes internet research harder, so let's set up an AP VIF with SSID `PETdemo` nearby.
|
||||
|
||||
Once the AP VIF was online on channel 6, I rebooted the board, and in a moment:
|
||||
|
||||
```console
|
||||
root@iakob:~# iw dev
|
||||
:
|
||||
Interface cube2
|
||||
ifindex 17
|
||||
wdev 0x5
|
||||
addr 16:91:82:95:26:a2
|
||||
ssid PETdemo
|
||||
type AP
|
||||
channel 6 (2437 MHz), width: 20 MHz, center1: 2437 MHz
|
||||
txpower 30.00 dBm
|
||||
multicast TXQ:
|
||||
qsz-byt qsz-pkt flows drops marks overlmt hashcol tx-bytes tx-packets
|
||||
0 0 94 0 0 0 0 10796 94
|
||||
:
|
||||
```
|
||||
|
||||
And our DHCP server has it!
|
||||
|
||||
```console
|
||||
root@gandalf:~# logread
|
||||
:
|
||||
Sat Sep 7 14:00:49 2024 daemon.info dnsmasq-dhcp[25723]: DHCPDISCOVER(br-lan) dc:4f:22:10:8d:70
|
||||
Sat Sep 7 14:00:49 2024 daemon.info dnsmasq-dhcp[25723]: DHCPOFFER(br-lan) 10.0.7.152 dc:4f:22:10:8d:70
|
||||
Sat Sep 7 14:00:49 2024 daemon.info dnsmasq-dhcp[25723]: DHCPREQUEST(br-lan) 10.0.7.152 dc:4f:22:10:8d:70
|
||||
Sat Sep 7 14:00:49 2024 daemon.info dnsmasq-dhcp[25723]: DHCPACK(br-lan) 10.0.7.152 dc:4f:22:10:8d:70 cube-16
|
||||
:
|
||||
```
|
||||
|
||||
Not sure what I was expecting to change:
|
||||
|
||||
IMG: 2024-09-07T14:03:46-04:00 screenshot. It shows the webpage has not changed.
|
||||
|
||||
`nmap` agrees, there's not much server-side going on here:
|
||||
|
||||
```console
|
||||
mkennedy@jafar:~$ nmap -Pn 10.0.7.152/32 -vvvv
|
||||
:
|
||||
PORT STATE SERVICE REASON
|
||||
80/tcp open http syn-ack
|
||||
:
|
||||
```
|
||||
|
||||
OK, so what *is* it doing?
|
||||
|
||||
```console
|
||||
root@iakob:~# tcpdump -i br-lan ether host dc:4f:22:10:8d:70
|
||||
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
|
||||
listening on br-lan, link-type EN10MB (Ethernet), capture size 262144 bytes
|
||||
14:06:51.662194 IP cube-16.lan.laboratoryb.org.45368 > home.laboratoryb.org.53: 43776+ A? amcqcgzs4o4el.iot.us-east-1.amazonaws.com. (59)
|
||||
14:06:51.684355 IP home.laboratoryb.org.53 > cube-16.lan.laboratoryb.org.45368: 43776 NXDomain 0/1/0 (136)
|
||||
14:06:56.732683 ARP, Request who-has cube-16.lan.laboratoryb.org tell home.laboratoryb.org, length 46
|
||||
14:06:56.740107 ARP, Reply cube-16.lan.laboratoryb.org is-at dc:4f:22:10:8d:70 (oui Unknown), length 46
|
||||
# unplug, re-plug
|
||||
14:07:39.563487 dc:4f:22:10:8d:70 (oui Unknown) > Broadcast Null Unnumbered, xid, Flags [Response], length 6: 01 00
|
||||
14:07:39.568304 IP 0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from dc:4f:22:10:8d:70 (oui Unknown), length 308
|
||||
14:07:39.569479 IP home.laboratoryb.org.67 > cube-16.lan.laboratoryb.org.68: BOOTP/DHCP, Reply, length 313
|
||||
14:07:39.606896 IP 0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from dc:4f:22:10:8d:70 (oui Unknown), length 308
|
||||
14:07:39.611395 IP home.laboratoryb.org.67 > cube-16.lan.laboratoryb.org.68: BOOTP/DHCP, Reply, length 313
|
||||
14:07:39.614915 ARP, Request who-has cube-16.lan.laboratoryb.org tell 0.0.0.0, length 28
|
||||
14:07:39.941547 ARP, Request who-has cube-16.lan.laboratoryb.org tell 0.0.0.0, length 28
|
||||
14:07:40.442128 ARP, Request who-has cube-16.lan.laboratoryb.org tell cube-16.lan.laboratoryb.org, length 28
|
||||
14:07:40.522697 ARP, Request who-has home.laboratoryb.org tell cube-16.lan.laboratoryb.org, length 28
|
||||
14:07:40.522944 ARP, Reply home.laboratoryb.org is-at b4:75:0e:69:0a:22 (oui Unknown), length 46
|
||||
14:07:40.526392 IP cube-16.lan.laboratoryb.org.45358 > home.laboratoryb.org.53: 41216+ A? amcqcgzs4o4el.iot.us-east-1.amazonaws.com. (59)
|
||||
14:07:40.526954 IP home.laboratoryb.org.53 > cube-16.lan.laboratoryb.org.45358: 41216 NXDomain 0/0/0 (59)
|
||||
14:07:40.558685 IP cube-16.lan.laboratoryb.org > all-systems.mcast.net: igmp v2 report all-systems.mcast.net
|
||||
14:07:42.492738 IP cube-16.lan.laboratoryb.org.45359 > home.laboratoryb.org.53: 41472+ A? amcqcgzs4o4el.iot.us-east-1.amazonaws.com. (59)
|
||||
14:07:42.494259 IP home.laboratoryb.org.53 > cube-16.lan.laboratoryb.org.45359: 41472 NXDomain 0/0/0 (59)
|
||||
```
|
||||
|
||||
... the minimalism appears to be a theme, and should not be surprising.
|
||||
|
||||
So, we're not going to get a backdoor in the existing firmware -- nor, honestly, is that a reasonable goal. Instead, let's focus on the IR LED controller library being used, if any.
|
||||
|
||||
## Mongoose OS - maybe our library is packaged?
|
||||
Strings on the webpage, and in the dump, indicate the use of an OS build base called Mongoose OS:
|
||||
|
||||
IMG: 2024-09-07T14:15:22-04:00 screenshot of a 502, womp womp, mongoose' community forum is gonezo
|
||||
|
||||
... oh, that Mongoose OS. Pepe Silvia is reduced to an echo.
|
||||
|
||||
|
||||
However, the Docker image *this* was built on is still available:
|
||||
|
||||
```console
|
||||
mkennedy@jafar:~/blog/mr_go_led_cube$ strings fw-backup-4M.bin | less -SR
|
||||
:
|
||||
, "build_image": "docker.io/mgos/esp8266-build:2.2.1-1.5.0-r4"
|
||||
:
|
||||
mkennedy@jafar:~$ podman image pull mgos/esp8266-build:2.2.1-1.5.0-r4
|
||||
✔ docker.io/mgos/esp8266-build:2.2.1-1.5.0-r4
|
||||
Trying to pull docker.io/mgos/esp8266-build:2.2.1-1.5.0-r4...
|
||||
Getting image source signatures
|
||||
Copying blob b2c3620e896f done |
|
||||
:
|
||||
Copying config 1a393c77ce done |
|
||||
Writing manifest to image destination
|
||||
1a393c77ce99780a85765b0c456583e35017cd71751c518872d6c63441c043ff
|
||||
```
|
||||
|
||||
Neato! What's in it?
|
||||
|
||||
```console
|
||||
mkennedy@jafar:~$ podman run -it 1a393c77ce99 /bin/bash
|
||||
root@b98bb413dd9e:/# grep -r /opt -ie infrared
|
||||
root@b98bb413dd9e:/#
|
||||
```
|
||||
|
||||
Sigh.
|
||||
|
||||
So, we're not going to find the library by building from source. But I have the dump, and I've connected the board to my network with details from it -- maybe there's other hints in the dump!
|
||||
|
||||
Remember: I *only* need a reference implementation of the signaling used by the LED PWN driver IC. I don't actually need the code -- I can write that.
|
||||
|
||||
## RPC: Really Patient cURL
|
||||
|
||||
I noted there was config previously
|
||||
|
||||
todo: grab `strings` output showing /rpc enabled.
|
||||
|
||||
I've never been so happy to be told to buzz off by an LED cube:
|
||||
|
||||
```console
|
||||
mkennedy@jafar:~$ # Q: Knock Knock?
|
||||
mkennedy@jafar:~$ curl 'http://10.0.7.152/rpc'
|
||||
Invalid request
|
||||
mkennedy@jafar:~$ # A: Go Away!
|
||||
```
|
||||
|
||||
So, how about other requests?
|
||||
|
||||
```console
|
||||
mkennedy@jafar:~$ curl --header "Content-Type: application/json" 'http://10.0.7.152/rpc/status-v1'
|
||||
No handler for status-v1
|
||||
```
|
||||
|
||||
Well, now! `No handler for %.*s` is straight out of the `strings` output, so I'm onto something here.
|
||||
|
||||
I ended up chucking a bunch of strings out of the dump into this `curl`, until I reach the successful `Cube.TurnOn`:
|
||||
|
||||
|
||||
```sh
|
||||
curl --header "Content-Type: application/json" -vvv 'http://10.0.7.152/rpc/Cube.TurnOn'
|
||||
```
|
||||
|
||||
... at which point I am blinded by the light, which is sitting about 20cm from my face. This is quickly followed by `Cube.TurnOff`.
|
||||
|
||||
## The other RPC endpoints
|
||||
|
||||
- Cube.SetColor
|
||||
- Cube.TurnOn
|
||||
- Cube.TurnOff
|
||||
- Cube.ListColors
|
||||
|
||||
I immediately try Cube.ListColors, which produces *nothing*. However, some attempts later,
|
||||
|
||||
```sh
|
||||
curl --header "Content-Type: application/json" 'http://10.0.7.152/rpc/Cube.SetColor' --data '{"color":"CUBE_BLUE"}'
|
||||
```
|
||||
|
||||
... turns the LEDs blue - and among the others mentioned in the dump (`CUBE_{RED,GREEN,BLUE,WHITE,ORG_RED,LT_GREEN,LT_BLUE,FLASH,ORANGE,SKY_BLUE,DK_PURPLE,STROBE,YEL_ORANG,AQUA,PURPLE,FADE,YELLOW,TEAL,PINK,SMOOTH}`), the few I tested follow without issue.
|
||||
|
||||
## Scoping things out
|
||||
|
||||
Now that I have a reference implementation, I can figure out what the ESP8266 sends to our unabeled U1.
|
||||
|
||||
I initially assumed this was a serial protocol, but got nowhere decoding it as such using our Rigol MSO5074's Decode function.
|
||||
|
||||
IMG: 2024-09-07T15:56:46-04:00: Rigol view. Clearly, we have some odd non-serial protocol, because we have these long-haul "0x00"s, and other than those, all "low" pulses are only one pulse long (never more).
|
||||
|
||||
I worked out a "baud rate" of 1800, based on a ~555us for the short pulses
|
||||
|
||||
IMG: 2024-09-07T15:58:53-04:00: odd pulse length of 555us.
|
||||
|
||||
... and after some Google-fu, found this [EEVblog post](https://www.eevblog.com/forum/microcontrollers/asynchronous-serial-on-attiny85/), where forums users quickly identified the same protocol as the NEC protocol!
|
||||
|
||||
# WHAT?!
|
||||
|
||||
I swore I tried NEC! What did I mess up?!
|
||||
|
||||
[Let's look at an article](https://www.sbprojects.net/knowledge/ir/nec.php).
|
||||
|
||||
Perhaps the issue was not knowing *what* to send through NEC?
|
||||
|
||||
N: I attempted to decode what I had on the screen, and generated:
|
||||
|
||||
```console
|
||||
irsend { "Protocol": "NEC", "Bits": 32, "Data": 133710334}
|
||||
```
|
||||
|
||||
... which resulted in the following view:
|
||||
|
||||
IMG: 2024-09-07T16:36:27-04:00: Osc showing extra nonsense.
|
||||
|
||||
Looks like Tasmota is eager to also generate the 38kHz carrier frequency for the NEC protocol. Ugh.
|
||||
|
||||
## Sending it, raw!
|
||||
|
||||
|
||||
|
||||
N: Since I need a zero carrier-frequency ("always on"), I tried an adjustment of the example [they posted](https://tasmota.github.io/docs/IRSend-RAW-Encoding/#examples-for-bitstream-command-syntax):
|
||||
|
||||
```console
|
||||
IRSend raw,1,8620,4260,544,411,1496,010101101000111011001110000000001100110000000001100000000000000010001100
|
||||
```
|
||||
|
||||
... they break down this example into:
|
||||
|
||||
- `1`: carrier frequeny
|
||||
- `8620`: header mark
|
||||
- `4260`: header space
|
||||
- `544`: bit mark
|
||||
- `411`: zero space
|
||||
- `1496`: one space
|
||||
|
||||
So, going back to [that article](https://www.sbprojects.net/knowledge/ir/nec.php) ...
|
||||
|
||||
I'd reckon:
|
||||
|
||||
- Keep the carrier frequency of 1 (we don't modulate it here),
|
||||
- Header mark: 9000
|
||||
- header space: 4500
|
||||
- bit mark: 563
|
||||
- zero space: 562
|
||||
- one space: 1687
|
||||
|
||||
So, that's
|
||||
|
||||
```console
|
||||
IRsend raw,1,9000,4500,563,562,1687,11111111000010000011111111000000
|
||||
```
|
||||
|
||||
Unfortunately, U1 did not respond to this stream. I can think of a few reasons:
|
||||
|
||||
- The timing was a bit odd (a bit mark and zero-space would take 500ms and 600ms, respectively -- could be a misconfiguration of the timer in the PWM setup, or could be simply poorly timed sender code?)
|
||||
- Because the D1 pin of the ESP stays at logic low by default, the NEC frame's header was not correctly sent (the start of the header mark wasn't visible). I could not see a way in Tasmota-IR to change the default level here.
|
||||
|
||||
After a bit more tracing with my probes, I confirmed that all U1 was doing was generating PWM to drive a set of four SOT-23 transistors (Q3-Q6) through some resistors; the through-hole pads in the center of the board were connected directly to these same lines (with a convenient GND pin at position 5). So, let's do this another way.
|
||||
|
||||
Goodbye, U1!
|
||||
|
||||
## `U_1` is `U_Gone`. Enter new problems.
|
||||
|
||||
I snagged a WeMoS D1 mini -- also an ESP8266 board, but with a more amenable profile -- and flashed it with WLED. I soldered a female connector onto the RGBW channel pins, plus ground, and connected the D1 Mini's D{1,2,3,4} and G pins to it.
|
||||
|
||||
IMG: https://i0.wp.com/randomnerdtutorials.com/wp-content/uploads/2019/05/ESP8266-WeMos-D1-Mini-pinout-gpio-pin.png?w=715&quality=100&strip=all&ssl=1
|
||||
|
||||
I found out after some play that the ESP8266 must be able to pull its GPIO4 (the D2 pin) high during boot, and anything pulling it lower will cause a boot loop; the speed of said boot loop had the LEDs shining bright green.
|
||||
|
||||
I swapped from D[1234] to [D8765], ran a purpose-fit ground wire, and went to configuring.
|
||||
|
||||
## Y U Reboot?
|
||||
|
||||
While playing with WLED, the board would occasionally reset (wtf?). I chalked this up to the voltage regulator browning out, but crashes only ever occurred while moving sliders or colorwheel positions, and the voltage spread during brightest operation was not very wide (from ~4.6?V to 5.1V):
|
||||
|
||||
IMG: 2024-09-08T13:34:32-04:00: Photo of oscilloscope, showing the wide voltage variation
|
||||
|
||||
I probed the 3.3V pin on the board, and nailed the coffin shut on that theory.
|
||||
|
||||
I did some further research, and it seems that [WLED has issues with crashing when the WebUI is being used](https://github.com/Aircoookie/WLED/issues/3609). Since the board does boot right back up after this, and since I wasn't about to stop using WLED, I shrugged and moved on.
|
||||
|
||||
## Packaging and wrapping up
|
||||
|
||||
I reused a sacrificed USB cable to supply 5V power:
|
||||
|
||||
IMG: /home/mkennedy/Sync/Camera5/Camera/PXL_20240908_183148185.jpg
|
||||
|
||||
... and screwed it all back together:
|
||||
|
||||
GIF: 2024-09-08T15:28:43-04:00: Cube in heartbeat mode
|
||||
BIN
mr_go_led_cube/fw-backup-4M.bin
Normal file
BIN
mr_go_led_cube/fw-backup-4M.bin
Normal file
Binary file not shown.
BIN
mr_go_led_cube/img/PXL_20240907_172104853.MP.jpg
Normal file
BIN
mr_go_led_cube/img/PXL_20240907_172104853.MP.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 MiB |
BIN
mr_go_led_cube/img/charlie_dying_to_talk_about_the_dump.jpg
Normal file
BIN
mr_go_led_cube/img/charlie_dying_to_talk_about_the_dump.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
Reference in New Issue
Block a user