4. 固件依赖关系解析

固件依赖关系解析是固件成分分析(SCA)与漏洞治理的起点。若依赖关系识别不完整,后续的 CVE 匹配、影响面评估和修复决策都会建立在错误的基础上。固件与普通桌面软件最大的差异在于:它通常是一个由引导程序、内核、根文件系统、内核模块、用户态库和应用程序共同拼装而成的镜像,依赖关系既可能被编译期固化,也可能在运行时才建立,还可能被构建配置裁剪或替换。因此,解析固件依赖必须同时理解四种依赖形态:静态链接、动态链接、内核模块和配置选项。

一、四种依赖形态及其识别特征

静态链接把库代码直接复制进可执行文件,产物中不再保留独立的库文件,也不存在运行时加载记录。识别静态链接依赖,主要依靠符号表、字符串常量和特征代码片段。例如,一个静态链接了 zlib 1.2.11 的二进制,即使文件系统中没有 libz.so,仍可通过 inflate 相关函数符号或版本字符串推断其存在。静态链接的难点在于版本判定:符号存在只能证明“用了这个库”,不能直接证明“用的是哪个版本”,需要结合字符串、构建路径、编译器注释等旁证。

动态链接通过 ELF 的 .dynamic 段、DT_NEEDED 条目和 RPATH/RUNPATH 记录依赖的共享库。解析时先读取每个 ELF 文件的 DT_NEEDED,得到“需要哪些 soname”,再在根文件系统的 /lib、/usr/lib 以及 RPATH 指定目录中查找实际文件,最后读取库文件自身的版本信息。动态链接的陷阱是 soname 与真实版本脱钩:libssl.so.1.1 可能对应 OpenSSL 1.1.1k,也可能对应 1.1.1w,必须读取库内嵌的版本字符串或构建标识才能确定。

内核模块以 .ko 文件形式存在,依赖关系记录在模块的 modinfo 中,包括 depends 字段和 vermagic。depends 描述模块之间的加载顺序依赖,vermagic 描述模块与内核版本的兼容性。内核模块常静态链接了内核内部库函数,也可能引用内核导出符号,因此其依赖识别要同时看模块间依赖和模块对内核版本的绑定。

配置选项不直接产生依赖,但会决定依赖是否被编译进去。例如 BusyBox 的配置决定启用哪些 applet,内核的 CONFIG_* 决定哪些驱动和协议栈被编入,OpenSSL 的编译选项决定启用哪些算法和协议。配置选项的影响是“开关式”的:同一个源码版本,在不同配置下可能包含完全不同的漏洞面。解析配置选项,需要从 .config、编译产物中的条件编译痕迹以及运行时行为反推。

固件依赖关系解析:四种形态与解析路径 固件镜像 引导程序 U-Boot / GRUB 内核镜像 vmlinuz / zImage 内核模块 *.ko 根文件系统 squashfs / ext4 用户态程序 ELF 可执行文件 配置文件 .config / 脚本 静态链接 库代码嵌入二进制 无独立库文件 动态链接 DT_NEEDED / soname 运行时加载共享库 内核模块 modinfo depends vermagic 绑定内核 配置选项 CONFIG_* / .config 决定依赖是否编入 解析技术与关键数据源 静态链接解析 符号表 .symtab / 字符串常量 / 特征代码 工具:strings、nm、radare2、BinDiff 动态链接解析 readelf -d / ldd / objdump -p DT_NEEDED → 查找 .so → 读版本字符串 内核模块解析 modinfo *.ko / modprobe --show-depends depends 链 + vermagic 版本匹配 配置选项解析 提取 .config / 编译日志 / 条件编译痕迹 反推启用组件与漏洞面 输出:SBOM 依赖清单 + 版本证据 + 配置上下文 → 漏洞匹配与影响面评估

二、解析技术的实际展开

对动态链接的解析,最直接的方式是遍历根文件系统中的 ELF 文件,逐个读取 .dynamic 段。以 readelf 为例,readelf -d /usr/bin/curl 会输出类似 NEEDED libcurl.so.4、NEEDED libssl.so.1.1 的条目。拿到 soname 后,在文件系统中定位实际文件,再用 strings libssl.so.1.1 | grep "OpenSSL 1" 读取版本字符串。若固件中库文件被裁剪或重命名,则需要结合 RPATH 和符号版本(.gnu.version_d)判断。对于静态链接,nm -D 看不到符号时,可尝试 strings 搜索库名和版本号,或用 objdump -d 观察函数序言特征。

内核模块的解析相对规整。modinfo xfrm.ko 会输出 depends、vermagic、srcversion 等字段。depends 给出模块间依赖,vermagic 给出内核版本和 SMP/预抢占等配置标识。若固件中模块被压缩或打包,需要先解包再解析。配置选项的解析更依赖上下文:从 /boot/config-* 或内核源码 .config 中提取 CONFIG_* 项,与已知漏洞的编译条件比对。例如,某个漏洞只在 CONFIG_USER_NS=y 时可达,那么即使内核版本匹配,若配置中该选项为 n,风险等级也应下调。

三、配置选项对依赖识别的放大效应

配置选项的特殊性在于,它不增加新的库文件,却会改变已有组件的漏洞面。以 OpenSSL 为例,若编译时启用了 enable-weak-ssl-ciphers,则即使版本较新,也可能暴露弱加密相关漏洞;若禁用了 no-heartbeat,则 Heartbleed 类漏洞可能被重新引入。因此,固件依赖解析不能只输出“用了 OpenSSL 1.1.1”,还要输出“启用了哪些编译选项”。实践中,可以从库文件的字符串、符号导出和运行时行为反推配置:例如,若 libssl 中不存在 SSL_heartbeat 符号,则说明心跳扩展被禁用。

解析固件依赖时,建议按“先动态、后静态、再内核模块、最后配置”的顺序推进。动态链接的 DT_NEEDED 最容易批量提取,能快速建立依赖骨架;静态链接和内核模块用于补全骨架;配置选项用于修正漏洞可达性判断。

四、一个可复用的解析流程示例

假设拿到一个 squashfs 根文件系统和一个内核镜像,可按以下流程操作:

  1. 解包根文件系统:unsquashfs rootfs.squashfs -d rootfs/。
  2. 提取所有 ELF 文件的动态依赖:find rootfs/ -type f -exec file {} \; | grep ELF,再对每个文件执行 readelf -d,汇总 DT_NEEDED 列表。
  3. 定位共享库并读取版本:对每个 soname,在 rootfs 中查找实际文件,用 strings 提取版本字符串,记录“库名—soname—实际版本—证据来源”。
  4. 识别静态链接组件:对没有 DT_NEEDED 但体积较大的二进制,用 strings 搜索常见库名和版本号,标记疑似静态链接。
  5. 解析内核模块:解包内核镜像中的模块目录,对每个 .ko 执行 modinfo,记录 depends 和 vermagic。
  6. 提取配置选项:从 /boot/config-* 或内核镜像内嵌配置中提取 CONFIG_*,与漏洞数据库中的编译条件比对。
  7. 合并输出:形成“组件—版本—依赖来源—配置上下文—证据”五列清单,作为后续 CVE 匹配的输入。

这个流程的关键在于证据链:每个版本判定都要有可追溯的来源,不能仅凭文件名或 soname 推断。例如,libz.so.1 只能说明是 zlib 1.x,具体是 1.2.11 还是 1.2.13,必须由库内字符串或构建标识确认。只有证据链完整,后续的漏洞匹配和修复决策才可靠。

五、常见误判与规避

第一类误判是把 soname 当版本。libcurl.so.4 可能对应 7.68.0,也可能对应 7.88.1,必须读库内版本。第二类误判是忽略静态链接。很多嵌入式程序把 busybox、dropbear 静态编译,文件系统中没有对应 .so,若只扫动态依赖会漏报。第三类误判是忽略配置选项。同一个内核版本,在不同 CONFIG_* 下漏洞可达性完全不同,必须把配置作为依赖解析的一部分。第四类误判是内核模块版本与内核版本脱钩。vermagic 不匹配的模块无法加载,解析时应以 vermagic 为准,而不是模块文件名中的版本号。

固件依赖关系解析的目标不是罗列文件,而是建立“组件—版本—依赖路径—配置条件”的可信映射。静态链接、动态链接、内核模块和配置选项分别对应四种证据来源,解析技术则围绕这四类证据展开。只有把四者结合,才能为后续的漏洞风险治理提供完整、可追溯的依赖基础。