ARTICLE DETAIL

资讯详情

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

ARM安全固件ATF深度解析:从TrustZone到安全启动的实践指南

ARM安全固件ATF深度解析:从TrustZone到安全启动的实践指南 写这篇东西之前先说个场景这几年做ARM平台BSP适配十次有八次都会被问到“你们的安全启动是怎么做的”。很多从MCU转过来的工程师第一反应是“不就是一个bootloader吗”然后被BL1、BL2、BL31、OP-TEE、TBBR这一串缩写砸晕。Arm Trusted FirmwareATF恰恰是整个ARM安全体系里最容易被低估、又最绕不开的一环。这篇文章是我这些年从源码层面拆ATF的实践笔记会讲架构全景、安全固件的工程审计点最后用一份可落地的平台移植流程收尾给正在搞TrustZone、安全启动或者准备在新板子上跑ATF的朋友做参考。1. 先搞清楚ATF在ARM体系里的位置1.1 没有EL3就没有真正的“安全世界”很多人看ARM架构文档最先记住的往往是异常级别Exception Level也就是EL0到EL3。普通应用程序跑在EL0操作系统内核跑在EL1虚拟化相关的Hypervisor跑在EL2而EL3是整个执行体系的最高权限层。与此同时ARM还有一个“安全世界”Secure World和“普通世界”Normal World的划分也就是TrustZone技术。这两个世界共享同一套物理核但在缓存、中断、内存访问权限上做了隔离。ATF做的事情简单说就是常驻在EL3负责把Secure World初始化好再把启动流程一级一级往下传递。没有它Linux内核即使在EL1跑起来了也无法安全地操作电源管理、安全启动、可信存储这类关键功能。可以类比成小区的安保系统EL3不是住户也不是物业前台而是那个拿总钥匙、管监控室、控制整个门禁体系的人。1.2 ATF不是一个固件是四个固件的接力看ATF源码最迷惑人的一点是它不是一个单一的bootloader。按照启动顺序它被拆成了四个阶段BL1固化在芯片内部ROM里的启动代码叫Boot ROM出厂就写死了。它的任务极其简单初始化最小环境把BL2从Boot Device比如eMMC、SPI Flash加载到SRAM然后跳转。BL2运行在SRAM里的可信启动引导负责验证和加载后续镜像包括BL31、BL32、BL33。BL31EL3 Runtime固件启动完成后常驻内存处理SMCSecure Monitor Call请求、PSCI电源管理、Secure中断路由等。BL32可选的Secure-EL1固件通常就是OP-TEE这类可信执行环境OS。BL33非安全世界的引导程序U-Boot、UEFI等都算。把这五段串起来看其实就是一个接力赛BL1把棒交给BL2BL2验证完身份后把棒交给BL31BL31撑起安全运行环境最后把普通世界的控制权交给BL33。理解这个顺序再看源码就不会迷路。2. ATF源码结构深度拆解别被一堆目录吓住2.1 顶层目录一图流每个目录负责什么下载ATF源码官方仓库名是arm-trusted-firmware/tf-a解压之后看顶层目录常见的有这些bl1/ bl2/ bl31/ bl32/ plat/ common/ drivers/ lib/ include/ make_helpers/ docs/ tools/bl1、bl2、bl31、bl32分别对应上一节说的四个固件阶段plat放的是平台相关代码也就是移植时主要要动的部分。common是各阶段共享的通用逻辑drivers是各类外设驱动lib里比较关键的是el3_runtime、psci、xlat_tables。tools里面有几个重要工具比如生成证书链的cert_create。我把这套结构理解成一个“核心引擎可替换外壳”的设计引擎是通用的ARM帮你把启动流程的骨架写好了而外壳——也就是具体板子的寄存器地址、内存布局、串口型号——需要你根据芯片手册去填。移植ATF说白了就是在plat目录里写一套符合固定接口的“填表代码”。2.2 启动流程代码走读从BL1到BL31用实际源码路径说话。BL1入口在bl1/bl1_main.c核心函数是bl1_main()它会做早期平台初始化bl1_early_platform_setup()然后调用bl1_load_bl2()填充BL2镜像的信息最后通过bl1_run_bl2()跳转。BL2的逻辑写在bl2/bl2_main.c里bl2_main()会调用bl2_platform_setup()初始化平台然后依次加载BL31、BL32、BL33期间会做镜像验证如果开了TBBR的话。这些镜像的信息全部存在一个叫bl31_params_head的结构体链表里后面BL31启动时要靠它获取参数。BL31的主控代码在bl31/bl31_main.cbl31_main()会先做运行时初始化注册各种运行时服务Runtime Service最后跳转到BL33。从这一层开始系统就不再是“直线启动”了而是进入一个随时待命的状态响应来自普通世界的SMC请求。这也是为什么很多人说BL31是ATF的核心它承担的不只是启动工作还有整个运行时的安全监控职责。2.3 PSCI与SMCATF给上层留的那扇门没有ATF之前Linux要关一个CPU核心可以自己操作寄存器。引入EL3之后普通世界的权限被降级Linux不能直接碰电源管理相关寄存器必须通过SMC指令“请”EL3代劳。PSCIPower State Coordination Interface就是这套约定的标准协议Linux内核通过PSCI调用让ATF去执行CPU_ON、CPU_OFF、SYSTEM_SUSPEND、SYSTEM_RESET这些操作。源码里对应位置是lib/psci/psci_main.c入口函数psci_smc_handler()根据SMC的功能ID分发到不同处理函数。举个例子Linux要启动一个额外的CPU核时会触发PSCI_CPU_ONATF收到后会在EL3完成核的电源上电、入口地址设置、上下文恢复等操作。这套机制的好处是把电源管理从OS层抽象出来芯片厂商只需要在ATF里适配自家电源控制器就行。可以这么理解普通世界的软件只会喊“我要开一个核”“我要关机”具体怎么操作寄存器全部由EL3里的ATF说了算。这样既保证了安全隔离也让OS的移植成本大大降低。3. 安全固件工程审计源码评测里真正要盯的地方3.1 证书链与密钥管理安全启动的命门ATF里最值得做工程审计的就是TBBRTrusted Board Boot这套信任链。它通过数字证书的方式一级验证一级BL1验证BL2的证书BL2验证BL31、BL32、BL33的证书。每一级镜像都有对应公钥私钥则保存在安全的地方。实际操作中tools/cert_create这个工具负责生成证书和签名。审计时最常看到的问题有三个。第一开发环境为了方便把根私钥ROT Key直接放在源码仓库里一旦泄露整条信任链就崩溃。第二升级固件后没有妥善管理密钥版本导致旧固件无法验证新签名。第三证书里包含的镜像哈希算法还在用SHA-256虽然目前够用但长远看逐步迁移到SHA-384是趋势。这里给一个审计建议生产环境的ROT Key一定要离线保存最好有硬件加密机保护。开发环境可以用测试密钥但必须通过环境变量或独立文件传入构建系统不能写死在Makefile里。我见过有团队把所有证书生成过程打在一个脚本里不同项目复用同一套密钥这是安全隐患一旦芯片流向市场基本无法更换根密钥。3.2 内存映射与MMU配置权限隔离的底线ATF里每个镜像都有自己独立的内存映射表放在各个平台目录下的plat_${platform}_mmap结构里。审计时我会重点看每个内存区间的属性标志常见标志包括MT_SECURE、MT_NS、MT_RO、MT_RW、MT_EXECUTE。最容易出问题的是安全内存被映射成了非安全可读写或者把可执行权限给了数据区。前者等于把保险柜门打开让人随便拿后者则给了攻击者注入代码的机会。之前审计过一块板子厂商为了调试方便把OP-TEE占用的安全DRAM区域映射成了MT_NS | MT_RW导致任何非安全世界的进程都能读写安全世界内存这种低级错误在量产固件里一旦被利用后果非常严重。另外一个高频坑是MMU的Granule大小和页表层级配置不对。很多平台默认使用4KB粒度和4级页表如果平台DRAM大小超过512GB就必须调整PLAT_PHY_ADDR_SPACE_SIZE和PLAT_VIRT_ADDR_SPACE_SIZE这两个宏。审计时可以查看构建生成的${BUILD_DIR}/bl31/bl31.map文件确认最终内存映射是否符合预期。3.3 调试开关与日志泄漏量产固件的隐形风险ATF提供了非常方便的日志系统通过LOG_LEVEL宏控制。开发时大家喜欢把LOG_LEVEL调到50甚至更高对应VERBOSE级别串口会打印大量调试信息包括内存地址、寄存器值、甚至证书内容。但如果这套配置带到了量产版本等于把自家芯片的底牌全暴露给了攻击者。我在审计中就见过一个案例某款设备量产固件里DEBUG标志没有关闭BL31启动时会把所有SMC调用的参数原样打印出来。攻击者只需要用串口接上调试引脚就能看到每次安全调用的完整输入输出进一步构造恶意调用。正规做法是量产构建强制DEBUG0且LOG_LEVEL20以下仅保留ERROR级别并且在发布前用二进制扫描工具检查固件里是否残留调试字符串。3.4 常见攻击面与整改清单我把做固件审计时最常检查的攻击面整理成了一张表每一条都对应到ATF的配置或代码位置攻击面风险描述审计点整改建议调试接口串口、JTAG暴露内部状态PLAT_*_DEBUG_UART、JEDEC寄存器量产时禁用或加访问控制镜像降级攻击者替换为旧版本固件TBBR证书版本号、镜像hash启用Anti-Rollback机制非安全内存读写安全数据被普通世界访问内存映射属性表严格标注MT_SECURE与MT_RO固件篡改修改固件内容后重新刷写ROT Key与证书链强化签名校验私钥离线保管SMC调用滥用上层构造恶意SMC请求runtime_svc注册表校验调用来源和参数白名单这张表不是模板每一行都是实际踩过或见过的教训。做安全固件的工程师建议把这张表挂在自己工位上每完成一次移植就过一遍。4. 平台移植落地全流程从零开始适配一块新板子4.1 准备工作先答完这三个问题再动手移植ATF最忌讳的是拿到板子就开始写代码。我的习惯是先花半天时间把三件事搞清楚CPU是什么架构、核心簇怎么分布、支持的地址空间多大SRAM和DRAM的物理起始地址和大小特别是ATF运行时要占用的Secure RAM区域调试串口用的是哪个IP基地址是什么。以最常见的ARMv8架构四核A53平台为例你需要的基本信息大致是UART用的是PL011基地址0x1c090000DRAM从0x80000000开始大小2GBSRAM或OCRAM只有64KB只能放BL1和BL2GIC版本是GIC-400支持安全中断路由。没有这些参数后面每一步都会卡壳。另外一件很重要的事是选参考平台。ATF源码plat目录下已经有很多官方或芯片厂维护的平台比如arm/fvpFixed Virtual Platform、rockchip、mediatek、qemu。移植新平台时最好的起点是找一个硬件资源最接近的参考平台把它的目录复制一份来改比从零搭建省很多事。4.2 搭建平台目录platform_def.h与platform.mk在plat下新建一个以你的板子命名的目录比如plat/myboard然后在里面建这样几个核心文件platform_def.h、platform.mk、plat_helpers.S、plat_common.c、plat_psci.c。platform_def.h定义的是“板子的基本参数”举几个关键项#define PLATFORM_CORE_COUNT 4 #define PLATFORM_CLUSTER_COUNT 1 #define PLATFORM_MAX_CPUS_PER_CLUSTER 4 #define PLATFORM_CLUSTER0_CORE0 0 #define DEBUG_CONSOLE_BASE 0x1c090000 /* PL011 */ #define PLAT_DRAM_BASE 0x80000000 #define PLAT_DRAM_SIZE 0x80000000 /* 2GB */ #define PLAT_SRAM_BASE 0x10000000 #define PLAT_SRAM_SIZE 0x10000 /* 64KB */ #define BL31_BASE 0x80000000 0x100000 #define BL32_BASE 0x80000000 0x200000 #define BL33_BASE 0x80000000 0x300000platform.mk则告诉构建系统要编哪些源文件以及启用哪些特性核心配置像这样PLAT_BL_COMMON_SOURCES : \ plat/myboard/plat_common.c \ plat/myboard/plat_helpers.S \ lib/xlat_tables/xlat_tables_common.c BL2_SOURCES \ plat/myboard/plat_bl2_setup.c BL31_SOURCES \ plat/myboard/plat_psci.c \ plat/myboard/plat_topology.c \ lib/psci/psci_main.c ARM_ARCH_MAJOR : 8 ARM_CORTEX_A53 : 1 NEED_BL32 : yes NEED_BL33 : yes ENABLE_PSCI : 1这里有个容易忽略的点PLAT_BL_COMMON_SOURCES里的文件是所有BL阶段都会编译的里面绝对不能写依赖特定阶段的函数不然链接时就会出现符号未定义的错误。4.3 最小化启动先让串口吐出一个字符真正写代码时我不建议一上来就追求完整启动链而是先做一个“最小化系统”让BL1能把BL2加载起来BL2能初始化串口并打印一句版本信息然后BL31能跳到BL33哪怕BL33是个死循环。这一步跑通了后面的事情才有意义。实现串口打印以PL011为例核心代码只需几行#define UART_BASE 0x1c090000 #define UART_DR 0x00 #define UART_FR 0x18 static void uart_putc(int c) { while (*(volatile unsigned int *)(UART_BASE UART_FR) (1 5)) ; *(volatile unsigned int *)(UART_BASE UART_DR) c; } void plat_console_putchar(int c) { uart_putc(c); }然后实现plat_helpers.S里的最小启动汇编代码主要任务就是设置栈指针、清零BSS段、跳转到C函数。这个阶段不需要配MMU直接工作在物理地址模式下代码简单直接。编译命令是make PLATmyboard DEBUG1 bl2 make PLATmyboard DEBUG1 bl31生成的镜像在build/myboard/debug/bl2/bl2.elf等路径下。烧录时把BL1放进芯片Boot ROM要求的位置然后把BL2放到SRAM开头BL31放到DRAM里的目标地址。如果一切顺利串口会输出ATF版本字符串和一个NOTICE级别的启动日志那一刻的成就感堪比第一次点亮LED。4.4 接入OP-TEE与U-Boot组合完整启动链最小系统跑通后接下来就是接入BL32通常是OP-TEE和BL33U-Boot。这一阶段的坑集中在加载地址上。BL32和BL33必须加载到它们编译时指定的链接地址如果ATF加载地址和镜像本身的期望地址不一致跳转后必然异常。一个我在实际项目中遇到过的问题U-Boot编译时指定了TEXT_BASE0x80040000但在ATF的platform_def.h里把BL33_BASE误设成了0x80080000结果BL31跳转过去直接掉进未初始化内存串口没有任何输出。排查了很久最后用JTAG看PC指针才定位到。所以每次修改U-Boot的链接地址一定要同步检查ATF的BL33_BASE。OP-TEE的接入需要额外确认Secure内存区域有没有正确映射。ATF里OP-TEE依赖的SRAM或DRAM安全区域必须用MT_SECURE | MT_RW属性映射并且要确保非安全世界的设备无法访问这块区域。很多平台在这里还要做TZASCTrustZone Address Space Controller配置把DRAM切成安全和非安全两个区域否则即使页表配了MT_SECURE物理层面依然存在绕过风险。4.5 移植完成后把这些信息加进平台文档平台移植完成后我习惯在plat/myboard/下补一份readme.md写明编译命令、镜像加载地址、串口参数、支持的特性清单。这件事看起来不起眼但实际维护固件时价值很大。因为三个月后回来看代码你大概率会忘记当初的串口波特率是115200还是1500000以及BL31为什么预留了2MB空间。5. 移植与调试中的典型问题速查5.1 “串口完全没输出”怎么排查遇到串口没反应我一般按这个顺序查先量串口引脚电平排除硬件开路再确认ATF代码是否真的执行到串口初始化函数可以用逻辑分析仪抓TX引脚的信号最后检查波特率配置和分频寄存器。如果用了PL011分频系数由uart_clk / (16 * baud)决定时钟频率算错会导致输出乱码而不是完全没输出。完全没输出的情况多半是代码根本没跑到串口初始化比如BL1加载BL2失败。5.2 “卡死在BL2后续镜像根本没加载”这种情况最典型的原因是加载地址不正确。ATF有一套镜像描述结构体用来记录每个镜像的目标加载地址这个地址必须是物理地址且在对应内存区域内。如果BL33_BASE落在了一个没有物理内存的区域BL2虽然完成了加载但BL31跳转后就触发Data Abort。排查时可以先开DEBUG1看BL2加载每个镜像时打印的地址再对照板子手册验证。5.3 “启用了TBBR之后板子直接变砖”TBBR开启后最麻烦的问题是证书链不完整。ATF在BL2阶段会严格验证每个镜像的签名如果cert_create生成的证书和图镜像不匹配启动会直接拒绝执行。注意每次修改了镜像Related必须在Makefile里让构建系统重新执行cert_create否则旧证书里记录的哈希值会和新的镜像内容不匹配。遇到这种问题先删掉整个build目录重新编译能排除80%的“假TBBR故障”。5.4 调试工具链推荐与心得体会真机调试ATF我依赖的三件套是串口拿到最低层启动日志、JTAGGDB挂载ELF文件设断点查PC、查栈、逻辑分析仪抓特定GPIO电平变化。其中JTAG是定位复杂问题的最强工具在BL31跳转前给bl31_main()设个断点基本能确认问题出在加载阶段还是运行阶段。用GDB连接ATF的ELF文件命令大致是这样arm-none-eabi-gdb build/myboard/debug/bl31/bl31.elf target remote :3333 monitor reset halt break bl31_main continue如果条件允许强烈建议先在FVPFixed Virtual Platform上把整个启动链模拟一遍。FVP是ARM官方提供的仿真平台能在不依赖真实硬件的情况下完整运行ATFOP-TEEU-BootLinux非常适合验证逻辑层面的bug再上真机验证硬件相关的问题。6. 安全固件工程升级方向别让板子只是“能开机”移植完成只是起点要把ATF用好还需要在工程化上做几件事。一是建立自动化构建和签名流程把证书生成、镜像打包、签名校验全部串进CI流水线确保每次构出来的固件都是可信的、可追溯的。二是做固件版本管理在BL31里记录固件版本号、构建时间、Git提交ID出错时才能快速定位。三是定期做安全审计不只是看代码还要看密钥管理、日志开关、调试接口这些“隔壁老王也能碰”的部分。我之前接手过一个项目固件已经量产但后来发现BL31的日志级别还是完整模式串口在特定时序下能刷出大量调试信息。修这一处看似简单但要安排返厂升级固件代价非常大。所以在初期做平台移植时就把安全开关按量产标准配置能省掉后面无数麻烦。另外值得关注的是ATF的生态一直在演进新版引入了RMERealm Management Extension支持、FF-AFirmware Framework for Arm A-profile规范、Measure Boot等特性。如果做的是面向未来3—5年的产品建议尽早评估这些新特性的支持情况避免到时候推倒重来。开发安全固件和做普通应用完全是两种思路普通应用追求功能和性能安全固件追求可控和可信。我在实际项目里最大的体会是ATF的代码本身并不难难的是理解它背后那套“层层设防”的安全哲学。每新增一个功能都要想想它会不会打破原有的信任边界每一次编译都要确认自己不是在逆向拆除一个安全机制。保持这个习惯才能在复杂的ARM安全体系里少踩坑、走远路。
返回列表