An ESP32 Start to Finish, Serial to Flash
Nearly everything written up about cracking open a cheap gadget points you at embedded Linux- the little routers, the IP cameras, the NAS boxes. Good advice, honestly.. it is the easier place to start. But what is it, exactly?
So What Is Embedded Linux?
It's the same Linux kernel that runs servers and a good chunk of the internet, trimmed down and squeezed onto a small board that does one job. Your router is a tiny computer. It has a processor, some RAM, a flash chip holding the whole operating system, and a kernel booting up underneath the web page you log into. Crack one open and you'll usually find a bootloader (very often U-Boot), a compressed read-only filesystem (squashfs is the usual suspect), BusyBox standing in for all the everyday commands, and a pile of plain-text config files and shell scripts that make it all go.
That last part is why everyone points beginners there. A Linux device is made of files. Pull the firmware, carve out the filesystem with binwalk, and you can read how the thing works- passwords sitting in config files, startup scripts, the web server's own code. Hook up to the serial port and there's a fair chance it drops you at a root shell. You're poking around a computer you already know how to drive.
...and what it isn't
A microcontroller is a different animal. An ESP32 or an STM32 has no Linux kernel, no filesystem and no shell. It runs one compiled program straight out of flash- either "bare metal" or on top of a small real-time OS like FreeRTOS, which is what Espressif's ESP-IDF builds on. Memory is measured in hundreds of kilobytes, not hundreds of megs (an ESP32 has about 520 KB of on-chip SRAM). Pull the firmware off one of these and you don't get a folder of readable files.. you get one big blob of machine code, and reading it means disassembling it.
For the folks in the back who already know all this- yes, the line isn't perfectly clean. The real divide is the MMU. Linux-class chips (Cortex-A, the MIPS parts in routers) have one and microcontrollers mostly don't. uClinux has run on MMU-less parts for decades, and people have booted Linux on an ESP32-S3 as a stunt. Nobody ships a baby sound machine that way though- in the real world, microcontroller means firmware blob.
Half the interesting gadgets on a bench are running a microcontroller- an ESP Espressif chip or other. There just aint much written about pulling those apart. For me, had I come across a post like this 3- 4 years ago... I'd be well ahead of the learning curve- Sure, plenty is out there- many tear downs on YouTube... but, details are skimmed over- experience assumes the viewer/ reader knows to use A hardware B software C do the chicken dance and Z you got yourself some firmware extracted- the observer is left deer in headlights, only to understand a year or 2 later, many dead microcontrollers later, many monies up in the magic smoke, much time not given back- sure, eventually a lesson is learned- why not mitigate all that nonsense and actually teach the studious student appropriately? For the next time one of them lands in front of us we know where to start, follow through, and end victorious... Where to start?.... The last time I did this dance it was a router (Extracting Firmware from Routers)- this time the target is a little ESP32 baby sound machine that was headed for the trash.
Purpose of this post: get from a sealed device to a firmware.bin sitting on your disk that you can read. On this particular chip that turned out to be wide open.. no encryption, no locked fuses, nothing in the way. Lucky us... Not every chip will be this friendly- but when it is, it really is a handful of commands. No problem, a refresher for the experienced, a repository of commands to come back to- for those new to this world, a must to bookmark, save.
Reading The Silk, Then The Datasheet-
"Silk" being silk screen... - some PCBs are more verbose than others- some go graphics heavy on the creative side- others are a true enigma... First job is knowing what you're even looking at. Flip the board, find the module, read the marking under the RF can. This one said ESP32-WROVER-E. That is one of Espressif's own modules- an ESP32 with onboard flash and some extra PSRAM tucked under the metal shield, and it comes in 4, 8 and 16 megabyte flash flavours. The nice part for us is the package: it's got castellated edges, those little half-circle plated notches down the sides, so every pin is exposed right there on the edge of the board.
Then you go get the datasheet. I know- "elite hacking skills", reading a PDF. But that is where half the time goes on this kind of work, and it's where the whole thing lives or dies. Pull the ESP32-WROVER-E datasheet straight from Espressif and skip past the electrical characteristics to the pin definitions. Two pins matter first: TX and RX, the serial lines. Just like an embedded Linux box, the UART is the front door and it's the first thing that we want to be looking for. Then note where IO0 and IO2 sit- what we are doing is going to require placing this into boot mode/ download mode so to say...sometimes there's a convenient button which does this for us- again; reading the "silk" we are looking for the word "boot"- some times, it can be a sequence of button presses, a convenient button to press, or in this case from the data sheet we've identified IO0 and IO2- those are the two we'll drag to ground later to force the chip into its download mode. Careful poking around- There's also a "keep out zone" marked up top by the antenna- leave that alone.
Getting On The UART
Hook a USB-to-serial adapter to three pads: GND, TX and RX. The one thing people trip on- cross them. Your adapter's TX goes to the board's RX, and RX to TX. If you're staring at silence, you've almost certainly got those straight instead of crossed.
For the physical contact on those castellated edges I like spring-loaded probes, but you don't need fancy gear- you can solder a wire straight to the notch. And when you run out of probes, there's a scrappy trick that works surprisingly well: an acupuncture pin. Thin, pointed, and it flexes, so you can wiggle the tip into a tight spot and lean a little tension on it for a solid connection. Clip it into a magnetic third hand to hold it, then either solder a wire to the metal or just bite onto it with a test clip. I said a test clip... not your mouth...
Power it on and watch the boot log scroll by. There is a lot in there if you slow down and read it, same as you would with a Linux dmesg dump:
- the ESP-IDF version it was built against- Espressif's IoT dev framework- and here it was a slightly dirty fork of it
- the SPI flash read, with the partition offsets and labels laid out- Wi-Fi data, OTA data, RF calibration, the app segments
- the project name (this one literally read as a "rest" build) and a compile date, July 17th 2025.. so, fresh
- the MAC address being read out of the eFuses
- an MQTT block, so we already know it phones home to some server
- and a pile of errors- a touch controller wanting an update, an SD card mount that failed, a little crash dump
Grab a copy of that log and read it offline later. It's a free map of the device before you've done anything clever.
That Little Command Prompt
One thing worth trying before we go near the flash. This is not Linux, there is no shell to drop into- but plenty of these microcontroller builds still leave a small "command parser" sitting on the serial line. A command parser being the bit of firmware that reads whatever you type at it and tries to match it against a list of words the developer put in there... a debug menu, basically, left behind by whoever wrote the thing and never taken out.
Type at this one and it bites back with command of zero length and a parameter error. So something in there is listening. It takes input, too. Ask it help and we get nothing. Ask it ? and nothing again. The words are in there somewhere- we just don't know them yet, and guessing at them all afternoon is no way to spend an afternoon. Leave it. The list of words it wants is sitting inside the firmware we are about to pull off, which is a good reason to do this in the order we're doing it.
Into Boot Mode
The interesting stuff over serial needs the chip in download mode- boot mode, same thing, the names get used interchangeably- and that is a hardware move, not something we can type at it. So, back to the datasheet for IO0 and IO2. To enter download boot we pull IO0 low at the moment of reset... that's the pin a "boot" button sits on when a board is kind enough to have one, and this little device wasn't. IO2 wants to be low as well. A lot of the time IO2 is already sitting at ground and IO0 is the only one we have to force- pull both down anyway to be safe, hold them there, and power-cycle the board.
Did it work? The reset banner tells us. Instead of the usual app boot we get a power-on-reset line, a boot mode of (2), and the words we came for:
waiting for download
Which is the chip having given up on running its own program, waiting for a tool to talk to it instead. Nothing to do at this point but leave it alone- pins where they are, power where it is, hands off the test clip.
esptool, And What The Fuses Say
The tool for all of this is Espressif's own esptool, a Python package. One heads-up before typing it out- you will hear it said out loud as "pip install esp32" in a walkthrough or two, and that is not the package. The real one is:
pip install esptool
That drops three commands on your path: esptool, espefuse and espsecure. Recent esptool (v5 and up) dropped the old .py suffix and moved the sub-commands over to hyphens- both eras are still floating around online, so don't be thrown when the syntax doesn't match. Confused? Rightfully so- it won't hurt you to try either or- let us just clarify what the differences are- so for the older chips it looks like this: esptool.py read_flash that now has been changed to esptool read-flash... no more python extension aka removed the .py also the read_flash is now read-flash... from a "_" to a "-". No harm- no fowl if mixed up, just try the other- a tip to the wise.. when trying out different variations, do yourself a huge favor and write down- physically write the exact syntax you'd / you are trying- if anything like me if you don't, thinking you're the wiser and able to remember- how hard is it to remember that you entered in a - instead of a _... seriously... pen to paper... not even typed but actually written down. The moment you've finished that keystroke, entering the version of the command you think it was, is the exact moment you instantly forgot.. you end up typing in the wrong version times 100- write it down- test it-.
Before dumping anything, ask the chip what it will even let us do. That's espefuse. The eFuses are actual one-time fuses burned into the silicon at the factory- "one-time" being the part to sit with: once a bit is blown it stays blown, there is no putting it back. A manufacturer uses them to store the MAC, sure, but also to flip protections: turn on flash encryption, disable JTAG, kill the very UART download mode we just used to get in, lock in secure boot, stash encryption keys. Read the summary:
espefuse --port /dev/ttyUSB0 summary
(swap /dev/ttyUSB0 for your adapter- it'll be a COM port on Windows). On this device the page came back unblown, top to bottom: flash encryption off, JTAG not disabled, UART download mode not disabled, no encryption keys set. The only thing actually burned in was the MAC address. So the next step is a short one for us. That same page is also the exact set of switches a careful vendor would have blown in production to keep us out- so read it properly, because it is the page that tells you whether the rest of this post applies to the thing on your bench or not.
Dumping 16 Megs
First, prove we can talk to the flash at all. These two are the handshake:
esptool --port /dev/ttyUSB0 chip-id
esptool --port /dev/ttyUSB0 flash-id
The flash ID comes back with the size- 16 megabytes on this one, so that is how much there is to read. Then the dump itself. We get to pick the baud rate- the speed of the serial link- and it is a real trade: lower is slower but safe, higher is quicker but starts throwing errors and can hand back a corrupted image that looks fine until the moment you need it to be fine. A solid middle is 460800. You can push to 921600 and roughly halve the wait, but that is where the corruption creeps in, so for a read I care about I leave it alone.
esptool --port /dev/ttyUSB0 --baud 460800 read-flash 0x0 0x1000000 firmware.bin
Reading from offset 0x0- the very start- for 0x1000000 bytes, which is 16 MB written in hex, out to firmware.bin. Hex arithmetic not your idea of fun? Newer esptool takes the keyword ALL in place of the size and works it out itself:
esptool --port /dev/ttyUSB0 --baud 460800 read-flash 0x0 ALL firmware.bin
Then we wait. At that baud a full 16 MB is about six and a half minutes. Don't unplug anything, don't nudge the clip, don't go rearranging the wires while it runs- a dump that drops out half way is a dump you start again.
What's In It
The dump is on the disk. Before anything clever, two commands that cost us nothing:
file firmware.bin
strings -n 8 firmware.bin | less
Real, readable strings pouring out of strings tell us the image isn't encrypted- we expected that from the fuse page, but it is nice to have it confirmed by the file itself. This is also where those mystery commands from the serial prompt might be hiding, in plain text, waiting to be read.
Then the reflex is to reach for binwalk:
binwalk firmware.bin
On an ESP32, binwalk mostly hands back noise- a pile of "Unix paths" and other signatures that are false positives. Binwalk goes looking for known signatures and embedded filesystems. There is no tidy filesystem in here for it to find, so it shrugs and guesses. An ESP32 build is usually a real-time OS or bare-metal firmware, not the squashfs/ jffs2 layout we'd pull off an embedded Linux router. The "extract the filesystem" move from the router world just does not apply. The work from here is loading the binary into Ghidra and reversing it as raw code- a whole thing of its own, for another day.
Enjoy the dump.
Oh- one more thing before you go: if the fuse page on yours comes back with flash encryption on, or secure boot enabled, do not start burning fuses to "fix" it. That is a one-way trip and a dead board teaches us nothing. Read, don't write.
Ever pulled firmware off a microcontroller instead of an embedded Linux box- and hit a chip that wasn't this wide open, fuses blown and JTAG dead? What did you do next? Leave a comment, I'd like to hear how far you got. And if you want the softer intro, the router version and a pile of cool tools for hacking are already up here.
Glossary
- Embedded Linux — a trimmed-down Linux kernel and filesystem running on a small single-purpose device like a router or camera.
- U-Boot — the common bootloader on embedded Linux boards; it loads the kernel from flash at power-on.
- squashfs — a compressed, read-only filesystem often used to pack an embedded Linux system into flash.
- BusyBox — a single small program that provides the common Unix commands (ls, cp, sh...) on embedded Linux.
- Microcontroller — a small chip with its own processor, memory and flash that runs one compiled program directly, with no full operating system.
- FreeRTOS — a small real-time operating system that ESP-IDF builds on; it schedules tasks but has no filesystem or shell.
- MMU — memory management unit; the hardware Linux normally needs, found on application processors and mostly missing from microcontrollers.
- ESP32-WROVER-E — an Espressif module built around an ESP32 chip, with onboard SPI flash (4/8/16 MB) and extra PSRAM under the shield can.
- UART — universal asynchronous receiver/transmitter, the plain two-wire serial link (TX and RX) that most boards expose for logs and debug.
- Castellated edges — the plated half-circle notches along a module's edge, so every pin is reachable from the side of the board.
- Boot / download mode — a chip state entered by holding certain pins low at reset, in which a host tool can read and write the flash instead of running the app.
- IO0 / IO2 — the ESP32 strapping pins pulled to ground to force download mode.
- eFuse — one-time-programmable fuses inside the chip, burned at the factory to store the MAC and to flip security switches (flash encryption, JTAG disable, secure boot).
- esptool / espefuse — Espressif's Python command-line tools for talking to the chip over serial and reading its fuses.
- ESP-IDF — Espressif's IoT Development Framework, the SDK these firmwares are built with.
- read_flash / read-flash — the esptool command that copies flash contents out to a file; takes a start offset and a size (or
ALL). - Baud rate — serial link speed; higher reads faster but risks a corrupted dump.
- binwalk — a firmware analysis tool that scans a binary for known signatures and embedded filesystems.
- Ghidra — a free reverse-engineering suite (from the NSA) for disassembling and decompiling binaries.
- RTOS — real-time operating system; the kind of lightweight firmware an ESP32 typically runs, with no Linux-style filesystem to carve out.
Sources
- esptool basic commands — Espressif docs — verbatim syntax for read-flash, flash-id and the port/baud flags (checked 2026-09-28).
- esptool on PyPI — the real package name and current version (5.4.0), install with
pip install esptool. - espefuse reference — Espressif docs — the summary command and what each eFuse protection does.
- ESP32-WROVER-E datasheet (PDF) — pin definitions, flash sizes, PSRAM, the keep-out zone.
- Boot mode selection — Espressif docs — which strapping pins force download mode.
- Ghidra — free disassembler/decompiler for the reverse-engineering stage.
- binwalk — firmware signature scanner, useful caveats on RTOS images.
Comments
Post a Comment