ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

嵌入式系统安全实战:从启动到OTA的闭环加固

嵌入式系统安全实战:从启动到OTA的闭环加固 做嵌入式安全这几年我越来越有一个体会安全这事如果只在出厂前做一轮检查基本等于白做。设备一旦铺到现场固件怎么被读出来、串口谁在接、OTA包有没有被篡改、文件系统有没有被异常写入这些问题不会挑你有空的时候才发生。所以我在做方案设计时一直坚持一个原则——把安全能力做成运行态而不是交付物。这套东西做下来就是“Embedded System Security Live”这个名字的由来核心一句话让设备在启动、运行、维护的每一个环节都有实时可查、实时可响应、实时可追溯的闭环。这篇内容适合嵌入式工程师、物联网平台负责人、以及所有正在给设备做安全加固但不知道从哪下手的团队。我会把攻击面、加固思路、具体命令和踩坑记录全部摊开讲。不是什么高深的理论全是实际项目中验证过的东西。1. 嵌入式系统安全现状与Live模式的设计思路1.1 为什么“实时安全”是嵌入式系统的刚需传统IT安全可以做到每周扫描、月度补丁因为服务器有人看着网络有人守着。但嵌入式设备不一样一台工控机、一个边缘网关、一批智能终端部署下去之后基本是“无人值守”状态。攻击者可以在凌晨三点用调试口接进来可以在设备使用现场直接捅U盘也可以伪造一台服务器下发恶意升级包。这个场景下你不可能指望用户自己开机杀毒更不可能频繁派人去现场打补丁。更麻烦的是嵌入式设备的生命周期。大多数设备的预期寿命是五年甚至十年芯片算力有限存储空间紧张很多设备连TLS握手都费劲。这意味着Linux发行版上那套“装个Agent 云端分析”的安全模型在嵌入式环境里基本跑不动。安全方案必须足够轻必须嵌入到系统本身的启动流程、权限模型、文件系统布局和升级链路里让设备天生就有一定的“自保能力”。Live模式就是在这个背景下提出的。它强调的不再是“某个时间点安全”而是“整个运行周期里系统本身能感知异常、限制异常、记录异常”。比如启动时校验固件签名运行时锁定文件系统事件发生时能留下审计日志。这套体系不需要很强的CPU几个shell脚本和系统配置就能搭起来但效果却非常直接。1.2 Live安全体系的三层结构我把整个Live安全体系拆成了三层启动层、运行层、响应层。每一层解决的是不同阶段的安全问题三层合起来才能形成一个闭环。启动层解决“设备要可信地启动”。具体做的事情包括引导程序校验、内核镜像签名、根文件系统完整性验证。这一层的目标是哪怕攻击者把Flash芯片焊下来重写设备也会因为校验失败而拒绝开机或者自动恢复到上一个可信版本。业界常见的实现是Secure Boot 签名内核 dm-verity。运行层解决“设备在运行时不能被乱动”。具体包括文件系统挂载选项只读、noexec、nosuid、权限最小化、关键目录的ACL和文件属性、对外服务的认证授权、以及USB/调试口的访问策略。这一层的核心是让攻击者在进入系统后寸步难行想落盘落不了想执行执行不了。响应层解决“出了问题要知道、能追溯”。包括日志记录、关键文件哈希监控、端口和服务变化监控、告警上报。这一层不需要很重关键是敏感事件不能静默无声。很多嵌入式项目出事都不是没防护而是防护被绕过之后根本没留痕最后连怎么被入侵的都说不清。三层的关系可以这样理解启动层管“门锁”运行层管“室内隔断和保险柜”响应层管“监控摄像头”。只装门锁攻击者撬窗进来就畅通无阻只装监控设备被改得面目全非才发现三层都做才有意义。后面我讲实操时会按这个顺序一层层来。2. 攻击面盘点嵌入式系统最容易被突破的五个入口很多团队做安全加固总是从“加个密码”开始这是典型的姿势不对。嵌入式系统的攻击面非常具体而且和业务强相关。我整理了五个最高频的突破口每一条都是实际项目中反复出现的。2.1 调试串口与USB接口的物理访问风险先看物理接口。国内不少设备出厂时调试串口是直接暴露的U-Boot里不设密码root shell随便进。更常见的是USB口既能插U盘拷数据也能插HID设备模拟键盘输入还能插恶意网卡改变网络拓扑。很多现场工程师觉得“物理接触都控制住了”实际上设备部署在公共区域、厂房角落、甚至户外机柜里谁都能碰到。我在安全基线检查里一定会做两件事。第一串口控制台必须设置访问认证或者干脆在量产固件里禁用调试串口。第二USB接口做策略管理识别到非授权的USB设备直接阻止并在内核日志里记录事件。比如配置USBGuard或者在udev规则里做白名单绝大多数Linux内核都支持。这里有个容易忽略的点有些设备保留USB口是为了维护方便一旦禁用维护成本上去了。我的建议是维护口要做物理拨码开关或跳线保护默认关闭运维人员现场打开后再操作。这样既能满足维护需求又不会一直暴露攻击面。2.2 启动链缺失与Secure Boot游离态第二个突破口是引导链。很多设备的安全方案里写了“支持Secure Boot”但实际产品里Secure Boot根本没有使能或者只是启动了U-Boot的签名校验但内核和文件系统完全没校验。攻击者只要通过串口进入U-Boot修改bootargs指定一个假内核设备就完全被控制了。更隐蔽的是攻击者可以直接把整个Flash镜像读出来逆向分析后再制作一个篡改版写回去。Secure Boot的强度取决于“信任链”的长度。如果只校验第一级引导后面全裸奔那这个安全方案基本是摆设。正确的做法是ROM代码校验U-BootSPLU-Boot校验内核镜像内核校验根文件系统。每一级都验签验签的根密钥存储在一次性写入的OTP区域或独立安全芯片里保证攻击者拿不到私钥就签不出合法镜像。实际操作中Secure Boot最大的坑是“开启后启动失败”。原因通常是密钥没有烧录对、内核里没开启CONFIG_DM_VERITY、或者文件系统扩容后哈希不匹配。这些问题不是方案难而是很多团队根本不跑一次完整的“篡改固件-启动-失败”测试只在文档里写了“已支持”。2.3 文件系统权限错配与可写分区滥用第三个突破口是文件系统。嵌入式设备为了能远程升级、保存业务数据一般都会挂一个可写分区。这个分区如果恰好就是根文件系统分区那后果很严重攻击者拿到任意一个写入点就能替换脚本、修改配置、植入开机自启动程序。常见的设计错误包括根文件系统没有只读挂载、/tmp或/var/log用了可写权限却没有内存文件系统隔离、重要脚本的权限是777、敏感配置文件是644且所有用户可读。还有一个很典型的问题“could not set file security for file”说白了就是明明知道要设权限但没设成功或者设置的属性超过了底层文件系统的支持范围。比如在FAT32/exFAT的存储卡上执行chmod内核不会报错但权限根本不生效这就等于没设。文件系统加固必须明确划分分区角色系统分区只读数据分区单独挂载并加密临时目录用tmpfs日志要轮转。后面实操部分我会给出完整的fstab方案。2.4 Web管理与对外服务的认证缺陷越来越多的嵌入式设备提供了Web管理页面用来查看状态、修改参数、升级固件。这是业务需要但也成了攻击者的最爱。我见过不少设备Web服务开着默认密码后台接口不校验权限升级接口不验签攻击者只需要扫到IP就能直接接管设备。Web安全这块可以借鉴Spring Security这类成熟框架的“认证-授权-防护”三层思想但嵌入式环境通常跑不了这么重的框架。我的做法是能不开对外服务就不开必须开的话认证要放到独立的认证模块里避免前端校验接口权限要做角色区分登录和关键操作必须限流。所有后台服务默认绑定内网地址绝不直接暴露到公网。如果你的设备上有数据库或者视图类功能还要注意“最小权限”原则。不是所有业务账号都需要管理员权限也不是所有查询都能访问全库。嵌入式设备功能简单但越简单越要控制权限边界。2.5 证书信任链与OTA升级的中间人风险第五个入口是升级链路。OTA升级是嵌入式系统安全里最容易被忽视的环节。如果升级包没有强校验攻击者可以伪造一个升级包让设备更新成带后门的固件。如果证书信任链没处理好之前我遇到过“Android设备用adb命令往/system/etc/security/cacerts目录拷CA证书结果系统提示read-only file system”这就是典型的只读分区遭遇证书安装需求。证书和升级的安全设计包含几个层面。第一固件包必须有数字签名设备端验签后才能写入。第二负责验签的公钥要存放在防篡改区域绝对不能放在可写分区。第三如果业务确实需要新增受信任的CA证书那证书目录必须作为系统分区的一部分通过正式的OTA流程来更新而不是允许用户随便往系统目录里写文件。还有一点很多设备升级时只校验了下载通道比如HTTPS却没有校验升级包本身的签名。HTTPS只能保证“传输过程中没人改”但如果服务器本身被入侵或者设备被引导到一个伪造的HTTPS站点没有包签名就全完了。所以升级安全的标准动作永远是签名校验传输加密两条腿缺一不可。3. 核心实操从零搭建嵌入式Linux安全基线理论讲完了下面直接进入正题。我以一块常见的ARM64开发板、运行嵌入式Linux、使用U-Boot作为引导程序为例讲一套完整的安全加固流程。整个过程分四个阶段安全启动与分区规划、文件系统与权限加固、运行时监控与告警、OTA签名与证书管理。3.1 阶段一安全启动与分区规划分区规划是整个安全体系的地基。如果分区全是可读可写并且挤在一起后面所有安全措施都会大打折扣。我建议的分区结构如下分区挂载点文件系统权限策略作用mmcblk0p1/bootvfat只读ro存放内核镜像、dtb、U-Boot环境mmcblk0p2/ext4只读rodm-verity保护根文件系统mmcblk0p3/dataext4可写、加密业务数据、日志mmcblk0p4/mnt/updateext4可写、临时挂载升级包暂存区为什么要单独分一个/boot因为U-Boot默认从vfat分区读取内核镜像而vfat没有权限控制。如果这个分区可写攻击者直接往里面放一个篡改过的内核Secure Boot再严也没用。所以/boot必须在U-Boot阶段就校验完整性。分区完成后按下面步骤配置U-Boot签名校验# 在构建主机上生成RSA密钥对 openssl genrsa -out secure_boot.key 4096 # 生成公钥证书 openssl req -new -x509 -key secure_boot.key -out secure_boot.crt -days 3650 # 使用U-Boot的FIT工具对内核镜像签名 mkimage -f fitImage.its fitImage.itb注意私钥必须离线保存不能放在构建服务器上。产品量产时把公钥烧录到芯片的OTP区域或独立安全芯片中。代码里验签函数只用公钥验私钥不落设备端。根文件系统用dm-verity做只读校验。这样即使攻击者物理取下Flash修改了根文件系统内容重启后哈希校验会失败设备直接拒绝启动。dm-verity的配置大致如下# 构建根文件系统镜像 mkfs.ext4 -O encrypt rootfs.img # 生成verity哈希树 veritysetup format rootfs.img rootfs.verity # 生成verity启动参数 veritysetup status /dev/mapper/verity_root启动参数里要带上root hash和hash offset这些参数由U-Boot或内核cmdline指定并且需要签名保护。这里我想强调一点生成root hash后最好把hash值也做一次签名防止攻击者篡改cmdline指向一个自己构造的哈希树。3.2 阶段二文件系统与权限加固分区规划好之后文件系统权限加固就是重头戏。这一阶段的目标是即使攻击者以普通用户身份跑起来也无法写系统目录、无法执行新程序、无法读取敏感配置。第一件事是调整/etc/fstab的挂载选项# 系统分区只读挂载 /dev/mmcblk0p2 / ext4 ro,noatime,nodiratime,errorsremount-ro 0 1 # /tmp使用内存文件系统禁止执行文件 tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev,mode1777 0 0 # /var/log也放内存或挂到data分区并加noexec tmpfs /var/log tmpfs defaults,noexec,nosuid,nodev,mode0755 0 0 # data分区挂载时禁止执行app /dev/mmcblk0p3 /data ext4 defaults,noexec,nosuid,nodev 0 2这里有几个重点。第一根文件系统挂为ro后很多程序的配置写入会失败这是正常的业务程序应该把可变数据写到/data。第二/tmp挂成noexec后攻击者下载到/tmp的脚本无法直接执行这能拦掉一大类入侵后植入的恶意脚本。第三/data分区也建议noexec业务需要执行的程序放在系统分区数据分区只存数据。第二件事是设置关键目录和文件的权限。我一般用容器内生成的post-build脚本统一设置# 设置关键文件属主为root chown root:root /etc/passwd /etc/shadow /etc/sudoers # 设置权限 chmod 0644 /etc/passwd chmod 0400 /etc/shadow chmod 0440 /etc/sudoers # 关键目录700 chmod 700 /etc/ssl/private chmod 700 /root # 给系统脚本设置不可变属性 chattr i /etc/init.d/iptables.sh chattr i /usr/local/sbin/security_monitor.sh不可变属性i是个非常实用的技巧。大多数攻击者拿到root权限后会优先修改开机脚本、防火墙规则、监控脚本把检测逻辑干掉。加上i后文件连root都无法修改必须先执行chattr -i解除属性。而我们能通过把chattr命令本身移出系统或限制其使用来提高抗篡改能力。我还建议用ACL做更细粒度的权限控制。比如业务程序需要读取某个配置但不能修改就单独授权给一个特殊用户# 创建security_monitor用户 useradd -r -s /sbin/nologin security_monitor # 给配置目录设置ACL仅允许monitor用户读 setfacl -R -m u:security_monitor:r /etc/device_config setfacl -R -x u:security_monitor:r /etc/device_config/sensitive3.3 阶段三运行时安全监控与告警文件系统和权限做扎实之后接下来是Live体系里最有“Live”味的部分——运行时监控。设备是在持续运行的安全状态也在持续变化不监控等于没有状态。嵌入式设备没条件跑Security Onion这种重量级SIEM但用脚本和标准linux工具也能搭一个轻量版事件监控系统。我写过一个核心监控脚本主要监控四类事件关键文件哈希变化、开放端口变化、USB设备插拔、系统用户及启动项变化。这个脚本放在systemd里做成一个循环任务每30秒检查一次发现异常立即写入审计日志并按配置决定是否上报。#!/bin/bash # /usr/local/sbin/security_monitor.sh # 1. 关键文件哈希校验 declare -A file_hashes file_hashes[/etc/passwd]sha256 hash here file_hashes[/etc/shadow]sha256 hash here file_hashes[/usr/bin/busybox]sha256 hash here for file in ${!file_hashes[]}; do if [ -f $file ]; then current_hash$(sha256sum $file | awk {print $1}) if [ $current_hash ! ${file_hashes[$file]} ]; then echo [ALERT] Hash mismatch: $file /var/log/security_audit.log fi fi done # 2. 开放端口变化监控基线文件存/data/secure_baseline/ports.txt ss -tlnp /tmp/current_ports.txt 2/dev/null if [ -f /data/secure_baseline/ports.txt ]; then if ! diff -q /tmp/current_ports.txt /data/secure_baseline/ports.txt /dev/null; then echo [ALERT] Port change detected /var/log/security_audit.log fi fi这个脚本的作用不是防止入侵而是在入侵发生后第一时间发现并留下证据。日志要写到独立的内存缓存并且定期上传到后端安全平台防止攻击者直接删日志销毁证据。USB监控我用的是udev规则。在/etc/udev/rules.d/10-security-usb.rules里添加# 阻止非授权USB设备 ACTIONadd, SUBSYSTEMusb, ATTR{idVendor}!1d6b, RUN/usr/bin/security_usb_check.sh %ksecurity_usb_check.sh里维护一个厂商ID白名单白名单外的设备直接通过sysfs将其禁用并记录一条审计日志。实际项目中遇到过“USB device has been blocked by the current security policy”这样的提示多半就是这里产生了作用。需要注意这个方案的覆盖面取决于白名单是否完整必须提前收集现场会用到的所有合法USB设备ID。3.4 阶段四OTA签名与证书管理OTA升级是Live安全体系里最需要细心的一环。我见过很多项目前面所有安全加固都做得很好结果升级包没有签名校验一下子全盘崩溃。升级链路要解决两个问题升级包从哪里来和升级包是不是合法的。升级包签名可以这样做构建系统在产出固件后对固件做SHA256哈希再用私钥对哈希值签名生成.sig文件。设备端下载固件后先用内置公钥验签哈希再计算本地哈希并比对。只有当两个哈希完全一致时才允许写入系统分区。设备端验签程序用C或Go写逻辑很简单/* 伪代码设备端升级包验签 */ int verify_firmware(const char *fw_path, const char *sig_path) { // 1. 读取内置公钥 EVP_PKEY *pkey load_public_key(/data/security/fw_public.pem); // 2. 打开签名文件 FILE *sig_file fopen(sig_path, rb); // 3. 使用EVP_VerifyFinal验证哈希 // 4. 计算固件的SHA256 // 5. 比较两个哈希 // 6. 成功则返回0失败返回-1 }公钥放在/data/security目录是合理的吗这得加一个前置条件/data分区必须做完整性校验或者存放在安全芯片内部。我实际项目中更喜欢把公钥放在U-Boot的env区里由Secure Boot一并保护这样即使data分区被清空公钥也不会丢失。证书信任链的管理也要同步做。设备上的CA证书目录比如/etc/ssl/certs应该属于系统分区并且作为只读内容由OTA整体更新。如果业务需要临时安装CA证书不建议直接往系统目录里丢而是在应用层单独维护一个业务信任库并且在证书校验时同时校验系统证书库和业务证书库。之前遇到“cacerts目录read-only导致无法创建文件”就是因为把固定服务器的证书路径搞混了正确的做法是让业务代码去读自己的证书文件而不是去改系统证书目录。4. 常见问题与排查技巧实录安全加固做多了会发现很多问题的表象惊人一致但根因千差万别。我把实际项目里最常踩的坑整理成了一个速查表后面再逐个讲排查思路。每个问题背后都有一个安全设计上的盲点解决一个就能少一类的坑。问题现象常见根因解决关键could not set file security for file文件系统不支持权限/属性或挂载选项限制检查挂载参数换用ext4等原生Linux文件系统wrong security typeSELinux标签或安全属性类型不匹配确认安全策略上下文使用semanage/restorecon调整USB device has been blocked by the current security policyudev策略或USBGuard拦截补充合法设备白名单调整策略cacerts目录read-only安装证书失败证书目录在只读系统分区改到业务证书库通过OTA更新this action is not allowed with this security level configuration安全等级配置过高限制了合法操作细分角色与权限避免一刀切开启Secure Boot后内核无法启动签名未生效或root hash不匹配检查公钥是否烧录、哈希树是否同步更新4.1 文件安全属性设置失败could not set file security for file / wrong security type这个问题的典型场景是在加固过程中执行chmod或chattr系统提示设置失败。我遇到过的最常见原因有两个。第一个是挂载文件系统类型不支持POSIX权限。很多设备的数据分区用的还是vfat格式从Windows系统带过来的习惯。vfat本质上没有权限表chmod/chown命令不会报错但也不会生效任何用户都能读写。这个问题不解决后面所有基于权限的安全控制全是空转。排查很简单mount命令看一眼输出文件系统类型是vfat/fuse就基本可以确定问题。第二个原因是SELinux/AppArmor安全上下文不匹配。这类系统会报wrong security type或permission denied。排查时先用ls -Z看文件的SELinux标签再用semanage fcontext -l和restorecon修复上下文。嵌入式设备如果不想引入SELinux的复杂度可以暂时用AppArmor或干脆关掉强制模式但一定要知道自己在关什么。我的建议是安全模块要么不启用启用就必须把策略写完整否则误杀正常业务远比被攻击更早暴露。4.2 USB设备被安全策略阻断“USB device has been blocked by the current security policy”这个提示只会出现在有USB管控策略的系统里。设备本身可能没有开启USBGuard但udev规则里做了限制。排查思路是先断开异常USB设备然后看dmesg日志确认是哪条规则拦截的。如果判断为误阻断比如现场工程师的调试U盘是合法的那就把U盘的厂商ID和设备ID加入udev白名单。但如果这个设备处于高风险环境我建议默认全阻断只放行白名单设备。安全策略的粒度要看你希望用户多省心——安全性和易用性永远在拉扯你得在部署前想清楚。这里有个小技巧USB设备的厂商ID很容易伪造如果项目安全等级很高建议做USB设备序列号级别的精确白名单或者直接使用支持USB证书认证的方案而不是只匹配ID。4.3 证书安装失败只读文件系统的连锁反应前面提到Android设备上用adb往/system/etc/security/cacerts拷贝CA证书会提示read-only这是因为新版本安卓的system分区只读。嵌入式Linux也一样根文件系统做了只读挂载后所有往/etc目录的写入都会失败。这个问题的正确解法不是临时remount成rw而是把证书添加到业务自己的信任库。比如你的应用是HTTPS客户端可以配置SSL_CERT_FILE指向/data/certs/my_ca.pem。这样既不影响系统证书库的完整性又能满足业务需要。如果确实需要修改系统证书库比如出厂前预置企业根证书那必须在镜像构建阶段就把它打包进根文件系统然后通过OTA整体更新。允许运行时往系统证书目录写文件等于亲手把只读防线撕开一个口子。4.4 安全等级配置拒绝合法操作“this action is not allowed with this security level configuration”这句话我在调试安全级别比较高的系统时经常看到。它通常不是某个具体功能的问题而是安全等级拔高后一批操作被全局拦截。比如你把SELinux设为强制模式后原本能正常访问的业务进程突然全被拒了。这类问题最大的坑在于很多人以为多配置几个例外规则就行结果例外越加越多最后安全策略形同虚设。我的做法是先把安全等级从高到低重新审视一遍明确哪些操作是业务必需哪些只是运维便利。然后把业务必需的操作以最小授权的方式加白名单其余一律拒绝。运维便利的需求尽量通过正式运维通道解决而不是永久开白名单。4.5 Secure Boot开启后无法启动Secure Boot开启后启动失败这是所有安全功能里“翻车率”最高的一个。绝大多数情况是密钥对齐问题比如U-Boot验签用的是RSA2048但内核镜像签名时用了RSA4096或者签名工具产生的对象被填充为“SHA1 RSA”而内核只认“SHA256 RSA”。还有一类情况是烧录的公钥和签名用的公钥不是同一把纯属流程管理失误。我建议在所有新板卡上预留一套“安全启动调试模式”硬件上通过拨码进入U-Boot里设置一个外部开关使能时跳过验签。虽然这个模式有安全风险但只在开发和产线阶段开放量产时必须烧断或关闭。没有这个后门一旦Secure Boot出问题板子就变砖调试成本极高。有了它至少能快速定位到底是验签失败还是内核启动失败。5. 这套安全体系还能往哪走如果你把前面四步都做完了设备的安全性已经比市面上大部分同类产品高出一大截。但Live安全体系的价值不止于此它后续还能往几个方向延伸。比如把日志和告警接入统一平台。设备端负责采集云端负责聚合分析这样多台设备的安全事件就能关联起来发现“单台看起来无关紧要、连起来就是一次完整攻击链”的问题。我实际用的方案是让设备定时把加密审计日志上报到后端后端用简单的规则引擎分析异常再决定是否下发新的安全策略。这个闭环一旦跑起来设备就不再是孤岛而是安全体系里的一个传感器。再比如做差分升级和回滚保护。安全加固之后系统升级包通常只改模块文件但每次升级都要保证安全策略不被破坏。我的做法是升级包里面带上新的安全基线清单升级过程中校验新旧两份清单的差异防止有人利用升级流程“顺手”关掉某条安全规则。另外如果项目预算允许可以考虑给设备加一颗独立安全芯片。公私钥运算、证书存储、安全时钟都挪到安全芯片里即使主控被完全攻破关键密钥也拿不到。虽然这会让单板成本上升但对特定行业电力、交通、金融来说这点成本是值得的。我在实际项目中踩过的坑最后再提一句所有安全策略配置完之后一定要做一轮全功能回归测试。只读挂载会干碎不少写系统配置的软件noexec会拦住很多安装脚本SELinux强制模式会让业务进程大量报错。这些不是安全方案的问题而是你的业务程序之前太“随意”了。安全改造本质上是一次全面的卫生检查把之前所有不规范的地方翻出来然后一个一个解决。这个过程中会有一堆部门和你扯皮但坚持住把设备变得“难入侵、易发现、能追溯”这比任何花哨的功能都值钱。
返回列表