An earlier article, From the Binary, Not the Build Plan, made a point that a software bill of materials is a claim about a build, while the shipped device is the only authoritative record of itself. This article shows how it works in practice. It takes one real ESP32 image, works from the binary alone, and shows what that recovers, where it is more reliable than the declared inventory, where to concentrate the effort that follows, and how much of that groundwork can be automated.
The target is deliberately non-exciting. It is the prebuilt ESP-Hosted firmware that ESPHome ships for its ESP32 Hosted component, the widely used arrangement where one ESP32 acts as a Wi-Fi and Bluetooth radio for a separate host processor. Since its source is publicly available, it makes a suitable target, and you as a reader can verify the claims. Everything below is reproducible, and nothing here is a vulnerability disclosure. The point is not to highlight firmware flaws, but to show what a binary captures that an SBOM usually leaves out.
The standard toolchain finds nothing
Start where a market surveillance authority or a supplier doing due diligence starts. With the flash dump and nothing else. Run the tools that generate an SBOM from a filesystem, and this is the result on a 993 KB ESP32 image.
$ syft network_adapter_esp32.bin
no packages discovered
$ binwalk -e network_adapter_esp32.bin
0 files extractedThis is not a misconfiguration, and it is no fault of the tools. A microcontroller firmware is a single binary blob. There is no filesystem to walk, no package manager database, no shared objects with version strings. SBOM tools are built for filesystem-based platforms, where software arrives as packages that carry their own metadata, and a firmware image carries almost none of that. Plain strings does a better job by scraping a few leaked source paths, but it is still a fraction of what is actually in the image.
Reading Xtensa from the bytes up
The image is Xtensa, the architecture behind the classic ESP32. That is not the newest thing Espressif builds. The recent parts have moved to RISC-V, and the original ESP32 is now marked not recommended for new designs. That does not make it historical. It shipped in a vast number of connected products that are still in the field and still under support, and the Xtensa line lives on in the ESP32-S3. For a regulation tied to a product's supported life, being able to handle a platform like this is a capability worth having.

With the symbols gone, the first job is simply to find the functions. Xtensa's windowed calling convention leaves a recognisable prologue at the start of most functions, so espfw scans for that entry pattern and then walks the control flow from each one rather than sweeping the bytes linearly. On this image that recovers several thousand functions from a fully stripped binary, with no vendor analysis engine involved.
But finding functions is not the same as knowing what they are. The same function, compiled twice, might come out slightly different at the byte level, mostly because the compiler is free to hold values in different registers each time. So each function is reduced to a canonical form that ignores those register choices, and then matched against the same functions compiled from known SDK sources. A unique match gives the function a name.
Matching functions by a normalised signature is an old idea. IDA's FLIRT has done it since the 1990s, and every serious reverse-engineering tool has a version of it. What makes espfw's approach slightly different is that it brings a corpus built for this exact problem. It holds the ESP-IDF SDK compiled across its versions and common configurations. The canonicalisation is tuned to how Xtensa code varies from one build to the next. And each match records which of those builds it came from.
$ espfw symbols network_adapter_esp32.bin
network_adapter_esp32.bin chip esp32
corpus : 22 build(s), 12 version(s)
matched : 2,032 of 4,725 functions (43.0%)
ELF ADDRESS IMAGE OFFSET SIZE SYMBOL CORPUS SOURCE(S)
0x400efc2c 0x0005fc2c 24 hmac_sha1_vector 25:v5.4/default, 26:v5.4/size
0x400f028c 0x0006028c 27 fastpbkdf2_hmac_sha1 26:v5.4/size
0x400f4478 0x00064478 18 mbedtls_cipher_init 25:v5.4/default, 26:v5.4/size, 43:v6.0/default, 44:v6.0/size, 45:v6.1/default, 46:v6.1/size
0x40083dec 0x000e042c 12 spi1_start 42:v5.5.2/size, 44:v6.0/size, 55:v5.5.3/size, 56:v5.5.5/size
0x400850cc 0x000e170c 322 pp_post 45:v6.1/default, 46:v6.1/size, 56:v5.5.5/size
0x4008474c 0x000e0d8c 165 coex_core_request 55:v5.5.3/size, 56:v5.5.5/size
0x400846cc 0x000e0d0c 84 coex_core_ts_end 56:v5.5.5/sizeIn this case, more than 2,000 functions are matched, and their names alone tell you most of what is in there. The SHA and PBKDF2 primitives behind Wi-Fi key derivation, the mbedTLS cipher setup, the flash driver, the packet-processing and radio-coexistence cores. These are functions that would otherwise be unnamed, recovered before you start the work that really matters.
For each function, the last column lists every corpus build it matches, and no single row is decisive. Some functions pin to one build, like coex_core_ts_end to v5.5.5. Others carry little version signal and match an older or neighbouring build instead. So you read the aggregate, not a row. Across all 2,000 functions the matches cluster on the 5.5 line, with v5.5.5 on top, and that is the version the image reports in its own header. Two independent signals landing on one release is what turns a reading into a measurement.
The 43% match is a floor, not a ceiling. espfw names a function only on a match in its corpus of known SDK builds, and this firmware's own code is not in that corpus. So the stock SDK gets subtracted, and what remains is the code worth a closer look. espfw helps you locate that code, drawing the executable segments as a coverage map and listing the largest regions no known component claims.
layout: █ identified ▒ inferred from neighbours · unidentified
iram 0x40080000 ·············████████████···▒▒·██·····████████████████████··▒▒▒· 90,440 bytes
irom 0x400d0020 ··············▒▒▒▒▒███████████████████·▒▒▒▒▒▒▒▒▒▒▒▒·██████·█·▒█ 637,532 bytes
regions no component claims (>= 2 KB), largest first:
START END BYTES FUNCS BETWEEN
0x400d8734 0x400e759e 61,034 517 vfs | esp_driver_gpio
0x400f1ccc 0x400f585e 15,250 137 esp_driver_sdio | mbedcrypto
0x400eae28 0x400edf26 12,542 96 esp_hw_support | nvs_flash
0x400e8118 0x400e9c46 6,958 50 esp_partition | spi_flash
0x4008b6d4 0x4008d182 6,830 33 hal | log
0x400d59d8 0x400d7474 6,812 44 newlib | esp_driver_uart
0x4014f340 0x40150b35 6,133 47 wpa_supplicant | core
0x40130324 0x40131a07 5,859 32 esp_phy | lwip
0x401658b8 0x40166b23 4,715 22 esp_event | esp_system
0x400ee96c 0x400efbcc 4,704 44 esp_netif | wpa_supplicant
0x400d7520 0x400d85d4 4,276 42 esp_vfs_console | vfs
0x400d34e4 0x400d451e 4,154 36 esp_system | heapThe biggest is 61 KB and 517 functions, sitting between the VFS layer and the GPIO driver. Nothing in the corpus explains it, which puts it high on the list of areas to investigate.
The floor has a second cause, and it is not specific to ESP-IDF. RTOS and bare-metal SDKs are configured at compile time, because firmware runs under tight resource budgets and a build strips out what it does not use. ESP-IDF is no different. Its source is threaded with conditional blocks that the build keeps or drops according to the sdkconfig the engineers chose. A function's compiled shape therefore depends not only on its source but on that configuration, and the canonical form espfw computes absorbs a compiler's register choices, not code that a different config compiled in or out. So when the target was built with a configuration espfw holds no matching build for, its config-sensitive functions quietly fall into the unmatched pile. The ones that survive are the config-insensitive functions, enough to anchor the identification, not enough to lift coverage much past that floor.
The build identifies itself
The image also states what it is outright. Before any of the matching, espfw reads its header and the embedded metadata back out.
$ espfw flash-info network_adapter_esp32.bin
network_adapter_esp32.bin 993,248 bytes sha256:c0f05a4b06e4ed92...
input kind : app_partition
chip : esp32
app: offset 0x0, entry 0x400811e8, 993,248 bytes selected
app: eh_cp_bt_wifi_hosted_hci_mcu 1, IDF v5.5.5, built Jul 27 2026 16:29:02
0x3f400020 241,380 data drom image+0x20
0x3ffbdb60 20,748 data dram image+0x3af0c
0x400d0020 637,532 code irom image+0x40020
0x3ffc2c6c 2,996 data dram image+0xdba84
0x40080000 90,440 code iram image+0xdc640
0x50000000 32 data rtc_data image+0xf2790In one command the image gives up its chip, its project name, the exact framework it was built against, ESP-IDF 5.5.5, its build date, its entry point and its full memory layout. None of this was asserted about the file. It was measured from it.
That distinction is important. A bill of materials might list ESP-Hosted, and the ESP-IDF version it is built on. What it does not include is the compile-time configuration those versions were built with, and on an embedded target that is often what decides the security posture. The binary does not declare it either, but it was built from it, so it can be recovered through reverse-engineering rather than taken on trust.
Binary-first is not guessing
The reasonable objection is that matching compiled functions is inference, and inference can be wrong. The risk that matters is a firmware's own function labelled as an SDK function and potentially overlooked. espfw guards against that by naming a function only on a unique match.
Since this target is open source, names can be verified. The ones that mattered were checked against the upstream source and confirmed accurate.
Built to be driven by an agent
There is a lot of repetitive work in the above. Carving thousands of functions, applying 2,000 recovered symbols, triaging the remainder for the handful of functions worth reading closely, annotating results back into the reverse-engineering platform. It is tedious work, and espfw is built to help automate that process.
That is a design choice, not an afterthought. Every command emits structured, machine-readable output rather than a wall of text. The results carry their own provenance, a per-function confidence score, and an explicit list of what did not match. Each stage is a discrete step with a stable interface, the flash-info and symbols output above being two of them. Output shaped like that is equally legible to a person and to an automated agent, which can call espfw, read the result, decide the next step, and drive a reverse-engineering process through the same tools an analyst would use.
Getting the image into a disassembler at all is the first thing espfw does. A raw flash dump is a seemingly flat blob with no notion of where each byte lives when the chip runs it, which is not something an RE platform can typically load and reason about. espfw reads the segment map shown earlier and writes the image back out as a proper ELF, each segment a section fixed at its true runtime address and the code marked executable. That reconstructed ELF is what everything downstream analyses, and it is what turns a stripped dump into an ELF a disassembler can work with.
The tool
MKR Labs is releasing espfw, the function-identification tool at the centre of this. It carves functions from a stripped Xtensa image, canonicalises them, and matches them against known SDK builds. Naming the SDK code puts a spotlight on the rest, the non-vendor code, far smaller than the whole image, and the part that deserves the most attention. It is research code, published as method rather than product, with no support promise and no roadmap. That is as far as espfw goes. Deciding what the remaining code means is the work MKR Labs does for clients.
That work goes beyond the firmware. Processing firmware the way this article does is one step in a product security assessment, an important one, but a step. The full assessment runs the whole of a connected device, the physical hardware and its debug interfaces, the firmware, the companion app, and the backend it talks to. The flaws worth finding often sit on the boundaries between those layers, where no single team owns the responsibility. espfw is what makes the firmware layer manageable.
If there is one line to take from this, it is that a stripped firmware image is not a black-box. When processed properly, it is a document, and it is the most honest one a product has, because it is the thing that shipped. espfw is a good way to start recovering that document.
References and reproduction
- espfw, the ESP32 Xtensa function-identification tool, released open source by MKR Labs.
- ESP-Hosted firmware, the open-source project analysed here. The behaviour described is its documented upstream default and was confirmed against the source at the matching release.
- ESP-Hosted for ESPHome, the distribution the analysed image comes from.
- From the Binary, Not the Build Plan, the companion piece that argues the position this one demonstrates.
- Regulation (EU) 2024/2847, the Cyber Resilience Act. The SBOM obligation and the essential requirements are in Annex I.