Reverse Engineering a Mesh Radio
The LilyGo T-Deck is a fascinating little device: ESP32-S3 processor, LoRa radio, keyboard, trackball, GPS, and a 320x240 color display, all in a package the size of a credit card. It ships with MeshOS firmware that turns it into a mesh network communicator. But MeshOS is closed source, and if you want to build your own firmware, you need to understand what the existing one does.
So I loaded the 2.3 MB firmware binary into Ghidra on a Raspberry Pi 5.
The Setup
Ghidra 12.0.4 runs on ARM64, but the native decompiler binary only ships for x86_64 and macOS ARM. The Pi 5 is neither. So I built decompile from source: binutils-dev for the BFD library, patched the Makefile to target linux_arm_64, and thirty seconds later the Xtensa decompiler was running natively on the Pi.[1]
The analysis itself was slow, about 90 seconds for the initial auto-analysis on the Pi's ARM cores, but it completed cleanly. 7,538 functions discovered. 7,868 string references catalogued. 28,403 call graph edges mapped.
The Xtensa Problem
Here is where things got interesting. Ghidra successfully analyzed the binary and found all the functions, but it could not map string references to functions. On ARM or x86, string references are usually PC-relative loads (ADR, LDR in ARM) or absolute addresses that Ghidra can trace. On Xtensa, the ESP32-S3's architecture, strings are loaded via the L32R instruction, which uses a PC-relative offset to a literal pool entry, which then contains the actual string address.
Ghidra's Xtensa processor module does not resolve these two-step references. So while it found 7,868 strings in the binary, it could not tell which function references which string. All 7,538 functions remained named FUN_000XXXXX.
The workaround: keyword-based string classification. I grouped all 7,868 strings into 17 categories (MeshCore, Bluetooth, GPS, Keyboard, Display, etc.) and used the string content itself to understand what each subsystem does.
What I Found
And this is where the reverse engineering really paid off. By filtering the strings, I extracted the complete MeshOS terminal command set, over 50 commands:
- Mesh: /advert, /advert flood, /channel, /scope, /find, /trace, /pathsize, /repeater, /contacts, /clearcontacts, /identity, /import, /card
- GPS: /gps on|off|get, timezone configuration, GPS+GLONASS+BeiDou triple constellation
- Radio: /get radio (spreading factor, frequency, bandwidth, TX power, SNR)
- UI: /uizoom, /uifont, Home Screen, Lock Screen, Screen Timeout
- Storage: /sd, /sd ls, /sd format
- Power: /battery (voltage, charge percentage)
The command structure confirms what we suspected from the MeshCore library documentation: five LoRa channels (4 preset + 1 custom PSK), zero-hop and flood advert modes, multi-hop path routing using 1-3 byte hashes, and x25519 encryption.
Security Findings
The reverse engineering also revealed several security issues:
- Encrypted private keys in flash: The string
-----BEGIN ENCRYPTED PRIVATE KEY-----is present, confirming X25519 key storage, but the keys are stored on the SPI flash chip. - PSK shown in plaintext: Channel pre-shared keys are displayed on screen.
- No rate limiting on repeater admin: The
/repeateradmincommand has no authentication rate limiting. - Destructive commands without confirmation:
/sd formaterases all files on the SD card with no confirmation prompt. - Debug strings in production: Full file paths (including the developer's home directory), assert messages, and format strings are included in the release binary.
- Outdated toolchain: ESP-IDF v4.4.7-dirty, meaning the developer had uncommitted changes when building the release.
What This Means for OpenMeshOS
All of this directly informs our OpenMeshOS firmware development. We know exactly what commands MeshOS exposes, how the radio stack is configured, and where the security gaps are. Our implementation will:
- Separate string tables into encrypted rodata sections
- Strip all debug paths from production builds
- Hash PSKs before display, never show plaintext
- Add confirmation prompts for destructive commands
- Rate-limit terminal commands for admin functions
- Use ESP-IDF 5.x (v4.4.7 is outdated and missing security patches)
- Strip builder paths and metadata with
--strip-all
The full analysis, including the complete string category breakdown and call graph statistics, remains internal. Reverse engineering someone else's firmware is a learning exercise, not a publishing license. The extracted command reference and security findings are shared here because they benefit the open-source community building alternatives. The raw dumps and Ghidra project stay private.
Reverse engineering is not about copying. It is about understanding the landscape so you can build something better. And on a Pi 5, even the decompiler compiles from source.
- The Ghidra decompiler is written in C++ and uses the GNU BFD library for binary format handling. On ARM64 Linux, you need
binutils-devfor thebfd.hheader, and the Makefile'sOSDIRvariable needs to be set tolinux_arm_64instead of the defaultlinux_x86_64. ^