# Firmware Inspector

> Open a firmware .bin in your browser: entropy map, embedded format signatures, readable strings and decoded uImage, TRX and ESP headers. Nothing uploaded.

- URL: https://bytescope.dev/en/firmware-inspector/
- Locale: en · Also: [ja](https://bytescope.dev/ja/firmware-inspector.md) · [zh-Hant](https://bytescope.dev/zh-Hant/firmware-inspector.md)
- Runs: The image is read in this tab and analysed in a Web Worker — nothing is uploaded.
- Requires: Any modern browser (Chrome, Edge, Firefox, Safari), desktop and mobile.
- Updated: 2026-09-08

## What it does

### The first ten minutes with an unknown image

You have a `.bin` pulled off a router, a camera or a dev board, and no documentation. The first questions are always the same four: how big is it, which parts of it are compressed, which known formats are embedded in it, and does it say anything about itself in plain text. That is one `binwalk` run and a couple of `strings | grep` invocations, and it is what this page does — in a Web Worker, in your own browser, on a file that never moves. What it deliberately does not do is unpack: this tool identifies and slices, so you leave with offsets and lengths rather than a directory of extracted files.

### The entropy map, and the one thing it cannot tell you

Entropy is a single number per block: how evenly the 256 possible byte values are spread across it, from `0` for a block that is one repeated byte to `8` for a block where every value appears equally often. Erased flash sits at zero, a bootloader's code and tables land in the middle, and a compressed payload pins the top of the chart and stays there — which is why the boundary between two sections is usually a visible step rather than a number you had to look for. The block size is yours: 4096 bytes shows the shape of the whole image, 256 finds the exact edge once you know roughly where it is. The four bands are `padding`, `structured`, `mixed` and `compressed`, and the last one is not called “encrypted” on purpose. A byte histogram cannot separate a compressed blob from an encrypted one; a tool that labels it for you is reporting something it does not know.

### Signatures, regions, and the gaps between them

The scan walks the image once and reports every offset where a known magic sits: squashfs in four byte orders, JFFS2, CramFS, UBI and UBIFS, uImage, TRX, DfuSe, the DFU trailer, Android boot images, device tree blobs, Netgear CHK, PEM and SSH key markers — plus the whole shared format catalog this site already uses for file-type detection. Overlapping hits are all reported, because a squashfs magic inside a gzip blob is a real fact about the file rather than a mistake to tidy up. Each hit then starts a region that ends at the next hit, or at the length the header itself declared where there was one, which in practice is uImage and TRX and nothing else. When a declared length is shorter than the distance to the next hit, the map is left with a gap — and the gap is drawn, because bytes that no signature claimed are a finding, and stretching a region over them would be a guess wearing a border.

### Strings, headers, and a guess that is allowed to say no

Printable runs are extracted in ASCII and UTF-16LE and tagged by shape: `url`, `ip`, `email`, `path`, `version`, `private-key`, `ssh-key`, `certificate`, `credential`, `busybox`, `linux-version`, `u-boot`. A tag is a shape and not a claim — `1.2.3.4` carries both `ip` and `version` because it honestly is both, and showing you what a string could be beats picking one for you. Where a header parses you get its fields with the format's own names on them: the uImage load address, entry point and CRCs; the ESP segment table with each segment's load address and length; the TRX offsets; the Android boot page size and kernel command line; the DFU suffix read from the end of the file. And where the architecture follows from one of those, or from a Linux version string that names its target, you get it with the evidence printed beside it. Where it does not follow from anything, you get `unknown` — never a default, and never the most popular answer.

### Nothing is uploaded, which matters more here than on most pages

The files people bring to a firmware inspector are the ones they are least free to send anywhere: an image pulled off a customer's device, a build that has not shipped, a dump from a box that belongs to someone else. This page is a static file with no API behind it. The image is read with the File API, transferred into a Web Worker, analysed there and dropped when the tab closes, and you can confirm it the direct way: leave DevTools open on the Network tab and watch it stay empty while a 64 MB image is analysed.

## Limits

Does not extract or decompress anything it finds (no gzip, LZMA or squashfs unpacking), does not disassemble code, and never labels a high-entropy region 'encrypted' — entropy cannot tell that apart from compressed. When no header or string states an architecture it reports unknown rather than guessing one.

## Machine output

Copy JSON copies an envelope `{ tool, url, generatedAt, userAgentHint, result }`. `tool` is `"firmware-inspector"`; `result` is the on-screen analysis below. The dropped file name, byte-view hex dump, and raw image bytes are omitted. Strings tagged `private-key` contribute offset / encoding / length only — never their text. High entropy is the band `"compressed"` (the histogram cannot tell compression from encryption apart).

- `byteLength` (number | null) — Image size in bytes. `null` = not supplied.
- `overallEntropy` (number | null) — Whole-input Shannon entropy in bits per byte (four decimals as on the overview card). `null` = not supplied.
- `band` (string | null) — Entropy band of the whole input: `"padding"` | `"structured"` | `"mixed"` | `"compressed"`. `null` = not supplied. Never `"encrypted"`.
- `architecture` (string | null) — ISA when a header/string rule fired (for example `"ARM"`). `null` = unknown (no rule fired); the page renders "unknown" and does not guess.
- `architectureEvidence` (array of strings) — Why `architecture` says what it says, on-screen order. Empty when `architecture` is `null`.
- `entropy` (object | null) — Chart facts: `overall`, `band` (`null` = the overview did not name a band; never inferred from the number), `blockSize` (or `null`), `blockCount` (or `null`). Per-block values are omitted. `null` = overall entropy was not supplied.
- `regions` (object) — Signature-derived regions in offset order, capped at 200. `rows`, `truncated`, `totalCount` (real region count, not the copied length).
- `hits` (object | null) — The signature-hit table as it was on screen, capped at 200 rows. `null` = hit page was not supplied. `rows` (`offset`, `id`, `name`, `source`, `confidence`, `size` or `null` if unknown, `detail`) are in offset order. `filtered` (boolean) — a filter was applied, so `rows` are matches from all over the image and NOT the first rows of the scan. `filter` — `null` when `filtered` is false; otherwise `{ minConfidence, source, textSearch }` in stable tokens (`minConfidence` `"low"` hides nothing; `source` `"all"` hides nothing; `textSearch` is only whether the search box held a term — the term itself is never copied). `matchedCount` — how many hits matched the filter; equal to `totalCount` when `filtered` is false. `totalCount` — how many hits the scan produced before any filter. `truncated` — more MATCHING hits exist than `rows` carries; it says nothing about the filter. `filterHidRows` — the filter excluded hits this copy does not carry. Empty `rows` with `totalCount: 0` = scanned, no hits.
- `hitsCapped` (boolean | null) — Scan stopped at the hit ceiling. `null` = not supplied.
- `strings` (object | null) — The string table as it was on screen, capped at 200 rows, excluding private-key material. `null` = string page was not supplied. Each row: `offset`, `encoding`, `text`, `tags`, `length`, in offset order. `filtered`, `filter`, `matchedCount`, `totalCount`, `truncated` and `filterHidRows` mean what they do for `hits`; the string `filter` is `{ tags, encoding, textSearch }` (empty `tags` and `encoding: "all"` hide nothing). `matchedCount` and `totalCount` both exclude private-key material and neither is the copied row count. Where the two counts contradict each other, `totalCount` is raised to `matchedCount` — a match is never dropped to make the numbers agree.
- `stringsCapped` (boolean | null) — Extraction stopped at the string ceiling. `null` = not supplied.
- `privateKeyCandidateCount` (number | null) — How many strings this tool tagged `private-key`. `null` = tags were not supplied; `0` = extraction ran, none found.
- `privateKeys` (object | null) — Private-key candidates: `offset`, `encoding`, `length` only — no text. Capped at 200. `null` = string page was not supplied. `totalCount` is the whole-image candidate count. Note that when `strings.filtered` is true these rows come off the same filtered table, so they are the candidates that matched that filter, not the first of `totalCount`.
- `tagCounts` (object | null) — Per-tag counts over the whole extracted list (the chip counts). `null` = not supplied.
- `headers` (object | null) — Parsed headers as shown: `uImage`, `esp`, `trx`, `android`, `dfu` (`null` = none parsed), `espHitCount`, `espCandidateCount`. `null` = headers were not supplied.

## FAQ

### Is this binwalk in a browser?

It is the first half of one. The entropy profile, the signature scan, the string extraction and the header parsers are the part of a `binwalk` run that tells you what you are holding, and they are here in full. Extraction is not: nothing is decompressed, no squashfs is mounted and no gzip member is inflated, so you get the offset and the length of the payload rather than its contents. That is a deliberate boundary rather than an unfinished feature — carve the region out with `dd` and hand it to the real tool, and you have lost nothing except the wait.

### Why does it say `unknown` for the architecture?

Because nothing in your image said otherwise. The guess has exactly three sources and they are tried in order: a uImage header whose `arch` field is a value U-Boot actually defines, an ESP image header, or a string tagged `linux-version` that names a target such as `mips`, `armv7l` or `x86_64`. If none of the three fires there is no fourth rule and no fallback, so the page prints `unknown` and lists which rules were tried. An architecture that nothing in the file supports would look exactly like one that something did, which is the whole reason the honest answer is worth more.

### A block is at 7.99 bits per byte. Is it encrypted?

Entropy cannot answer that, and neither can any other tool that only looks at the byte distribution. Compressed data and encrypted data are both close to uniform by design — that is what compression is for and what encryption requires — so `7.99` is consistent with gzip, LZMA, a JPEG, and AES alike. What can separate them is structure at the edges: a compression format almost always leaves a header, a magic or a length field somewhere near the start of the region, and the signature scan will find it. A high-entropy region with no format signature anywhere in or before it is the shape that is worth being suspicious about — but that is an observation to follow up, not a verdict this page will print for you.

### The scan found ten thousand hits. Which ones are real?

Start with the confidence column and the table it came from. Confidence is mechanical: eight bytes of magic or more, or four with an alignment constraint, is `high`; four to seven bytes free-scanned is `medium`; anything shorter is `low`. The flood is almost always `esp-image`, whose magic is the single byte `0xe9` and therefore matches by chance roughly once every 256 bytes — real information when it lands at offset `0` of an ESP flash dump, noise everywhere else, which is why the header panel only offers it at 4 KiB boundaries. Set the minimum confidence to `high` and the list usually collapses to the handful of hits that describe the actual layout.

### How large a file will it take, and what happens at the limits?

Up to 64 MiB, and every limit is reported rather than applied silently. The signature scan stops at 10,000 hits and string extraction stops at 200,000 strings; both caps keep the earliest entries by offset, so what is missing is the tail of the file — and the page says so instead of letting a truncated list read as a complete one. Stop cancels an analysis in flight and shows nothing partial, because a scan interrupted halfway has not seen the rest of the image and its results are indistinguishable on screen from a finished run.

## Related tools

- [Binary Diff](https://bytescope.dev/en/binary-diff.md) — Compare two builds of the same image byte for byte.
- [ELF Inspector](https://bytescope.dev/en/elf-inspector.md) — Once you know where the ELF is, read its sections and symbols.
- [Hex Viewer / Editor](https://bytescope.dev/en/hex-editor.md) — Open the raw bytes at an offset and edit them.
