ARTICLE DETAIL

资讯详情

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

深度拆解Arm Trusted-Firmware-A:从EL3安全边界到平台移植实践

深度拆解Arm Trusted-Firmware-A:从EL3安全边界到平台移植实践 第一次调试Arm Trusted-Firmware-AATF的时候我盯着串口上反复出现的同步异常日志整整两个晚上。板子复位之后能跑出BL1和BL2的几行信息到BL31就断片。换过U-Boot、换过编译优化等级、甚至怀疑过芯片本身有问题最后发现居然是一个链接脚本里异常向量表对齐宏写错导致EL3的异常向量没有落在它该在的位置。从那次起我就养成了一个习惯任何安全固件的问题先回到源码和地址空间里找答案而不是盯着日志猜。这篇记录是我基于ATF源码树做的一次深度源码评测式拆解顺便把我在安全固件工程审计和平台移植上积累的东西一并整理出来。内容会覆盖ATF在整个ARM系统中的定位、BL1/BL2/BL31/BL32/BL33的启动架构、从工程审计视角怎么去审查一份固件源码以及如何把一个最小可用的TF-A移植到新平台上。不管你是刚接触EL3固件的嵌入式开发者还是已经在做安全启动评估的固件工程师这篇文章应该都能给你一些可以直接拿去用的思路。1. ATF在ARM系统里到底解决了什么问题1.1 从异常级别模型说起EL3为什么绕不开ARMv8-A架构把系统运行状态分成了EL0到EL3四个异常级别再加上Secure世界和Normal世界这两套互不信任的运行环境。EL0跑普通应用EL1跑操作系统内核EL2给虚拟化方案使用而EL3就是整个系统里权限最高、最接近硬件的那一层。任何与安全世界通信、电源状态切换、可信启动校验相关的操作最终都要落在EL3这个级别上。ATF的全称是Arm Trusted Firmware是ARM提供的EL3参考固件实现。它的核心价值不是帮U-Boot引导内核而是建立一条普通世界不可绕过的安全边界。举个最直观的例子Linux要启动多个CPU核心、要进入休眠、要安全关机这些操作在ARMv8-A架构里都要通过一个叫SMC的指令陷入EL3然后由运行在EL3的固件去访问电源管理硬件。没有ATF或者没有类似的安全监控器Linux连secondary CPU都叫不醒。我在实际项目里最深的感受是很多人把ATF当成一个“必须跑的启动loader”但它本质上是安全监控器加运行时服务的组合体。这块代码如果出了问题后果不是板子起不来这么简单而是整个安全世界都暴露给了普通世界。1.2 一次冷启动背后发生了什么ARM标准的冷启动路径可以简化成复位之后从BootROM开始然后依次经过BL1、BL2、BL31最后进入BL33。BL1通常是芯片出厂时固化在BOOTROM里的一小段代码负责最基本的CPU初始化然后把BL2加载到SRAM里执行。BL2是可信引导阶段负责从FIP里读取BL31、BL32、BL33这几个镜像做完验证之后把它们放到各自的内存位置。BL31是长时间驻留在EL3的运行时固件它不像BL2那样启动完就没了而是会一直活在整个系统生命周期里为Normal世界的操作系统和Secure世界的可信OS提供安全监控服务。BL33一般指U-Boot或者UEFI运行在Normal世界的高权限模式之后再由U-Boot引导Linux内核。很多初学者会问为什么不能BL2加载完BL33就直接跳过去因为BL33只运行在Normal世界它没有权限去管理Secure世界的中断也没办法执行电源管理操作。BL31先把EL3环境建立好再以合适的上下文切到Normal世界这样后续所有安全相关的请求才能通过SMC回到EL3来处理。这个“切换上下文”的机制是ATF最核心的设计之一。1.3 源码目录结构和构建产物我建议第一次接触ATF的人先用qemu或者FVP平台跑一遍默认构建把整个编译产物体系建立起来。你执行完下面这两条命令之后会对ATF有一个非常直观的感知git clone https://github.com/ARM-software/arm-trusted-firmware.git cd arm-trusted-firmware make PLATqemu CROSS_COMPILEaarch64-none-elf- DEBUG1 all make PLATqemu BL33../u-boot/u-boot.bin DEBUG1 fip第一条命令会编译出bl1.bin、bl2.bin、bl31.bin第二条命令则会把它们和BL33一起打包成一个FIP镜像。FIP的全称是Firmware Image Package它本质就是个容器格式把多个固件镜像和它们的负载地址、校验信息打包在一起方便BootROM或者BL2统一加载。源码顶层目录我觉得可以这样快速记住bl1、bl2、bl31、bl32分别对应启动流程里每一级固件的代码common和lib目录放的是镜像描述、内存管理、异常处理、PSCI这类公共逻辑plat目录则是一个一个平台端口drivers目录下面是GIC、串口、认证算法、IO驱动。后面每个大的技术点基本都能从这些目录里找到对应实现。2. 架构全景从BL1到Kernel的启动链条源码拆解2.1 BL1与BL2信任根、镜像加载和证书验证BL1在ATF源码里的体量很小它要做的事非常收敛初始化最基础的CPU状态、设置栈、加载并验证BL2然后把控制权交给BL2。在实际的商用SoC里BL1往往不是这篇源码里的这份而是厂商BootROM自己实现但ATF参考实现的逻辑仍然值得研读因为信任根的建立方式基本一致。BL2是整个可信引导过程的主角。它会解析FIP里的镜像描述表找到BL31、BL32、BL33各自的位置在加载前后做TBBR要求的哈希验证或者证书链验证。如果想从源码理解这个过程重点看bl2/bl2_main.c和common/desc_image_load.c。尤其是image_desc_t这个结构体它把镜像ID、加载地址、认证方法、入口函数地址全部串起来看懂了它FIP加载流程就懂了一半。我实操时有个习惯在BL2加载每个镜像前加一个NOTICE日志打印镜像ID、物理目的地址、大小。很多莫名其妙的“BL31起不来”问题其实都是BL2把BL31放错位置或者大小字段被截断。有了这些日志再配合FIP里的元数据基本一眼就能定位。2.2 BL31核心工作异常向量、运行时服务和世界切换BL31是ATF里最值得看的部分因为它承担了最复杂的职责EL3异常向量分发、运行时服务注册、Power状态管理、上下文切换。开机过程中BL31完成初始化后会通过一条eret指令把CPU上下文切换到Normal World此后每次普通世界执行SMC指令CPU又会陷入EL3从bl31/aarch64/runtime_exceptions.S里的异常向量开始处理。ATF把SMC请求抽象成了一种服务机制。任何模块想在EL3处理SMC命令都可以用DECLARE_RT_SVC宏注册自己的服务描述符。这个设计非常像Linux内核里的struct file_operations把服务ID、初始化函数、处理函数、支持的调用类型绑在一起。你在审计一份ATF源码的时候找到所有DECLARE_RT_SVC的注册位置基本就等于画出了整个EL3的攻击面。世界切换是BL31的看家本领。为了在Secure世界和Normal世界之间安全跳转ATF需要把CPU的寄存器组、系统寄存器、中断状态全部保存下来。lib/el3_runtime/aarch64/context_mgmt.c里就是这部分逻辑。初次看这里会觉得很绕但核心思路并不复杂每个世界维护一份独立上下文切换时就换掉寄存器和栈指针同时更新系统寄存器里的安全相关位。2.3 BL32和Trusted OS是怎么接进来的BL32并不是必须存在的镜像。如果你的系统里没有TEE、没有安全指纹方案、没有DRM的secure world模块完全可以不集成BL32。但一旦需要BL32会作为独立的可信OS镜像加载进Secure World然后由BL31管理它的生命周期。OP-TEE是最常见的BL32实现。ATF构建时指定BL32路径FIP打包时就会把OP-TEE镜像放进去。BL31启动后期会通过一个SMC协议把控制权交给OP-TEEOP-TEE初始化完成后再通过另一个SMC把控制权交回BL31最后BL31才进入BL33。这个“两次握手”的过程很关键因为BL31和BL32之间要协商共享内存位置、中断路由等一堆问题。审计时我最关注的是OP-TEE与ATF之间的共享内存配置。很多TEE漏洞并不在密码算法里而在共享内存权限过大、普通世界可以直接访问Secure世界数据。看平台代码时务必确认platform_def.h里定义的共享内存物理地址和大小是否真的只开放了必要的区域给Normal World。2.4 从U-Boot到LinuxPSCI与SMC调用链当BL33接管之后ATF看似退居幕后实际上普通世界里的每一次CPU热插拔、系统休眠、关机重启都会触发SMC陷入EL3。Linux内核里有专门的PSCI驱动U-Boot也实现了PSCI调用逻辑。这条调用链可以简化为Linux/UBoot在EL1/EL2发起smc指令CPU陷入EL3ATF里的PSCI服务根据函数ID执行对应的电源管理操作最后返回结果。这块在移植时特别容易出错。很多板卡把PSCI做成一个可选的“启动协议”但现代ARM64 Linux已经默认PSCI为标准电源管理接口不再依赖spin-table方式做secondary CPU启动。如果你的ATF平台里PSCI回调函数没实现好Linux启动时会出现CPU1无法唤醒系统只能跑单核的诡异现象。从影响范围看ATF几乎覆盖了所有使用ARMv8-A架构的通用计算产品服务器、手机、网关、工控板卡都能看到它的身影。所以理解这条调用链不只是为了移植更是为了后续做固件安全评估时有清晰的上下文。3. 工程审计视角我这样读一份ATF源码3.1 审计前先把工具和基线搭好做安全固件工程审计我的第一步不是打开代码看逻辑而是先建立“已知正常”的构建基线。先把目标平台的源码clone下来确保能用官方toolchain编译出一个官方参考版。如果连baseline都编译不出来后面审计所得出的任何结论都可能因为构建差异而失真。我常用的工具组合比较朴素clang-tidy和cppcheck做静态扫描coccinelle做模式匹配git grep做关键函数追踪。ATF的代码质量总体不错但静态分析工具照样能扫出一些重复定义、空指针分支之类的问题。更重要的是先把docs/design/firmware-design.rst和docs/design/trusted-board-boot.rst读一遍这两份文档会告诉你ATF官方自己划定的设计边界在哪里。审计工作一定要有范围。ATF代码量不算小一份平台代码加公共库动辄几十万行。我一般先从BL2的镜像加载、BL31的SMC处理、PSCI状态机、TBBR的证书验证四条主线开始因为这几条路径都直接面对来自普通世界的输入攻击面最大。3.2 重点盯防的四个安全边界第一类是SMC服务入口。普通世界传入的参数很可能被恶意构造比如传入一个伪装成物理地址的用户态地址或者把一个长度字段改到溢出。合格的固件应该在访问前做地址范围和大小校验。第二类是FIP解析逻辑。FIP说到底是一种文件格式解析时如果对长度检查不严就可能越界读。第三类是MMU与共享内存配置。安全世界如果错误地把Secure内存映射到了Normal World可见的地址空间那么代码写得再安全也白搭。第四类是PSCI电源状态机。电源管理的状态转换如果没做合法性校验攻击者可能通过一个伪造的系统关机命令让整块设备进入不可恢复的状态。我经常会用grep去搜这几类高风险调用点比如mmio_write_32、memcpy、bl31_get_next_image_info这些函数然后逐个向上回溯调用者看有没有谁绕过检查直接访问了危险地址。这种“从危险函数反推调用链”的方式比顺序读代码要高效得多。3.3 一张照着做就够的审计速查表审计维度重点文件常见风险MMU与页表配置lib/xlat_tables_v2/Secure内存被错误映射、权限开放过大FIP镜像解析common/desc_image_load.c镜像大小字段越界、加载地址绕过校验TBBR证书验证drivers/auth/mbedtls/证书链校验不完整、算法降级PSCI电源状态机lib/psci/非法状态转移、CPU_ON参数绕过核心编号检查SMC运行时服务bl31/bl31_main.c服务ID冲突、参数未校验就访问内存我审计时会拿着这张表逐项过。每一行都要能回答三个问题数据从哪里来我能控制哪些字段如果我把某个字段改成极值会发生什么如果你能针对每个维度写出对应的攻击demo那这份ATF源码的安全底线就大致评估出来了。4. 平台移植落地把ATF从参考板搬到你的板子4.1 动手移植前必须搞明白的三件事移植ATF最怕的不是写代码而是对硬件一无所知就照抄参考平台。我每次接到新板子至少要把三份信息确认清楚第一是内存映射包括BootROM、SRAM、DRAM的物理地址范围和大小以及BL31最终要运行在哪个地址第二是外设基地址主要就是UART、GIC、定时器这三个东西在早期调试中缺一不可第三是启动流程也就是BootROM支持加载哪些镜像、是否强制要求FIP格式、SCP是否存在。这三件事没搞清楚之前我不建议直接建平台目录。否则你会遇到各种“看起来代码没问题但就是跑不起来”的情况最后发现是GIC基地址写错或者UART时钟没打开。移植ATF本质上是把一个通用安全监控器适配到特定硬件事务上硬件的TRM才是真正的权威。4.2 一个最小平台目录应该长什么样ATF的平台端口都在plat/目录下一个最小平台通常需要以下几个文件plat/myboard/ ├── platform.mk ├── plat_def.h ├── plat_setup.c ├── myboard_pm.c ├── aarch64/ │ └── plat_helpers.S └── include/ └── platform_def.hplatform.mk是最重要的构建入口告诉构建系统这个平台需要编译哪些源文件、定义哪些宏、使用什么样的链接脚本。plat_def.h和platform_def.h存放平台相关的内存地址、外设基地址等宏定义。plat_setup.c负责最基础的平台初始化比如UART、GIC和定时器。如果手上板子的GIC和UART与某个参考平台很像可以复制参考平台但务必要逐个核对地址。我吃过一次亏就是从某个平台复制的GIC代码忘了改GICD/GICC基地址导致所有中断全部丢失板子看起来像死机实际上EL3一直在处理中断。4.3 先别急着做PSCI让BL31先跑起来平台移植最稳妥的路线是先把BL31跑起来。这个过程不需要马上实现完整的PSCI只要能让串口打印出ATF的启动日志、能进入BL33就算成功。编译命令可以先用最小集合make PLATmyboard CROSS_COMPILEaarch64-none-elf- DEBUG1 RESET_TO_BL311 bl31把生成的bl31.elf通过JTAG或者OpenOCD加载到DDR里然后检查程序计数器是否停在bl31_entrypoint附近。如果这一步能过再用U-Boot做BL33进一步验证SMC入口和Normal World切换。我的经验是在bl31_early_platform_setup2里最开始的地方先直接往UART寄存器写一个固定字符。别看这个小动作很土它能最快区分出三类问题代码根本没跑到这个地址、串口配置错误、MMU翻译表把这段外设地址映射错了。把“最小字符串输出”调通之后整个移植的信心立刻就不一样了。4.4 PSCI最小实现要怎么落地PSCI是实现电源管理最关键的部分但第一次移植不需要马上实现CPU挂起、系统挂起这种复杂流程。先做一个“最小可用PSCI”包含SYSTEM_OFF、CPU_ON、CPU_OFF、SYSTEM_RESET几个基础回调验证调用链通顺后再逐步补齐。ATF平台代码里需要提供一个plat_psci_ops结构体里面放着平台相关的电源操作回调。比如实现一个最简单的关机就写一个往电源管理寄存器写值的函数然后填到system_off回调里static void myboard_system_off(void) { uintptr_t reg MYBOARD_PM_BASE 0x1004; mmio_write_32(reg, 0x1); } static const struct plat_psci_ops myboard_psci_ops { .system_off myboard_system_off, };当然真实平台没这么简单因为还要处理GIC断电、唤醒源、最后一核关闭时的特殊逻辑。但这个最小框架能先把SMC从Normal World到EL3的回调链路打通后面填充不同power state时你只需要聚焦在具体硬件行为上不需要再排查协议层面的问题。5. 踩坑实录与快速排查手册5.1 FIP打包和镜像地址不匹配我遇到最多的问题是BL2提示加载BL31失败但BL31本身编译出来是好的。检查顺序很简单先用fiptool查看FIP里记录的BL31加载地址再用nm查看bl31.elf里入口符号的虚拟地址两者一对比问题基本就浮出水面。tools/fiptool/fiptool info build/myboard/debug/fip.bin nm -n build/myboard/debug/bl31/bl31.elf | grep bl31_entrypointFIP里保存的加载地址最终会被BL2用来把bl31.bin拷贝到指定内存位置。如果链接脚本里BL31的基地址是0x60000000但FIP里写的是0x80000000BL31能够被加载但一跳进去立刻跑飞因为代码里所有绝对地址引用还是按0x60000000算的。这个坑非常隐蔽所以移植一个新平台时改动内存布局之后一定要同步重新生成FIP不要只重编bl31.bin就把旧的FIP直接扔进去。5.2 BL31 PANIC和同步异常的排查思路ATF一旦发生同步异常特别常见的现象是串口打印出一行PANIC后面跟着异常类型和PC值。很多新手看到PANIC就蒙了其实这个信息非常有用。你只需要做三件事第一确认异常发生时的异常级别如果ESR是EL3的异常说明问题出在ATF内部第二把打印出来的PC值拿addr2line转成源码行号第三看看当时是否正在执行SMC指令如果正在SMC处理路径里重点查参数校验和地址访问。另一个常见诱因是MMU配置导致的内存属性不匹配。我之前在某个平台上把BL31的.bss段误映射成了device memory结果代码在这个段里做自旋锁初始化时缓存和总线行为跟普通内存完全不一样程序直接卡死在一个看似“正常”的循环里。直到我用JTAG读内存才发现写进去的值根本没有回读到同一地址。从那以后我每次调整平台内存映射都会先检查内存属性表绝不让普通变量区域跟在device memory里。5.3 工具链选择别在AC5上死磕最近还经常看到有人在搜Arm Compiler 5.06相关的编译链但ATF新源码已经基本移除了对AC5的构建支持。AC5是当年Arm环境里很经典的老编译器很多老工程师手里有很多基于AC5的脚本但拿它来编现在的TF-A会遇到一堆宏定义缺失和构建错误。我的建议是新项目直接用GCC交叉编译器或者Arm Compiler 6官方支持的成熟度最高。export CROSS_COMPILEaarch64-none-elf- make PLATqemu DEBUG1 V1 all工具链版本波动也要注意。太老的GCC可能不支持新的-march扩展选项太新的GCC偶尔会在严格告警开启时把平台代码里遗留的小问题全挖出来。遇到告警报错时优先修代码不要往下绕。ATF是对安全和质量要求很高的固件带着告警上线早晚会付出代价。5.4 早期调试最有效的三板斧一是加临时打印。在串口驱动已经可用的前提下用tf_printf在关键路径打日志。调试BL31早期启动时我甚至会在每个平台初始化函数入口打一行。日志多不影响最终发布因为真正发布时本来就该重新梳理日志等级。二是善用JTAG硬件断点。有些问题只靠打印根本定位不了比如在中断处理里死循环或者寄存器上下文被破坏。这时候用OpenOCD拉一个硬件断点到异常向量处单步看寄存器比猜代码快得多。三是尽量拿QEMU/FVP做对照。X86主机上的QEMU和ARM官方FVP都能跑ATF碰到平台行为诡异的时候先在虚拟平台上跑同一段逻辑如果虚拟平台没问题那问题大概率出在平台初始化或者硬件配置上。这个“排除法”帮我节省过大量时间。6. 从“能启动”到“更完整的固件工程”后面还差什么6.1 后续扩展安全启动、FF-A和更多运行时服务最小平台能启动只是刚迈过门槛。产品要落地安全启动基本是硬性要求。ATF里把TBBR相关的证书生成、验签、防回滚作为一个可选模块集成进来配置项主要是TRUSTED_BOARD_BOOT和GENERATE_COT。开启之后BL2对每个镜像的认证会从简单哈希升级到RSA/ECDSA证书链整个信任根就真正落到芯片OTP根密钥上了。再往后就是FF-AFirmware Framework for Arm A-profile时代。FF-A是比传统SMC分发更规范的固件框架用来统一管理Secure Partition和Normal World的通信。ATF新版本已经提供了FF-A的参考实现如果你要在设备上跑多个安全分区这部分值得尽早关注。它把“服务注册”变成了“分区调度”架构上更加安全但也更复杂。6.2 工程审计与平台落地时的建议如果这个固件最终要过安全评估我会建议至少做四件事跑一遍完整的静态分析并把所有高危告警清零给BL31的SMC入口做一轮模糊测试专门往函数ID和参数里塞边界值做TBBR证书链的负向测试验证篡改任何一个byte都能让启动被拒绝最后审一遍平台内存映射图确认没有把Secure区域泄露给Non-Secure世界。平台移植也一样不要因为能从A平台照搬一份代码跑起来就满足了。每一行平台相关代码都要对着TRM确认过。不同SoC的GIC版本、UART时钟、PSCI回调实现都有细微差别照搬过来的代码如果基地址差一个偏移排查起来比从零写还要痛苦。我在这些项目里学到最有用的一件事是ATF不是“能开机”就结束的代码它是安全边界的第一道门。调试时多花半小时看源码和链接脚本后面能省下好几个通宵。如果你正在移植或者审计类似的固件希望上面这些记录能帮你少踩几个我当年踩过的坑。
返回列表