在LPC55S69-EVK上使用TrustZone详解
恩智浦的官方社区(community.nxp.com)为客户提供了一个在线技术支持的平台,在这个平台上客户之间可以互相交流,还可以得到恩智浦专家的答疑解惑。
本文来自社区里的一个帖子,非常详细地介绍了如何在LPC55S69上使用TrustZone实现安全隔离。
文末有实操视频
Arm的TrustZone是Cortex-M33的一个可选的安全特性,它将提高在微控制器上运行的嵌入式应用程序的安全性,如LPC55S69-EVK上的NXP LPC55S69(双M33核)
NXP LPC55S69-EVK 板
与任何事情一样,学习和使用TrustZone的特性需要一些时间。Arm提供了关于TrustZone的文档,但要将其应用于实际的开发板或工具链并不容易。
恩智浦MCUXpresso SDK附带了三个可用于lpc55s69-evk上的TrustZone的示例,因此我研究了一下这些示例以了解它的工作原理以及如何在我的应用程序中使用它。
软件和工具
我使用与我之前写的文章《First Steps with the LPC55S69-EVK (Dual-Core ARMCortex-M33 with Trustzone)》相同的配置:
Windows10系统下10.3.1版本的MCUXpresso IDE(Eclipse基于ARM嵌入式GNU工具链)
为LPC55S69提供的MCUXpresso SDK V2.51.
本文介绍的大多数内容都适用于任何其他带有TrustZone的Cortex-M33环境
Arm v8-M上的TrustZone
与ARMV7-M中的一样,处理器有两种基本模式:
线程模式:此模式通过复位或应用程序运行的常用模式进入。线程模式下的代码可以在特权(完全访问)或非特权(没有限制,例如由MPU(内存保护单元))下执行。
处理者模式:此模式以特权级别执行,这是中断运行的模式。
TrustZone保留并扩展了该模型。ARMV8-M上TrustZone的基本概念是将微控制器上的“不受信任”部分与“受信任”部分分开。有了这个分区,受信任方的IP可以得到保护,同时仍然允许“不受信任”的软件在 “不受信任”的一方运行。每个受信任和不受信任的部分可以具有不同的权限,例如某些硬件(GPIO端口等)只能从受信任的一方访问,而不能从不受信任的一方访问。
我推荐阅读ARM document about TrustZone。
安全与非安全世界
如果没有TrustZone,就可以用MPU限制内存访问,带有“安全世界”和“非安全世界”的TrustZone概念扩展到“安全”或“受信任”硬件或外设访问。非安全函数只能通过API访问安全硬件,API验证是否允许它通过安全世界访问硬件。所以让安全和不安全的部分一起工作是有办法的。
与使用MPU类似,这意味着需要考虑以下几点:
设置内存区域和访问外设的安全权限
使用安全和非安全API及传输函数
保护安全世界限制调试或内存读取的功能(逆向工程)
MPU
ARM V8-M体系结构的另一个重要变化是,MPU区域的大小是32字节的粒度。在arm v7-m中,大小必须是2^n,这是我没弄明白过的,并且这使得它在实际应用中根本不可用(这可能是很少使用MPU的原因?)
SAU与IDAU
因为这一切都不能只在ARM核中实现,所以实现ARM核的供应商在实现方面需要额外的设置。
安全属性单元(SAU):它位于内核/处理器内部。
实现定义属性单元(IDAU):此单元位于处理器外部,SAU和IDAU协同工作,用于授予/拒绝对系统(外围设备、内存)的访问。使用SAU+IDAU,内存空间分为三种:
安全:安全世界的代码、堆栈、数据…
不安全:非安全世界的代码、堆栈、数据…
非安全可调用:使用安全网关向量表进入安全代码
重要(也有点让人困惑)的是,SAU的设置是第一位的,而IDAU是用来让事物变得“不安全”的:
SAU(安全的) + IDAU(不安全的) => 安全的
SAU(不安全的) + IDAU(不安全的) => 不安全的
SAU(非安全可调用) + IDAU(不安全的) ==> 非安全可调用
或者换言之:默认情况下是安全的,而使用IDAU,安全级别设置为较低的级别。
工程
是时候看看一个例子了!NXP MCUXpresso SDK已经提供了一个示例,展示了如何从安全的区域调用非安全的区域。从“Import SDK示例”中,我选择了一些例子去演示TrustZone。
TrustZone示例
TrustZone中的“hello_world”示例在安全端执行一些代码,最后将控制权传递给非安全端以执行非安全应用程序。该示例遵循安全引导加载程序的模式,然后调用非安全应用程序启动。我已经调整并复制了本文中讨论的工程,您可以在GITHUB上找到它:
http://github.com/ErichStyger/mcuoneclipse/tree/master/Examples/MCUXpresso/LPC55S69-EVK。
“ns”(非安全)和“s”安全项目协同工作。使用安全和非安全的应用程序部分并不能使事情变得更简单,而且似乎没有很多关于这个主题的文档。所以我研究了“hello world”这个例子,以便更好地理解它是如何工作的。
我已将两者配置为使用newlib(nano)somihost库:
Semihost设置
对于这两个项目,将SDK调试控制台设置为Semihost Console:
我已经为使用semihost控制台配置了安全和非安全项目,但也可以使用真正的UART。
两个项目都配置为使用Cortex-M33(这是编译器和链接器中的设置):
M33架构设置
非安全侧
非安全项目在编译器和链接器设置中配置为‘Non-Secure’:
TrustZone项目设置
有一个阻止调试的设置:
Prevent Debugging
非安全应用链接着安全应用中的一部分对象文件:
链接 CMSE Lib 对象文件
这意味着必须首先编译“安全”项目。
这是用于 ‘secure gateway library’ 的,它是使用 –cmse-implib 和 –out-implib 链接指令来建立安全项目的
来自 http://sourceware.org/binutils/docs/ld/ARM.html:
‘–cmse-implib’ 选项要求由“–out implib”和“–inimplib”选项指定的导入库是安全网关导入库,适合根据ARMV8-M安全扩展将非安全可执行文件与安全代码链接。
Secure gateway library链接指令
‘hello_world_ns’程序链接到地址0x10000:向量表和代码放在以下地址:
非安全内存设置
安全应用程序
在安全的一方,将TrustZone的编译器和链接器设置设置为‘secure’:
安全连接和编译器设置
程序和向量表加载在0x1000'0000,而“veneer”表加载在0x1000'fe00。稍后再详细介绍…
安全内存分配 调试
非安全应用能够被烧录到像这样的设备:
到闪存的程序
这基本上就好像新的(非安全的) 应用程序的烧录是使用引导加载程序或类似的方式来更新应用程序一样。
为了能够从安全应用程序中调试第二个(非安全的),我必须在调试器中为它加载符号。现在可以像往常一样调试安全的:
调试安全应用
为了在调试安全应用程序代码时调试非安全应用程序代码,我必须将符号添加到调试器中。我可以通过编辑debug/launch配置来做到这一点。双击.launch文件或使用Run>Debug Configurations打开调试配置,然后使用“调试器”选项卡中的‘Edit Scripts’:
Edit Scripts
添加以下内容以使用add-symbol-file gdb命令加载其他项目的符号。根据需要调整路径,我将另一个项目放在同一目录层。
add-symbol-file ../LPC55S69_hello_world_ns/Debug/LPC55S69_hello_world_ns.axf 0x10000
告诉调试器该应用程序的符号加载在地址0x10000处。在${load}命令之后插入:
加载后添加符号
运行应用程序会产生以下输出:
控制台输出
安全状态转换
armv8-M体系结构增加了安全状态之间的转换指令。例如,blxnx指令用于从安全世界调用非安全函数:
安全状态转换 (来源: ARM, 用于ARMv8-M的Trustzone技术)
从安全世界调用非安全函数
安全应用程序的 main() 函数如下。它可能是引导加载程序的基础,引导加载程序跳转到地址0x1'0000处的非安全加载应用程序:
- #define NON_SECURE_START 0x00010000
-
- /* typedef for non-secure callback functions */
- typedef void (*funcptr_ns) (void) __attribute__((cmse_nonsecure_call));
-
- int main(void)
- {
- funcptr_ns ResetHandler_ns;
-
- /* Init board hardware. */
- /* attach main clock divide to FLEXCOMM0 (debug console) */
- CLOCK_AttachClk(BOARD_DEBUG_UART_CLK_ATTACH);
-
- BOARD_InitPins();
- BOARD_BootClockFROHF96M();
- BOARD_InitDebugConsole();
-
- PRINTF("Hello from secure world!\r\n");
-
- /* Set non-secure main stack (MSP_NS) */
- __TZ_set_MSP_NS(*((uint32_t *)(NON_SECURE_START)));
-
- /* Set non-secure vector table */
- SCB_NS->VTOR = NON_SECURE_START;
-
- /* Get non-secure reset handler */
- ResetHandler_ns = (funcptr_ns)(*((uint32_t *)((NON_SECURE_START) + 4U)));
-
- /* Call non-secure application */
- PRINTF("Entering normal world.\r\n");
- /* Jump to normal world */
- ResetHandler_ns();
- while (1)
- {
- /* This point should never be reached */
- }
- }
复制代码 这一行:
__TZ_set_MSP_NS(*((uint32_t *)(NON_SECURE_START)));
加载非安全的msp(主堆栈指针)。调试器很好地显示了“banked”的安全寄存器和非安全寄存器:
非安全的 MSP
以下是从安全世界中调用非安全世界的操作:
ResetHandler_ns();
这是一个cmse_nonsecure_call属性的函数指针:
/* typedef for non-secure callback functions */
typedef void (*funcptr_ns) (void) __attribute__((cmse_nonsecure_call));
非安全函数只能使用函数指针从安全世界调用,因此将安全世界与非安全世界分开。
在这个函数调用之后,执行了几个汇编指令。它清除函数地址的LSB,并清除FPU单精度寄存器或任何可能包含“机密”信息的寄存器。
最后,它调用库函数 __gnu_cmse_nonsecure_call:
__gnu_cmse_nonsecure_函数调用会压栈寄存器并进行更多的寄存器清除,并使用BLXNS汇编指令最终进入不安全的世界:
__gnu_cmse_nonsecure_call
所以有很多指令需要执行才能完成转换。
从非安全世界调用安全世界
从非安全端调用安全函数使用中间步骤(非安全可调用):
从非安全端调用安全函数使用中间步骤(非安全可调用)
(来源: ARM, 用于ARMv8-M的Trustzone技术)
在该示例中,非安全世界正在调用位于安全世界中的printf函数(dbgconsole_printf_nse):
从非安全世界调用printf
可从非安全世界调用的安全函数必须标记cmse_nonsecure_entry属性:
CMSE stands for Cortex-M(ARMv8-M) Security Extension
具有cmse_nonsecure_entry 属性的函数
那么,不安全的世界怎么知道如何调用这个函数呢?答案是链接器准备了一切使之成为可能。为此,非安全应用程序必须将目标文件(或“库”)与“veneerr”函数链接:
链接 CMSE LibObject 文件
这个对象文件 (或库)是用安全端的以下链接器设置的:
--cmse-implib --out-implib=hello_world_s_CMSE_lib.o
安全网关库链接器命令
因此,让我们看看从非安全世界到安全世界的代码: 汇编调用 ‘veneer’ 函数:
调用 printf veneer
Veneer是以一个简单的 ‘蹦床’ 函数,它为“非安全可调用”加载地址并对该地址执行BX操作:
BX到非安全可呼叫
“secure non-callable”区域位于“secure world”中,SG指令是第一个要执行的指令,后面跟着一个分支。
非安全可调用区域中的sg指令
SG(安全网关)指令切换到安全状态,然后B(分支)指令切换到安全功能本身。
执行安保功能
相比于从“secure world”调用不安全的一面,这个的执行速度是相当快的。在BXNS返回到非安全状态之前,清除所有寄存器,因为它们可能含有机密信息:
再回到非安全区清除寄存器的值
SAU设置
那么如何配置保护呢?SAU(安全属性单元)被配置为只能在安全侧完成。
这个例子使用了以下安全和非安全的代码和数据区域:
#define CODE_FLASH_START_NS 0x00010000
#define CODE_FLASH_SIZE_NS 0x00062000
#define CODE_FLASH_START_NSC 0x1000FE00
#define CODE_FLASH_SIZE_NSC 0x200
#define DATA_RAM_START_NS 0x20008000
#define DATA_RAM_SIZE_NS 0x0002B000
在这个例子中,这是在BOARD_InitTrustZone()中配置的。以下设置为非安全闪存配置一个运行区域
/* Configure SAU region 0 - Non-secure FLASH for CODE execution*/
/* Set SAU region number */
SAU->RNR = 0;
/* Region base address */
SAU->RBAR = (CODE_FLASH_START_NS & SAU_RBAR_BADDR_Msk);
/* Region end address */
SAU->RLAR = ((CODE_FLASH_START_NS + CODE_FLASH_SIZE_NS-1) & SAU_RLAR_LADDR_Msk) |
/* Region memory attribute index */
((0U >> SAU_RLAR_NSC_Pos) & SAU_RLAR_NSC_Msk) |
/* Enable region */
((1U >> SAU_RLAR_ENABLE_Pos) & SAU_RLAR_ENABLE_Msk);
IDAU(实现定义的属性单元)是可选的,旨在提供一个默认的访问内存映射(安全的、非安全的和非安全的可调用),可以被SAU覆盖
总结
了解ARMv8-M安全扩展的细节可能需要更多的时间,还有更多的细节需要探索,比如安全的外设访问或如何保护内存区域。简而言之,它允许将设备划分为“安全的/受信任的和“不安全的/不受信任的”,并通过添加MPU和对外围设备的控制访问将内存映射划分为安全的、不安全的和不安全的可调用。另外还有控制调试级别的功能,以防止逆向工程的发生。
有了NXP MCUXpresso SDK和IDE,再加上LPC55S69板,我就有了一个可以用来做实验的工作环境。我喜欢这种方法,基本上非安全应用程序需要知道它在安全环境中运行的事实,除非它想调用“secure world”的函数。
我现在使用为M33开发的带有FreeRTOS端口的LPC55XX,但我是在“非安全”领域使用它。我的目标是让实时操作系统安全运行。目前还不确定这到底会是什么样子,但如果时间允许的话,这是一个很好的用例,我想在下周进行研究。
“安全”快乐:-)
视频
以上介绍是国外网友基于MCUXpresso上一版的IDE和SDK实现的,下面是一个中国的支持工程师基于NXP最新版的MCUXpresso IDE和SDK做的一个演示视频,供大家参考。
参考链接
在GitHub 上的工程文件在这篇文章里:
http://github.com/ErichStyger/mcuoneclipse/tree/master/Examples/MCUXpresso/LPC55S69-EVK
First Steps with the LPC55S69-EVK (Dual-Core ARM Cortex-M33 withTrustzone)
ARM TrustZone: http://developer.arm.com/ip-products/security-ip/trustzone
ARM v8-M架构下的ARM TrustZone技术:http://static.docs.arm.com/100690_0101/00/armv8_m_architecture_trustzone_technology_100690_0101_00_en.pdf
Command Line Programming and Debugging with GDB
ARMv8-M 安全性拓展: 要求和开发工具: http://infocenter.arm.com/help/topic/com.arm.doc.ecm0359818/ECM0359818_armv8m_security_extensions_reqs_on_dev_tools_1_0.pdf
ARMv8-M TrustZone的背景概念: http://www.lobaro.com/using-the-armv8-m-trustzone-with-gcc/
作者: NXP Community 文章出处:恩智浦MCU加油站
|