FirmAgent
September 3, 2026 · View on GitHub

FirmAgent is a research toolkit for collecting runtime taint/coverage data from authorized, re-hosted IoT web services. It combines IDA-based pre-fuzzing, QEMU instrumentation, API fuzzing, and optional LLM-assisted taint analysis.
Only use it on firmware and services that you own or are explicitly authorized to test.
Requirements
- Python 3.8+
- IDA Pro with
idat/idat64for pre-fuzzing - Docker and a prepared Greenhouse/re-hosting image for runtime tests
- An instrumented QEMU binary and a shared host directory for its trace log
- An LLM endpoint only when using API extraction or
LLMATaint.py
Install Python dependencies:
pip install -r requirements.txt
Set tools and LLM configuration through the environment. Do not commit keys:
export IDAT_BIN=/path/to/idat64
export Private_API_KEY=...
export LLM_BASE_URL=https://your-endpoint
export LLM_MODEL=your-model
Reproduction workflow
1. Pre-fuzzing
Run IDA decompilation, sink-scope extraction, and CFG distance calculation:
Pre_Fuzzing/run.sh /path/to/httpd
The command writes artifacts next to the target binary:
export-for-ai-httpd/ IDA decompilation evidence
sink_scope_addr.txt QEMU address filter ranges
sink_cfg.json real CFG, roots, edges, and sink blocks
sink_distance_scores.json per-block distance/proximity/score
sink_distance_scores.csv tabular version of the scores
API and parameter extraction is an optional LLM step. The command is intentionally commented out in run.sh; run it explicitly when needed:
python3 Pre_Fuzzing/llm_extract_api_params.py \
--input /path/to/export-for-ai-httpd \
--output /path/to/Pre_fuzzing.json
Pre_fuzzing.json must contain at least:
{
"api_endpoints": ["/apply.cgi"],
"para": ["action", "mode"]
}
It may also contain mutation values:
{
"dictionary": {
"global_values": ["0", "1", "enable", "disable"],
"parameter_values": {
"action": ["apply", "save"],
"mode": ["ap", "sta"]
}
}
}
2. Start the re-hosted service
Create a running Greenhouse/re-hosting container with /scratch mounted to a host directory. Start the service under instrumented QEMU and restrict tracing to the sink scope:
FuzzingRecord/rehost_session.sh start \
--container <container> \
--dfilter "$(python3 FuzzingRecord/make_dfilter.py /path/to/sink_scope_addr.txt)"
The launcher sets:
QEMU_DFILTER comma-separated sink-scope ranges
QEMU_LOG=exec,nochain
QEMU_LOG_FILENAME=/scratch/qemu_trace.log
Use FuzzingRecord/rehost_session.sh stop --container <container> after fuzzing. This resets the trace log. Keep the traced service running only during an active session.
3. Run the fuzzer
Legacy taint traversal (no distance/coverage feedback):
python3 FuzzingRecord/Fuzzer.py \
--json-file /path/to/Pre_fuzzing.json \
--host <container-ip> \
--delay 0.5 \
--tag-mode unique
CFG-guided feedback mode (requires both a distance file and QEMU log):
python3 FuzzingRecord/Fuzzer.py \
--json-file /path/to/Pre_fuzzing.json \
--distance-file /path/to/sink_distance_scores.json \
--qemu-log /host/shared/qemu_trace.log \
--host <container-ip> \
--feedback-settle 0.3 \
--iterations 1000 \
--random-seed 1 \
--tag-mode unique
In feedback mode the fuzzer combines parameter dictionary mutations with runtime basic-block/edge coverage and shortest distance to a sink. If the trace file is missing, rotated, or truncated, that iteration is marked invalid and the loop continues.
Outputs are written beside Pre_fuzzing.json:
result.json request/response and feedback records
result_stream.ndjson append-only request stream
fuzzing_summary.json coverage, distance, corpus, and mutation statistics
4. Build Source.json and analyze taint
For exact request-to-trace attribution, run the sidecar before fuzzing and use unique tags:
python3 FuzzingRecord/taint_monitor.py \
--stream /path/to/result_stream.ndjson \
--qemu-log /host/shared/qemu_trace.log \
--out /path/to/trace_windows.jsonl \
--settle 0.3
After fuzzing:
python3 FuzzingRecord/build_source_json.py \
--qemu-log /host/shared/qemu_trace.log \
--windows /path/to/trace_windows.jsonl \
--result-stream /path/to/result_stream.ndjson \
--output /path/to/Source.json
Then run the optional LLM analysis:
python3 LLMATaint.py \
-b /path/to/httpd \
-p True \
-t command_injection \
-o /path/to/results
Place Source.json in the same directory as the target binary. Indirect_call.json or indirect_data.json may also be supplied when available.
Example Commands Recap
Run pre-fuzzing (single command):
./Pre_Fuzzing/run.sh /path/to/httpd
This generates Pre_fuzzing.json, sink_scope_addr.txt, and distance scores in the same directory as the binary.
Run fuzzing (host):
⚠️ Remember to customize FuzzingRecord/Fuzzer.py request templates first!
export QEMU_DFILTER="Sink scope" (docker)
python FuzzingRecord/Fuzzer.py \
--json-file Pre_fuzzing.json \
--delay 0.5 \
--host 192.168.0.1
This generates result.json (request packets and responses). A separate runtime-monitoring step should generate Source.json / source.json for taint analysis.
Run taint-to-PoC agent:
Place Source.json or source.json in the same directory as the binary, then:
python LLMATaint.py \
-b ./bin/httpd \
-p False \
-t ci \
-o ./results/ASUS \
-m R1_official
Reference
If you use or cite this work, please reference:
@inproceedings{Ji2026FirmAgent,
author = {Ji, Jiangan and Zhang, Chao and Gan, Shuitao and Lin, Jian and Liu, Hangtian and Liu, Tieming and Zheng, Lei and Jia, Zhipeng},
title = {FirmAgent: Leveraging Fuzzing to Assist LLM Agents with IoT Firmware Vulnerability Discovery},
booktitle = {Network and Distributed System Security (NDSS) Symposium},
year = {2026},
month = {February},
pages = {1--16},
address = {San Diego, CA, USA},
doi = {10.14722/ndss.2026.231943},
isbn = {979-8-9919276-8-0},
url = {https://dx.doi.org/10.14722/ndss.2026.231943},
}