ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

嵌入式系统安全设计实战:从威胁建模到安全启动的完整指南

嵌入式系统安全设计实战:从威胁建模到安全启动的完整指南 开发完这个项目之后我最大的感触是在嵌入式系统里做安全和你在一台云服务器上配防火墙完全是两码事。前者是在螺丝壳里做道场算力、存储、功耗全都卡着你还要面对物理接触、侧信道、供应链这几类传统互联网场景根本不会考虑的威胁。这篇文章就围绕我最近完成的一个安全嵌入式系统项目聊聊从威胁模型分析、信任根设计到产线烧录这一整条链路里的关键环节包括我踩过的坑和一些只有实际动手才能积累出来的经验。内容会比较长适合正在做安全类物联网设备、需要给自己的MCU系统加入安全启动和加密存储能力的开发者对安全方案选型拿不准的朋友也可以当一份参考。1. 先想清楚对手是谁威胁模型与安全设计思路1.1 嵌入式安全不是加上一个加密芯片就完事很多人一说到嵌入式安全第一反应就是我集成一个ATECC608B或者SE050就安全了。这个想法不能算错但思路太窄。我见过不止一个项目加密芯片买了密钥也写进去了结果固件本身没有任何签名校验攻击者直接通过串口把整个Flash读出来反汇编之后照样拿到全部逻辑。安全是一个系统属性不是你堆一个安全元件就能解决的。一个完整的嵌入式安全方案至少应该覆盖物理安全、数据安全、通信安全和供应链安全四个维度。很多硬件厂商会把Secure Boot、TrustZone、加密引擎这些能力做进芯片里但这些能力默认是不生效的需要你在启动代码、链接脚本、烧录流程里一层层手动打开。就算你选了一个特别强的安全MCU如果密钥管理策略一塌糊涂相当于把金库门装在了纸墙上。所以做安全嵌入式系统第一步不是选芯片而是做威胁模型分析。你要想清楚你的设备在什么环境里运行看护人是谁攻击者又有哪些接触途径。1.2 从攻击面反推防御四个必须防住的环节我做项目时习惯把攻击面分成四个维度这样设计思路会非常清晰物理接触攻击攻击者能拆开设备外壳用烧录器、探针或者故障注入设备直接操作芯片。软件远程攻击设备联网后攻击者通过网络漏洞或者协议弱点尝试入侵。通信信道攻击中间人窃听、篡改设备与服务器之间传输的数据。供应链攻击设备在制造、传输、部署过程中被植入后门或者从非正规渠道流入伪造硬件。我那个项目的产品是一台工业设备的数据采集网关现场部署环境是无人值守的室外机柜站点保安一般管得比较松物理接触的可能性很高。考虑到这一点我把安全策略重点放在了两块一是安全启动确保设备只能运行经授权的固件二是敏感数据保护确保即使攻击者物理拆机也无法直接读到密钥和业务数据。至于通信层因为采集的数据本身不是甲方最敏感的东西TLS加密就够用了不必为了追求绝对安全把架构复杂化。1.3 设计原则分层防护而不是单点寄托我一直坚持的一个设计原则是纵深防御。安全设计不能指望某一个环节永远不被攻破而是要设计成如果某一层失效还有下一层兜底。打个比方安全启动就像小区门口的保安他在系统运行前就检查固件有没有被篡改。但保安不可能拦下所有攻击所以你还得给房间装指纹锁——这就是MPU/TrustZone做运行时隔离。即使攻击者骗过了保安拿到了某个内存区域的写权限他也无法直接控制整个系统。具体到我的项目里我用了四层防护第一层硬件的Secure Boot逐级验签确保Boot ROM只能加载经过签名的Bootloader。第二层TrustZone做安全世界/普通世界隔离密钥只在安全世界内使用。第三层文件系统加密业务数据、日志、配置文件在非易失存储里都是密文。第四层通信TLS双向认证设备与云端之间跑的是双向校验的加密通道。每一层都不是固若金汤的但叠在一起之后攻击者要付出的时间成本会指数级上升。安全这个事的本质就是把攻击成本抬高到超过攻击收益而不是追求某种理论上的绝对防御。2. 信任根与安全启动让设备只跑该跑的代码2.1 信任根密钥与OTP的布局所有安全体系都要有一个信任根这个根一旦被攻破整个体系就失去意义了。在MCU平台上信任根通常来自两个地方一个是芯片内部的OTP一次性可编程存储区另一个是外部安全芯片里的密钥槽。我用的主控是一颗带硬件加密引擎和TrustZone的Cortex-M33内核MCU内部有独立的OTP区可以一次性写入信息之后熔断之后再也无法修改。信任根就在这里固化一个是根公钥的哈希值防止根公钥本身被篡改另一个是芯片唯一ID和用于派生存储密钥的设备密钥种子。这里有一个值得强调的点OTP区的写入时机一定要尽早最好在贴片完成后的第一次上电就完成并且写入之后立刻熔断。我见过有同事为了调试方便把熔断操作延后到样机测试阶段结果有一台样机在测试过程中被别人通过烧录器改了OTP区内容导致整台设备的信任链全部失真最后只能返厂换芯片。OTP这个东西的定位就是写了就锁任何想留后路的想法都会给安全防线开口子。2.2 安全启动链的逐级验签安全启动是分级的。以典型的嵌入式Linux设备举例完整的启动链是Boot ROM → BootloaderSPL→ U-Boot → kernel → rootfs每一级都承担一个职责验签下一级镜像验签通过才跳转执行。验签算法通常使用ECDSA P-256因为它比RSA-2048需要的密钥更短、签名验证更快更适合算力有限的MCU平台。具体到实现我建议配置如下Boot ROM阶段读取OTP里的根公钥哈希对Bootloader镜像做ECDSA签名校验。Bootloader阶段将完整的kernel镜像、设备树文件、initramfs打包进一个FIT image使用kernel签名密钥做验签。Kernel阶段验证rootfs的dm-verity哈希树确保根文件系统的每个块都未被篡改。每一步验签都会引入一定的启动延迟。我实测下来在Cortex-M33上做一次P-256验签大概需要80到120毫秒加上文件系统校验整个启动时间会比不做安全校验多出约0.5秒。这个延迟对绝大多数工业应用场景是完全可以接受的但如果你做的是对开机时间极敏感的消费类产品就要权衡一下验签频率和完整性校验的粒度了。2.3 常见坑回滚攻击与熔丝位置记错安全启动配置过程中最容易被忽略的是版本回滚防护。攻击者如果拿到一台旧版本固件的设备发现旧版本有已知漏洞他就可以把新版本固件降级到旧版本绕过安全启动里的漏洞修复。解决办法是在Bootloader里设置版本号寄存器而版本号寄存器本身也要存储在OTP或者防回滚的存储区里确保它只能递增、不能递减。我自己的项目就遇到过类似情况。当时为了保证出问题的设备能远程升级我在Bootloader里留了一个允许降级的开关想着方便测试。结果样机阶段这个开关被开着测试人员在一台设备上把固件刷回了旧版旧版里恰好有个调试串口是打开的这个问题一直到送第三方安全检测时才被发现。所以这个开关在量产固件里一定要彻底关掉而且要加编译期宏和产线检查双重保障。另外熔丝操作前一定要反复确认OTP的块地址和映射关系。不同厂家的芯片文档里OTP地址的表示方式不一样有的用物理地址有的用逻辑扇区编号。我一次在配置某家MCU时误把一个存放厂商校准数据的OTP区域给熔断了导致芯片的ADC基准电压校准值永久丢失整个批次的设备模拟量采集偏差都达不到规格。后来所有项目我都要求同事在熔断前先做一次干跑即先用虚拟地址表记录预期值检测无误后再执行真实的烧写操作。3. 存储与数据保护密钥、固件和日志都不能裸奔3.1 密钥不落地加解密密钥如何存放做过安全设计的人应该听说过一个原则密钥永远不应该以明文形式存储在非易失存储介质里。但在实际项目中明文存储这个问题却非常普遍。常见的原因是无非两个一是没想清楚密钥管理方案就先把代码写出来了二是为了调试方便直接把测试密钥写死在固件里。我在这个项目里的做法是把所有密钥分成了三类分别使用完全不同的管理策略设备唯一密钥用于加密本地数据由芯片内部的硬件真随机数发生器生成生成后直接写入安全世界的受保护存储区软件层永远拿不到明文。通信证书私钥在产线阶段由安全芯片内部生成私钥不出安全芯片只导出对应的公钥用于证书签发。固件签名密钥这套密钥不出现在设备上只存在于离线签名服务器里设备端只有对应的验签公钥。这个策略的核心思路是最小可用性原则任何一层软件都只拿到它完成当前任务所必需的最小权限。应用层代码需要加密一段数据时它只是发一个指令给安全世界安全世界用设备唯一密钥完成加密再把密文返回。应用层从头到尾都接触不到密钥明文。3.2 固件加密与实时解密硬件加速的必要性除了启动时验签另一个常见需求是固件本身在外部Flash里以密文形式存放。这主要是为了防反向工程和防抄板。外部Flash通过SPI/QSPI接口连接如果不做加密攻击者直接把Flash芯片拆下来接编程器就能读走全部固件。固件加密的常见做法是在Bootloader阶段从外部Flash读取密文镜像经过AES解密后写入SRAM执行或者启动时解密后写回外部RAM。如果芯片有硬件加密引擎解密过程几乎是零开销的但如果芯片不支持硬件解密纯软件解密的吞吐量会非常难看。这里要提一个很多人容易忽略的硬件配置AXI总线上的Non-Secure属性使能AXI Non-Secure enablement。现代支持TrustZone的MCU在TrustZone以外还有一个AXI层的安全属性配置。如果AXI层的Non-Secure属性没有正确配置即使你在处理器内部做好了世界隔离外部DMA控制器或者调试接口仍然可以通过非安全路径访问安全存储区。这个配置项藏在芯片的系统控制寄存器里数据手册往往只写一小段但它直接影响整个隔离体系是否成立。我调试过的一个平台就是因为出厂默认AXI SLAVE的安全属性是Non-Secure导致安全世界的数据可以被普通DMA通道读取最后改配置才堵上这个洞。3.3 日志与调试信息的脱敏日志是最容易被忽略的数据泄露通道。开发阶段为了排查问题大家习惯把调试信息打得特别详细什么错误码、寄存器值、内存地址全往串口终端里打。这些日志一旦到了生产环境就等于把系统的内部构造白送给了攻击者。我在这个项目的日志管理上做了三个调整生产固件统一使用日志级别编译开关关闭DEBUG级别的输出。所有日志输出前经过脱敏处理密钥、证书指纹、Token等关键字段用hash摘要代替明文。串口调试功能默认关闭只有通过安全通道发送特定指令后才临时开启并且30秒无操作自动关闭。其中第二点最有实际意义。我见过一个项目他们把TLS握手过程中的会话密钥直接打印到了调试日志里方便用Wireshark做流量分析。开发阶段确实好用但产线固件忘记关这个开关等于把密钥公开了。这算是开发便利性和生产安全性最典型的一次冲突强烈建议做安全方案评审时把日志输出清单单独拿出来过一遍。3.4 顺带说一句secure system占内存的问题有热搜词secure system 占内存我猜不少人是在做安全方案评估时被安全组件的内存占用吓到了。嵌入式系统上的安全组件确实会吃资源这是正常的。TrustZone的隔离机制、安全操作系统的运行时、安全存储的缓存这些都会占用额外的RAM和Flash。以我用过的一个中等规模方案为例安全OS自身占大约64KB RAM安全存储区缓存分配了32KB加密引擎的DMA缓冲区又占了8KB。对于一颗集成256KB RAM的MCU来说这确实是一笔不小的开销。但这不意味着安全方案就不能用关键是在选型阶段就为安全组件预留资源而不是等硬件定死了再想办法压缩安全组件的内存。如果芯片确实紧张可以考虑把一部分安全存储区放到外部Flash用加密引擎做按需解密牺牲一点性能换内存空间。这个取舍要在方案初期就做否则后期改起来成本极高。4. 通信安全TLS并不仅是把证书挂上去4.1 设备端证书体系的搭建设备联网之后通信安全主要靠TLS/DTLS。但TLS不是简单地在代码里调用一次初始化函数就完事它需要一整套证书体系的支撑。我在项目里采用的是双向TLS认证设备端持有设备证书和私钥云平台持有平台证书。握手时设备用平台证书校验服务器的身份防止中间人冒充。云平台用设备证书校验设备的身份确保只有合法设备才能接入。这个方案执行起来证书签发流程是最耗时的一环。每一台设备在量产时都需要申请一张独立的设备证书而设备证书里的公钥由安全芯片内部生成私钥永不出硬件。产线设备发起证书签名请求CSR后工厂内部CA为其签发证书再把证书回传给设备。整个流程看似简单实际落地时却要处理CSR排队、证书存储格式、过期轮换等问题。我的建议是开发阶段就要把证书申请的自动化流程搭好不要用手工脚本录数据。我见过一个项目产线临时用Excel表格管理证书和设备的绑定关系结果到了第二年证书轮换的时候发现几百台设备的绑定关系是混乱的挨个排查浪费了整整两周时间。4.2 调试和运维通道的安全SSH与SecureCRT场景很多开发者只关注业务数据的通信安全却忽略了调试和运维通道。嵌入式Linux设备维护时一般会用到SSHWindows主机上很多人习惯用SecureCRT这类终端管理工具。这个通道如果不加安全措施和裸奔没区别。在设备端我应该提醒几个设置默认关闭密码登录只允许密钥登录且私钥以加密形式存放。SSH服务绑定到管理网口或者特定VLAN不要暴露在业务网段。运维操作要记录审计日志日志内容包括谁在什么时间执行了什么命令。关于SecureCRT这类工具我要多说一句工具本身支持保管箱、SSH密钥管理也支持保存会话密码。但你保存密码的时候工具会调用操作系统的凭据保护机制而不是明文存到配置文件里。即便如此我还是建议运维人员不要勾选保存密码改用证书认证这样即使笔记本丢了攻击者也无法直接通过已知密码登录设备。在嵌入式设备这个领域运维通道被攻破的例子太多了而且经常是从一台运维人员的个人电脑横向扩散到整个设备网络的。另外有条热搜叫secure shell下载说明不少人还在到处找SSH客户端。这里统一说一句Windows 10以上的系统自带OpenSSH客户端直接在命令行输入ssh就能用根本不需要额外下载第三方工具。SecureCRT的价值主要在会话管理和多协议支持上不是必须的。4.3 常见连接报错与自签证书的坑埋点踩雷这块我想多写一点因为光是我在通信安全调试中遇到的连接报错就有好几种。其中一条热搜原文是if you believe the connection should be secure, but python cannot see the这其实是Python做TLS连接时很常见的提示。具体场景是这样的你在设备端用自签证书搭了一个TLS服务然后用Python脚本测试连接结果报SSL: CERTIFICATE_VERIFY_FAILED很多人的第一反应是我这是不是地址错了其实不是问题出在Python的SSL上下文默认需要验证证书链而自签证书不在系统信任列表里。解决办法有两个方向在测试脚本里显式加载设备的自签证书然后构造SSLContext时传入。临时设置verifyFalse仅限开发测试绝对不能用于生产。我自己的习惯是开发阶段直接用脚本加载设备证书做验证这样能在早期发现证书配置错误而不是等部署到场地后再出问题。如果你在Python端看到这个报错先不要怀疑网络问题优先检查证书是不是已经被正确加载。4.4 Web管理页面中的HTTP与insecure origins陷阱很多智能设备都带一个本地的Web管理页面开发者为了省事经常直接用HTTP。Chrome浏览器访问HTTP页面时控制台会显示一条提示This page is insecure...这还不是最危险的真正危险的是Chrome有一个调试用的Flag叫insecure origins treated as secure把某些本地IP地址标记为安全源。这个Flag的本意是方便开发者在本地调试一些必须跑在HTTPS环境下的API比如navigator.geolocation、service worker等。但如果你为了省事在量产固件的说明文档里建议用户开这个Flag那就非常糟糕了。为什么因为一旦本地Web页面被标记为安全源它就可以调用一些敏感浏览器API比如读取地理位置、访问USB设备、调起摄像头等。如果设备的管理页面本身又有命令注入漏洞攻击者构造一个恶意页面让用户用Chrome访问就能通过这个假安全源直接调用浏览器的高级能力进而攻击设备本身。我在这类设备上的做法是能上HTTPS就尽量上HTTPS哪怕是自签证书至少保证传输内容是加密的。如果某些老设备性能实在孱弱跑不动加解密那也要确保Web页面的功能极度受限不得开放任何敏感操作接口并且对页面来源做严格校验。5. 调试接口与供应链安全量产前的最后一公里5.1 一定要锁死JTAG/SWD但留好升级后门开发阶段用JTAG/SWD调试接口是很正常的事但量产固件如果还把调试接口开着就等于给攻击者留了一扇后门。攻击者用几块钱的调试器就能通过JTAG口直接读取内存、修改寄存器和导出固件。所以我强烈建议量产固件里通过配置寄存器强制关闭调试接口并且将相关配置位用OTP锁死防止攻击者通过软件重新启用JTAG。但这里有个矛盾调试接口关了后期产品出问题想远程排查怎么办常规做法是预留一个安全调试模式设备通过某种带外方式比如按下特定按键组合、通过安全通道发送特定指令重新开启JTAG但要先经过身份认证。认证逻辑放在Bootloader里使用存储在安全世界的密钥进行挑战应答。这个安全调试模式的认证机制一定要有防暴力破解能力比如连续失败三次后锁定12小时。否则攻击者可以写一个脚本反复尝试开启调试接口。我在实际测试中试过强行爆破这种接口如果芯片没有延迟锁定机制一般几十万次尝试就能把5位数字口令爆破出来。5.2 从开发板到量产板的密钥切换这是几乎每个项目都会踩的坑开发阶段用测试密钥跑通整个流程到了量产阶段忘了换正式密钥结果所有设备的通信加密体系和签名校验用的都是公开的测试密钥。测试密钥一般是公开的有的甚至直接放在Gitee/GitHub的公开仓库里。如果你把测试密钥量产了攻击者拿到编译工具链再看到你的固件就能直接签出自己写的恶意固件设备会以为这是官方固件乖乖升级。量产密钥切换的关键是密钥生成和分发流程。我建议所有项目组都把密钥分成三套开发密钥Dev Key用于开发调试所有开发板共用。样机密钥QA Key用于测试和认证可以共享给测试团队。量产密钥Prod Key只保存在离线安全服务器里只有授权人员能调用产线设备写入时通过加密通道获得。量产密钥的私钥部分我甚至建议不要保存在普通的服务器硬盘里而是放在HSM硬件安全模块或者离线加密U盾里。这样即使研发内部人员也无法直接把私钥文件拷贝走只能通过受控接口调用签名服务。5.3 设备内账号密码的存储策略这条是从热搜词cisco secure client 的用户的密码如何保存延伸出来的。很多人问这类客户端保存的密码到底放在哪里为什么重装系统前备份出来、重装后还能用。原因是这类工具用了操作系统的凭据保护机制比如Windows的DPAPI加密、macOS的Keychain并不是把密码明文写到本地文件里。嵌入式设备里也一样。设备上如果需要保存Wi-Fi密码、业务账号密码之类的东西绝对不能用明文配置文件。正确做法是密码经过设备唯一密钥加密后存储在文件系统里读取时在安全世界解密用完即焚不留内存缓存。我在一个Wi-Fi配网模块上遇到过一个问题设备把路由器的Wi-Fi密码存在一个JSON配置文件里而且权限是644任何本地进程都能读。结果模块的Web服务有一个路径遍历漏洞攻击者通过构造特殊URL直接把这个JSON文件读出来了整个现场的网络密码泄露。后来改成加密存储并加了访问权限控制和审计日志之后这类问题才算闭环。这里顺带提一个细节加密后的密码文件也要防重放。攻击者虽然读不到明文但可以尝试把旧设备的加密文件整个复制到新设备上如果新设备的密钥派生方式和旧设备完全一样那等于密文直接迁移。解决办法是密钥派生时绑定芯片唯一ID让每台设备的加密密钥都不同。6. 常见问题与排查技巧实录做安全嵌入式系统调试过程和普通软件项目差别挺大。很多问题不是马上报错而是看起来正常实际有漏洞所以排查思路往往比具体命令更重要。我整理了几个项目中高频踩坑的点问题现象可能原因排查思路解决方式安全启动后设备无法启动Bootloader签名不匹配检查是否烧录了正确的公钥哈希重新烧写OTP公钥注意熔断前先核对安全世界外设无法访问TrustZone配置遗漏外设边界检查外设分配表确认外设归属哪个世界调整TZASC外设寄存器配置OpenSSL握手报证书链不完整未上传CA中间证书用openssl s_client -showcerts查看证书链在设备端证书链中加入中间CAPython连接设备报CERTIFICATE_VERIFY_FAILED自签证书未加载到Python信任库用--tls或环境变量指定CA证书路径显式加载设备证书到SSLContext关闭JTAG后无法调试调试功能已被OTP锁死确认是否预留安全调试模式使用安全调试开启指令固件回滚后安全启动失败回滚保护版本号不匹配查看Bootloader版本号寄存器值将版本号寄存器一并升级设备日志泄露敏感信息日志脱敏策略不严格搜索生产固件中的debug打印编译期关闭DEBUG增加脱敏过滤外部DMA可读取安全区域AXI Non-Secure属性配置错误检查系统总线安全属性寄存器配置为Secure属性重新编译烧写这些问题的共性是它们的根因不在业务代码而在系统底层安全配置。传统的打印日志定位问题思路在安全调试里经常不够用更有效的做法是一层一层回溯信任链先从OTP的信任根开始核对然后看Bootloader的配置再往上检查内核和文件系统。顺序反了你很可能在错误的层面浪费大量时间。比如之前遇到过安全启动后设备无法启动这个问题光看现象像是kernel起不来实际排查了一圈才发现问题是Bootloader里的公钥哈希在烧录时被写错了。通过openssl dgst -sha256 -hex正确计算哈希后重新烧写OTP问题立刻消失。这类问题如果一开始就按信任链顺序排查可能半小时就能定位但我当时因为急着看应用层日志绕了一道弯路。还有一点心得是安全功能的验证一定要自动化。我在CI系统里加了一道安全配置检查任务每次编译完成后自动检查固件中是否包含调试接口开启标记、是否包含明文密码、OTP配置参数是否匹配等任何一项不过关就直接阻断发布。这套流程跑了半年拦截了至少三次重大问题有一次是同事在发布分支里意外打开了串口调试如果没有自动检查这个问题大概率会流到客户的产线上。7. 最后再分享一个从实际交付中悟出来的经验项目交付后我又回看了整个设计过程和落地细节最大的感受是在嵌入式安全这个领域方案架构固然重要但真正决定成败的往往是那些写在文档之外的小习惯。比如烧录密钥的电脑绝不联网签名服务器上绝不装聊天工具量产固件的发布必须经过双人复核调试日志里出现的每一个密钥片段都立刻修正。这些习惯单个看起来微不足道但它们共同构成了安全体系的人防层。技术层做得再好如果人可以把密钥随便拷走那一切都是白搭。还有一个被反复验证的规律是安全方案在立项阶段介入的成本远低于产品定型后再补的成本。如果等到硬件设计冻结、软件框架定型了才想起来要加安全启动、要换加密存储那几乎是推倒重来。我见过最极端的案例是一个项目在量产前两个月才决定要加安全功能结果不得不带着两个硬件改版上线整个团队的加班强度堪称灾难。所以如果你正准备启动一个新项目哪怕现在还不知道具体要用哪颗芯片也请务必在需求阶段就把安全需求写清楚需要防什么、防到什么程度、预算多少资源来换安全能力。这些问题想清楚了后面的选型和设计会顺畅得多。从我个人的实际经验看做一个安全性过得去的嵌入式系统真正难的不是某个加密算法怎么写也不是某个安全功能怎么开而是把所有安全组件像拼图一样精确地组合在一起并且保证每一块的边界都严丝合缝。这个过程没有捷径只能靠项目经验一点一滴积累。希望这篇文章能帮你少走一些弯路至少在踩坑的时候知道坑大概在哪个方向。
返回列表