1. 项目概述:为什么SoC安全越来越依赖裸金属与片上分析
最近在调试一块带多核Cortex-A55和Cortex-M4协处理器的SoC时,我踩了一个很典型的坑。板子跑着完整的Linux系统,想通过软件日志排查一次内存越界写入的来源,结果发现系统日志里早就淹没了关键痕迹,等我在用户态抓到数据时,攻击者已经擦掉了现场。后来我换了思路,把安全监控逻辑放进一个独立的M4核上,用裸金属(Bare Metal)方式运行,直接读取芯片内部的硬件性能计数器和跟踪单元,才发现问题的根源是某个驱动在DMA传输时越界写入了保留内存区域。
这个经历让我重新审视了一个趋势:当SoC的复杂度越来越高,攻击面从软件层延伸到硬件层时,纯粹依赖操作系统层面的安全机制已经不够用了。把安全分析能力下沉到芯片内部,用裸金属固件直接驱动片上分析(On-Chip Analytics)硬件,正在成为嵌入式安全领域一条非常实用的技术路线。
裸金属环境意味着你的代码直接运行在硬件之上,没有Linux、没有RTOS、没有虚拟内存的抽象。它牺牲了开发效率,但换来了极致的可控性和可预测性。而片上分析则是指SoC内部自带的那一套硬件监控设施——比如ARM的CoreSight跟踪调试单元、性能计数器、总线监测器、温度电压传感器等。这两者结合之后,安全监控逻辑可以在操作系统不可信、甚至被完全攻破的情况下,依然独立地观察整个芯片的运行状态。
这篇文章适合三类人看:一是做嵌入式安全的工程师,想了解怎么利用芯片自带硬件做主动防御;二是做SoC固件开发的同行,想知道裸金属安全模块和主系统之间怎么协作;三是刚入门芯片安全方向的学生,想搞清楚这些概念之间的逻辑关系。我尽量把自己实际调试中获得的经验写清楚,包括那些文档里不会提的坑。
2. 核心需求解析:老思路确实搞不定新威胁了
2.1 带OS的安全方案存在什么盲区
传统SoC安全方案基本都离不开操作系统。Linux下有SELinux、AppArmor、IMA,Windows下有Credential Guard、VBS等等。这些机制的共同点是:它们都运行在被保护对象之上,或者说,运行在和攻击者相同的抽象层里。
这带来的后果是,一旦内核本身被提权漏洞攻破,所有依赖内核提供服务的安全机制都失去了可信基础。攻击者可以挂钩系统调用、篡改审计日志、伪造进程状态,让上层安全软件看到的一切都是假象。我见过不少产品把安全软件做得很花哨,但内核被root后基本就是裸奔状态。
另一个盲区是性能开销和语义鸿沟。操作系统级别的监控要考虑系统调用开销,不能每读一次性能计数器就陷入内核,所以只能做粗粒度的采样。而且OS看到的性能数据经过进程调度、中断处理、缓存污染之后,已经离硬件真实行为很远了。很多异常行为在一开始只是硬件层面的微小信号——比如某段代码突然触发了大量非对齐访问、某块内存区域出现了异常的读命中率、某个外设中断频率剧烈波动——这些信号在操作系统层根本观测不到。
2.2 裸金属安全模块的价值是提供一个“独立观察者”
当安全监控逻辑跑在裸金属环境时,它的定位就变了:不再是“被保护系统内部的一个组件”,而是“站在系统之外的独立观察者”。
拿我常用的架构举例。主系统跑Linux,负责业务逻辑;独立的安全核跑裸金属固件,负责监控和响应。安全核有自己的代码段、数据段,通过硬件方式与主系统共享部分内存(或完全隔离),它的执行不受主系统影响。主系统被攻破之后,攻击者即使拿到了root权限,也拿不到安全核的执行上下文。
这个架构能成立的前提是硬件层面提供了隔离机制。ARM的TrustZone、RISC-V的PMP/IOMMU、以及各类SoC自带的secure boot机制,都是用来保证“独立观察者”本身不被污染的。裸金属环境没有复杂的调度器、没有虚拟内存映射表,安全逻辑的执行路径非常简单可控,这就让形式化验证和故障注入测试变得可行——而这些在带OS的环境里几乎做不了。
2.3 片上分析提供的是“硬件级情报”
片上分析本质上是SoC在设计时埋入的一整套硬件测量设施。ARM CoreSight是其中最典型的代表,它包含:
- ETM/PTM(嵌入式跟踪宏单元):实时跟踪CPU指令执行流
- CTI(Cross-Trigger Interface):在硬件事件之间建立跨核触发链条
- 性能监测单元PMU:统计缓存命中率、分支预测失败、总线访问次数等
- 总线跟踪器:监控AXI/ACE总线上的读写事务
这些硬件模块的存在,让SoC有能力回答一类非常关键的问题:某个安全告警发生时,整个芯片内部到底发生了什么?CPU正在执行哪段指令?哪个外设正在发起DMA写操作?总线上的流量模式是否与正常状态偏离?
更重要的是,这些分析数据的采集不依赖操作系统。即使Linux内核完全崩溃,即使恶意固件篡改了中断向量表,跟踪单元依然在独立记录CPU的指令流。事后复盘时哪怕攻击者已经清理了软件日志,我们依然可以从硬件跟踪缓冲区里恢复出攻击路径。
2.4 用场景来理解“裸金属+片上分析”的组合
我用一个具体场景来说明这套组合的价值。某IoT设备固件启动时,BootROM校验通过后跳转到Bootloader。Bootloader加载主系统镜像,并在主系统运行前启动安全核上的监控固件。监控固件启动后立刻配置PMU和总线监测器,并设置一组基线阈值——比如“CPU0每秒最多允许读取SPI Flash区域100次”或者“DMA写操作的目标地址必须在预设范围内”。
主系统正常运行后,如果攻击者植入了一段恶意代码,试图通过DMA读取安全核的固件密钥。总线监测器会捕捉到源地址不合法、目标地址指向安全内存区域的写事务,触发一个硬件中断给安全核。安全核收到中断后,不需要等Linux做出任何反应——它就是硬件本身的一部分,可以直接通过TrustZone的隔离寄存器快速冻结相关外设,或直接复位整个系统,把攻击面关停在最小范围。整个过程发生在微秒到毫秒级别,而任何纯软件的安全方案都不可能做到这个响应速度。
3. 方案选型与架构设计思路
3.1 双核异构方案还是TrustZone单核方案
落地“裸金属安全+片上分析”架构时,首先面临一个选型问题:用两颗核还是一个核跑两种世界。
双核异构方案(比如Cortex-A系列主核 + Cortex-M系列安全核)的优点是隔离彻底。M核往往有自己的Flash和RAM,即使主系统被整体攻破,也难以直接访问M核的物理内存。两个核之间的通信可以通过共享内存+门铃中断(Doorbell)来实现,通信协议可以做得非常简。
单核TrustZone方案的优点是成本低。在一个Cortex-A核上用TrustZone划分出Secure World和Normal World,安全监控逻辑跑在Secure World里,业务系统跑在Normal World。缺点是一旦CPU核本身被硬件漏洞攻破(比如幽灵、熔断类攻击),两个世界都会受影响。实际项目中我一般遵循一个原则:如果安全等级要求达到Common Criteria EAL5+以上,优先选双核异构;如果主要是防软件攻击、成本压力大,TrustZone单核方案就够用了。
3.2 裸金属固件应该内置哪些基础能力
安全核上的裸金属固件不是简单地写一个while循环轮询。它的设计要遵循一套相对完整的框架:
- 可信启动链:安全核固件本身需要由高可信度的启动代码(BootROM)校验签名,确保加载到安全核上的每一段代码都来自可信源。
- 硬件资源初始化:必须显式配置MPU或PMP来限定安全核可访问的地址范围,越权访问一律触发异常。
- 事件采集代理:周期性或事件驱动地读取PMU计数器、总线监测器数据、电源温度管理等遥测信息,缓存到环形缓冲区。
- 策略判断引擎:设置一套匹配规则,比如阈值检测、偏差检测、访问控制表,用来判断当前事件流是否构成安全威胁。
- 响应执行器:定义安全事件发生时应执行的动作——记录日志、挂起外设、触发中断给主核、或直接复位。
- 通信接口:通过共享内存或Mailbox与主系统交换状态信息,但不允许主系统对安全核的代码和关键数据做写入操作。
3.3 设计上需要权衡的三个方面
裸金属环境最大的好处是简化,但简化本身也是有代价的。我在设计时通常会做三个维度的权衡:
第一个是功能完整性和攻击面之间的平衡。裸金属固件功能越复杂,自身潜在漏洞越多。所以我倾向于把安全核上的代码控制在一万行以内,能用硬件实现的功能就不用软件,比如内存访问控制直接交给MPU,异常触发直接走CTI硬件链路。
第二个是监控粒度和性能开销的平衡。片上分析硬件的精度很高,但如果每秒钟采集几百万条跟踪记录,会占用大量总线带宽,反而干扰系统正常运行。我的策略是分两级:常态下做低精度的周期采样,只有检测到异常信号时才切换到高精度全量跟踪。
第三个是安全分析和调试便利之间的平衡。处理器的调试接口(JTAG/SWD)本身就是一个巨大的攻击面,很多SoC的安全漏洞都是通过调试口攻破的。生产环境中必须关闭调试访问,但开发阶段又需要保留。我的做法是增加一个物理跳线管脚,只有跳线存在时调试接口才可用,同时安全固件启动时会检查这个管脚状态并在日志中留下记录。
4. 实操过程:手把手搭一套裸金属片上分析安全原型
我以一块基于Cortex-M4 + Cortex-A7组合的SoC为例,演示怎么从零搭建这套安全监控原型。这里不依赖具体厂商的SDK,但会沿用CMSIS和ARM CoreSight的标准接口,方便迁移到其他平台。
4.1 环境准备与工程结构
需要准备的工具有三样:ARM交叉编译链(我用的是arm-none-eabi-gcc 10.3以上版本),OpenOCD或J-Link调试器,以及一块带CoreSight功能的SoC开发板。
工程目录建议这样组织:
sec_core/ ├── src/ │ ├── startup.s # 启动代码 │ ├── main.c # 主逻辑 │ ├── monitor.c # 事件采集功能 │ ├── policy.c # 安全策略判断 │ ├── respond.c # 响应执行逻辑 │ └── comms.c # 与主系统通信 ├── include/ │ ├── soc_regs.h # SoC寄存器定义 │ ├── security_config.h # 安全配置结构体 │ └── debug_uart.h # 调试串口驱动 ├── tools/ │ └── analyze_trace.py # 跟踪数据分析脚本 └── Makefile编译时的关键选项有两个。-mcpu=cortex-m4是指定CPU类型,-mthumb是使用Thumb指令集。还有两个很重要的编译参数:-ffunction-sections -fdata-sections配合链接时的--gc-sections,可以自动删除未使用的函数和数据段,把最终固件体积压下来。对于安全固件,我习惯加上-fstack-protector-all来启用栈保护,并设置--specs=nano.specs来精简C库,减少运行时依赖。
4.2 裸金属启动代码与安全初始化
安全核的启动代码和普通嵌入式开发略有区别,核心是要在跳转到main函数之前把硬件隔离机制全部准备好。
我这里给出启动代码的关键理解思路。第一条是向量表重定位,M4核的向量表默认在地址0x0处,但安全核的Flash可能映射在其他地址,所以必须把向量表设置到正确位置。第二条是MPU配置,必须在开启中断之前就配好MPU的访问规则——不配MPU就开中断,相当于信息裸奔。第三条是看门狗,安全核必须有自己的独立看门狗,防止主系统的恶意代码想办法让安全核卡死。
MPU配置是最容易被忽略的部分。我在配置时会至少设置5个区域:Flash区域设置为只读可执行,SRAM数据区设置为读写但不执行,外设寄存器区按需设置访问权限,共享内存区设置为读写且强制非缓存(否则一致性协议会引起隐蔽的数据串扰),最后一个区域覆盖整个地址空间,默认禁止访问,作为兜底策略。这个兜底区域非常重要,它保证所有未显式配置的地址空间都无法被安全核访问。
4.3 片上分析功能配置的两种方式
以ARM CoreSight为例,实际操作中配置分析功能有两种方式。一种是直接用CoreSight的寄存器编程接口,另一种是通过CoreSight的CSAL(CoreSight Access Library)这种高层库函数。我建议先用寄存器方式理解原理,再用库函数做工程实现。
一个简单的PMU配置示例,用来统计缓存缺失率:
// 使能PMU访问权限 CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 配置PMU计数器0为缓存缺失计数事件 DWT->CTRL &= ~DWT_CTRL_CYCCNTENA_Msk; // 选择特定事件类型 DWT->MASK = 0;如果MCU不带PMU,也可以用DWT组件提供的周期计数器(CYCCNT)来近似统计执行时间,再用PCSR(程序计数器采样寄存器)定期对当前执行的指令地址做采样。PCSR采样对排查“代码执行流有没有跳到异常地址”特别有用。
总线跟踪器一般都在系统级调试组件里配置。这类模块通常支持设置过滤条件,比如只监控写事务、只监控特定地址区域。把这些过滤条件配合阈值中断使用,就可以实现“当某个外设异常发起DMA写入时自动触发告警”的逻辑。这不是轮询,而是硬件级别的即时响应。
4.4 事件采集与策略判断引擎
事件采集代码要解决一个核心问题:怎么在不停机的情况下高效地把分析数据从硬件寄存器搬到内存缓冲区。
我的方案是用双缓冲加DMA。一组缓冲区在采集时,另一组缓冲区在处理时。这样采集和处理可以并行进行,不会因为处理过慢导致硬件FIFO溢出。缓冲区大小我一般定为4KB,每512字节给一个时间戳标记,方便后续离线分析对齐时间线。
策略判断引擎我建议用查表而非复杂的规则解释器。比如给每个事件类型定义一个最大阈值,超过阈值就执行对应响应动作。用查表的方式虽然灵活性差一些,但执行路径固定,没有解释器的漏洞,也不需要担心动态内存分配问题。
一个简化的策略表结构可以这样定义:
typedef struct { uint32_t event_id; // 事件编号 uint32_t threshold; // 触发阈值 uint32_t action; // 响应动作 uint32_t log_enable; // 是否记录日志 } security_policy_t;这样每一条策略就是一行16字节的数据,整个策略表可以放在只读Flash里。即使被攻击者看到了,也无法修改它。
4.5 安全响应逻辑的正确设计姿势
安全响应逻辑里最容易犯的错误是:为了让系统更安全,设置了太多的响应动作,结果影响到了正常功能,导致安全系统被业务方“主动关闭”。
我的思路是分级响应。第一级是观察级,只记录事件日志和计数器,不打断系统。第二级是提示级,通过中断通知主系统“检测到异常行为”,但不确定一定是攻击,可能是正常的瞬态波动。第三级是限制级,冻结涉及的外设,挂起相关DMA通道,然后等待进一步指令。第四级是止损级,直接复位相关子系统或整个芯片。
每一级都需要单独验证,不能直接写死。另外要强调一个点——在任何响应动作执行前,先把当时的硬件上下文完整保存下来。这里的硬件上下文包括核心寄存器、相关的跟踪缓冲区和关键外设状态,这些是从片上分析数据里分析出攻击路径的核心依据。如果直接复位系统,这些证据就丢失了,安全分析就变得毫无意义。
4.6 共享内存通信的安全设计
安全核和主系统之间传递数据,最常用的技术是共享内存加门铃中断。主系统往共享内存里的某个地址写入一条消息,然后写一个特定寄存器来触发中断给安全核(或者反过来)。但这套机制有两个风险。
第一个是数据一致性问题。如果主系统的软件栈配置了缓存,共享内存里的数据可能会停留在缓存中而没有真正落盘到DDR,安全核读到的是旧数据。解决方案是在共享内存区域配置为非缓存属性,或者在每次读写前后显式执行clean和invalidate操作。我倾向于前者,省心且性能影响可接受。
第二个是消息伪造问题。主系统被攻破后,攻击者可能伪造“正常”消息来欺骗安全核。解决办法是给通信协议加一个简单的序列号机制:安全核维护一个递增计数器,只有消息序号连续递增时才执行消息内容。这在安全领域叫“新鲜度保护”,用极低的成本挡住了重放攻击。
5. 常见问题与排查技巧实录
5.1 裸金属固件在真实芯片上不运行
这是最常见的问题,通常出现在第一次移植的时候。如果你确认编译和烧录没有问题,但固件就是跑不起来,优先排查三个方向:时钟、管脚复用和电源域。很多SoC的安全核需要先由主系统或者某个特定的电源管理单元完成上电,然后才会释放reset。你得先看参考手册的电源域章节,确认安全核的电源域处于开启状态,且没有被某些“省电策略”误关。
另外一个高概率坑是中断向量表偏移没设置。Cortex-M内核有一个VTOR寄存器,专门用来设置向量表的基地址。如果你把固件烧录到地址0x08020000但没把向量表偏移到那里,那么任何中断触发都会让CPU跳到一个随机的地址,直接进HardFault。
5.2 片上分析导致系统性能明显下降
硬件跟踪单元在同时采集多路数据时会占用不少总线带宽,特别是ETM指令流跟踪,它能把系统整体性能拉低20%。如果你只是做安全监控,不需要跟踪完整的指令流,可以把ETM关掉,只保留PMU周期计数和总线事务计数,这样跟踪带来的性能开销基本降到1%以下。
还有一种情况:虽然控制逻辑设计得很简洁,但事件处理函数里做了太多耗时操作——比如往SD卡写日志、往串口打印详细状态。这类操作在安全事件触发时会阻塞很长一段时间,期间大量新事件涌入FIFO导致溢出丢数据。我的建议是事件处理函数保持极简,只把原始数据存到环形缓冲区就返回,打印和分析全部留到后台任务做。
5.3 安全响应频繁误触发
阈值设置过高会产生漏报,设置过低就会疲劳轰炸。我在配置初期稳定运行一段时间收集基线数据,覆盖正常业务流程的峰值和谷值,然后按2.5倍标准差来设置告警阈值。这不是什么高深理论,只是统计学上比较合理的经验值,但效果比拍脑袋定阈值强得多。
误触发的另一个来源是主系统的某些合法驱动行为不规律,比如休眠唤醒时集中进行Flash写入。针对这类情况,可以在策略引擎里加入一个白名单窗口:允许某个时间窗口内出现特定模式的流量,窗口之外再触发才告警。这个窗口要短,不能超过500毫秒,否则会遮挡真正的攻击行为。
5.4 安全核自身的固件更新怎么做才安全
裸金属固件的更新机制很容易被忽略。我见过不少项目安全核固件写死后就再也不动了,结果发现漏洞也没法修。合理的方案是采用A/B双镜像加失败回滚:固件存储在Flash的两个分区里,其中一个作为当前运行版本,另一个作为备份。更新新固件时只覆盖非活动分区,校验成功后切换启动指针,下次上电启动新版本;如果新版本起不来,看门狗超时后自动回滚到旧版本。
这个方案的复杂点在于安全核的BootROM需要在加载固件之前完成签名校验。也就是说,更新机制本身必须建立在可信启动链之上,否则攻击者可以伪造固件镜像,直接替换掉你的安全核固件——那就真的是引狼入室了。
5.5 调试口和日志输出在安全方案中怎么取舍
实际工程中很多人为了调试方便,把调试串口长期开放,或者把安全核的详细日志打到共享UART上。这等于给攻击者送信息。我现在的习惯是:安全核的详细日志只放在内存里,通过调试器的底层接口拉出来看,不通过UART输出。UART上只每秒输出一个心跳信号,表示安全核还活着。
另外,量产前一定要检查调试接口。默认情况下一定要关闭安全核的调试访问,并把主系统的调试端口设置为需要认证才能访问。在安全方案里,一切能被外部物理访问的调试接口都是攻击者的入口,宁可开发时麻烦一点,也不要给售后留大坑。
6. 一些实际操作的收尾经验
裸金属安全加片上分析这套组合,我已经在三个实际项目里落地了。每次踩坑之后最重要的体会是:安全分析模块一定要从项目启动的第一天就参与到架构设计里,不要等主系统开发完了再外加一个安全核上去。后面再塞,隔离边界、共享内存地址、中断优先级的冲突会让你改到崩溃。
另外一个体会是:不要追求监控到所有细节,要追求关键细节不丢失。片上分析的能力再强,也无法覆盖所有攻击场景。但通过设定清晰的安全边界,配置好硬件级隔离,再配合片上分析做实时感知,就能把大多数攻击拦截在早期阶段,并且在拦截的同时留下关键证据链。做好了这一点,后期的安全事件复盘会轻松非常多。
最后说一个建议:在正式交付生产版本之前,可以尝试做一次“红队演练”,专门模拟攻击者拿到了主系统root权限的场景,看你的裸金属安全模块能不能独立防御住——这就是这套方案和传统软件安全方案最本质的区别所在。能挡住这一层,你的SoC安全设计才算真正及格。