# Firmware Inspector

> 在瀏覽器裡打開韌體 .bin：熵值圖、內嵌格式簽章、可讀字串，還有 uImage、TRX、ESP 標頭欄位。整份檔案都留在你的裝置上，不會上傳。

- URL: https://bytescope.dev/zh-Hant/firmware-inspector/
- Locale: zh-Hant · Also: [en](https://bytescope.dev/en/firmware-inspector.md) · [ja](https://bytescope.dev/ja/firmware-inspector.md)
- Runs: 韌體在這個分頁裡讀、在 Web Worker 裡分析，一個位元組都不會上傳。
- Requires: Any modern browser (Chrome, Edge, Firefox, Safari), desktop and mobile.
- Updated: 2026-09-08

## What it does

### 拿到陌生映像的頭十分鐘

你手上有一份從路由器、攝影機或開發板挖出來的 `.bin`，沒有任何文件。要問的問題永遠是那四個：它多大、哪幾段被壓縮過、裡面埋了哪些已知格式、以及它有沒有用明文講出自己的來歷。那就是跑一次 `binwalk` 再配上兩三行 `strings | grep`，而這頁做的就是這件事 — 在 Web Worker 裡跑，在你自己的瀏覽器裡跑，檔案一步都不用移動。刻意不做的是解開：這個工具只負責認出來跟切開，你帶走的是位移跟長度，不是一整個解出來的目錄。

### 熵值圖，以及它唯一答不出來的那件事

熵值是每個區塊一個數字，量的是 256 種位元組數值在裡面散得多平均：整塊都同一個位元組是 `0`，每種數值出現次數都一樣是 `8`。抹除過的 flash 貼在零，bootloader 的程式碼跟表格落在中段，壓縮過的內容則會貼著上緣一路不動 — 所以兩個段落的交界通常是一眼看得見的落差，而不是你得去翻的數字。區塊大小由你決定：4096 位元組看得到整份映像的輪廓，256 位元組則是在你大概知道位置之後，用來釘出精確邊界的。四個分帶是 `padding`、`structured`、`mixed`、`compressed`，最後一個不叫「加密」是刻意的。位元組分布分不出壓縮跟加密；替你貼上加密標籤的工具，是在講它其實不知道的事。

### 簽章、區段，以及它們之間的空隙

掃描把整份映像走一遍，把所有坐著已知 magic 的位移都挑出來：四種位元組序的 squashfs、JFFS2、CramFS、UBI 與 UBIFS、uImage、TRX、DfuSe、尾端的 DFU suffix、Android boot image、device tree、Netgear CHK、PEM 與 SSH 金鑰標記 — 再加上這個站本來就拿來判斷檔案類型的那整份共用格式目錄。重疊的命中一律照列，因為 gzip 區塊裡出現 squashfs magic 是關於這個檔案的事實，不是需要收拾掉的錯誤。每個命中會開一個區段，一路延伸到下一個命中，或者延伸到標頭自己宣告的長度 — 而會宣告長度的實際上只有 uImage 跟 TRX。當宣告的長度比到下一個命中的距離還短，地圖上就會留下一段空隙，那段空隙會照畫。沒有任何簽章認領的位元組本身就是一項發現；把區段硬撐過去蓋住它，只是替猜測畫了一個邊框。

### 字串、標頭，以及一個可以說「不知道」的推測

可讀片段會以 ASCII 與 UTF-16LE 兩種方式抽出來，再依長相打標籤：`url`、`ip`、`email`、`path`、`version`、`private-key`、`ssh-key`、`certificate`、`credential`、`busybox`、`linux-version`、`u-boot`。標籤講的是形狀，不是主張 — `1.2.3.4` 同時掛上 `ip` 跟 `version`，因為它老老實實兩種形狀都符合，而把可能性攤給你看，比替你挑一個有用。標頭解得開的話，欄位就照那個格式自己的名字列出來：uImage 的載入位址、進入點跟兩個 CRC；ESP 每一段的載入位址與長度；TRX 的 offsets；Android boot 的頁面大小與核心命令列；還有從檔案尾端讀出來的 DFU suffix。如果架構能從其中之一推出來，或能從有寫出目標平台的 Linux 版本字串推出來，就會連同依據一起列。推不出來就寫 `unknown` — 不套預設值，也不填最常見的答案。

### 什麼都不上傳，而這件事在這頁特別重要

會被拿來丟進韌體檢視器的檔案，剛好是最不能往外送的那一種：從客戶裝置上挖出來的映像、還沒出貨的版本、別人機器上倒出來的 dump。這頁是一個背後沒有 API 的靜態檔案。映像用 File API 讀進來、轉交給 Web Worker、在那裡分析完，關掉分頁就沒了。驗證方式也很直接：把 DevTools 停在網路分頁上，然後丟一份 64 MB 的映像進去分析，你會看到那一欄從頭到尾都是空的。

## 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

### 這是瀏覽器版的 binwalk 嗎？

只有前半段。熵值量測、簽章掃描、字串抽取、標頭解析 —— 一次 `binwalk` 裡負責告訴你「手上這份是什麼」的部分，這裡都齊了。抽取則沒有：不做任何解壓縮、不掛載 squashfs、不展開 gzip 成員，所以你拿到的是內容的位移與長度，不是內容本身。這是刻意畫的界線，不是還沒做完的功能 —— 用 `dd` 把區段切出來丟給真正的工具，你少掉的只有等待的時間。

### 架構為什麼顯示 `unknown`？

因為你這份映像裡沒有任何東西講出來。推測的依據只有三種，而且照順序試：`arch` 欄位對得上 U-Boot 實際定義值的 uImage 標頭、ESP 映像標頭、或是帶著 `linux-version` 標籤、字面上寫著 `mips`／`armv7l`／`x86_64` 這類目標平台的字串。三種都沒中就沒有第四條規則、也沒有備援，所以頁面直接寫 `unknown`，並列出它試過哪些規則。一個檔案沒有撐住的架構名稱，擺在畫面上跟有憑有據的名稱長得一模一樣 —— 這就是誠實那個答案比較值錢的原因。

### 某個區塊是 7.99 bits/byte，代表它被加密了嗎？

熵值答不出這題，任何只看位元組分布的工具也一樣答不出來。壓縮資料跟加密資料在設計上都會接近均勻 —— 壓縮的目的就是這個，加密則是必須這樣 —— 所以 `7.99` 跟 gzip、LZMA、JPEG、AES 全都相符。真正能分開兩者的是邊緣的結構：壓縮格式幾乎一定會在區段開頭附近留下標頭、magic 或長度欄位，而簽章掃描就會撈到它。一段高熵值、但裡面跟前面都找不到任何格式簽章 —— 那個形狀才值得起疑。不過那是一條要繼續追的線索，不是這頁會替你下的判決。

### 掃出一萬筆命中，哪些是真的？

先看信心度那一欄，還有它來自哪張表。信心度是機械規則：magic 有 8 個位元組以上、或 4 個以上且帶對齊條件算 `high`；4 到 7 個自由掃描算 `medium`；再短就是 `low`。淹掉清單的通常是 `esp-image`，它的 magic 只有 `0xe9` 一個位元組，隨機碰運氣大概每 256 個位元組就中一次。它出現在 ESP flash dump 的位移 `0` 是真資訊，出現在別處就是雜訊 —— 這也是標頭那一區只把 4 KiB 邊界上的位移當候選的原因。把最低信心度拉到 `high`，清單通常會縮回真正描述版面配置的那幾筆。

### 檔案最大可以多大？碰到上限會怎樣？

上限是 64 MiB，而且每一道上限都會講出來，不會默默套用。簽章掃描在 10,000 筆停手，字串抽取在 200,000 筆停手，兩者都是留位移比較前面的，所以少掉的是檔案後半段 —— 這件事頁面會明說，不讓一份被截斷的清單假裝自己跑完了。按下停止會取消進行中的分析，而且不給半套結果：掃到一半被打斷的清單根本沒看過後半個檔案，可是排在畫面上時，它跟跑完的清單長得一模一樣。

## Related tools

- [Binary Diff](https://bytescope.dev/zh-Hant/binary-diff.md) — 把同一份映像的兩個版本逐位元組比對。
- [ELF Inspector](https://bytescope.dev/zh-Hant/elf-inspector.md) — 知道 ELF 在哪之後，進去讀它的 section 跟符號。
- [Hex Viewer / Editor](https://bytescope.dev/zh-Hant/hex-editor.md) — 把那個位移的原始位元組打開來改。
