ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

ARM Trusted Firmware(ATF)深度解析:启动链路、安全审计与平台移植实践

ARM Trusted Firmware(ATF)深度解析:启动链路、安全审计与平台移植实践 前阵子我拿到一块自研ARM SoC的评估板任务是把Arm Trusted FirmwareATF跑通让U-Boot能正常起来。板子刚上电什么现象都没有JTAG拉出来一看BL1还在Boot ROM里打转根本没跳去加载BL2。折腾了三天最终问题就藏在一个宏的值里PLAT_PHY_ADDR_OF_DRAM配置不对导致BL2访问FIP镜像时地址换算直接错位。这个事给我印象很深。ATF这套东西看着只是“启动代码”但里面的架构设计、安全模型、平台相关部分混在一起没有一张清晰的路线图很容易一头扎进去出不来。这篇文章我把整个ATF从源码结构、启动链路、安全审计到平台移植的全过程梳理一遍。内容适合三类人一是要在自研平台或评估板上做固件bring-up的嵌入式工程师二是做安全评估、固件审计的安全工程师三是想把TrustZone和EL3机制彻底搞清楚的ARM底层爱好者。1. ATF的定位与启动链路全景1.1 为什么ARM把可信固件放在EL3先说清楚一个底层问题ATF到底在CPU里处于什么位置。ARMv8-A引入了4个异常级别Exception LevelEL0对应用户态EL1对应操作系统内核EL2是虚拟化层HypervisorEL3是最高特权级别只运行可信固件。ATF就是跑在EL3上的那套代码。这里要顺带讲一下TrustZone。TrustZone把整个系统在架构上分成两个世界安全世界Secure World和普通世界Normal World。普通世界跑Linux、Android、各种RTOS安全世界跑可信OS比如OP-TEE、安全应用、以及EL3的监控器Secure Monitor。两个世界之间通过SMC指令完成安全切换ATF里的smc #0就是入口。理解这个模型很多代码逻辑就顺了。ATF在系统中的角色可以概括成三件事引导加载从Boot ROM接管完成AP侧固件的完整引导链安全监控作为Secure Monitor处理SMC请求在安全世界和普通世界之间切换运行时服务提供PSCI电源状态协调、系统复位、可信OS调度等运行时服务。EL3之所以需要独立一套固件而不是让内核自己管核心原因就是ARM的安全模型要求普通世界能访问的资源必须在安全世界控制之下。EL3就是这个控制点的落点。1.2 六阶段启动链路BL1、BL2、BL31、BL32、BL33的交接班ATF的启动链路可以看作一条流水线每一个阶段干一摊活干完把控制权交给下一个。下面这个表格是全景视图阶段运行位置主要职责可信属性Boot ROMSoC内部ROM加载BL1校验BL1的签名不可变信任根BL1SRAM初始化少量硬件加载BL2并验证不可变BL2SRAM/DRAM可信引导核心从FIP加载BL31、BL32、BL33并验证可变但需签名BL31DRAMEL3运行时固件Secure MonitorPSCI等可变但需签名BL32可选DRAMOP-TEE等可信OS可变但需签名BL33DRAMU-Boot、EDK2或裸机程序进入普通世界可变但需签名启动顺序是这样的。SoC上电后Boot ROM先执行它从ROM里取BL1验签通过后加载到SRAM并跳转。BL1做的事情很少关缓存、设栈、初始化DRAM控制器有的平台这一步在BL2里做、加载BL2到SRAM。BL2是“可信引导”真正常说的那个阶段它负责解析FIP固件包。这里要特别解释一下FIPFirmware Image Package。有时候你会看到ATF目录下有个fiptool它就是把多个镜像打包成一个文件的工具。BL2通过在FIP中按UUID查找镜像把BL31、BL32、BL33分别加载到各自指定的基地址逐一提签名和证书然后跳转到BL31。BL31完成EL3运行时的初始化包括GIC、定时器、串口、平台设置等然后会派发两个分支如果配置了OP-TEE这类可信OSBL32先将CPU切到安全世界启动它随后再切到普通世界的EL2把BL33通常是U-Boot拉起来。到这一步ATF的使命基本完成它退居幕后作为Secure Monitor等待后续SMC调用。1.3 源码目录地图与阅读顺序ATF源码初看很大但结构其实非常工整。核心目录如下bl1/ bl2/ bl31/ plat/ common/ drivers/ lib/ fdts/ tools/ docs/bl1/第一阶段的全部实现入口是bl1_entrypoint.Sbl2/可信引导阶段入口在bl2_entrypoint.S核心逻辑是加载镜像、认证镜像bl31/EL3运行时固件包含runtime_svc、dispatcher、PSCI实现入口在bl31_entrypoint.Splat/平台相关代码各厂商在这下面建自己的目录比如plat/arm/board/fvp、plat/qemu/qemudrivers/各类驱动包括Arm通用时钟、GICv3、PL011串口、NAND、MTD、证书认证相关驱动lib/底层库例如el3_runtimeEL3运行时入口、xlat_tables_v2MMU页表、psci电源管理tools/构建工具主要是fiptool、证书生成工具cert_createdocs/重要文档最值得先读的是docs/porting-guide.rst所有平台移植工作都围绕它展开。我给第一次接触ATF源码的人一个阅读顺序建议先读docs/porting-guide.rst了解平台API再读bl31/aarch64/bl31_entrypoint.S看EL3入口干了什么然后跟着一次完整的SMC请求路径去读bl31/runtime_svc.c最后再回头啃BL2的认证逻辑。不要一上来就钻某个平台目录会被平台细节淹没。2. 安全固件工程审计到底审什么2.1 先理清风险面Secure世界边界和异常级别做安全审计不能上手就翻代码先把风险面画出来。我把ATF审计的风险面归纳成四块TrustZone外围隔离哪些外设属于安全世界哪些属于普通世界总线上有没有额外的地址空间控制器比如TZC-400在做过滤SMC接口暴露面EL3向普通世界开放了哪些SMC功能每个服务函数是否校验了参数安全启动链从Boot ROM到BL33的每一跳签名和证书验证是否可靠信任根从哪里来密钥与防回滚私钥存哪里公钥不可变存储是哪块有没有防回滚计数这四块每一块都有对应的审计切入点。我审计时通常先跑一遍全量编译打开所有安全检查选项然后针对启动阶段逐个做代码走查。2.2 SMC调用与运行时服务暴露面审计SMC是普通世界进入EL3最直接的路径也是安全审计最关注的入口。ATF将SMC请求按功能ID分发到不同的runtime service有标准的PSCI服务、标准架构服务std_svc还有厂商自定义的SIP服务。先看一个简化版的服务注册结构体你就能明白审计入口在哪typedef struct rt_svc_desc { uint8_t rt_svc_name[8]; uint8_t rt_svc_owner; uint8_t rt_svc_range; uint8_t rt_svc_flags; rt_svc_init_t rt_svc_init; rt_svc_handle_t rt_svc_handle; } rt_svc_desc_t;每个服务会声明自己的处理函数。审计时重点看处理函数里每个参数的实际使用传入的物理地址是否与平台允许的内存范围做校验如果没有校验普通世界可以构造一个SMC让EL3去写任意物理内存直接拿到提权漏洞传入的长度、偏移是否可能造成整数溢出很多固件漏洞就是base offset之后的地址越界对共享内存的访问是否有世界归属判断ATF里有几个宏可以判断地址是否落在非安全地址空间比如检查ARM_TZC_*属性后的地址合法性服务函数是否在启动早期执行有没有可能被设计成竞争条件利用的点。我自己做审计时会先用make编译、开启ENABLE_STATIC_LINK等常规检查再用静态分析工具扫bl31/和plat/目录里SMC处理函数的关键参数路径把可疑调用点列一个跟进清单。很多时候漏洞不是功能本身危险而是“功能太方便”比如厂商加了一个内存读写调试服务但忘了在生产固件里关掉这种情况我见过不止一次。2.3 Secure Boot与证书链审计Secure Boot是DDR数据在复位后无法绕过的关键防线也是审计的重点。ATF的Trusted Board BootTBB采用一层层证书链的方式做验签。以平台默认的COTChain of Trust为例BL1的公钥和哈希固化在SoC的OTP/efuse里作为信任根BL1验BL2的X.509证书证书里带BL2镜像的哈希和签名BL2验BL31、BL32、BL33的证书每个阶段都有防回滚计数器Non-Volatile Counter防止攻击者跑旧版本的固件。审计时我会看这几个点OTP里放的到底是完整公钥还是公钥哈希。如果是完整公钥攻击者可利用仓储中永久有效的签名来打包任意镜像如果只放哈希则代码里要确认没有跳过“比较公钥”这一步。另一个点是证书解析代码对异常格式的处理X.509解析器是出了名的容易有内存解析类问题尽量用ATF自带的mbedTLS封装不要自己造轮子。fiptool也提供校验FIP包内容的命令可以快速验证当前固件的签名链有没有被破坏fiptool verify --tb-fw bl2.bin --soc-fw bl31.bin --nt-fw u-boot.bin fip.bin2.4 工程审计Checklist把审计经验沉淀成清单每次做ATF安全评估都照这个过一遍审计项具体检查点风险等级SMC参数校验地址、长度是否合法是否越界高调试后门调试SMC服务是否保留在生产固件中高OTP信任根密钥存储位置是否为公钥哈希高防回滚是否启用NVCounter且初始值正确高证书校验每个镜像是否强制验签有无跳过路径高内存映射权限EL3页表是否出现RWX同权限区域中外设隔离普通世界可访问的安全外设中启动日志Debug日志是否泄露内存地址和密钥信息中这套清单对于做PSA认证里固件部分评估也适用。它不能替代完整的形式化验证但把工程里最容易出事的点覆盖住了。3. 平台移植的落地路径3.1 最小可启动的plat目录长什么样ATF的平台移植以plat/厂商/平台为单位。比如说官方的FVP平台在plat/arm/board/fvpQEMU平台在plat/qemu/qemu。一个新平台刚起步时最有效率的方式不是从零写而是找结构接近的参考平台复制然后替换。一个最小可启动平台通常至少包含这些文件plat/yourvendor/yourboard/ ├── platform_def.h # 板级宏地址、外设基址、内存大小 ├── platform.mk # 编译选项、源文件列表 ├── plat_helpers.S # 最早期汇编设置栈、清零BSS ├── plat_topology.c # CPU电源域拓扑 ├── plat_psci.c # PSCI相关回调CPU_ON等 ├── plat_sip_svc.c # 可选厂商自定义SMC服务 └── aarch64/ └── plat_common.c # ARM标准平台函数编译命令基本长这样make PLATyourboard CROSS_COMPILEaarch64-none-elf- DEBUG1 V1在platform_def.h里要确保这么几类东西正确SRAM基址和大小、DRAM地址范围、UART基址、GIC基址、BL31/BL32/BL33的装载地址。这块容易犯的错就是直接照抄参考平台的地址。不同SoC的SRAM大小和位置完全不同一旦BL1/BL2实际地址超出平台SRAM空间结果是BL1立刻异常你连串口日志都看不到。3.2 串口和MMU第一个可看见的成功平台bring-up最大的成就是“看到输出”。ATF早期阶段基本靠UART打印所以串口驱动正确初始化是所有调试的起点。以使用最多的PL011串口为例你要在平台代码里实现plat_get_console()返回一个console_t结构体实例里面包含UART基址和波特率相关的配置信息static console_pl011_t uart_console; console_t *plat_get_console(void) { static int setup_done; if (!setup_done) { if (console_pl011_register(PLAT_UART_BASE, PLAT_UART_CLOCK, PLAT_UART_BAUDRATE, uart_console) ! 0) return NULL; setup_done 1; } return uart_console.console; }串口能打印之后紧接着就是MMU。ATF里MMU配置走的是lib/xlat_tables_v2它把你的虚拟地址区域映射到物理地址。这个库用起来非常简单平台代码里列一个映射表static const mmap_region_t plat_mmap[] { MAP_REGION_FLAT(DRAM_BASE, DRAM_SIZE, MT_MEMORY | MT_RW | MT_NS), MAP_REGION_FLAT(PLAT_PERIPHERAL_BASE, PERIPH_SIZE, MT_DEVICE | MT_RW), {0} };但请注意几个点。第一安全世界使用的内存区域属性要加MT_SECURE否则安全世界内存可能被普通世界访问。第二普通世界内存比如BL33所在的区域要标MT_NS否则Linux起来后会因为NS位不对而崩溃。第三页表属性尽量不要让同一段内存同时有可写和可执行权限这既是为了安全也是为了应对部分SoC对TLB行为的约束。3.3 GIC、定时器与PSCIBL31的核心家底BL31初始化的默认顺序中平台相关的一步会调用bl31_platform_setup()。多数平台在这个函数里完成GIC的初始化。GICv3的情况下ATF用的是gicv3_driver_init()和gicv3_init()这套APIgicv3_driver_init((uintptr_t)GICD_BASE, (uintptr_t)GICR_BASE, GICR_FRAMES, PLATFORM_CORE_COUNT); gicv3_init(FVP_GICD_ADDR, FVP_GICR_ADDR, gicv3_data);GIC配置错误最常见的现象是CPU起来后中断始终不触发或者一开中断就卡死。问题大多出在GICD和GICR基址搞混或者Redistribitor的frame数组长度与CPU core数量不匹配。PSCI则是所有电源管理的基础。ATF实现了PSCI的主要调用但不同的平台需要填充具体的底层回调比如cpu_on_handler要执行secondary boot的核释放动作system_reset_handler要对复位控制器写值。还有一个绕不开的东西叫热点握手hot plug也就是CPU hotplug时的mbox通信。ARM平台的常见做法是主核把一个唤醒地址写到某个平台约定的共享内存然后发送SGI中断给从核从核把自己唤起后开始执行BL31早期初始化。这一步我自己调试过的坑是PSCI CPU_ON不认识从核。现象是Linux里cpu_up之后从核完全不响应。排查到最后发现是平台拓扑里注册的core位置和实际硬件affinity值不一致——plat_topology.c里定义的电源域树和GIC的affinity mapping对不上SGI中断就派发不出去。解决方法是查阅SoC多核affinity编码把平台拓扑里的值修正为硬件实际值。3.4 内存布局与三个EL镜像的地址对齐ATF的镜像地址错位是“无声无息死掉”的重灾区。BL31、BL32、BL33之间有一个隐含约束BL31在加载完BL32和BL33之后这两个镜像的位置和入口地址必须完全一致。假如BL31编译时链接的BL33基址是0x02000000而实际从FIP加载BL33时平台代码里指定的加载地址写成了0x02080000那跳转过去就是直接执行非预期内存系统的行为完全不可预测。我整理了一个最小平台的地址分配规划方式镜像建议位置说明SRAM平台固定BL1和BL2通常跑这里BL31DRAM中段例如0x1000_0000需要预留足够大小至少2MBBL32OP-TEEBL31之后编译时通过OPTEE_BASE固定BL33U-BootDRAM后段与U-Boot的CONFIG_SYS_TEXT_BASE完全一致非安全共享内存固定一块供BL31与BL33传递启动信息关键经验是先把地址规划成文档放在设计目录里再动手写代码。盲调地址省不了时间只会把问题变得不可排查。4. 用FVP/QEMU先验证再上真实硬件4.1 虚拟平台的价值刚接触ATF移植时强烈建议先在Arm FVP或者QEMU的虚拟平台上验证理解。虚拟平台的好处是复现稳定、断点方便、不担心烧坏硬件。用QEMU跑ATF和U-Boot的最小配置完全可以本地跑通命令也非常简单qemu-system-aarch64 -machine virt -cpu cortex-a57 -nographic \ -bios bl1.bin -d unimpQEMU的virt平台把BL1放在flash里ATF编译时指定PLATqemu就能得到目标文件。这比在实体板子上调试舒服太多。4.2 用fiptool打包一个最小可测FIPATF的BL2要解析FIP因此不管是不是要烧TBB都建议把镜像打包成FIP再测试。用fiptool创建FIP的命令fiptool create --tb-fw bl2.bin --soc-fw bl31.bin \ --nt-fw u-boot.bin --tof u-boot.dtb fip.bin提示fiptool create默认会为没签名的镜像生成fake key所以本地测试没问题但要记住这只是开发模式。量产固件的镜像必须由cert_create生成正式证书并配合烧入OTP的信任根使用。在FIP里BL33的入口地址就是U-Boot的链接地址。要让AArch64的U-Boot跑起来编译时把CONFIG_SYS_TEXT_BASE和ATF平台里的BL33_BASE对齐这样集成才不会出幺蛾子。4.3 从“无声”到“有日志”的排错路径我遇到过很多次编译烧录之后串口毫无输出的情况。遇到这种“死寂”状态排查动作要成体系先确认BL1是否有动作。如果能接JTAG/调试器看PC是否走过了bl1_entrypoint如果BL1卡住优先怀疑BL1使用的栈空间不够或者SRAM映射错误如果BL1已跳转但BL2无输出检查BL2有没有被加载进正确地址以及FIP中镜像的UUID是否匹配确认串口初始化函数是否真的被执行。ATF的log系统在LOG_LEVEL不够时是静默的所以先把LOG_LEVEL调成LOG_LEVEL_VERBOSE再复跑。在FVP上调试时可以直接加载ELF符号GDB attach后观察PC值add-symbol-file bl31.elf配合反汇编可以快速定位Synchronous Abort发生在哪里。4.4 我遇到过的三个高频翻车现场第一个是BL2从NAND读FIP失败。自研平台里FIP烧在NAND但ATF默认没有NAND驱动支持需要在BL2阶段把NAND控制器的驱动加进去并且处理好坏块跳过逻辑。否则BL2会一直读到坏块然后挂死。第二个是GICv3作为GICv2初始化。平台代码里看着用的是GICv3 API但实际SoC的GIC版本是GICv2两个的基址结构完全不同。这种错配一般会在gicv3_driver_init后第一次访问寄存器时触发同步异常。第三个是BL31页表里漏掉了OP-TEE共享内存区域。OP-TEE和BL31之间有一块共享内存用于参数传递如果这块区域没有在EL3页表里映射为安全内存一旦OP-TEE启动BL31去访问共享内存就会异常。解决方法是把共享区域加进plat_mmap并标注MT SECURE | MT_RW。5. 源码精读的额外收获5.1 EL3运行时服务框架让SMC生态非常清晰ATF把EL3的SMC服务用“运行时服务”框架统一管理。当普通世界执行SMC #0EL3会读取X0里的函数ID解析出服务提供者owner和功能号然后调转到对应服务。在实际代码里bl31/runtime_svc.c维护一张服务表每个服务用一个16字节的描述符注册。BL31运行时遇到SMC后查找这张表找到匹配项则调用服务处理函数找不到则返回NOT_SUPPORTED。这个设计的好处是新增服务非常简单平台代码只要实现DECLARE_RT_SVC(...)宏就能注册自己的SIP服务。这给做安全审计提供了一个便捷思路直接枚举ELF里的rt_svc_desc结构体就能把所有SMC入口找出来完全不需要每个平台手工翻代码。5.2 xlat_tables_v2的内存映射细节ATF的MMU管理用xlat_tables_v2这套库在不同EL场景下表现很稳健但有几个容易忽略的细节。关于granule size的选择ATF多数平台用4KB但有的平台为了简化页表用16KB或64KB。你如果从一个大页表平台复制代码到4KB页表平台映射范围会差异很大导致访问不到某些地址。另外一个隐含细节是映射属性里的MT_EXECUTE_NEVER。EL3页表要求把所有非代码段映射成不可执行。如果偷懒把所有区域都标成可执行一些ARM平台会在运行时检查中被拒绝即使机器上跑过了在生产环境的安全审查阶段也是个隐患。5.3 构建配置里那些“开不得”和“关不得”的选项ATF构建系统里有很多会影响最终行为的宏。拿几个关键的举例配置项推荐值原因ENABLE_ASSERTIONS发布版关调试版开断言会增加体积但能快速定位问题LOG_LEVEL调试40发布20日志里的地址信息对安全审计不友好ENABLE_PIE按平台需求支持地址无关但会增加BL1复杂度ARM_TSP_RAM_LOCATION_ID必须与平台规划一致决定TSP位置TRUSTED_BOARD_BOOT量产开开发板按需开启后所有镜像必须有签名我个人的经验是开发期间全部打开调试相关的配置量产前用脚本把枚举过的选项切到安全配置重新编译一轮并且把最终配置固化到自己的构建文档里。排查问题最怕的就是开发环境和生产环境编译参数差几个宏导致现象完全不可复现。还有一个容易被忽略的点BL31的尺寸是链接脚本决定的典型平台下BL31大概占512KB到1MB不等。如果你的平台设计时给BL31预留的空间不足修改代码后可能编译成功但链接产物超出了预设的ELF加载范围。ATF构建时有一些相关检查但最大程度规避还是要在链接脚本里手工确认BL31_SIZE的余量足够。移植ATF这件事代码只是冰山一角。绝大多数时间花在理解自己平台的硬件特性、地址资源和启动约束上。如果你正要开始一个平台的ATF bring-up我建议先别急着改代码把docs/porting-guide.rst完整读一遍然后按文章里的顺序在QEMU或FVP上把默认的BL1/BL31/U-Boot链路跑通再开始动手改平台目录。等你第一次在真实板子上看到NOTICE: BL31: v2.9那行输出就说明整套链路已经初步打通了——后面那些坑慢慢踩也是一种乐趣。
返回列表