English | 简体中文
Automated reverse engineering and vulnerability discovery for IoT firmware
binaries. This repository implements the analysis core described in
firmware-analysis-requirements.md.
Defensive / authorized use only. Analyze firmware you own or are authorized to assess. See requirements §1.3.
The web UI ships with the Go service layer (service/) and is bilingual
throughout — every screen below exists in both English and Chinese.
| Dashboard | Findings |
|---|---|
![]() |
![]() |
| Per-scan totals, severity histogram and a breakdown by vulnerability class. | Filter by severity, class, triage state, confidence or free text; export the filtered set as CSV/JSON. |
The Chinese UI is shown in
dashboard-zh.png and
login-zh.png.
Per the agreed plan, the first iteration delivers binary reverse engineering (FR-RE) and vulnerability discovery (FR-VUL). It assumes ingestion and extraction (FR-ING / FR-EXT) have already produced files on disk — point it at a single ELF or an extracted firmware tree.
Engines wrap existing tooling: capstone (multiarch disassembly, works for MIPS/ARM regardless of host), pyelftools + binutils (ELF parsing, mitigations). A heavier taint engine (angr) is an optional future drop-in behind the same interface.
┌───────────────────────────── Python analysis core (this repo) ──────┐
extr. │ loader/ re/ vuln/ inventory/ scripts/ fs/ │
arts ──┼► load_elf ─► disassemble ─► dangerous_funcs ► secrets shell hard-│─► JSON /
(FR- │ (arch, mitigations taint (src→sink) + rules/ (cmd- ening │ Markdown
EXT) │ imports, call-graph cve correlation signatures injection) │ + SBOM
│ strings) xrefs cross-binary + + entropy │ (CycloneDX)
│ prioritize/triage │
└─────────────────────────────────────────────────────────────────────┘
▲
│ exec `ifda.cli analyze --progress` per job
Go service / orchestration layer (service/) ← queue, workers,
REST API (FR-INT-1), dedup cache, web UI with live progress
The Python core is a library + CLI. The hybrid architecture chosen for the
project puts batch/corpus orchestration, the REST API (FR-INT-1), queueing
(FR-ING-4), and a web UI in a separate Go service layer (service/, built)
that drives this core one job at a time via the CLI. The JSON model
(ifda/model.py) is the contract between the two; the core streams
@@IFDA@@<json> progress events so the service shows live task progress. See
service/README.md to build and run it.
Analysis core (Python 3.10+)
capstone— multiarch disassembly (x86/x86_64, ARM, ARM Thumb-2, AArch64, MIPS LE/BE)pyelftools— ELF parsing and mitigation detection (NX/canary/RELRO/PIE/FORTIFY), no externalbinutilsprocess neededcve-bin-tool— required dependency for FR-VUL-1's broad CVE correlation (NVD/OSV/RedHat/GitLab Advisory/Curl, 350+ components)yara-python(optional) — externalized YARA signature-rule bridgeangr(optional, future) — heavier taint/symbolic-execution engine, planned drop-in behind the existing taint interfaces
Service layer (Go 1.22+)
- Standard library only, zero third-party Go dependencies — the enhanced
net/httpServeMux(method + path-parameter routing), a hand-rolled PBKDF2 (crypto/hmac+crypto/sha256) for password hashing,image/image/pngfor the login CAPTCHA - Server-Sent Events (no polling, no WebSocket dependency) for live job progress
Web UI
- Alpine.js 3.14.1 (vendored locally, no build step, works air-gapped)
- Plain CSS with custom-property theming (7 built-in themes, light/dark aware)
Optional enrichment
- Ghidra (headless mode) — decompiled pseudocode enrichment for findings
- EMBA — the open-source IoT firmware analyzer whose
approach to CVE correlation and sensitive-data hunting shaped several design decisions here.
IFDA follows EMBA's lead in delegating broad CVE correlation to
cve-bin-toolrather than maintaining a hand-curated CVE list, and itsconfig/signature files (deep_key_search.cfg,password_regex.cfg, the*_files.cfgsensitive-path lists) informed both the sensitive-string keyword dictionary and a PEM-key detection gap this project fixed. Thanks to the EMBA team for the prior art. - cve-bin-tool (an OpenSSF project) — the actual engine behind FR-VUL-1's broad CVE coverage, and the same tool EMBA itself wraps for CVE correlation.
ifda runs on Linux and macOS (Apple Silicon and Intel). Analysis happens on
your machine, not on the firmware's target CPU: capstone disassembles
MIPS/ARM/etc. regardless of the host architecture, so no cross-compilers or
emulation are needed just to analyze a foreign-arch firmware. The Go service
layer builds natively on both platforms — it uses a pure-Go SQLite driver
(modernc.org/sqlite, no cgo), so go build needs no C toolchain and produces
a self-contained binary. Scan job control (pause / resume / cancel) uses POSIX
process groups, which work the same on Linux and macOS (Darwin).
Windows is not targeted; use WSL2 there.
Full step-by-step environment setup (Go upgrade, cross-compilers, Ghidra + JDK pitfalls, verification) is in
ENVIRONMENT.md. On macOS, see the dedicated macOS guide below.
# Python analysis core
apt-get install -y python3-capstone python3-pyelftools # or: pip install -e .
pip install cve-bin-tool # required: FR-VUL-1 broad CVE coverage (NVD/OSV/RedHat/GitLab/Curl)
pip install yara-python # optional: enables the YARA stage if data/yara/*.yar exist
# Go service layer (optional — only if you want the REST API + web UI)
cd service && go build -o ifda-service . # Go 1.22+The commands above assume Debian/Ubuntu. On macOS the only differences are the package manager and the Ghidra/JDK install; everything else is identical.
# 1. Toolchain via Homebrew (https://brew.sh)
brew install python@3.12 go # Go 1.22+ for the service layer
# 2. Python analysis core — capstone/pyelftools ship arm64 + x86_64 wheels,
# so pip works the same on Apple Silicon and Intel. A venv is recommended
# so it doesn't touch the system/brew Python.
python3 -m venv .venv && source .venv/bin/activate
pip install -e . # pulls in capstone + pyelftools
pip install cve-bin-tool # required: broad CVE coverage
pip install yara-python # optional: YARA stage
# 3. Go service layer (optional — REST API + web UI). Pure-Go SQLite means no
# Xcode/CGO is required; it builds and runs natively on arm64 and Intel.
cd service && go build -o ifda-service . && cd ..
# 4. Ghidra pseudocode enrichment (optional; the --decompile flag / service
# enrichment). Needs a JDK 17+ and Ghidra on PATH or at GHIDRA_HOME.
brew install --cask ghidra # also pulls a JDK; or: brew install openjdk@17
export GHIDRA_HOME="$(brew --prefix)/opt/ghidra/libexec" # adjust to your installThen run exactly as on Linux — the CLI and service invocations in Usage below are platform-independent:
# CLI
python3 -m ifda.cli analyze /path/to/extracted_rootfs --json report.json
# Service (REST API + web UI at http://localhost:8080)
cd service && ./ifda-service -addr :8080macOS-specific notes:
- Gatekeeper: a Homebrew-installed Ghidra is already trusted; only a
manually-downloaded Ghidra.app may need one right-click → Open, or
xattr -dr com.apple.quarantine /path/to/ghidra. - Extraction tools (binwalk, unsquashfs, etc.) are ingestion-side and out of
this repo's scope, but install cleanly via
brew install binwalk squashfswhen you need to unpack an image before pointing ifda at the extracted tree. - File permissions / hardening checks read standard Unix modes, which macOS
provides — the
weak_permissionsfindings (world-readable/etc/shadow, SSH host keys, etc.) work identically.
cve-bin-tool maintains its own local CVE database (NVD + OSV + RedHat + GitLab
Advisory + Curl, ~1GB+); the first scan on a machine downloads/updates it, so
expect that run to be slow and to need network access. Without it, analyze()
still runs but CVE correlation falls back to the small curated
data/vuln_db.json and the report carries a visible warning about the missing
dependency (NFR-USE-1 — one missing piece degrades, it doesn't crash the job).
There are two ways to run ifda: the CLI (single run, scriptable) and the service (queue + REST API + web UI with live progress).
# Analyze a binary or an extracted tree; JSON to stdout by default.
python3 -m ifda.cli analyze /path/to/extracted_rootfs --json report.json --md report.md
# Emit a CycloneDX SBOM alongside the report.
python3 -m ifda.cli analyze ./rootfs --json report.json --sbom sbom.json
# Stream machine-readable progress events on stderr (used by the service).
python3 -m ifda.cli analyze ./rootfs --json report.json --progress
# Enrich findings with Ghidra decompiled pseudocode (opt-in; needs Ghidra,
# set GHIDRA_HOME or install to /opt/ghidra). Slow; degrades to no-op if absent.
python3 -m ifda.cli analyze ./rootfs --md report.md --decompile
# Triage a finding; persists across re-scans (FR-VUL-8).
python3 -m ifda.cli triage triage.json <finding_id> false_positive
python3 -m ifda.cli analyze ./rootfs --triage triage.json # muted findings droppedanalyze accepts a single ELF or a directory (an extracted firmware tree). It
walks the tree once and runs every stage: per-binary RE/vuln, cross-binary
taint, secrets + signature rules + entropy, shell and PHP/Python/Lua script
injection, and filesystem hardening.
cd service
go build -o ifda-service . # Go 1.22+, first time only
./ifda-service -addr :8080 -core .. # -core = repo dir (auto-detected if omitted)
# open http://localhost:8080Flags: -addr, -core, -workers (default 2), -queue, -data (triage state
- uploads dir; default
$TMPDIR/ifda-service),-auth(defaulttrue),-user/-pass(seed an account on first run only — doesn't overwrite a password already changed via the web UI; add-reset-passto force it). Seeservice/README.mdfor the full explanation.
Web UI (Alpine.js, embedded — no build step, works air-gapped): submit a server path or upload a file, then watch live SSE progress and explore:
- Dashboard — finding/critical/high/binary/CVE cards, severity and vuln-class distribution bars, component/config-file/certificate counts (click-through), a BusyBox overview card row, and a network-services/ open-ports card.
- Findings — filter by severity toggles, vuln class, triage state, confidence threshold, and full-text search; sort by severity/confidence; expand for evidence, taint path, and Ghidra pseudocode; triage inline (confirm / false-positive / accept-risk / reset).
- Binaries — per-binary arch, libc, mitigation chips (NX/Canary/RELRO/PIE/ FORTIFY, color-coded), function count, CVEs (server-side paginated).
- Files — full non-ELF file listing, filterable by kind (binary/script/ config/symlink/other); click a row to expand a syntax-highlighted source preview inline.
- BusyBox — compiled-in vs. reference-list "crippled" applets, extra
bin/sbin executables at any tree depth, and
/etc/init.dscript source. - Network Services — statically identified services (name/category/ banner-derived version/port/port-source/binary path), filterable by category with keyword search.
- CVE Database — browse the tool's actual CVE coverage (cve-bin-tool's broad set + the curated offline fallback) independent of any scan; CVE numbers throughout the UI link out to the matching NVD page.
- Sensitive-string dictionary — standalone page to add/remove/reset the keyword dictionary used for secret matching, no scan required.
- Compare — diff findings/files/strings between two scans (added/ removed/unchanged).
- Export — download JSON / Markdown / SBOM.
- User center — change password, log out.
REST endpoints (full table in service/README.md):
# enqueue a job (returns {"id": ...}); dedup cache returns cached unchanged targets
curl -XPOST localhost:8080/api/jobs -H 'Content-Type: application/json' \
-d '{"target":"/path/to/extracted_rootfs"}'
curl -N localhost:8080/api/jobs/<id>/events # live progress (SSE, no polling)
curl localhost:8080/api/jobs/<id>/report # findings JSON (triage overlaid)
curl "localhost:8080/api/jobs/<id>/report?format=md" # or format=sbom; add &download=1
# triage a finding — persisted by fingerprint, survives restarts & re-scans (FR-VUL-8)
curl -XPOST localhost:8080/api/jobs/<id>/triage -H 'Content-Type: application/json' \
-d '{"finding_id":"<finding_id>","state":"false_positive"}'
# upload an artifact server-side, then submit the returned path as a target
curl -F "file=@firmware.bin" localhost:8080/api/upload # -> {"target": "...", "name": ...}Triage states: new | confirmed | false_positive | accepted_risk. Re-submitting
an unchanged target returns a cached result.
A static scan of a real firmware image easily produces tens of thousands of
findings — most of them strcpy/system call sites with no proven input path.
Reading that by hand is where the time goes. The service can hand a completed
scan to an LLM that works through the findings with tools: it pulls evidence
and pseudocode for the calls that look real, reads the init scripts and BusyBox
applets to see what actually runs at boot, checks which services are network-
reachable, and then writes up what's worth chasing and what's noise. In
practice this cuts the false-positive triage down a lot and gets you to the
handful of genuinely exploitable issues faster than scrolling the findings list.
Set it up in the web UI under AI settings: add a provider (custom Host URL, API key, model), pick OpenAI-compatible or Anthropic protocol, and run the analysis from a finished scan's AI Analysis tab. Keys are encrypted at rest; results stream live and are saved to the database.
It works with any OpenAI-compatible or Anthropic-protocol endpoint. If you can't reach the official APIs directly (common in mainland China) or just want cheaper access, a relay gateway works well — I run this against Claude (Opus / Sonnet) through aihuangniu, which is the gateway these AI features were tested on. Claude models in particular do a noticeably better job of grounding the analysis in the actual call sites instead of hand-waving. Any gateway that speaks the OpenAI or Anthropic wire format will do — the link is just the one I've been using.
| Format | How | Contents |
|---|---|---|
| JSON | --json / GET …/report |
full structured report (the integration contract) |
| Markdown | --md |
executive summary + per-finding detail |
| CycloneDX SBOM | --sbom |
components + detected CVEs (Dependency-Track ready) |
| Requirement | Status | Where |
|---|---|---|
| FR-RE-1 Disassembly (multiarch) | ✅ | re/disasm.py (capstone) |
| FR-RE-3 CFG / call graph / fn boundaries | ✅ (symbol + linear fallback) | re/disasm.py |
| Import call resolution | ✅ x86/x86_64 + ARM/AArch64 (PLT) + MIPS (GOT/jalr $t9) |
re/disasm.py |
| ARM/Thumb-2 + interworking veneers | ✅ (mapping-symbol mode switch, veneer follow) | re/disasm.py |
| FR-INV-4 Embedded secrets/credentials | ✅ (keys, hashes, hardcoded creds/tokens, entropy fallback) | inventory/secrets.py |
| FR-INT-3 Externalized signature rules (YARA-style) | ✅ (updatable JSON rule library; optional yara-python bridge) | rules/engine.py, data/secret_rules.json |
| FR-INV-3 Script analysis (shell/CGI cmd-injection) | ✅ (taint-tiered) | scripts/shell.py |
| FR-INV-3 Script analysis (PHP/Python/Lua injection) | ✅ (cmd/code/file-incl/deserialization) | scripts/langs.py |
| FS hardening (setuid, world-writable, weak perms, init) | ✅ | fs/hardening.py |
| FR-RE-5 Mitigations (NX/canary/RELRO/PIE/fortify) | ✅ | re/mitigations.py |
| FR-RE-6 Cross-references (call sites, strings) | ✅ (call/import xrefs) | re/disasm.py |
| FR-RE-7 Scriptable API | ✅ (library + CLI) | pipeline.py, cli.py |
| FR-RE-2 Decompiled pseudocode | ✅ (opt-in Ghidra headless; enriches findings) | re/decompile.py |
| FR-VUL-1 Known-CVE correlation | ✅ curated offline DB + required cve-bin-tool (NVD/OSV/RedHat/GitLab/Curl, 350+ components); kernel version (own detector, not cve-bin-tool's checker) also correlated |
vuln/cve.py, vuln/cve_bin_tool.py, data/vuln_db.json |
| FR-VUL-2 Dangerous-function detection | ✅ | vuln/dangerous_funcs.py |
| FR-VUL-3 Taint / reachability | ✅ (call-graph heuristic) | vuln/taint.py |
| FR-VUL-4 Vuln-class coverage | ◑ overflow/cmdi/code-inj/file-incl/deserialization/fmt/weak-crypto/path-traversal/auth-logic | vuln/catalog.py, vuln/auth_weak.py, scripts/langs.py |
| FR-VUL-5 Cross-binary analysis | ✅ (global call graph, CGI→lib) | vuln/crossbinary.py |
| FR-VUL-7 Prioritization + evidence | ✅ | vuln/findings.py, model.py |
| FR-VUL-8 Triage state persistence | ✅ | vuln/findings.py |
| FR-VUL-6 Emulated/dynamic validation | ⬜ planned (optional, sandboxed) | — |
| FR-REP-1 JSON + Markdown output | ✅ | report/ |
| FR-REP-2 SBOM (CycloneDX 1.5) | ✅ (SPDX TODO) | report/sbom.py |
| FR-INT-1 REST API + queue + web UI (dashboard, filtering, triage, SSE) | ✅ (Go service layer, Alpine.js) | service/ |
| FR-ING-4 Batch submission + dedup cache | ✅ (workers + path-mtime cache) | service/job.go |
| Config-file classification & content hardening audit | ✅ (kind classification; content rule for telnet/anonymous-FTP/debug/SNMP-default-community/TLS-verify-off/WPS/UPnP) | inventory/firmware_meta.py, vuln/config_audit.py |
| BusyBox applet audit | ✅ (compiled vs. reference-list diff, extra bin/sbin executables, init.d dump) | inventory/busybox_audit.py |
| Certificate detection | ✅ (per-certificate RSA/non-RSA counts via cryptography, bundle/chain-aware) |
inventory/secrets.py |
| Network service identification | ✅ (static-only; banner-derived versions, UCI/inetd/init.d-flag/default port inference) | inventory/service_id.py |
| Stripped-binary function-boundary recovery | ✅ (direct-call + prologue discovery, ARM/Thumb auto-detection) | re/disasm.py |
Legend: ✅ done · ◑ partial · ⬜ not started
Decompilation-free static analysis is heuristic (requirements §5). Findings carry confidence scores; precise call-site detections (0.8) rank above import-presence (0.4) and call-graph taint reachability (0.5). Taint findings are explicitly candidate paths for analyst validation, not proven exploits.
python3 -m pytest tests/ -q25 passed, 2 skipped with cross-compilers + Ghidra installed (the skips are
opt-in live tests — see ENVIRONMENT.md). They build real cross-compiled
samples (x86_64, MIPS LE/BE, ARM, ARM Thumb-2, AArch64) and cover: the seeded
command-injection acceptance case (source→sink path to system()), per-arch
import-call resolution and cross-binary RCE, mitigation detection, CVE
correlation — both the curated offline DB (including not flagging patched
versions) and cve-bin-tool's entry parsing — embedded secrets (externalized
signature rules + entropy fallback, with redaction), shell/CGI command
injection, PHP/Python/Lua injection (cmd/code/file-inclusion/deserialization),
filesystem hardening, CycloneDX SBOM, prioritization ordering, and triage
persistence.
Full technical detail (root causes, before/after numbers, test counts) is in
PROGRESS.md's 变更记录 (Chinese). Feature-level summary:
- v4.0 — AI analysis reliability + cross-platform docs. Interrupted
provider streams (no
[DONE]/finish_reasonormessage_stop/stop_reason) are now reported as interrupted, keeping the partial output, instead of being stored as a finished analysis ending mid-sentence. Fixes one scan's AI page showing another scan's cached analysis (stale frontend view state on job switch). Documents Linux + macOS support (Apple Silicon and Intel) with a macOS setup guide. - v3.9 — Live feedback while an AI analysis runs: a persistent status
strip (pulse, phase label, elapsed clock, hairline sweep), a blinking caret
on the streaming text, follow-the-tail output that yields to manual
scrolling, and
prefers-reduced-motionsupport. Fixes a progress indicator that vanished on the first token, leaving long pauses indistinguishable from a finished analysis. - v3.8 — AI-assisted analysis of scan results with a tool-calling agent. Per-provider config (custom Host URL, key, model, OpenAI-compatible or Anthropic protocol, max_tokens) with API keys encrypted at rest and a rotation command. NDJSON streaming with partial-content persistence. The model pulls scan data on demand — findings detail with evidence and pseudocode, init.d scripts, BusyBox audit, network services — instead of being fed a fixed sample, with automatic fallback for gateways lacking tool support. Finding selection collapses near-identical clusters and allocates round-robin across vulnerability classes so one CVE flood can't crowd out every other class.
- v3.7 — Kernel-version CVE correlation using our own kernel detector (cve-bin-tool's own checker fails on Ubuntu-style compiler strings). Fixed an 8MB scan cap that silently missed kernel banners past that offset on real 35MB images. Dirty Pipe is expressed as three branch-precise version ranges so already-backported branches aren't misflagged.
- v3.6 — FR-VUL-4 coverage: path traversal (reusing the existing taint engine with fopen/open/unlink/remove sinks) and auth-logic weakness (non-constant-time credential comparison in auth-named functions). Integer-overflow-into-alloc deliberately skipped as too noisy without argument-level analysis.
- v3.5 — Static network service identification (WEB/SSH/FTP/Telnet/gSOAP/ DNS/SNMP/UPnP/WiFi-management daemons); versions read only from embedded banner strings, never guessed; ports inferred from UCI config → inetd.conf → init.d launch flags → known defaults. New "Network Services" tab + dashboard service/port-count cards. Fixed duplicate service counting from same-named config/init.d files vs. the real ELF binary.
- v3.4 — Real certificate detection (RSA vs. non-RSA per certificate,
bundle/chain-aware, via the
cryptographylibrary instead of a PEM-header guess). Dashboard gains component/config-file/certificate counts and a BusyBox overview card row. - v3.3 — Fixed a v3.2 regression where config files silently lost their
click-to-preview affordance. Binaries/Scripts tabs moved from
?all=1bulk loads to real server-side pagination. - v3.2 — New "config" file classification (extension/basename/UCI-path/
content-sniff) with a server-side Files-tab Kind filter. New
config_audit.pycontent-hardening rule (telnet, anonymous FTP, debug mode, default SNMP community strings, disabled TLS verification, WPS, UPnP left enabled). CVE numbers throughout the UI now link to NVD. Zero-dependency syntax highlighting for file/BusyBox previews. Fixed compare-scan silently reporting "0 added / all removed" instead of a clear error on a failed report fetch. - v3.0 — Stripped-ELF function-boundary recovery (direct-call + prologue
discovery, ARM/Thumb auto-detection). CVE sync reliability fixes
(upstream
cve-bin-toolbug patches, NVD GitHub-mirror bootstrap script). Findings/binaries/scripts/components migrated from one JSON blob per job to SQLite with server-side pagination, fixing both a UI crash on large scans and a hidden export-truncation bug. New BusyBox applet audit tab (compiled vs. reference-list diff). Fixed a severe compare-scan accuracy bug caused by absolute-path fingerprint matching. Fixed the dedup cache silently serving pre-upgrade reports after an analyzer version bump. - v2.0 — Bilingual docs (this
README.zh-CN.md), a Tech stack section, and a References & acknowledgements section (crediting EMBA and cve-bin-tool). - v1.0 — Initial release: multi-arch disassembly & import-call resolution (x86/x86_64, ARM, ARM Thumb-2, AArch64, MIPS LE/BE), vulnerability discovery (dangerous functions, taint/reachability, cross-binary taint, CVE correlation, CycloneDX SBOM, prioritization + triage persistence), embedded-secrets and script-injection detection, filesystem hardening, optional Ghidra decompilation, and the Go service layer + Alpine.js web UI.
- Replace heuristic taint with angr behind the existing
detect_taint_paths/detect_cross_binary_taintinterfaces. - FR-VUL-4: integer-overflow-into-alloc/copy detection (skipped so far — a naive "tainted size reaches malloc/realloc" sink would be too noisy given how ubiquitous allocation is; needs real argument-level analysis, e.g. spot a multiply feeding the size operand, not just reachability).
- FR-VUL-6 optional sandboxed emulation to confirm reachability, and true
dynamic network-service probing (distinct from the static, signature-based
service identification already shipped — see
inventory/service_id.py). - FR-REP-2 SPDX output alongside CycloneDX.
- Service layer: unify job-queue storage (
service/job.go, per-job JSON files) with the report store (service/reportdb.go, SQLite) onto one backend. Auth (pbkdf2 + per-account lockout + login CAPTCHA,service/auth.goservice/captcha.go) and persistent report storage are already built.
Call resolution for x86/x86_64, ARM, ARM Thumb-2, AArch64, and MIPS LE/BE is all in place.



