安全软件设计考量
1. 初始分支到非安全世界
在相关示例中,非安全复位处理程序被声明为非安全函数指针,然后使用
BLXNS
指令调用以启动非安全世界。不过,复位处理程序可能仅包含一个返回语句,这将使代码执行流程返回到安全世界。所以,处理切换的安全代码必须能够应对这种情况,例如在分支后添加错误报告代码。
除了使用
BLXNS
分支到非安全世界,也可以使用内联汇编代码来使用
BXNS
指令。但使用此方法时,汇编代码需要在分支前手动清除寄存器组的内容(因为寄存器组可能包含安全信息)。此外,在执行
BXNS
指令之前,需要在安全栈上额外压入两个数据字。使用这种方法时,非安全代码无法返回安全代码,原因如下:
- 链接寄存器(LR)中没有返回地址或
FNC_RETURN
。
- 安全栈中没有堆叠的返回地址。
在分支到非安全软件之前,需要设置安全栈指针的栈指针限制。
2. 非安全可调用(NSC)
2.1 NSC 区域定义匹配
由 SAU/IDAU 定义的 NSC 区域的位置和大小,应仅覆盖链接脚本中的 CMSE veneer(“Veneer$$CMSE”)。如果 SAU/IDAU 中 NSC 区域定义过大,可能会覆盖其他程序的二进制数据,这些数据可能包含与 SG 指令匹配的二进制数据,从而导致意外的入口点。反之,如果定义过小,一些有效的入口点将不在 NSC 区域内。理想情况下,SAU/IDAU 定义的 NSC 应与链接脚本中的定义相匹配。
需要注意的是,在定义 NSC 区域时,由于产品生命周期中通常需要更新安全固件,在 NSC 区域中预留额外空间是有益的,即比现有入口点所需空间更大。当 NSC 内存空间大于入口点所需空间时,CMSE 兼容工具链会确保 NSC 中未使用的地址空间填充预定义的数据值(如 Arm 工具链中使用 0),这些值不与 SG 指令匹配,也不会导致意外的入口点。
2.2 SRAM 中的 NSC 区域
由于 SRAM 上电时其内容未知,在该区域初始化之前,不应设置 SRAM 中某区域的 NSC 属性。这一安全措施可防止因未知 SRAM 数据导致的意外入口点。
3. 内存分区
3.1 MPC 和 PPC 行为
根据内存保护控制器(MPC)和外设保护控制器(PPC)的实现情况,针对非安全内存部分或外设的安全事务很可能会被阻止。例如,如果 MPC/PPC 将某内存部分或外设定义为非安全,但 SAU 未启用,那么该内存部分或外设可能无法访问。这是因为当 SAU 禁用(即 SAU_CTRL 等于 0)时,内存地址的属性被视为安全(即访问时会生成安全事务)。然而,由于 MPC 或 PPC 期望针对非安全内存部分/外设的传输是非安全的,所以该传输可能会被阻止。
MPC 和 PPC 的这种行为是为了确保使用正确的配置。如果安全软件错误地配置 MPC/PPC,将本应是安全的内存部分/外设处理为非安全的,其他非安全总线主控可能能够访问该内存部分/外设。如果安全软件随后将该内存部分/外设视为安全的,并且允许处理器通过安全地址别名(使用安全事务)访问该内存部分/外设,安全数据可能会泄露,从而导致安全漏洞。
3.2 SRAM 页面安全属性的动态切换
在运行时,如果需要更新内存分区,将安全软件使用的内存页面从安全状态切换到非安全状态,安全软件必须执行以下步骤:
1. 清除 SRAM 页面中的安全信息。
2. 如果实现了系统级缓存,刷新该页面的缓存数据,以便也清除主内存系统中的数据。
3. 执行数据内存屏障,确保数据内存已更新(这通常是缓存维护例程的一部分)。
4. 写入 SAU 或特定设备寄存器(如 MPC),以更新内存页面的安全属性。
如果需要将 SRAM 中的非安全内存页面切换到安全状态,需要执行以下步骤以降低代码注入攻击的风险:
1. 如果存在系统级缓存,需要禁用非安全中断(例如,使用
BASEPRI_S
和
AIRCR.PRIS
的组合),以防止非安全 ISR 在切换过程中更新内存页面。
2. 设置安全 MPU,将该内存区域标记为 XN(永不执行)。
3. 如果存在系统级缓存,需要刷新该页面的缓存数据(如果要使用该内存页面中的数据)或使缓存数据无效(如果要丢弃该内存页面中的数据)。
4. 执行数据内存屏障,确保数据内存已更新(这通常是缓存维护例程的一部分)。
5. 写入 SAU 或特定设备寄存器(如 MPC),以更新内存页面的安全属性。
6. 如果为了切换而禁用了非安全中断,现在应重新启用它们。
7. 如果可以丢弃 SRAM 页面中的数据,应将其擦除。
8. 如果要使用 SRAM 页面中的数据,可能需要对数据进行验证。
3.3 外设安全属性的动态切换
当将外设从一个安全域切换到另一个安全域时,如果该外设产生中断,应更新 NVIC 的中断目标非安全状态寄存器(NVIC_ITNS[n])。
当将外设从安全世界切换到非安全世界时,应采取以下步骤:
1. 确保外设禁用时,其中没有残留安全数据。
2. 更新内存映射配置(例如,PPC 中的控制寄存器),将外设切换到非安全状态。
3. 更新外设中断的目标状态。
也可以将非安全世界使用的外设切换到安全状态。当将外设从非安全切换到安全时,应采取以下步骤:
1. 暂时禁用非安全中断生成(例如,使用
BASEPRI_S
和
AIRCR.PRIS
的组合),防止非安全中断处理程序在切换过程中重新启用外设。
2. 禁用外设。
3. 更新内存映射配置(例如,PPC 中的控制寄存器),将外设切换到安全状态。
4. 更新 NVIC_ITNS 寄存器,将外设中断的目标状态设置为安全。
5. 重新启用非安全中断。
暂时禁用非安全中断可防止非安全异常处理程序在第 2 步和第 3 步之间重新启用外设,否则意味着外设在外设过渡到安全状态时在非安全软件控制下被启用。
4. 输入数据和指针验证
4.1 非安全内存中输入数据的验证
当安全函数的输入数据通过指针传递时,需要在验证数据值之前将数据复制到安全内存中。如果数据未复制到安全内存中,数据可能会被非安全处理程序修改,从而导致安全漏洞。以下是相关代码示例:
// 错误代码示例
int __attribute__((cmse_nonsecure_entry)) entry2a(int *idx)
{
const char textstr[] = "Hello world\n";
if (cmse_check_pointed_object(idx, CMSE_NONSECURE|CMSE_MPU_READ) != NULL) {
if ((*idx>=0) && (*idx < 12)) {
return ((int) textstr[*idx]); // 使用非安全内存中的数据作为索引
} else {
return(0);
}
} else
return (-1);
}
上述代码虽然进行了指针检查和值范围检查,但使用的索引值(idx)位于非安全内存中。如果在执行此函数期间发生非安全中断,索引值(idx)可能会被更改,理论上黑客可以设置索引值(idx)来读取安全内存中的数据。
为了解决这个问题,需要在验证数据值之前将数据复制到安全内存中。以下是修正后的代码:
// 正确代码示例
int __attribute__((cmse_nonsecure_entry)) entry2(int *idx)
{
const char textstr[] = "Hello world\n";
int idx_copy;
if (cmse_check_pointed_object(idx, CMSE_NONSECURE|CMSE_MPU_READ) != NULL) {
idx_copy = *idx;
if ((idx_copy >=0)&&(idx_copy < 12)) { // 验证安全副本的索引
return ((int) textstr[idx_copy]); // 使用安全副本的索引
} else {
return(0);
}
} else
return (-1);
}
同样的问题也适用于存储在非安全内存中的任何数据,包括指针。例如,当双指针作为安全 API 的输入参数时,非安全 SRAM 中存储的指针可能随时被非安全异常处理程序更改。以下是相关代码示例:
// 错误代码示例
int __attribute__((cmse_nonsecure_entry)) entry3a(int **idx)
{
const char textstr[] = "Hello world\n";
int *idx_ptr;
int idx_copy;
// 检查指针的指针是否在非安全世界
if (cmse_check_pointed_object(*idx, CMSE_NONSECURE|CMSE_MPU_READ) != NULL) {
// 指针的指针是非安全的
idx_ptr = *idx;
// 验证 idx 的位置是否非安全
if (cmse_check_pointed_object(idx_ptr, CMSE_NONSECURE|CMSE_MPU_READ) != NULL)
{
// 问题:非安全世界中 idx 的指针可能被非安全 ISR 更改
idx_copy = **idx; // 复制值
if ((idx_copy>=0) && (idx_copy < 12)) { // 值验证
return ((int) textstr[idx_copy]); // 使用安全副本的索引
} else {
return(0);
}
} else
return (-1);
} else {
return (-1);
}
}
虽然在上述代码中验证了指针的指针、idx 的指针和 idx 的值,但仍然存在安全问题。因为如果在指针检查后发生非安全中断,非安全世界中索引的指针可能会被更改,最终指向安全地址。
为了解决这个问题,需要确保安全 API 创建索引指针的安全副本,然后使用安全副本将索引变量复制到安全内存中。以下是修正后的代码:
// 正确代码示例
int __attribute__((cmse_nonsecure_entry)) entry3b(int **idx)
{
const char textstr[] = "Hello world\n";
int *idx_ptr;
int idx_copy;
// 检查指针的指针是否在非安全世界
if (cmse_check_pointed_object(*idx, CMSE_NONSECURE|CMSE_MPU_READ) != NULL) {
// 指针的指针是非安全的
idx_ptr = *idx;
// 验证 idx 的位置是否非安全
if (cmse_check_pointed_object(idx_ptr, CMSE_NONSECURE|CMSE_MPU_READ) != NULL) {
idx_copy = *idx_ptr; // 使用验证过的指针复制值
if ((idx_copy>=0) &&(idx_copy < 12)) { // 值验证
return ((int) textstr[idx_copy]); // 使用安全副本的索引
} else {
return(0);
}
} else
return (-1);
} else {
return (-1);
}
}
需要注意的是,双指针可能以非安全世界数据结构中的指针形式存在。
4.2 使用指针前始终进行检查
在访问数据之前验证指针非常重要。即使安全 API 只是读取数据,并且指针检查在之后进行,如果在使用指针之前未进行验证,仍然可能出现安全问题。以下是相关代码示例:
// 错误代码示例
int __attribute__((cmse_nonsecure_entry)) entry4a(char * src, char * dest)
{
#define MAX_LENGTH_ALLOWED 128
int string_length;
// 指针在验证之前被使用
string_length = strnlen(src, MAX_LENGTH_ALLOWED);
if ((string_length == MAX_LENGTH_ALLOWED) && (src[string_length] != '\0')) {
// 字符串太长
return (-1); // 返回错误
} else { // 源指针检查
if (cmse_check_address_range ((void *)src, (size_t) string_length,
(CMSE_NONSECURE | CMSE_MPU_READ)) == NULL) {
return (-1); // 返回错误
}
// 目标指针检查
if (cmse_check_address_range ((void *)dest, (size_t) string_length,
(CMSE_NONSECURE | CMSE_MPU_READWRITE)) == NULL) {
return (-1); // 返回错误
}
memcpy (dest, src, (size_t) string_length); // 内存复制操作
return (0);
}
}
虽然在内存复制(即
memcpy
)之前进行了指针检查,但仍然可能存在安全问题,因为安全 API 指向的数据在指针验证之前被
strnlen
函数读取。当 “src” 指针指向安全外设,并且读取某些寄存器会影响安全世界的操作时,就会出现问题。例如,如果外设中有 FIFO 数据寄存器,FIFO 中的数据可能会因此丢失。
为了解决这个问题,需要将执行字符串长度函数的责任从安全世界移回到非安全世界。这将使字符串长度(即以下代码中的 “len” 参数)成为安全 API 函数的额外参数。以下是修正后的代码:
// 正确代码示例
int __attribute__((cmse_nonsecure_entry)) entry4b(char * src, char * dest,
int32_t len) // 正确示例 - 字符串长度作为参数
{
#define MAX_LENGTH_ALLOWED 128
if ((len < 0) || (len > MAX_LENGTH_ALLOWED)) return (-1);
// 源指针检查
if (cmse_check_address_range ((void *)src, (size_t) len, (CMSE_NONSECURE |
CMSE_MPU_READ)) == NULL) {
return (-1); // 返回错误
}
// 目标指针检查
if (cmse_check_address_range ((void *)dest, (size_t) len, (CMSE_NONSECURE |
CMSE_MPU_READWRITE)) == NULL) {
return (-1); // 返回错误
}
memcpy (dest, src, (size_t) len);
return (0);
}
4.3 安全 API 中 “printf” 的危险
虽然
printf
对于安全软件开发人员调试应用程序时是一个方便的函数,但在使用
printf
显示非安全世界的字符串消息时需要非常小心。原因如下:
- 非安全世界中存储的字符串消息在
printf
函数执行时可能被非安全异常处理程序修改,导致字符串不在非安全内存内终止。此时,与非安全内存相邻的安全内存内容可能会被打印出来,从而导致安全信息泄露。
- 如果显示的文本字符串包含 “%s” 格式说明符,安全 API 中的
printf
函数假设安全栈中的特定内存位置包含要显示的字符串的地址。但实际情况可能并非如此,如果非安全软件使用安全世界提供的
printf
函数打印包含 “%s” 格式说明符的消息,函数调用参数中可能没有有效的字符串指针。
printf
代码会错误地将安全栈中的数据用作字符串指针,这可能被解释为安全地址位置,从而导致安全信息泄露。
printf
函数的另一个风险是使用特殊格式说明符 “%n”,它将字符输出的数量写入输入参数中指定的数据指针。例如:
// %n 在 printf 中的使用示例
int lcd_cursor_x_pos; // LCD 光标 x 位置
int lcd_cursor_y_pos; // LCD 光标 y 位置
...
printf ("Speed: %d %n", currend_speed, &lcd_cursor_x_pos);
// LCD 光标信息由 printf 更新
if (lcd_cursor_x_pos > 30){ // 移动光标到下一行
lcd_cursor_x_pos = 0;
lcd_cursor_y_pos++; // 下一行
...
不幸的是,如果安全 API 允许非安全软件使用安全世界中的
printf
函数,这个功能可能会导致安全问题。如果指针指向安全内存位置,非安全函数可以构造特殊的字符串消息,使安全 API 将值写入安全内存位置,这将导致严重的安全漏洞。
综上所述,应避免在从非安全世界获取文本字符串的 API 中提供 “printf” 功能。如果需要用于显示的 API,可以考虑其他更安全的实现方式。
5. 总结与建议
5.1 关键要点回顾
为了确保安全软件设计的可靠性和安全性,我们对各个方面的要点进行了详细探讨,以下是关键内容的总结:
| 类别 | 要点 | 详细说明 |
|---|---|---|
| 初始分支到非安全世界 |
使用
BLXNS
或
BXNS
|
使用
BLXNS
启动非安全世界时要处理复位返回情况;使用
BXNS
需手动清除寄存器内容和压入额外数据字,且非安全代码无法返回安全代码,分支前要设置安全栈指针限制
|
| 非安全可调用(NSC) | 区域定义匹配 | SAU/IDAU 定义的 NSC 区域应与链接脚本匹配,可预留额外空间,避免意外入口点 |
| 非安全可调用(NSC) | SRAM 中 NSC 区域设置 | SRAM 区域初始化前不设置 NSC 属性,防止未知数据导致意外入口点 |
| 内存分区 | MPC 和 PPC 行为 | MPC 和 PPC 会阻止针对非安全内存或外设的安全事务,确保正确配置,防止安全数据泄露 |
| 内存分区 | SRAM 页面安全属性切换 | 安全到非安全:清除信息、刷新缓存、执行屏障、更新属性;非安全到安全:禁用中断、设置 MPU、处理缓存、更新属性、验证数据等 |
| 内存分区 | 外设安全属性切换 | 切换时更新 NVIC_ITNS 寄存器,安全到非安全:清除数据、更新配置、更新中断目标;非安全到安全:禁用中断、禁用外设、更新配置、更新寄存器、重新启用中断 |
| 输入数据和指针验证 | 非安全内存输入数据验证 | 数据通过指针传递时,先复制到安全内存再验证,避免非安全处理程序修改数据 |
| 输入数据和指针验证 | 使用指针前检查 | 访问数据前验证指针,避免指针指向安全外设时读取数据导致问题 |
| 输入数据和指针验证 | “printf” 在安全 API 中的危险 |
避免使用
printf
显示非安全世界字符串,防止安全信息泄露
|
5.2 操作建议
基于上述要点,我们给出以下操作建议,以帮助开发者更好地进行安全软件设计:
5.2.1 分支到非安全世界操作
graph LR
A[开始] --> B[声明非安全复位处理程序为非安全函数指针]
B --> C{选择分支方式}
C -->|BLXNS| D[使用 BLXNS 指令启动非安全世界]
D --> E[在分支后添加错误报告代码]
C -->|BXNS| F[使用内联汇编代码使用 BXNS 指令]
F --> G[手动清除寄存器组内容]
G --> H[在安全栈压入两个数据字]
H --> I[执行 BXNS 指令]
D --> J[设置安全栈指针限制]
I --> J
J --> K[结束]
5.2.2 NSC 区域定义操作
- 确定链接脚本中 CMSE veneer 的位置和大小。
- 根据此定义 SAU/IDAU 中的 NSC 区域,可适当预留额外空间。
- 使用 CMSE 兼容工具链确保未使用地址空间填充非 SG 指令匹配的数据值。
5.2.3 SRAM 页面安全属性切换操作
安全到非安全
- 调用清除函数清除 SRAM 页面中的安全信息。
- 若有系统级缓存,调用刷新缓存函数刷新该页面缓存数据。
- 执行数据内存屏障指令。
- 写入 SAU 或特定设备寄存器更新安全属性。
非安全到安全
-
若有系统级缓存,使用
BASEPRI_S和AIRCR.PRIS组合禁用非安全中断。 - 设置安全 MPU 将内存区域标记为 XN。
- 根据数据使用情况刷新或使缓存数据无效。
- 执行数据内存屏障指令。
- 写入 SAU 或特定设备寄存器更新安全属性。
- 若之前禁用了非安全中断,重新启用。
- 若可丢弃数据,擦除数据;若使用数据,进行验证。
5.2.4 外设安全属性切换操作
安全到非安全
- 禁用外设,确保无安全数据残留。
- 更新 PPC 等控制寄存器,将外设切换到非安全状态。
- 更新 NVIC_ITNS 寄存器,设置外设中断目标状态。
非安全到安全
-
使用
BASEPRI_S和AIRCR.PRIS组合暂时禁用非安全中断。 - 禁用外设。
- 更新 PPC 等控制寄存器,将外设切换到安全状态。
- 更新 NVIC_ITNS 寄存器,设置外设中断目标为安全。
- 重新启用非安全中断。
5.2.5 输入数据和指针验证操作
非安全内存输入数据验证
// 正确做法示例
int __attribute__((cmse_nonsecure_entry)) entry2(int *idx)
{
const char textstr[] = "Hello world\n";
int idx_copy;
if (cmse_check_pointed_object(idx, CMSE_NONSECURE|CMSE_MPU_READ) != NULL) {
idx_copy = *idx;
if ((idx_copy >=0)&&(idx_copy < 12)) {
return ((int) textstr[idx_copy]);
} else {
return(0);
}
} else
return (-1);
}
使用指针前检查
// 正确做法示例
int __attribute__((cmse_nonsecure_entry)) entry4b(char * src, char * dest,
int32_t len)
{
#define MAX_LENGTH_ALLOWED 128
if ((len < 0) || (len > MAX_LENGTH_ALLOWED)) return (-1);
if (cmse_check_address_range ((void *)src, (size_t) len, (CMSE_NONSECURE |
CMSE_MPU_READ)) == NULL) {
return (-1);
}
if (cmse_check_address_range ((void *)dest, (size_t) len, (CMSE_NONSECURE |
CMSE_MPU_READWRITE)) == NULL) {
return (-1);
}
memcpy (dest, src, (size_t) len);
return (0);
}
避免使用 “printf” 显示非安全世界字符串
在设计 API 时,避免提供从非安全世界获取文本字符串并使用
printf
显示的功能,可考虑使用其他安全的显示方式。
5.3 未来展望
随着技术的不断发展,安全软件设计将面临更多的挑战和机遇。未来,我们可以期待更智能、更自动化的安全机制,例如基于人工智能的安全漏洞检测和防范系统。同时,硬件技术的进步也将为安全软件设计提供更好的支持,如更强大的内存保护和加密功能。
在实际应用中,开发者需要不断关注安全领域的最新动态,及时更新自己的知识和技能,以应对不断变化的安全威胁。通过遵循本文所介绍的安全软件设计原则和操作建议,我们可以构建出更加安全可靠的软件系统,为用户提供更优质的服务和保障。
总之,安全软件设计是一个复杂而重要的领域,需要我们从多个方面进行综合考虑和精心设计。希望本文的内容能够对开发者在安全软件设计方面提供有价值的参考和指导。
超级会员免费看
2924

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



