IRIS:从 HTTP 000 到断点可命名,固件重托管平台
原创
JiuZero
JiuZero
90Safe
2026年10月12日 12:30
广东
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
做固件仿真的师傅大概都被同一个东西磨过:HTTP 000
binwalk 解包成功,QEMU 也起了,串口日志刷得欢快,浏览器一访问,就一个 HTTP 000。
是没解包?内核没起?网卡没配 IP?路由不通?服务没监听?端口转发没建立?同一个符号,六种完全不同的病因。于是研究者的时间大量花在重新手工走一遍工具内部的每一步,而不是花在设备本身上。
学术界这条路线做了十年,Avatar → firmadyne → FirmAE → FirmPilot,瓶颈从来不是「跑不起来」,而是跑不起来的时候说不清为什么。更糟的是失败会伪装成结论,我自己的评测记录里,DIR-868L 的结论被推翻过两次(第八节细说),两次都记录得煞有介事,两次都是错的。
IRIS(IoT Rehosting & Interconnection Simulator,鸢尾)
设计目标可以写成一句话,不去承诺一个更高的成功率,承诺每一次失败都能被命名
一、先交代问题设定
上面那六种病因,工具一个都答不上来。但它们其实分属完全不同的层,有的在固件里,有的在宿主上,有的在两者之间的网络上。同一个 HTTP 000 下面,藏着的可能是一个坏固件,也可能是一个可以两行配置修好的宿主问题。
把「失败说不清楚」当成一个工程问题来解,是 IRIS 整个设计的起点。它不追求在跑不起来的固件上硬变出 200,它追求的是每一次跑不起来的时候,你手里多一张写着断点名字和下一步动作的纸。
二、系统总览
整个系统分五层。L1 提取层做格式识别与 rootfs 解包,squashfs 是唯一自动剖分路径,加密厂商格式只识别不解密,解出来过 ELF 架构校验才入库。
L2 仿真层用 QEMU 全系统仿真,铺了 mipsel / mipseb / armel / arm64 四条架构通道,外加 Docker 桥接、端口转发和串口采集。L3 是规则层,L5 是交付面,CLI、FastAPI 和 Web 工作台共用同一套 API。
能力概览 
工作流程图 
主流程走起来是这样一条线。上面一行是成功路径,下面一行是 IRIS 最大的差异点,绝大多数工具走到「Web 不可达」就停了,IRIS 从那里开始才真正干活。
三、创新一:分层失败画像,把 HTTP 000 拆成能落库的词
为什么要有它 
一开始大家的想法都一样,失败嘛,把报错字符串打出来就行。
结果挺难看。提取层一个函数里写 \"unsupported-arch: ...\",仿真层另一个函数里写 f\"Unsupported architecture: {arch}\",还有一个函数干脆一句散文,连归类都没有。没法定义就没法计数,仪表盘想统计失败分布,只能去 grep 英文句子。我翻文档的时候还发现 docs/eval-log.md 声称「8 个失败阶段」,代码里实际只有三个不同的字面量。文档在许愿,代码不认账。
问题出在「失败描述」这件事被允许自由发挥,而统计这种东西一旦自由发挥就没法做。
于是换成一张封闭词表(src/iris/failures.py)。
class FailureKind(StrEnum):
\"\"\"Every way a firmware can fail to come up, and nothing else.\"\"\"
# -- extraction: the rootfs could not be recovered from the image -----------
NO_ROOTFS = \"no-rootfs\"
ENCRYPTED_FIT = \"encrypted-fit\"
...
# -- network: measured, layer by layer, by ``emulate.linkprobe`` ------------
LINK_NO_ROUTE = \"link-no-route\"
LINK_NO_ARP = \"link-no-arp\"
...
用封闭 StrEnum 而不是开放字符串,是因为封闭词表的点就在于它是不完整的反义词。一个新的失败如果在表里没人登记过,会以未映射值的形式炸出来,而不是悄悄滑进一个没人聚合的新桶。
同一种死法之间,还隔着诊断价值 
词表里我最得意的是几对「看起来一样、其实必须分开」的拆分,源码注释写得很直白。
#: A panic the kernel could not continue past... Kept apart from REBOOT_LOOP
#: (a guest can panic and stop rather than ask for a reboot), because
#: \"no init\" and \"one process died\" call for different work.
GUEST_KERNEL_PANIC = \"guest-kernel-panic\"
#: The kernel took an exception, dumped registers and kept running. This is
#: not a failure and is never counted as one: ... calling that a failed run
#: would put a working signal into the failure histogram.
GUEST_KERNEL_OOPS = \"guest-kernel-oops\"
翻译成人话,「guest 内核崩了再也起不来」和「内核吐了个 oops 但还活着,只是 web 面忽好忽坏」是两种病。前者要查启动链,后者要查那个死掉的进程。把后者算进失败直方图,等于往统计里掺一个还活着的信号。
再比如REBOOT\_LOOP和NVRAM\_UNREADABLE也刻意分开。「guest 重启了 28 次」和「guest 重启是因为 nvram 分区读出来是坏的」需要相反的处理。一个该等更久,一个该去仿真 flash。一个只认识「重启循环」的直方图,会把人往「再等等」上引。
一次失败,对应一个下一步 
这个区分不是措辞偏好。link-no-service 意味着二层通、地址配好、但固件里根本没有 web 服务,下一步是换固件或接受能力边界。link-no-arp 意味着该去修宿主内核。词表把「下一步去哪」写进了失败本身。
运行详情页的失败归因,结论、运行参数、四层链路探测表、失败画像与修复账本在同一屏。失败不再是一个孤零零的 HTTP 000。
四、创新二:四层链路主动探测,失败时告诉你包死在哪一层
失败画像负责命名,链路探测负责取证(src/iris/emulate/linkprobe.py)。
失败路径上,IRIS 会按 route → ARP → ICMP → service 逐层实测。二层有没有路由,邻居表里有没有那个 IP,ICMP 通不通,端口有没有人监听。最后直接产出一张链路分层表,告诉你断在哪一层、哪一层没测到,而不是丢给你一句「网络不通」。
这里有一条我特意写进产出的诚实边界,只在失败路径上探测,成功路径不探测。所以成功的运行没有这张表。这不是偷懒,是设计。只有需要解释失败的时候,才应该付探测的代价;给成功运行硬画一张四行全绿表,除了好看没有任何信息量。
连带两条已知细节也写在文档里。ARP 层在没有邻居表条目时无法区分「没有这个地址」和「问过但没人应」,此时该层如实记 unknown。四层状态与首个断点会落库,但每层的原始探测文案不落库。能画图的数据和不能画图的数据,页面分开说清楚,而不是画一张空表装样子。
五、创新三:零 Python 的规则引擎,修复决策是可审计的 YAML
很多同类工具的「自动修复」是往固件里塞一个 httpd 了事。IRIS 不这么干,注入外部服务会改变被测设备的真实行为,仿真出来的「可达」是假的。
IRIS 的修复决策由规则引擎承担(src/iris/rules/engine.py)。规则是纯 YAML,零 Python,放在 rules/ 目录,目前 6 条实证规则。看一条真实的,Tenda 系固件 web 服务拉不起来时的前置修复,节选。
id: tenda-web-server-forced-start
stage:service
detect:
# rcS 单独一条会命中语料里每一台固件,所以必须
# 「rc 链 + 厂商 web 二进制」同时成立
-all:
-path_exists:'etc/init.d/rcS'
-any:
-path_exists:'opt/goahead/goahead'
-path_exists:'bin/boa'
actions:
-guest_shell:
-\"if [ -x /opt/goahead/goahead ]; then\"
-\" touch /opt/goahead/route.txt\"
-\"fi\"
post_action_verify:
check_files:
-path:'opt/goahead/route.txt'
expected:'required by iris_net_fix.sh before it will launch goahead'
health_check:
command:\"test -f /opt/goahead/goahead && { test -f /opt/goahead/route.txt || test -f /etc/boa/boa.conf; }\"
expected_exit_code:0
这条规则能立得住,靠的是咬合在一起的几层设计。
- detect 条件防误伤,rcS 单独一条会命中语料里每一台固件,规则等于到处开火,所以 detect 要求 rc 链和厂商 web 二进制同时成立,规则只在该出手的地方出手。
- 修复后带证据校验,post\_action\_verify 不信「我改好了」这句话,只认文件和 health check 的返回码,每条修复都能被验证、被回滚、被回归测试覆盖。
- 上传先校验再落盘,Web 工作台可以上传外部规则插件,但引擎先完整校验,只要有一个键不认识、或加载时产生任何告警,就拒绝安装并把原因原样退回,磁盘上不留文件。
有师傅可能要问,都 2026 年了,为什么不整一个大模型自动修。我的想法是,修复决策目前由可审计的规则承担,因为每条修复都要能复现、能回滚,不可复现的模型输出没有资格直接碰被测设备。AI 的位置我已经想好了,草稿层。长尾失败由 LLM 起草规则,过三道安全门(校验、证据、人工确认)之后沉淀回 YAML 规则库
Web 工作台的插件中心,内置与外部规则同屏管理,上传先校验再落盘,有告警即拒绝安装
六、和 FirmAE 掰手腕:同批实测
说创新不 SOLagan,直接上数据。同批 9 台有效语料(10 台剔除 1 份 GPON 残包):
逐台看过去,IRIS 占优 4 台(Newifi D2 与 US TES7002 为完整占优,WRT1200AC、R7800 仅在提取层占优),平 4 台,FirmAE 占优 0 台,无法判定 1 台。
在 FirmAE 能够进入仿真的同一批语料上,IRIS 的 Web 可达数是它的两倍(4 : 2),且 FirmAE 没有一台 IRIS 做不到而它能做到
赢要赢得清楚,输不输也要说明白。没法判定的那台 i27V11br 是厂商加密 FIT(YZTenda),两侧都解不开,这是语料属性,不是任何一方的能力短板,不计入能力比较。WRT1200AC 和 R7800 上 IRIS 的优势只在提取层,仿真侧两侧都失败,根因是重宿主内核自身的 BUG(validate\_nla,nlattr.c:41,两台 pc 完全相同),不在 IRIS 代码内可修,既不能算 IRIS 的优势,也不能算 IRIS 的劣势。
最有意思的是架构判定。US TES7002(aarch64)上 FirmAE 把它误判成 armel,用 32 位内核跑出 kernel panic(ENOEXEC),IRIS 判对并拿到 HTTP 302。但差别在 IRIS 的判定链上多一个第二证据源(解压后 ELF census,639/640 命中 aarch64),不是「IRIS 的架构处理更严谨」。IRIS 自己的 inspect 命令在同一份固件上也判错了,这在 docs/08 §9.1 如实记录,本轮未修。
第三条值得多说一句。aarch64 这台是 arm64 通道的直接受益者,Alpine generic virt 内核加自建 initramfs,直跑厂商 /sbin/init,Web 登录页 HTTP 302(96.8s)。这本来就是 FirmAE 处理不了的语料类型。但 IRIS 的判定链也不是魔法,它只是多查了一遍解压后的 ELF 普查。多一个证据源,比多一份自信有用。
七、Web 工作台:一条命令把以上全部摊开给你看
上面这些能力,不需要翻命令行手册。iris web 一条命令,浏览器打开就用。
cd web && npm install && npm run build:pkg # 首次必须一次
iris web # http://127.0.0.1:9000/
打开总览页,先看到四张统计卡,仿真记录、Web 可达率、失败聚类、快速开始依次摆下来。失败聚类按最近出现的排序,因为你要处理的多半就是最近那次。所有数字服务端聚合,与 CLI 口径同源,工作台看到的和命令行算出来的是同一份。
「实例」页是记录查看与清理。托管中的实例与全库运行历史两张表,点任意一行打开该次运行的详情窗口,结论、运行参数、四层链路、失败画像、修复账本。清空只删运行记录,不动已登记的固件语料。
「插件中心」列引擎会加载的全部规则插件,内置的 rules/\*.yaml 加上传到安装的外部插件,每条都能看到版本、启用状态和校验结果。「工作策略」页给出的是能力矩阵,工作台公开承诺什么、不承诺什么,一屏看完。
工作台本身也按「诚实」的标准做的。启动接口是阻塞的,页面只显示已等待秒数,不画进度条,因为一个什么都不报的进度条是带百分号的谎话。「设置」是底栏最右侧的一个纯图标,不占侧栏导航位,侧栏里每一项都是关于「工作本身」的入口,换主题不是其中之一。这种取舍不一定对,但它是一致的。
「外观」的设置浮层可选 12 套主题(10 套深色含 aurora / sunset / ocean,2 套浅色),界面密度、字体与动效开关也在里面。偏好只存浏览器本地。0.3.30 把只能手改 .env 的十项配置变成了面板可写(覆盖层优先级,危险配置默认锁死),命令面板 Ctrl/Cmd+K 可以两步切换主题。
八、失败会伪装成结论:DIR-868L 的两次翻案
这一节是整个项目动机的真实注脚,我的评测记录里就躺着它。最初记成「服务起、VLAN 路由不通」,记录得煞有介事,是错的。0.3.11 改记成「宿主与 guest 不同子网导致全网失败」,看起来更像回事了,还是错的。
最终查明真实根因。run\_qemu.sh 把宿主桥放在 192.168.1.254/16,
而目标网络是 192.168.0.0/24,宿主地址不在 guest 子网内被静默丢弃。
修复是在宿主桥上补一个落在 guest 子网内的地址(.254,guest 占用时退 .253)。
结论是宿主基础设施缺陷,不是固件缺陷,也不是 VLAN 问题。两次「像样的结论」都活了一段时间,如果没有落库的失败画像和口径工具,第三次翻案迟早还会来。
所以后来加了 iris corpus eval,判定从语料清单加实测记录里算出来,而不是手写维护。三种口径分开报。对外报可达率时,分母含环境失败条目,因为用户依然没拿到设备。
算能力口径时,分母剔除环境失败,只衡量宿主健康时能做到什么。另外按设备点名回归,因为 4/9 涨到 5/9 完全可能藏着一修一坏,比率相等时尤其如此。
数字不骗人的前提是,算数字这件事本身可执行、可回归
九、当前能力与诚实边界
| 层 | 能力 | 状态 |
|---|---|---|
| L1 提取 | 格式识别(squashfs / uImage / UBI / FIT magic / 加密厂商格式仅识别不解密)、rootfs 解包、ELF 架构校验入库 | ✅ |
| L2 仿真 | QEMU 全系统仿真,四架构通道 mipsel / mipseb / armel / arm64;架构预检不符直接给正确建议;Docker 桥接 + 端口转发 + 串口采集 |
✅ |
| L2 诊断 | 链路分层主动探测 :失败路径实测 route / ARP / ICMP / service,直接命名断点所在层 | ✅ |
| L3 规则 | YAML 修复规则引擎(6 条实证规则),可插拔、可回归,修复后带证据校验 | ✅ |
| L4 交互 | RTSP/ONVIF 媒体面 | 🔬 规划中 |
| L5 交付面 | Typer CLI + FastAPI 一条 /api/v1/pipeline + Web 工作台 + API 鉴权与仿真归属隔离 |
✅ |
README 里写着六条设计边界,每条一句话。其中我最想让人知道的是这三条。兜底刻意不注入外部 httpd,IRIS 只复用固件自带且已识别的 web 服务,所以「固件里没有 web 服务」这种情况 IRIS 兜不出来,这是能力边界,不是缺陷。arm64 通道与 MIPS/ARM32 的插桩通道能力不等价,arm64 下不提供 libnvram 与 console 劫持,这是为了覆盖 FirmAE 处理不了的 aarch64 语料做的取舍。固件镜像自动剖分只认 squashfs,ext4、cramfs 等会明确落到 no-rootfs 失败画像,而不是静默产出一份错误的 rootfs。
另外还有些已知缺陷,inspect 与 emulate 架构判定不一致、busybox 版本误判之类,全部已定位、如实记录、不美化,索引在 docs/12-设计边界与技术限制.md
十、开源地址
- GitHub:github.com/JiuZero/IRIS
- 技术栈:Python 3.11+ / QEMU / Docker / FastAPI / React + TypeScript
- 华为云码道Agent创新赛展览:
- https://gallery.developer.huaweicloud.com/gallery/0055251646800000
写在最后
这个项目花时间的地方多半集中在跑不起来之后:把一个 HTTP 000 拆成能命名的断点、让每个断点对应明确的下一步、以及让统计口径不骗人。
成功的故事千篇一律,失败的名字各有各的写法。
作为把课余时间(以及相当一部分睡眠时间)投进去的大学生,我很清楚 4/9 这个数字离「好用」还有距离
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论