The ARM TrustZone is an optional secu=rity feature for Cortex-M33 which shall improve the security for embedded applications running on microcontroller as the NXP LPC55S69 (dual-core M33) on the LPC55S69-EVK.
As with anything, using and learning the TrustZone feature takes some time. ARM provides documentation on TrustZone, but it is not easy to apply it for an actual board or toolchain. The NXP MCUXpresso SDK comes with three examples for TrustZone on the LPC55S69-EVK, so I have investigated these examples to find out how it works and how I can use it in my application.
I’m using the same setup as in my earlier article (“First Steps with the LPC55S69-EVK (Dual-Core ARM Cortex-M33 with Trustzone)“):
Most of the things presented in this article are applicable to any other Cortex-M33 environment with TrustZone.
As on the in the ARMv7-M, there is two basic modes the processor can be in:
TrustZone keeps that model and extends it. The basic concept of TrustZone on ARMv8-M is to separate the ‘untrusted’ from the ‘trusted’ parts on a microcontroller. With this division IP inside the trusted side can be protected while still allowing ‘untrusted’ software to run on the ‘untrusted’ side of the world. Each trusted and untrusted part can have different privileges, such as some hardware (GPIO ports, etc) only could be accessible from the trusted side, but not from the untrusted one.
I recommend to read the ARM document about TrustZone.
While without TrustZone it is already possible to restrict memory access with an MPU, the TrustZone concept with ‘secure world’ and ‘non-secure world’ extends the concept to ‘secure’ or ‘trusted’ hardware or peripheral access. A non-secure function only can access secure hardware through an API which verifies if it is allowed to access the hardware through the secure world. So there are ways that the secure and non-secure parts can work together.
Similar to using an MPU, it means that there are several things to consider:
The other important change in the ARMv8-M architecture that the size of an MPU region has now a granularity of 32 bytes. In ARMv7-M the size had to be a 2^N which I never understood and makes it not usable at all in real world applications (this is probably the reason the MPU is rarely used?).
Because this all cannot be only implemented in the core (provided by ARM), there are extra settings needed on the implementation side by the vendor implementing the ARM core.
The SAU and IDAU work together and are used to grant/deny access to the system (peripherals, memory). Using the SAU+IDAU, the memory space gets separated into three kind:
The important (and somewhat confusing) thing is that the SAU settings are first, and IDAU is used to make things ‘unsecure’:
Or in other words: by default things are secure, and with the IDAU the security level is set to a lower one.
Time to have a look at an example! The NXP MCUXpresso SDK already comes with an example showing how to call the non-secure land from the secure one. From the ‘Import SDK example(s) I can select examples demonstrating the TrustZone.
The ‘hello_world’ TrustZone example executes some code on the secure side and finally passes control to the non-secure side to execute the non-secure application. The example follows the pattern of a secure bootloader then calling the non-secure application to start.
I have tweaked and replicated the projects discussed in this article, you can find them on GitHub: https://github.com/ErichStyger/mcuoneclipse/tree/master/Examples/MCUXpresso/LPC55S69-EVK
The ‘ns’ (non-secure) and ‘s’ secure projects work together. Using secure and non-secure application parts do not make things simpler, and there seems not to be a lot of documentation about this topic. So I investigated that ‘hello world’ example to better understand how it works.
I have configured both to use the newlib (nano) semihost library:
For both project, set the SDK Debug Console to ‘Semihost console’:
I have both the secure and non-secure projects configured for using the semihost console, but a real UART could be used too.
Both projects are configured to use the Cortex-M33 (this is a setting in the compiler and Linker):
The non-secure project is configured in the compiler and linker settings as ‘Non-Secure’:
There is a setting to prevent debugging:
The non-secure application links in an object file which is part of the secure application:
This means that the ‘secure’ project has to be built first.
This is for the ‘secure gateway library’ which is built in the secure project using the –cmse-implib and –out-implib linker commands:
From https://sourceware.org/binutils/docs/ld/ARM.html:
The ‘–cmse-implib’ option requests that the import libraries specified by the ‘–out-implib’ and ‘–in-implib’ options are secure gateway import libraries, suitable for linking a non-secure executable against secure code as per ARMv8-M Security Extensions.
The ‘hello_world_ns’ program is linked to address 0x10000: the vector table and code gets placed at this address:
On the secure side the compiler and linker settings for TrustZone are set to ‘secure’:
The program and vector table is loaded at 0x1000’0000 with a ‘veneer’ table loaded at 0x1000’fe00. More about this later…
The non-secure application can be flashed to the device like this:
This basically is as if the new (non-secure) application has been programmed using a bootloader or similar way to update the application.
To be able to debug the second (non-secure) from the secure application, I have to load the symbols for it in the debugger. The secure one can now be debugged as usual:
In order to debug the non-secure application code when debugging the secure one, I have to add the symbols to the debugger. I can do this by editing the debug/launch configuration. Double-click on the .launch file or open the debug configuration with Run > Debug Configurations, then use the ‘Edit Scripts’ in the Debugger tab:
Add the following to load the symbols of the other project using the add-symbol-file gdb command. Adapt the path as needed, I have the other project at the same directory level.
add-symbol-file ../LPC55S69_hello_world_ns/Debug/LPC55S69_hello_world_ns.axf 0x10000
to tell the debugger that the symbols of that application are loaded at the address 0x10000. Insert that after the ${load} command:
Running the application produces the following output:
The ARMv8-M architecture has added instructions to transition between the security states. For example the BLXNX instruction is used to call a non-secure function from the secure world:
The main() of the secure application is like below. It could be the base of a bootloader which jumps to the non-secure loaded application at address 0x1’0000:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
#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 */ } } |
The line with
__TZ_set_MSP_NS(*((uint32_t *)(NON_SECURE_START)));
loads the non-secure MSP (Main Stack Pointer). The debugger nicely shows both the secure and non-secure registers which are ‘banked’:
The following will call the non-secure world from the secure one:
ResetHandler_ns();
This a function pointer with the cmse_nonsecure_call attribute:
1
2
|
/* typedef for non-secure callback functions */ typedef void (*funcptr_ns) ( void ) __attribute__((cmse_nonsecure_call)); |
Non-Secure functions can only be called from the secure world using function pointers, as a result dividing the secure from t
Behind that function call there are several assembly instructions executed. It clears the LSB of the function address and clears the FPU Single Precision registers, or any registers which could contain ‘secret’ information. At the end it calls the library function __gnu_cmse_nonsecure_call:
The __gnu_cmse_nonsecure_call does push the registers and does more register cleaning and uses the BLXNS assembly instruction to finally enter the non-secure world:
So there are quite a few instructions to be executed to make that transition.
Calling a secure function from the non-secure side uses an intermediate step (Non-secure Callable):
Calling secure Function from non-secure side (Source: ARM, Trustzone technology for ARMv8-M Architecture)
In the example the non-secure world is calling a printf function (DbgConsole_Printf_NSE) which is located in the secure world:
The secure functions which are callable from the non-secure world hae to be marked with the cmse_nonsecure_entry attribute:
CMSE stands for Cortex-M (ARMv8-M) Security Extension
So how does the non-secure world know how to call this function? The answer is that the linker prepares everything to make it possible. For this the non-secure application has to link an object file (or ‘library’) with the ‘veneer’ functions:
This object file (or library) is created with the following linker setting on the secure side:
--cmse-implib --out-implib=hello_world_s_CMSE_lib.o
So let’s follow the code from the non-secure to the secure world: The assembly calls a ‘veneer’ function:
The veneer is a simply ‘trampoline’ function which loads the address for the ‘non-secure callable’ and does a BX to that address:
The ‘secure non-callable’ area is in the ‘secure world’ with a SG instruction as the first one to be executed, followed by a branch.
The SG (Secure Gateway) instruction switches to the secure state followed by the B (Branch) instruction to the secure function itself:
Compared to calling the unsecure side from the secure world this was rather fast. The clearing of all the registers because they can contain secret information is done just before the BXNS returns to the non-secure state:
So how is the protection configured? For this the SAU (Secure Attribution Unit) is configured which only can be done on the secure side.
The example uses the following secure and non-secure code and data areas:
1
2
3
4
5
6
|
#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 |
In the example this is configured in BOARD_InitTrustZone(). The following setting configures a region for the non-secure FLASH execution:
1
2
3
4
5
6
7
8
9
10
11
|
/* 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); |
The IDAU (Implementation Defined Attribution Unit) is optional and is intended to provide a default access memory map (secure, non-secure and non-secure-callable) which can be overwritten by the SAU.
It probably will take me some more time to understand the details of the ARMv8-M security extensions.There are more details to explore such as secure peripheral access or how to protect memory areas. In a nutshell, it allows to partition the device into ‘secure’/trusted and ‘unsecure’/not-trusted and divides the memory map into secure, non-secure and non-secure-callable with the addition of MPU and controlled access to peripherals. Plus there is the ability to control the level of debugging to prevent reverse engineering.
With the NXP MCUXpresso SDK and IDE plus the LPC55S69 board I have a working environment I can use for my experiments. I like the approach that basically the non-secure application does need to know about the fact that it is running in a secure environment, unless it wants to call functions of the secure world.
I have now FreeRTOS working on the LPC55xx with the FreeRTOS port for M33, but I’m using it in the ‘non-secure’ world. My goal is to get the RTOS running on the secure side. Not sure yet how exactly this will look like, but that’s a good use case I want to explore in the next week if time permits.
Happy Securing
- - -
Originally published on April 27, 2019 by Erich Styger
ここにコメントを追加するには、ご登録いただく必要があります。 ご登録済みの場合は、ログインしてください。 ご登録がまだの場合は、ご登録後にログインしてください。