2. 固件依赖生态全景

固件并不是一块“从零写起”的代码。任何一款可量产的固件,其二进制体积中往往只有很小一部分是团队自己编写的业务逻辑,其余大部分来自外部依赖:芯片原厂提供的 SDK、开源 RTOS、第三方协议栈、通用算法库、驱动代码以及各类工具链生成的运行时代码。理解这些依赖在固件中的分布位置与作用,是后续做依赖清点、漏洞映射和风险治理的前提。

一、固件依赖的六类构成

从工程视角看,固件依赖可以按“离硬件的远近”和“来源”两个维度划分。常见的有六类:

  • SDK(芯片/模组原厂包):由 SoC 或模组厂商提供,包含启动代码、时钟与外设初始化、HAL、链接脚本、示例工程。它通常决定了固件的启动流程和内存布局,是依赖树的“根”。
  • RTOS 与调度内核:如 FreeRTOS、Zephyr、RT-Thread、ThreadX。提供任务调度、信号量、消息队列、内存管理。它位于 SDK 之上、业务之下,是并发模型的基础。
  • 协议栈:TCP/IP(lwIP、uC/TCP-IP)、BLE(BlueZ、NimBLE)、Zigbee/Thread、MQTT、CoAP、TLS(mbedTLS、wolfSSL)。协议栈通常以库形式链接,是网络攻击面最集中的区域。
  • 第三方通用库:cJSON、FatFs、LittleFS、LZ4、mbedTLS、libcurl(部分嵌入式裁剪版)、SQLite 等。它们解决解析、存储、压缩、加密等通用问题。
  • 驱动代码:外设驱动(SPI/I2C/UART/ADC)、传感器驱动、Flash 与存储驱动、无线射频驱动。驱动直接操作寄存器,是稳定性与安全性的关键边界。
  • 工具链与运行时:newlib/picolibc、libgcc、启动文件 crt0、C 运行时。它们不显式出现在源码目录,却真实存在于最终镜像中。

这六类依赖在固件中的分布并非平铺,而是有清晰的层次关系。下图给出一个典型的 MCU 固件依赖分层结构,并标注了各层的主要来源与典型风险关注点。

典型 MCU 固件依赖分层结构 自下而上:硬件 → 原厂 SDK → 内核/协议栈 → 通用库 → 业务逻辑 硬件层 MCU / SoC · Flash · RAM · 外设寄存器 · 射频前端 物理边界 原厂 SDK / HAL 启动代码 · 时钟/电源初始化 · 外设 HAL · 链接脚本 · 示例工程 版本碎片化 RTOS 内核 FreeRTOS / Zephyr / RT-Thread 调度 · 信号量 · 队列 · 内存管理 驱动层 SPI / I2C / UART / ADC / Flash / 射频 直接操作寄存器 · 中断上下文 协议栈 lwIP · mbedTLS · NimBLE · MQTT · CoAP · Zigbee/Thread 网络攻击面最集中 · 解析逻辑复杂 · 历史 CVE 高发 高危区 第三方通用库 cJSON · FatFs · LittleFS · LZ4 · SQLite · 算法库 解析/存储/压缩 · 传递依赖多 · 许可证需审查 业务逻辑 / 应用层 设备功能 · 状态机 · 配置管理 · OTA 客户端 团队自研代码 · 依赖调用方 · 漏洞触发点 依赖调用方向 ↑

二、依赖在固件中的“分布”意味着什么

“分布”不只是说这些组件存在于工程里,而是指它们在最终镜像中占据不同的地址区间、运行在不同的特权级、承担不同的职责。以一款基于 Cortex-M4 的 Wi-Fi 智能插座固件为例,其 Flash 占用大致呈现如下分布(以下为教学示例,非实测数据):

区域典型占比主要来源治理关注点
Bootloader4%~8%原厂 SDK + 自研签名校验、回滚保护
RTOS 内核6%~12%开源 RTOS内核 CVE、配置裁剪
协议栈20%~35%lwIP、mbedTLS、Wi-Fi 驱动解析漏洞、TLS 版本
第三方库10%~18%cJSON、FatFs 等传递依赖、许可证
驱动与 HAL15%~25%原厂 SDK版本锁定、补丁状态
业务逻辑10%~20%团队自研调用方式、输入校验

可以看到,协议栈与第三方库合计往往超过固件体积的一半。这意味着:固件的安全水位,很大程度上不由自研代码决定,而由外部依赖决定。一个团队即使业务代码写得再规范,只要 lwIP 或 mbedTLS 停留在存在已知漏洞的版本,设备就仍然暴露在风险中。

三、各层依赖的作用与风险特征

SDK 是“地基”,也是版本锁定的源头。原厂 SDK 通常以压缩包形式发布,内部又嵌套了 RTOS、协议栈、驱动和第三方库。很多团队直接使用 SDK 自带版本,导致无法单独升级其中的 lwIP 或 mbedTLS。治理时首先要做的,是把 SDK 内部的依赖“拆出来”单独识别,而不是把整个 SDK 当作一个黑盒。

RTOS 决定并发模型,也决定漏洞影响面。FreeRTOS 的队列、信号量、任务栈溢出等问题,一旦被触发往往导致整机复位或提权。Zephyr 等较新的 RTOS 引入了更多安全机制(如 MPU 隔离、栈保护),但配置复杂度也更高。选择与配置 RTOS 时,需要明确哪些内核功能被裁剪、哪些调试接口被关闭。

协议栈是攻击面最集中的区域。以 lwIP 为例,其 IP 分片重组、TCP 选项解析、DHCP/ARP 处理都曾出现过 CVE。mbedTLS 的握手解析、证书验证同样是高危区。协议栈的风险特征是:输入完全来自网络,解析逻辑复杂,且往往运行在中断或高优先级任务中。因此协议栈的版本管理和补丁跟踪必须最严格。

第三方库的风险在于“传递依赖”和“使用方式”。cJSON 本身很小,但如果业务代码在解析不可信 JSON 时未限制嵌套深度,就可能造成栈溢出。FatFs 在挂载恶意构造的 FAT 镜像时也可能越界。这类库的漏洞往往不是库本身“写错了”,而是调用方没有按安全边界使用。

驱动与 HAL 的风险偏向稳定性与权限边界。驱动直接操作寄存器,一旦缓冲区长度校验缺失,可能造成 DMA 越界或外设异常。原厂驱动的更新通常随 SDK 发布,独立打补丁困难,因此需要在选型阶段就评估其维护活跃度。

四、从“知道有哪些依赖”到“能治理”

要真正治理依赖风险,第一步是建立固件依赖清单(FBOM,Firmware Bill of Materials)。它不是简单列一个库名列表,而应至少包含:组件名称、版本号、来源(原厂/开源/自研)、许可证、在固件中的位置(静态库/源码/预编译二进制)、已知 CVE 状态、是否可独立升级。

实际操作中,可以通过以下线索交叉识别依赖:

  • 构建系统中的包管理文件(如 Zephyr 的 west.yml、PlatformIO 的 lib_deps、CMake 的 FetchContent)。
  • 源码目录中的第三方文件夹(third_party、external、components)。
  • 最终镜像中的版本字符串、许可证文本、调试符号。
  • 二进制比对:将已知开源库的编译产物与固件中的代码段做相似度匹配。
依赖清点不是一次性任务。每次 SDK 升级、协议栈替换或新增第三方库,都应同步更新 FBOM,并重新评估 CVE 影响。建议把 FBOM 纳入版本管理,与固件版本一一对应。

当依赖清单建立起来后,后续的漏洞映射、补丁评估、升级决策才有依据。否则,面对一个“某协议栈存在高危漏洞”的通报,团队甚至无法在几分钟内回答:我们的设备用没用这个组件、用的是哪个版本、能不能单独升级。这正是固件依赖生态全景要解决的核心问题——先看清,再治理。