安全软件开发中的CMSE特性与应用
1. 调用非安全函数
在安全软件开发中,安全软件是允许调用非安全函数的。不过,这个过程并不像普通函数调用那样直接。因为在编译安全软件时,要调用的非安全函数的地址往往是不可用的,这是由于非安全软件的编译通常在安全软件编译之后进行。
解决这个问题最常见的方法是使用安全API将非安全函数指针从非安全软件传递到安全世界。下面我们通过一个示例来详细说明这个过程:
首先,需要创建一个安全API,以便将非安全世界的函数指针传递到安全世界。假设要调用的非安全函数有一个整数输入和一个整数返回值。
typedef int __attribute__((cmse_nonsecure_call)) tdef_nsfunc_o_int_i_int(int x);
int __attribute__((cmse_nonsecure_entry))
pass_nsfunc_ptr_o_int_i_int(tdef_nsfunc_o_int_i_int *callback);
void default_callback(void);
// 声明函数指针 *fp
// fp 可以指向安全或非安全函数
// 初始化为默认回调
tdef_nsfunc_o_int_i_int *fp = (tdef_nsfunc_o_int_i_int *) default_callback;
// 这是一个以函数指针作为输入参数的安全API
int __attribute__((cmse_nonsecure_entry))
pass_nsfunc_ptr_o_int_i_int(tdef_nsfunc_o_int_i_int *callback) {
// 函数指针的结果
cmse_address_info_t tt_payload;
tt_payload = cmse_TTA_fptr(callback);
if (tt_payload.flags.nonsecure_read_ok) {
fp = cmse_nsfptr_create(callback); // 非安全函数指针
return (0);
} else {
printf ("[pass_nsfunc_ptr_o_int_i_int] Error: input pointer is not NS\n");
return (1); // 函数指针无法从非安全端访问
}
}
void default_callback(void) {
__BKPT(0);
while(1);
}
这个安全API(
pass_nsfunc_ptr_o_int_i_int
)使用了两个CMSE定义的内置函数:
-
cmse_TTA_fptr
:该内置函数使用TT指令检查函数指针,确保函数指针可从非安全世界访问,并且函数代码是可读的。它使用
cmse_address_info_t
数据结构返回一个32位结果。
-
cmse_nsfptr_create
:该内置函数将普通函数指针转换为非安全函数指针(即将位0清零),以便BLXNS指令可以将其作为非安全函数调用处理。
在上述示例代码中,定义了一个默认回调函数
default_callback
,这是为了防止安全软件在使用安全API设置非安全函数指针之前尝试调用非安全函数。
当用于接收函数指针的安全API准备好后,非安全软件就可以使用这个安全API将函数指针传递到安全世界,示例代码如下:
extern int __attribute__((cmse_nonsecure_entry)) pass_nsfunc_ptr_o_int_i_int(void *callback);
...
int status;
...
// 将非安全函数指针传递到安全世界
status = pass_nsfunc_ptr_o_int_i_int(&my_func);
if (status==0) {
// 调用安全函数
printf ("Result = %d\n", entry1(10)); // 注意:这个安全API调用 my_func
} else {
printf ("ERROR: pass_nsfunc_ptr_o_int_i_int() = %d\n", status);
}
int my_func(int data_in)
{
printf("[my_func]\n");
return (data_in * data_in);
}
一旦从非安全世界接收到非安全函数指针,安全软件就可以使用以下代码调用非安全函数:
int call_callback(int data_in) {
if (cmse_is_nsfptr(fp)){
return fp(data_in);
// 非安全函数调用
} else {
((void (*)(void)) fp)(); // 对默认回调的普通函数调用
return (0);
}
}
上述代码使用了CMSE内置函数
cmse_is_nsfptr
来检测函数指针是否为非安全的(通过检查位0的值)。如果是,则可以调用非安全函数;如果不是,意味着非安全函数指针尚未传递,在这个示例中,将执行默认回调函数。
C/C++编译器生成的安全代码确保在调用非安全函数时,除了函数参数外,寄存器组中不会保留任何安全数据。因此,在执行BLXNS指令之前,需要将一些寄存器的内容保存到安全栈中。从非安全调用返回后,再从安全栈中恢复寄存器组中之前保存的安全内容。
2. 指针检查
由于安全API通常需要代表非安全软件执行操作,因此非安全软件需要将数据指针传递给安全软件,以指示数据源的位置和操作结果的存放位置。
当安全API代表非安全软件处理数据时,存在以下安全风险:
- 传递给安全API的指针可能指向安全数据,而通常非安全软件是不应该访问这些数据的。如果指针指向安全地址位置且未进行指针检查,安全API可能会读取或修改安全数据,这是一个严重的安全问题,必须避免。
- 非安全无特权软件可能会将指向仅允许特权访问地址的指针传递给安全API。在这种情况下,如果安全API不进行指针检查,非安全软件可能会利用安全API绕过非安全世界的安全机制(如非安全MPU)。
为了进行指针检查,TT(Test Target)指令被设计用于此目的。为了在C/C++编程环境中更方便地进行这些操作,Arm C语言扩展(ACLE)定义了一系列用于处理指针检查的内置函数。
以下是一些可用于非安全和安全软件的内置函数,即使未实现TrustZone也可以使用:
| 内置函数 | 语义 |
| — | — |
|
cmse_address_info_t cmse_TT(void *p)
| 生成一个TT指令 |
|
cmse_address_info_t cmse_TT_fptr(p)
| 生成一个TT指令,参数p可以是任何函数指针类型 |
|
cmse_address_info_t cmse_TTT(void *p)
| 生成一个带有T标志的TT指令 |
|
cmse_address_info_t cmse_TTT_fptr(p)
| 生成一个带有T标志的TT指令,参数p可以是任何函数指针类型 |
这些内置函数返回一个32位结果(有效负载),称为
cmse_address_info_t
。对于非安全软件和安全软件,
cmse_address_info_t
的定义有所不同:
非安全软件的
cmse_address_info_t
定义:
typedef union {
struct cmse_address_info {
unsigned mpu_region:8;
unsigned :8;
unsigned mpu_region_valid:1;
unsigned :1;
unsigned read_ok:1;
unsigned readwrite_ok:1;
unsigned :12;
} flags;
unsigned value;
} cmse_address_info_t;
安全软件的
cmse_address_info_t
定义:
typedef union {
struct cmse_address_info {
unsigned mpu_region:8;
unsigned sau_region:8;
unsigned mpu_region_valid:1;
unsigned sau_region_valid:1;
unsigned read_ok:1;
unsigned readwrite_ok:1;
unsigned nonsecure_read_ok:1;
unsigned nonsecure_readwrite_ok:1;
unsigned secure:1;
unsigned idau_region_valid:1;
unsigned idau_region:8;
} flags;
unsigned value;
} cmse_address_info_t;
安全软件的
cmse_address_info_t
定义提供了额外的位字段,详细说明了非安全世界的访问权限和安全区域的属性。
为了让安全软件能够处理对非安全软件的指针检查,还提供了TTT和TTAT指令,以及相应的内置函数:
| 内置函数 | 语义 |
| — | — |
|
cmse_address_info_t cmse_TTA(void *p)
| 生成一个带有A标志的TT指令 |
|
cmse_address_info_t cmse_TTA_fptr(p)
| 生成一个带有A标志的TT指令,参数p可以是任何函数指针类型 |
|
cmse_address_info_t cmse_TTAT(void *p)
| 生成一个带有T和A标志的TT指令 |
|
cmse_address_info_t cmse_TTAT_fptr(p)
| 生成一个带有T和A标志的TT指令,参数p可以是任何函数指针类型 |
此外,CMSE还定义了用于检查数据对象和地址范围的内置函数:
| 内置函数 | 语义 |
| — | — |
|
void *cmse_check_pointed_object(void *p, int flags)
| 检查指定对象是否满足标志中概述的访问权限。检查失败返回NULL,成功返回
p |
|
void *cmse_check_address_range(void *p, size_t size, int flags)
| 检查指定地址范围是否满足标志中概述的访问权限。检查失败返回NULL,成功返回
p |
在进行指针检查时,可以使用CMSE中定义的C宏来指定访问权限条件,常见的组合如下:
| # | C宏组合(用于指针检查) | 使用场景(指针检查通过所需的访问权限) |
| — | — | — |
| 1 |
CMSE_MPU_NONSECURE \| CMSE_MPU_READWRITE
| 地址范围/对象可由非安全世界调用者读写 |
| 2 |
CMSE_MPU_NONSECURE \| CMSE_MPU_READ
| 地址范围/对象可由非安全世界调用者读取 |
| 3 |
CMSE_MPU_NONSECURE \| CMSE_MPU_READWRITE \| CMSE_MPU_UNPRIV
| 地址范围/对象可由非安全无特权软件读写 |
| 4 |
CMSE_MPU_NONSECURE \| CMSE_MPU_READ \| CMSE_MPU_UNPRIV
| 地址范围/对象可由非安全无特权软件读取 |
当数据指针直接从调用者传递到安全API时,前两种组合通常足以处理指针检查。后两种组合包含
CMSE_MPU_UNPRIV
标志,当软件服务请求和相应的数据指针来自非安全无特权调用者,并且软件服务通过非安全特权软件重定向到安全API时,需要使用这种设置。
地址范围和对象检查函数在检查失败时返回NULL。例如,在以下代码中,如果指针检查失败,则调用
cmse_abort()
函数:
int DmaCopy_1(void *dest, void *src, size_t, num){
void *dest_chk, *src_chk;
// 检查源指针。复制源只需要读权限
src_chk = cmse_check_address_range(*src, size, CMSE_MPU_NONSECURE | CMSE_MPU_READ);
if (src_chk==0) {
cmse_abort();
}
// 检查目标指针(读写)
dest_chk = cmse_check_address_range(*dest, size, CMSE_MPU_NONSECURE | CMSE_MPU_READWRITE);
if (dest_chk==0) {
cmse_abort();
}
...
}
cmse_abort()
函数是C运行时库的一部分,当启用CMSE支持时可用。它有一个“弱”声明,因此可以用自定义的特定于应用程序的中止处理代码覆盖。在实际应用中,当指针检查失败时,可以使用特定于应用程序的错误处理代码来处理软件错误。
3. 其他CMSE特性
除了指针检查内置函数外,C/C++编译器中的CMSE支持还提供了其他几个函数:
| 函数 | 用途 |
| — | — |
|
cmse_nsfptr_create(p)
| 返回p的值,其位0被清零。参数p可以是任何函数指针类型 |
|
cmse_is_nsfptr(p)
| 如果p的位0未设置(为零),则返回非零值;如果p的位0设置,则返回零。参数p可以是任何函数指针类型 |
|
cmse_nonsecure_caller(void)
| 用于安全API,判断入口函数是由非安全状态还是安全软件调用的。如果是从非安全状态调用,返回非零值;否则返回零 |
|
cmse_abort()
| 默认的CMSE错误处理函数,默认情况下调用
abort()
函数 |
cmse_nonsecure_caller()
函数允许安全API确定其调用者是安全软件还是非安全软件。例如,在之前的
DmaCopy
函数中,可以使用这个函数来决定是否进行指针检查:
int __attribute__((cmse_nonsecure_entry)) DmaCopy_1(void *dest, void *src, size_t, num){
void *dest_chk, *src_chk;
if (cmse_nonsecure_caller()) {
// 调用者是非安全的。需要进行指针检查
// 检查源指针。复制操作的数据源只需要读权限
src_chk = cmse_check_address_range(*src, size, CMSE_MPU_NONSECURE | CMSE_MPU_READ);
if (src_chk==0) {
cmse_abort();
}
// 检查目标指针。需要读写权限
dest_chk = cmse_check_address_range(*dest, size, CMSE_MPU_NONSECURE | CMSE_MPU_READWRITE);
if (dest_chk==0) {
cmse_abort();
}
}
...
}
4. 跨安全域传递参数
即使在C/C++编译器中包含并启用了CMSE支持,C编译器也不一定支持在跨安全域API中使用栈传递参数。这是因为根据CMSE规范,使用栈内存传递参数是一个可选功能(非强制性)。
因此,创建安全API的软件开发人员如果不知道非安全软件开发人员将使用哪种C/C++编译器,就必须确保所有安全API参数都可以仅使用寄存器(如r0 - r3)传递。
关于参数和结果传递的详细信息记录在Arm规范文档《Procedure Call Standard for Arm Architecture》(也称为AAPCS)中。
5. 未使用TrustZone时的软件环境
当实现TrustZone时,出于安全原因,默认情况下,不可屏蔽中断、硬故障和总线故障异常会指向安全状态。如果应用程序完全在非安全状态下运行且不使用TrustZone,则可以将上述异常的目标状态更改为非安全状态。这可以通过设置AIRCR(应用中断和复位控制寄存器)中的BFHFNMINS位来实现。
不过,AIRCR.BFHFNMINS位仅应在不使用安全世界时使用。在设计为同时支持TrustZone环境和非TrustZone环境的系统中,可以采用以下两种方法:
- 使用安全固件,在安全启动后禁用TrustZone功能,设置AIRCR.BFHFNMINS位,然后分支到非安全世界。
- 使用支持TrustZone的安全固件启动系统。安全固件提供一个安全API,用于禁用安全世界功能并禁止进一步访问所有安全API。非安全世界的初始化软件使用该安全API设置AIRCR.BFHFNMINS位并禁用安全世界的使用。
在这两种情况下,禁用安全世界功能需要:
- 移除所有非安全可调用(NSC)区域,这意味着非安全世界将无法再访问安全API。
- 如果已经初始化了安全软件框架(如Trusted Firmware - M),则禁用该框架提供的服务。
- 禁用后台安全服务(如安全定时器外设)。
- 禁用安全中断。
禁用安全世界功能的安全软件还可以选择擦除部分安全SRAM,并将擦除的SRAM空间释放给非安全世界。需要注意的是,安全硬故障处理程序仍然可能会触发,如果触发,安全硬故障处理程序应重置系统或关闭设备。如果使用关闭设备的方法,则处理器系统在退出关机状态时必须重置。
综上所述,在安全软件开发中,合理利用CMSE的各种特性和功能,可以有效提高软件的安全性和可靠性,同时应对不同的应用场景和安全需求。
安全软件开发中的CMSE特性与应用(续)
6. 调用非安全函数流程总结
为了更清晰地展示安全软件调用非安全函数的过程,我们可以用以下mermaid流程图来表示:
graph TD;
A[创建安全API] --> B[非安全软件传递函数指针];
B --> C{指针检查};
C -- 通过 --> D[设置非安全函数指针];
C -- 未通过 --> E[输出错误信息];
D --> F{判断是否为非安全指针};
F -- 是 --> G[调用非安全函数];
F -- 否 --> H[调用默认回调函数];
这个流程图清晰地展示了从创建安全API开始,到最终调用非安全函数或默认回调函数的整个过程。具体步骤如下:
1.
创建安全API
:定义一个安全API,用于接收非安全函数指针。
2.
非安全软件传递函数指针
:非安全软件将函数指针传递给安全API。
3.
指针检查
:安全API使用
cmse_TTA_fptr
等函数检查指针是否可从非安全世界访问。
4.
设置非安全函数指针
:如果指针检查通过,使用
cmse_nsfptr_create
函数设置非安全函数指针。
5.
判断是否为非安全指针
:使用
cmse_is_nsfptr
函数判断函数指针是否为非安全指针。
6.
调用函数
:如果是非安全指针,调用非安全函数;否则,调用默认回调函数。
7. 指针检查的实际应用
在实际的安全软件开发中,指针检查是非常重要的环节。下面我们通过一个更详细的示例来展示指针检查的应用。
假设我们有一个安全API,用于将非安全数据复制到安全缓冲区。为了确保数据的安全性,我们需要对传入的非安全数据指针进行检查。
#include <stdio.h>
// 假设的CMSE支持头文件
#include "cmse_support.h"
// 假设的安全API
int copy_nonsecure_data(void *secure_buffer, void *nonsecure_data, size_t size) {
void *data_chk;
// 检查非安全数据指针的读权限
data_chk = cmse_check_address_range(nonsecure_data, size, CMSE_MPU_NONSECURE | CMSE_MPU_READ);
if (data_chk == 0) {
printf("Error: Non-secure data pointer is invalid.\n");
return -1;
}
// 检查安全缓冲区指针的写权限
void *buffer_chk = cmse_check_address_range(secure_buffer, size, CMSE_MPU_READWRITE);
if (buffer_chk == 0) {
printf("Error: Secure buffer pointer is invalid.\n");
return -1;
}
// 进行数据复制
for (size_t i = 0; i < size; i++) {
((char *)secure_buffer)[i] = ((char *)nonsecure_data)[i];
}
printf("Data copied successfully.\n");
return 0;
}
// 非安全数据示例
int nonsecure_data_array[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 安全缓冲区示例
int secure_buffer_array[10];
int main() {
int result = copy_nonsecure_data(secure_buffer_array, nonsecure_data_array, sizeof(nonsecure_data_array));
if (result == 0) {
// 输出复制后的数据
for (int i = 0; i < 10; i++) {
printf("Secure buffer data at index %d: %d\n", i, secure_buffer_array[i]);
}
}
return 0;
}
在这个示例中,
copy_nonsecure_data
函数是一个安全API,它接收一个安全缓冲区指针和一个非安全数据指针。在进行数据复制之前,函数会对这两个指针进行检查,确保非安全数据指针具有读权限,安全缓冲区指针具有写权限。如果指针检查失败,函数会输出错误信息并返回错误码;如果检查通过,函数会将非安全数据复制到安全缓冲区,并输出成功信息。
8. 跨安全域参数传递的注意事项
在跨安全域传递参数时,由于使用栈传递参数不是强制支持的,我们需要特别注意以下几点:
1.
使用寄存器传递参数
:确保所有安全API参数都可以仅使用寄存器(如r0 - r3)传递。例如,在设计安全API时,尽量避免使用复杂的数据结构作为参数,而是将其拆分为基本数据类型,以便通过寄存器传递。
2.
遵循AAPCS规范
:详细了解《Procedure Call Standard for Arm Architecture》(AAPCS)中关于参数和结果传递的规则,确保代码的兼容性和可移植性。
3.
考虑参数大小
:如果参数大小超过寄存器所能容纳的范围,可能需要采用其他方法,如通过共享内存传递数据。
9. 未使用TrustZone时的系统配置流程
当不使用TrustZone时,将异常目标状态更改为非安全状态的系统配置流程可以用以下mermaid流程图表示:
graph TD;
A[选择系统模式] --> B{是否使用TrustZone};
B -- 否 --> C[设置AIRCR.BFHFNMINS位];
B -- 是 --> D[正常使用TrustZone];
C --> E[禁用安全世界功能];
E --> F[移除NSC区域];
E --> G[禁用安全软件框架服务];
E --> H[禁用后台安全服务];
E --> I[禁用安全中断];
F & G & H & I --> J[可选:擦除部分安全SRAM];
具体操作步骤如下:
1.
选择系统模式
:根据应用需求决定是否使用TrustZone。
2.
设置AIRCR.BFHFNMINS位
:如果不使用TrustZone,设置AIRCR寄存器中的BFHFNMINS位,将异常目标状态更改为非安全状态。
3.
禁用安全世界功能
:移除所有非安全可调用(NSC)区域,禁用安全软件框架提供的服务,禁用后台安全服务和安全中断。
4.
可选操作
:擦除部分安全SRAM,并将擦除的SRAM空间释放给非安全世界。
10. 总结
安全软件开发中的CMSE特性为我们提供了强大的工具来保障软件的安全性和可靠性。通过合理使用调用非安全函数、指针检查、跨安全域参数传递等功能,我们可以有效地应对各种安全风险。同时,在不同的应用场景下,如使用或不使用TrustZone,我们需要根据具体情况进行相应的配置和处理。
在实际开发过程中,我们应该深入理解这些特性的原理和使用方法,结合具体的应用需求,编写安全、高效的代码。同时,要注意代码的兼容性和可维护性,遵循相关的规范和标准,以确保软件的质量和稳定性。
希望通过本文的介绍,能帮助大家更好地掌握安全软件开发中的CMSE特性与应用,为开发出更安全可靠的软件提供有力支持。
超级会员免费看
4430

被折叠的 条评论
为什么被折叠?



