4. 嵌入式系统安全基础

嵌入式设备的安全问题,往往不是从应用层开始崩坏的,而是从系统底座就已经埋下隐患。面试中考察这一节,面试官真正想确认的是:你是否理解一个嵌入式设备从加电到业务进程运行之间,安全边界是如何一层层建立的,以及每一层最容易被攻破的点在哪里。下面按启动链条的顺序展开,把 RTOS、Bootloader、Linux 内核与根文件系统这几块讲透。

一、RTOS 的安全模型与常见误区

RTOS(如 FreeRTOS、RT-Thread、Zephyr)通常运行在资源受限的 MCU 上,没有 MMU,任务之间共享同一物理地址空间。这意味着一个任务越界写,可以直接破坏另一个任务甚至内核的数据。因此 RTOS 的安全设计核心不是“隔离”,而是“最小权限 + 内存保护单元(MPU)”。

以 FreeRTOS 为例,启用 configENABLE_MPU 后,可以为每个任务配置独立的 MPU 区域。典型做法是把任务栈和私有数据放在只允许本任务读写的区域,把共享外设寄存器设为特权态可访问,把常量区设为只读。面试中常被追问的一个点是:MPU 区域数量有限(Cortex-M 通常 8 个),如何取舍?合理的回答是优先保护栈、内核对象和关键外设,而不是试图覆盖全部内存。

另一个高频考点是任务栈溢出。FreeRTOS 提供 configCHECK_FOR_STACK_OVERFLOW,设为 1 时在任务切换时检查栈指针是否越界,设为 2 时还会在栈末尾填充魔数并在切换时校验。实际项目中建议设为 2,并配合 uxTaskGetStackHighWaterMark() 观察历史最低剩余栈空间,据此调整栈大小。示例:某任务创建时分配 512 字(Cortex-M 上 1 字 = 4 字节,即 2KB),运行一段时间后 high water mark 返回 120,说明峰值只用了 392 字,可安全缩减到 448 字,节省约 256 字节 RAM。

二、Bootloader:信任链的起点

Bootloader 是设备加电后执行的第一段代码,也是建立信任链(Chain of Trust)的根。安全 Bootloader 至少要解决三件事:镜像完整性校验、镜像真实性校验、防回滚。

完整性校验常用 CRC32 或 SHA-256。CRC32 只能防传输错误,不能防篡改,因为攻击者改完镜像可以重算 CRC。真实性校验必须用非对称签名,典型流程是:厂商用私钥对固件镜像的哈希值签名,Bootloader 内置公钥,启动时计算镜像哈希并用公钥验签。以 ECDSA P-256 为例,签名 64 字节,公钥 64 字节,验签在 Cortex-M4 上约几十毫秒,对启动时间影响可接受。

防回滚需要版本号配合。Bootloader 在 OTP 或受保护的 Flash 区域保存一个单调递增的版本计数器,新固件版本号必须大于等于该计数器才允许升级,升级成功后更新计数器。否则攻击者可以把设备刷回存在已知漏洞的旧版本。

下面是一个简化的安全启动判断逻辑示例:

// 伪代码,示意安全启动校验顺序
bool secure_boot(void) {
    image_hdr_t *hdr = (image_hdr_t *)APP_BASE;
    if (hdr->magic != IMG_MAGIC) return false;
    if (hdr->version < read_otp_counter()) return false;   // 防回滚
    uint8_t hash[32];
    sha256((uint8_t *)(APP_BASE + sizeof(image_hdr_t)),
           hdr->length, hash);
    if (!ecdsa_verify(PUBKEY, hash, hdr->signature)) return false;
    return true;
}

判断依据很直接:任一条件不满足就拒绝跳转到应用,进入恢复模式或安全停机。面试中如果被问到“公钥怎么保护”,要答出公钥本身可以公开,但必须防止被替换,通常烧录在 OTP 或带读保护的 Flash 扇区。

三、Linux 内核裁剪与攻击面收敛

嵌入式 Linux 的安全配置,第一步是内核裁剪。裁剪不只是为了省空间,更是为了减少攻击面。一个默认的发行版内核可能启用上百个驱动和协议栈,而实际设备只用其中十几个,剩下的都是潜在漏洞入口。

裁剪的基本方法是基于 defconfig 用 make menuconfig 逐项关闭不用的子系统。例如设备没有蓝牙、没有无线、没有 USB Host,就关闭 CONFIG_BT、CONFIG_CFG80211、CONFIG_USB_HOST。同时关闭不需要的文件系统(如 CONFIG_EXT4_FS 若只用 squashfs + overlayfs)、关闭 CONFIG_DEVMEM(防止通过 /dev/mem 直接读写物理内存)、关闭 CONFIG_KEXEC。

裁剪后要配合内核加固选项。常用的有:

  • CONFIG_STRICT_KERNEL_RWX:内核代码段只读可执行,数据段不可执行,防止代码注入。
  • CONFIG_STACKPROTECTOR_STRONG:栈保护,缓解栈溢出。
  • CONFIG_RANDOMIZE_BASE(KASLR):内核地址随机化。
  • CONFIG_SECURITY_DMESG_RESTRICT:限制非特权用户读取 dmesg,防止信息泄露。
  • CONFIG_SECCOMP 与 CONFIG_SECCOMP_FILTER:允许进程用 seccomp 限制可调用的系统调用。

裁剪与加固完成后,判断是否达标的依据是:ls /dev 下不应出现业务无关的设备节点;cat /proc/filesystems 只列出实际使用的文件系统;dmesg 中不应有未使用驱动的初始化日志。这些都能作为面试中“你怎么验证裁剪效果”的回答素材。

四、根文件系统与运行时安全配置

根文件系统层面,最关键的几条实践是:只读挂载根文件系统、用 overlayfs 承载可写数据、禁用 root 直接登录、用非特权用户运行业务进程。

只读根文件系统的典型做法是把 squashfs 作为 rootfs,启动时挂载为 ro,再把需要持久化的目录(如 /data、/etc/config)通过 tmpfs 或独立可写分区挂载。这样即使攻击者拿到 shell,也无法直接修改系统文件,重启后一切恢复。

权限方面,业务进程应以专用用户运行,例如创建 app 用户,用 systemd 的 User=app 或 BusyBox 的 su -c 启动。配合 chmod 700 保护配置目录,chmod 600 保护密钥文件。若设备支持,可进一步用 seccomp 限制进程只能调用必要的系统调用,例如一个只做串口通信的进程,可以只允许 read、write、ioctl、exit_group 等少数调用。

五、面试中的综合判断

当面试官问“给你一个嵌入式设备,你怎么评估它的系统安全”,可以按启动链条回答:先看 Bootloader 是否验签、是否防回滚;再看内核是否裁剪、是否开启加固选项;然后看根文件系统是否只读、权限是否最小化;最后看 RTOS 场景下是否有 MPU 隔离和栈保护。每一层都给出具体的检查命令或配置项,而不是停留在“应该加固”这种空话。这样回答,既体现了对系统结构的理解,也展示了可落地的工程能力。

需要提醒的是,安全配置往往与调试便利性冲突。例如关闭 /dev/mem 后,一些底层调试工具会失效;只读根文件系统会让现场升级变复杂。面试中如果能主动指出这些权衡,并说明在开发阶段与量产阶段采用不同配置(开发阶段保留调试接口,量产阶段关闭),会显著加分。