尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ATF安全固件架构解析与ARM平台移植实战

ATF安全固件架构解析与ARM平台移植实战 开篇为什么说ATF是ARM平台安全的中枢神经做ARM平台底层开发的工程师几乎都会在某个阶段撞上Arm-Trusted-FirmwareATF这堵墙。不管你是做TEE、做安全启动、做系统启动优化还是做芯片BSP适配ATF都是绕不过去的一环。我最早接触ATF是在一次SoC预研项目里当时要在一颗新流片的ARMv8处理器上把Linux跑起来板子还没点亮U-Boot能跑但一进内核就死查到最后发现是BL31的PSCI实现有问题——OS启动时的CPU热插拔和关机动作全都依赖EL3固件来处理。从那时起我才真正意识到ATF不是“可选的启动代码”而是现代ARM平台里承上启下的基础设施。这篇文章我不会泛泛而谈而是按照我自己做源码审计和平台移植的经验把ATF的架构全景、关键源码路径、安全审计要点以及移植落地的完整流程拆开来讲。适合正在做嵌入式底层、芯片验证、系统安全或者准备在自研平台上适配ATF的人阅读也适合刚入门但想搞清楚“ATF到底是什么”的同学按图索骥。我会尽量少讲空话多讲代码路径和实操步骤争取让不同基础的读者都能找到自己能直接用的东西。1. ATF不是“一个固件”而是一套安全固件家族1.1 用一个小区的比喻理解ATF如果你刚开始接触ATF最容易犯的错就是把它当成“另一个Bootloader”。实际上ATF是ARM官方维护的一组开源固件集合运行在AArch64架构的EL3和Secure EL1等特权级别它的核心职责是管理安全世界Secure World与普通世界Normal World之间的切换同时为普通世界的OS提供一系列安全服务接口。我用个生活化的比喻来说明整个ARM SoC就像一个小区的安保系统普通世界Linux/Android等OS是住户区安全世界TEE可信执行环境是金库区。ATF就是那个夹在两个区域之间的安保中心——所有人员进出金库区都要经过它的登记、盘问和放行。它自己跑在最高权限的EL3别的东西都管不到它但所有需要跨越安全边界的请求都必须通过它来转发。这也是为什么我在做安全审计时最先盯的永远是ATF的SMC入口和内存隔离配置——这两个地方是安全边界的“大门”。1.2 BL1、BL2、BL31、BL32、BL33——谁是干什么的ATF之所以叫“家族”是因为它内部被拆成了多个Boot Loader阶段每个阶段负责不同的启动任务。这里我把每个镜像的角色和它们在启动链中的位置整理成一张速查表镜像运行级别存放位置核心职责BL1EL3ROM/Flash复位后的第一个代码建立异常向量表加载BL2是整个安全信任链的信任根BL2EL3Flash由BL1加载负责后续镜像的加载与验签解析FIP包配置部分安全内存BL31EL3SRAM/DDR由BL2载入Runtime固件常驻内存处理PSCI、SDEI、SMC等运行时服务BL32Secure EL1DDR/安全内存通常是TEE OS如OP-TEE提供可信应用运行环境BL33EL2/EL1非安全内存即后续引导程序通常是U-Boot最终启动OS实际项目里BL1通常运行在片上ROM里因为复位后DDR尚未初始化代码必须在片上SRAM或ROM中执行。BL2则更像一个“大管家”它负责从FIPFirmware Image Package包里把BL31、BL32、BL33全部提取出来并完成验签。这里我特别提醒一句BL2本身运行在EL3但它最终是要被“用完即弃”的成功跳转到BL31后BL2占用的内存会被回收掉所以做内存布局时BL2和BL31的基地址不能规划得有冲突否则就是启动现场翻车现场。1.3 为什么它值得被当作“安全固件工程”而非普通代码很多做单片机出身、后来转应用层Linux的工程师第一次看ATF源码会觉得它非常“绕”。代码里充斥着smc指令、异常级别切换、MMU配置、cache一致性操作跟普通应用开发完全不是一个思路。原因其实很简单ATF处在系统安全边界上它要处理的是CPU最高特权级别下的异常路由、中断管理、电源状态转换和系统寄存器操作任何一行代码出错轻则系统崩溃重则可能被攻击者利用来提权。我在一次安全审计中曾经专门追踪过BL31里runtime_svc_init的初始化顺序。这个函数负责注册所有运行时服务包括PSCI、标准SMC服务、OP-TEE的SMC服务等。如果某个服务注册时使用了重复的SMC调用号或者处理函数指针为空那么一旦普通世界的攻击者调用到对应SMC指令就可能触发空指针异常甚至造成EL3异常。这类问题在代码review时非常隐蔽因为它不影响正常功能只在特定输入下暴雷这也是我建议所有做ATF底层开发的团队一定要在代码入库前跑一遍DEBUG1的编译模式加运行时断言的原因。2. 源码结构拆解从哪里入手读ATF2.1 顶层目录结构与各目录的真实职责下载ATF源码后第一眼看到的是十几个顶层目录。很多人会被这些目录搞得昏头转向但实际上它们的分工非常清晰。我按自己在源码审计时的阅读顺序把核心目录列出来bl1/、bl2/、bl31/三个主要BL阶段的入口代码和主流程plat/平台相关代码这是移植时主要改动的地方lib/通用库包括el3_runtime异常处理、psci电源管理、xlat_tables地址转换表drivers/外设驱动常见的有arm/的GIC、ti/的UART、io/的存储访问驱动include/头文件其中include/plat下是平台实现必须遵循的接口声明tools/构建辅助工具比如fiptool用于生成FIP包fdts/设备树源文件主要给FVP和固定平台用我个人的建议是第一次读ATF源码不要试图从头到尾逐行读完那会非常痛苦。正确顺序是先读docs/firmware-design.rst官方设计文档再读plat/xxx/platform.mk搞清楚一个最小平台需要实现哪些接口然后带着问题去读bl31主流程和lib/psci的代码。如果你一上来就钻进xlat_tables的页表管理代码里大概率会卡死在MMU配置上出不来。2.2 构建系统与交叉编译的关键参数ATF的构建系统用的是makefile没有采用复杂的Kconfig或CMake这点对熟悉Linux内核的人来说反而是个好消息。但它的平台扩展机制和Linux内核的arch/arm64加mach-xxx的机制不完全一样——ATF要求每个平台必须提供一份platform.mk里面通过PLAT_xxx宏和变量定义平台参数。一个典型的编译命令长这样make CROSS_COMPILEaarch64-none-elf- PLATqemu DEBUG1 LOG_LEVEL50 bl31各个参数的含义我拆开讲一下CROSS_COMPILE交叉编译工具链前缀。使用ARM官方aarch64-none-elf-也好使用GNU工具链的aarch64-linux-gnu-也好都行但要保证工具链版本不要太老。我实测过用GCC 8.x编译新版ATF2.8以上偶尔会遇到内建函数签名不匹配的告警最好用GCC 10以上。PLAT选择平台对应plat/目录下的子目录名。比如qemu、fvp、juno就是官方参考平台。DEBUG1开启调试版会包含大量断言和日志代码固件体积会大不少性能也会有损耗但排查问题必备。LOG_LEVEL日志级别从0无日志到50所有日志。移植初期建议设到40以上UART输出详情会非常有用。编译完成后产物通常在build/PLAT/release/或build/PLAT/debug/目录下核心文件是bl31.bin而完整的FIP包是用fiptool把BL2、BL31、BL32、BL33打包生成的。打包FIP时经常遇到一个坑BL33U-Boot的加载地址要和BL2里规划的LOAD_ADDR一致否则BL2把镜像搬运到内存后BL31跳转过去就是执行了一个空壳。2.3 运行时服务PSCI和SMC的分发机制ATF最核心的运行时机制是SMCSecure Monitor Call分发。简单说普通世界的代码执行smc指令后会陷入EL3ATF根据SMC调用号找到对应的服务处理函数。整个分发链条分为三层异常入口BL31在el3_exception中捕获SMC异常调用handle_smc服务路由根据Function ID的高16位判断服务类型常见的是0x84000000快速调用和0xC4000000标准调用具体服务通过DECLARE_RT_SVC宏注册的服务描述符找到处理函数并执行PSCI就是通过这个机制实现的标准服务之一。Linux内核里的psci_smp_boot_secondary、psci_cpu_suspend、psci_system_off等操作最终都会变成一条smc指令发送给EL3的PSCI服务。这也是为什么我在移植ATF时最先要验证的就是PSCI的CPU_ON调用——如果这个没有实现好多核系统根本无法启动从核系统只能单核跑。3. 安全固件工程审计从代码里挖出风险的5个重点3.1 可信启动链与信任根的确认做安全固件审计时第一步永远要回答一个问题这个系统的信任根在哪里ATF的标准设计里信任根是固化在SoC ROM里的BL1BL1验证BL2的镜像签名BL2再验证BL31/BL32/BL33的签名形成一条完整的信任链。这意味着如果BL1本身被篡改整个安全体系就会崩溃。但实际项目中很多团队为了省事会把TRUSTED_BOARD_BOOT关掉也就是跳过了签名验证步骤。这在开发阶段完全合理因为每次烧写镜像不需要重新签名很省时间。然而一旦进入量产阶段还保持关闭状态就等于把整个安全边界的大门敞开了——攻击者可以替换Flash里的固件实现完全控制设备。我在审计中列过一个检查清单分享给大家直接参考审计项检查方法通过标准信任根是否存在查看BL1是否支持TRUSTED_BOARD_BOOT配置强制开启且BL1上电后做自校验镜像签名算法强度检查mbedtls配置和证书链推荐RSA-3072或ECC-256以上公钥生命周期查看OTP一次性可编程存储公钥哈希应烧录在OTP中防篡改BL2对后续镜像验签检查BL2的auth模块每个镜像加载前均需验签调试接口隔离查看UART/JTAG等调试口量产固件应关闭或鉴权3.2 内存隔离与TrustZone的配置边界ATF运行在EL3拥有对所有内存的最高访问权限但这不代表它可以纵容普通世界的代码任意访问。TrustZone技术通过TZASCTrustZone Address Space Controller等硬件模块划分安全内存和非安全内存区域普通世界的DMA控制器、CPU访问通通无法穿透安全边界。审计时要重点看板的platform_get_isolated_memory、plat_get_ns_image_entrypoint等函数里安全内存的基地址和大小是否配置正确。我在一个项目里碰到过内存重叠问题——安全内存区域的边界被修改后TEE使用的内存和U-Boot加载Linux内核的地址区间发生了交叠导致TEE里的密钥数据偶尔会被内核的错误写入覆盖。这类问题平时不会出现只在内存压力大的时候偶发排查起来极其头疼。3.3 异常向量表与SMC处理路径的审计技巧ATF的每个异常入口都是攻击者最感兴趣的攻击面。常规做法是把bl31的vector表设置在一个安全的内存区域并且确保所有异常处理路径都经过安全检查。我审计时通常会用源码搜索的方式把所有SMC派发函数找出来检查是否有未注册的处理函数、是否有溢出风险。一个具体且常见的问题是SMC参数校验不充分。比如某个服务函数直接使用攻击者传入的寄存器值作为内存偏移量而没有检查边界。这类漏洞在ATF的第三方服务里出现概率很高官方核心服务反而比较少。所以如果你在ATF上扩展了自己的服务一定要在服务入口做参数白名单校验不能完全信任调用者传来的数据。3.4 日志泄露与信息暴露很多人觉得ATF日志只是开发辅助量产时关闭就行但其实日志泄露在安全审计里是非常靠前的一类问题。BL1/BL2/BL31的日志如果长时间开启可能会把一些敏感的内存地址、密钥存储路径、DDR初始化序列打印到串口上。攻击者在物理接触设备的情况下只需要接一根串口线就能从日志中逆向出关键内存布局。我建议的做法分三步第一量产固件必须LOG_LEVEL0完全关闭日志第二保留日志的时候也不要直接使用printf打印内存内容而是打印经过混淆的标识第三如果调试口无法物理禁用至少要在启动完成后将UART引脚复用到其他功能从硬件层面断绝串口窥探的可能。3.5 安全配置的回归验证安全审计不是一次性工作。每次平台代码更新、每次编译器升级都可能导致安全配置回归。我自己的习惯是在持续集成流程里加一个“安全构建”任务用TRUSTED_BOARD_BOOT1和DEBUG0编译ATF然后自动跑一遍启动冒烟测试确保安全配置的固件仍然能正常引导系统。这一步费用极低但能避免很多量产前的“最后时刻”事故。4. 平台移植落地从零到一把ATF跑上你的板子4.1 移植前需要准备的资料清单平台移植最忌讳的是拿过来就写代码写到一半发现缺这个缺那个白白浪费大量时间。我建议动手之前先把以下几份资料备齐SoC的TRMTechnical Reference Manual重点是存储器映射表、中断控制器GIC、UART、定时器的寄存器基址核心板/开发板的原理图确认DDR容量、启动介质类型、串口引脚是否复用参考平台的ATF代码比如QEMU或FVP平台代码作为对照交叉编译工具链我用的是aarch64-none-elf-的GCC 12版本一块支持JTAG调试的开发板配合Trace32或OpenOCD这能大幅缩短调试时间在这些资料里存储器映射表是我最看重的。ATF的MMU配置全部依赖各个外设的物理基址只要有一个地址配错板子大概率直接挂死而且不容易从日志中看出来——因为它可能根本没走到打印日志的代码段。4.2 最小可启动系统移植的步骤拆解做平台移植时我的目标不是一步到位搞出完整的量产固件而是先做出一个“最小可启动系统”BL1→BL2→BL31→BL33U-Boot→Linux能跑起来哪怕功能不完整但主流程通了后面的安全加固和功能扩展才有基础。具体步骤大概是这样第一步选择参考平台拷贝平台目录。从plat/下复制一份跟你硬件最接近的平台代码。如果你的SoC用的是Arm GIC-600、PL011串口那直接参考plat/arm/board/fvp比从零开始容易得多。第二步修改平台宏和基础参数。新建平台的platform_def.h配置关键参数PLAT_PHY_ADDR_SPACE_SIZE、PLAT_VIRT_ADDR_SPACE_SIZE、PLAT_MAX_OFF_STATE等。这些值决定了ATF的MMU地址空间范围和电源状态定义配置错了系统启动时会直接跳异常。第三步配置串口驱动。ATF的log信息全靠UART输出所以串口初始化必须最早完成。配置的核心是UART的寄存器基址和波特率计算公式。PL011串口的波特率计算涉及UARTIBRD和UARTFBRD两个分频寄存器ATF里通常已经封装好了只需要替换基址。第四步调通初级异常与时钟。让BL31能正常进入EL3在el3_common_exception能捕获到异常并打印信息。这一步意味着异常向量表工作正常GIC的初始化也至少是部分可用的。第五步打包并烧录验证。用fiptool生成FIP包然后烧录到Flash或通过JTAG加载观察串口输出。这几个步骤每走一步都要及时验证不要一口气改完全部代码再调试。我第一次移植的时候图省事改了十几个文件才烧录结果串口完全无输出根本不知道是哪一步出了问题后来只能回头一步一步加日志排查白白浪费了整整一天半的时间。4.3 从“跑起来”到“能用”的配置细节最小启动跑通之后接下来要做的是把平台的关键外设驱动配置完整尤其是GIC和系统计数器。GICGeneric Interrupt Controller是中断管理核心ATF在BL31启动阶段会把GIC重定向到安全状态并配置所有中断的默认路由策略。如果GIC配置错误Linux内核启动时就收不到任何定时器中断导致clocksource无法工作卡死在启动早期。GIC初始化最关键的是确认GICD_BASE、GICC_BASE或GICR基址是否正确以及中断组的默认targer设置。系统计数器System Counter则是ARM架构提供的一个64位全局时间源在单板上通常由SoC内部的某个定时器模块提供其频率信息需要写进设备树并且ATF里也要配置对应的SYS_COUNTER_FREQ_IN_TICKS。这个值的精度直接影响内核里的arch_timer时钟源的频率计算配错了虽然系统能启动但所有基于时间戳的性能统计数据都会失真排查起来非常费劲。4.4 移植后的安全加固与量产准备最小启动完成不等于平台移植完成。量产固件还需要做安全加固这个阶段我会重点处理三件事开启Trusted Board Boot、关闭调试口、配置安全内存隔离。开启Trusted Board Boot的步骤一般包括生成密钥对、创建证书链、把公钥哈希烧录到OTP、用fiptool生成带签名的FIP。这个过程早期就要在脚本环境里跑通因为后续每次版本发布都要走一遍签名和打包流程人工操作会非常容易出错。我推荐把整个流程写成CI脚本输入是各BL镜像的产物输出是签好名的FIP包这样既能保证一致性也能省去大量重复劳动。内存隔离配置方面重点是把TEE需要使用的DRAM区域标记为安全普通世界的U-Boot和Linux内核的加载地址、DMA使用的内存池都要落在非安全区域内。这里需要特别小心DMA缓冲区——如果某块内存被标记为安全但普通世界的DMA控制器仍然能访问它那TZASC的隔离就形同虚设需要联合硬件设计团队确认各个主控的访问权限配置。5. 常见问题与排查技巧实录5.1 串口无输出的排查路径移植过程中最常见的故障就是串口完全无输出。按照我自己的排查习惯顺序一般是这样的排查对象检查内容常见根因复位向量与启动介质芯片是否真正从Flash/ROM启动了BL1启动介质选择引脚配置错误Flash没选对UART硬件初始化基址是否配错、时钟是否使能平台宏里的UART基址是别的串口的地址时钟配置PLL是否锁定、UART参考时钟频率是否匹配UART波特率计算错误输出乱码或完全无数据编译链路交叉编译器是否与当前ATF版本兼容工具链过旧导致内建函数签名差异启动介质BL1是否成功从外部Flash加载BL2BL2镜像偏移地址与BL1期望值不一致串口无输出时第一步永远是确认你的代码是不是真的跑起来了。没有JTAG的话可以先接一个逻辑分析仪看UART引脚上有没有波形有JTAG的话直接用调试器看PC指针停在哪个地址非常高效。千万别拿着源代码猜猜来猜去除了浪费时间没有任何帮助。5.2 BL31启动卡死与CPU进入不可恢复异常BL31启动阶段卡死的根因通常集中在三个方向GIC配置、系统计数器配置、以及异常向量表跳转地址错误。我在一次QEMU平台上调试时BL31打印完第一条log后直接卡死回看代码发现是gicv3_driver_init里读取GICD_TYPER时GIC基址被我写成了另一个平台的导致读取出来的寄存器值全是0xFFFFFFFF系统进入死循环。这类问题最好的排查方式就是在卡死位置附近增加临时日志。先定位到具体卡在哪个函数再回头细查该函数里访问了哪些硬件寄存器对照TRM逐一确认基址和偏移是否正确。纯靠猜测或者大量打印所有可能的寄存器会浪费大量时间而且容易把问题搞得更乱。5.3 启动日志片段速览与判断技巧如果BL1、BL2的日志能正常打印但BL31之后U-Boot没有输出那问题大多出在BL31跳转BL33的环节。这时候要看BL31的日志里是不是有一行类似Running image at 0x...的信息如果这个地址和U-Boot实际链接的地址不一致就会跳转后立即死机。另外还要确认BL31里plat_get_ns_image_entrypoint返回的地址是否已经按照非安全世界的入口做了地址映射。这里有个容易被忽略的细节带MMU和不带MMU时NS_IMAGE_OFFSET的换算不一样如果平台在BL31阶段开了MMU而入口地址依旧是物理地址跳转时就会访问到非法虚拟地址。5.4 关于MbedTLS库与BL2镜像大小的经验开启Trusted Board Boot之后BL2会引入MbedTLS库来做验签运算这会让BL2镜像体积急剧膨胀。一些SoC的BL2运行区域只有几百KB的SRAM可能根本装不下MbedTLS的代码。解决思路有两个一是选用裁剪版的MbedTLS配置只保留验签所需的最小算法集合二是把BL2的验签运算移到BL31阶段去执行但这样会改动信任链的设计需要谨慎评估。我自己的建议是在项目早期就把BL2的体积预算定下来并把MbedTLS的裁剪和编译作为一个独立任务提前验证。不要在整体移植完成后才发现装不下那时候再动刀会非常影响进度。5.5 冷启动和热重启的一些额外关注点ATF的PSCI服务不仅要处理冷启动还要处理系统suspend/resume和CPU hotplug。在验证平台时一定要把几种场景全部测到系统重启PSCI_SYSTEM_RESET是否能成功有些平台需要配置特定的看门狗或复位控制寄存器否则会卡住。CPU热插拔CPU_ON/CPU_OFF是否稳定反复插拔几十次、几百次看是否出现从核起不来或者cache数据不同步的问题。深度休眠后唤醒内存内容是否还在这涉及DDR自刷新、PLL重新锁定等流程是移植里最难调的一块。我踩过最深的坑是某款SoC的SYSTEM_SUSPEND只支持在特定电源域组合下进入而ATF的默认PSCI状态机没考虑这个限制导致每次sleep都直接把系统带崩。最后是通过修改平台的pwr_domain_pwr_down_wfi实现在进入WFI前增加了对电源域状态的判断才勉强稳定下来。这种问题没有通用解法只能上板验证加反复对照TRM。6. 我个人对ATF平台移植的最后一点体会断断续续做了几年ATF相关的开发和审计我最大的感受是ATF的代码结构已经算是ARM开源项目里比较清晰的了真正的难点从来不在源码逻辑而在你手里的那块板子和源码假设之间的差异。每一个“为什么这么简单的东西还跑不通”到最后基本都能归结为某个寄存器基址、某个时钟频率或者某个内存地址不匹配。所以我不太建议大家直接照着别人平台的配置抄而是要认真读懂自己的TRM把每个关键地址都对照过再动手。另外有一个小技巧如果条件允许尽量在开发早期就把FVPFixed Virtual Platform或QEMU的模拟环境跑起来。在虚拟平台上先把ATF的主流程调通再上真实硬件很多基础性的代码错误可以在不烧板子的情况下提前暴露出来。我在实际项目中至少有三分之一的问题都是先在QEMU上发现的这比在真机上用JTAG反复断电重启省太多时间了。ATF的生态还在持续演进像FF-A、RME这些新特性都会在未来逐渐成为主流。但不管功能怎么变架构和信任模型的核心逻辑是稳定的。把今天讲的这些基础概念和排查思路吃透以后面对任何基于ATF的安全固件工程你都不会再觉得无从下手。
返回列表