feat: mr_go_cube blog

This commit is contained in:
2025-11-17 20:48:35 -05:00
parent 7ca10445db
commit e0a1faf91e
4 changed files with 401 additions and 0 deletions

401
mr_go_led_cube/README.md Normal file
View File

@@ -0,0 +1,401 @@
# Mr. Go with ESP reverse engineering
We received a bunch of IR-controlled LED cubes which had been ... touched:
![The best part is the glob of *stuff* keeping the wires on](PXL_20240907_172104853.MP.jpg)
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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB