74、安全软件开发的关键技术与实践指南

安全软件开发的关键技术与实践指南

1. 消息打印与安全API

在消息处理方面,像 “puts” 和 “putchar”/“putc” 这样的函数可以直接使用,同时需要添加自定义的安全API来处理整数和其他值的打印。使用 “puts” 时,由于该函数本身不提供最大字符计数功能,且非安全世界中的文本字符串可能会被非安全异常处理程序更改,因此在调用 “puts” 之前,需要将字符串从非安全内存复制到安全内存。以下是示例代码:

int __attribute__((cmse_nonsecure_entry)) entry5(char * src, int32_t len)
{
    int string_length;
    char ch_buffer[128];
    #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); // return error
    }
    // copy to buffer – please note the text could be changed by the Non-Secure ISR
    memcpy (&ch_buffer[0], src, (size_t) len);
    string_length = strnlen(src, MAX_LENGTH_ALLOWED);
    if ((string_length == MAX_LENGTH_ALLOWED) && (src[string_length] != '\0')) {
        // The string is too long
        return (-1); // return error
    }
    puts (ch_buffer);
    return (0);
}

操作步骤如下:
1. 检查传入的字符串长度是否在允许范围内。
2. 使用 cmse_check_address_range 函数检查源地址的访问权限。
3. 将字符串从源地址复制到缓冲区。
4. 检查字符串长度是否超过最大允许长度。
5. 调用 puts 函数打印字符串。

2. 使用CMSE指针检查函数

如果安全API需要代表非安全软件处理数据,必须使用CMSE定义的指针检查函数来检测来自非安全调用者的数据的访问权限。为防止非安全无特权软件攻击非安全特权软件,指针检查会考虑MPU权限级别。通常使用 CMSE_NONSECURE 标志,结合 CMSE_MPU_READ CMSE_MPU_READWRITE ,具体取决于函数是否需要修改数据。

TTA指令的执行会自动检测非安全软件的特权状态,并根据此信息确定访问是否受非安全MPU设置的限制。在某些场景下,安全API需要根据无特权软件的访问权限进行指针检查,此时会使用 CMSE_MPU_UNPRIV 标志。为了让安全软件为非安全特权软件和非安全无特权软件提供服务,需要两种不同的安全API变体:
1. 为非安全特权软件(如操作系统内核)和非安全无特权软件(非安全无特权软件直接访问安全API)服务的安全API,使用TTA指令进行指针检查。
2. 通过操作系统重定向机制为非安全无特权软件服务的安全API,使用TTAT指令进行指针检查(即使用 CMSE_MPU_UNPRIV 标志)。

对于从非安全世界传递过来的函数指针,也需要进行检查以确保其安全使用。函数指针可以通过以下两种方式处理:
- 方法 (a):使用 “cmse_nsfptr_create” 处理,清除地址值的第0位,以表明它是非安全的。
- 方法 (b):使用TTA/TTAT进行检查,例如使用 “cmse_TTA_fptr” 确保地址是非安全的。

方法 (a) 更快,因为它只需要清除地址值的第0位;方法 (b) 的优势在于允许用于传输指针的安全API立即返回错误状态。

3. 外设驱动
3.1 外设寄存器的定义

当使用外设保护控制器(PPC)时,通过PPC连接的外设具有安全和非安全别名地址。在定义外设时,需要先定义一个数据结构来表示外设中的寄存器,然后分别定义安全和非安全指针。以下是UART外设的示例:

/*------------- Universal Asynchronous Receiver Transmitter (UART) -----------*/
typedef struct
{
    __IOM uint32_t DATA;
    /* Offset: 0x000 (R/W) Data Register */
    __IOM uint32_t STATE;
    /* Offset: 0x004 (R/W) Status Register */
    __IOM uint32_t CTRL;
    /* Offset: 0x008 (R/W) Control Register */
    union {
        __IM uint32_t INTSTATUS; /* Offset: 0x00C (R/ ) Interrupt Status Register */
        __OM uint32_t INTCLEAR;
        /* Offset: 0x00C ( /W) Interrupt Clear Register */
    };
    __IOM uint32_t BAUDDIV;
    /* Offset: 0x010 (R/W) Baudrate Divider Register */
} IOTKIT_UART_TypeDef;

// Secure base addresses
#define IOTKIT_SECURE_UART0_BASE (0x50200000UL)
#define IOTKIT_SECURE_UART1_BASE (0x50201000UL)

// Non-secure base addresses
#define IOTKIT_UART0_BASE (0x40200000UL)
#define IOTKIT_UART1_BASE (0x40201000UL)

// Secure peripheral pointers
#define IOTKIT_UART0 ((IOTKIT_UART_TypeDef *) IOTKIT_UART0_BASE )
#define IOTKIT_UART1 ((IOTKIT_UART_TypeDef *) IOTKIT_UART1_BASE )

// Non-secure peripheral pointers
#define IOTKIT_SECURE_UART0 ((IOTKIT_UART_TypeDef *) IOTKIT_SECURE_UART0_BASE )
#define IOTKIT_SECURE_UART1 ((IOTKIT_UART_TypeDef *) IOTKIT_SECURE_UART1_BASE )

操作步骤如下:
1. 定义一个结构体来表示外设的寄存器。
2. 定义安全和非安全的基地址。
3. 定义安全和非安全的外设指针。

3.2 外设驱动代码

在创建驱动代码时,通常将外设指针作为驱动函数的参数传递,这样同一个函数可以用于外设的多个实例。调用者可以决定使用安全还是非安全版本的外设指针。以下是示例代码:

// Driver function
int UART_init(IOTKIT_UART_TypeDef * UART_PTR, int baudrate ...) {
    {
        ...
        UART_PTR->CTRL |= IOTKIT_UART_CTRL_TXEN_Msk|IOTKIT_UART_CTRL_RXEN_Msk;
        ...
    }
}
// caller
...
UART_init(IOTKIT_UART0, 9600, ...); // UART0 is set as Non-secure
...
UART_init(IOTKIT_SECURE_UART1, 9600, ...); // UART1 is set as secure
...

在某些情况下,安全库函数可能需要访问外设,而不考虑其配置为安全还是非安全。此时,代码会从外设保护控制器(PPC)读取外设的安全设置,以确定使用哪个外设指针。以下是示例代码:

// Driver function
int UART_putc(int ch ...) {
    {
        IOTKIT_UART_TypeDef * UART_PTR;
        ...
        if (check_uart0_is_Secure()) { // Automatically selects the right pointer
            UART_PTR = IOTKIT_SECURE_UART0;
        } else {
            UART_PTR = IOTKIT_UART0;
        }
        // Waits if the buffer is full
        while (UART_PTR->STATE & IOTKIT_UART_STATE_TXBF_Msk);
        UART_PTR->DATA = (uint32_t) ch;
        ...
    }
}

此外,外设驱动代码可以使用CMSE预定义宏来有条件地编译外设指针选择代码,使同一段代码可供安全和非安全软件开发者使用。修改后的代码如下:

// Driver function
int UART_putc(int ch ...) {
    {
        IOTKIT_UART_TypeDef * UART_PTR;
        ...
        #if defined (__ARM_FEATURE_CMSE) && (__ARM_FEATURE_CMSE == 3U)
            // Secure software might need to decide whether the peripheral is
            // accessible from the Secure or the Non-secure aliases
            if (check_uart0_is_Secure()) { // Automatically selects the right pointer
                UART_PTR = IOTKIT_SECURE_UART0;
            } else {
                UART_PTR = IOTKIT_UART0;
            }
        #else
            // Non-secure software only uses the Non-secure peripheral pointer
            UART_PTR = IOTKIT_UART0;
        #endif
        // Waits if the buffer is full
        while (UART_PTR->STATE & IOTKIT_UART_STATE_TXBF_Msk);
        UART_PTR->DATA = (uint32_t) ch;
        ...
    }
}

操作步骤如下:
1. 定义驱动函数,接受外设指针和其他参数。
2. 根据需要设置外设的控制寄存器。
3. 在调用驱动函数时,选择合适的外设指针。
4. 在需要根据外设安全设置选择指针的函数中,使用 check_uart0_is_Secure 函数进行判断。
5. 可使用CMSE预定义宏进行有条件编译。

3.3 外设中断

通常,系统不应配置为允许非安全软件生成安全异常,以避免诸如拒绝服务或触发安全软件未预期的安全中断事件等安全攻击。因此,当外设配置为非安全时,其中断也应配置为非安全(使用NVIC_ITNS寄存器)。不过,在以下几种情况下,非安全软件生成安全中断/异常是可以接受的:
- 故障异常:例如,当使用TrustZone时,非安全操作触发的总线错误会导致HardFault/BusFault异常指向安全状态。
- 处理器间通信(IPC):例如,在具有IPC邮箱的系统中,安全软件可能允许非安全软件为安全软件生成消息。

4. 其他一般建议
4.1 参数传递

根据CMSE规范,C编译器并非必须支持在跨安全域函数调用中使用栈进行参数传递。如果创建安全API,且不确定非安全软件开发人员使用的工具链是否支持此功能,则应确保函数的传递参数仅使用寄存器。使用寄存器传递参数可能会获得更好的性能,因为使用栈传递参数需要执行更多的软件步骤。如果安全API的操作需要大量参数,可以定义一个数据结构来组合这些参数,并将数据结构的指针作为单个参数传递给安全API。

4.2 定义具有XN属性的栈、堆和数据RAM

一般来说,使用MPU为安全SRAM中用于栈、堆和数据的区域设置XN(永不执行)属性是一个好做法。这是因为这些区域通常包含来自非安全世界的数据,例如在安全API执行期间,数据需要从非安全世界复制到安全世界进行处理;在安全API调用和安全异常期间,寄存器中的非安全数据可能会被压入安全栈。通过在安全MPU中使用XN属性,可以降低代码注入攻击的风险。

4.3 主栈的栈限制

软件开发人员应利用栈限制检查功能来降低栈溢出的风险。但在设置栈限制之前,需要估计所需的栈空间,特别是在处理主栈时。由于主栈用于系统异常(包括故障处理),如果主栈限制大小过小,一些故障异常将无法正常工作。因此,软件开发人员应避免使用过多的异常优先级级别,以减少嵌套中断的层数,从而避免主栈使用过度。

4.4 防止栈下溢攻击

在某些情况下,例如当安全栈为空(如新线程创建时),黑客可能会使用伪造的EXC_RETURN或FNC_RETURN操作来触发栈下溢场景。由于栈内存上方的内容不可预测,栈区域上方的32位数据值可能会与栈帧完整性签名或安全可执行地址值匹配。如果黑客使用伪造的操作从非安全世界切换到安全世界,这种非法操作可能不会被检测到。

为了检测和阻止此类攻击,安全软件开发人员可以在实际栈空间上方预留两个字(8字节)的栈内存,并放置一个特殊值0xFEF5EDA5。这个特殊值永远不会与栈帧完整性签名匹配,且由于地址范围0xE0000000到0xFFFFFFFF不可执行,它也不能用作程序地址,因此伪造的EXC_RETURN或FNC_RETURN操作总会导致故障异常。这种技术称为栈密封。安全特权软件应在设置CONTROL_S.SPSEL位之前密封安全进程栈,并在切换到线程级别时密封安全主栈。

操作步骤如下:
1. 估计主栈所需的空间。
2. 设置合适的栈限制。
3. 避免使用过多的异常优先级级别。
4. 在栈上方预留两个字的内存,并设置特殊值0xFEF5EDA5。
5. 在合适的时机进行栈密封操作。

5. 其他关键要点

5.1 检查所有安全入口点

由于项目间代码复制粘贴较为常见,一个项目中声明为安全入口点的函数在另一个项目中可能并非安全入口点。因此,重要的是审查函数原型声明,确保安全入口属性不会被错误复制。

5.2 限制安全库到无特权执行级别

如果安全软件库需要限制在无特权级别,则需要进行以下设置:
- (a) 设置安全CONTROL寄存器(CONTROL_S),使安全线程无特权,并在处于线程模式时使用PSP_S作为栈指针。
- (b) 设置安全MPU,使安全无特权软件无法访问特权内存。
- (c) 如果使用基于Armv8.1 - M的处理器(如Cortex - M55处理器),应将库代码的安全MPU区域配置为具有PXN MPU属性,以确保该区域的代码只能在无特权状态下执行。在这种情况下,库代码区域可以有自己的非安全可调用(NSC)区域。或者,可以实施系统级保护措施来达到类似的效果。如果使用的是Armv8.0 - M处理器,且没有部署系统级机制来防止库在特权状态下执行,则库代码内存区域不能配置为具有NSC属性。

操作步骤如下:
1. 配置CONTROL_S寄存器。
2. 设置安全MPU的访问权限。
3. 根据处理器类型配置库代码的MPU属性。

5.3 安全函数的可重入性

一些安全API可能不支持可重入性,如果是这种情况,需要采取额外措施来防止这些安全API函数发生重入。安全API的重入发生在以下序列中:
1. 非安全软件调用安全API。
2. 处理器接收非安全中断。
3. 非安全软件再次调用相同的安全API。

如果安全API需要在Handler模式下执行,应设计为支持可重入性。如果安全API无法处理重入,应使用Thread模式在无特权状态下执行。在这种情况下,可采取以下措施来防止软件问题:
- 当使用Armv8.0 - M处理器时,安全API在执行API函数代码之前,应执行额外的软件步骤来检测之前的API调用是否仍在进行。一种方法是在软件中实现一个简单的 “API忙” 标志,但要确保检查和设置标志的序列是线程安全的步骤,可使用LDREX/STREX指令实现。
- 当使用Armv8.1 - M处理器(如Cortex - M55)时,应将CCR_S.TRD位设置为1,以便在发生重入时进行检测并触发故障异常(注意:CCR_S.TRD位仅用于保护线程模式API)。

5.4 安全处理程序切换到无特权执行时密封安全主栈

一些安全中断处理程序可能需要在无特权级别执行。在这种情况下,安全中断应首先在安全固件中执行一个过程,将处理器切换到无特权级别,然后执行安全中断处理程序。切换过程涉及使用SVC异常,SVC的异常返回使用伪造的栈帧将处理器切换到无特权状态。除了在进程栈上创建伪造的栈帧外,SVC处理程序还应在执行启动无特权处理程序代码的异常返回之前密封主栈。当安全特权软件使用异常返回创建新的无特权进程时,也需要密封安全主栈。

操作步骤如下:
1. 确定安全API是否支持可重入性。
2. 根据处理器类型采取相应的防止重入措施。
3. 在需要切换到无特权执行的安全中断处理程序中,执行切换过程并密封主栈。

5.5 不使用安全世界时的措施

当AIRCR.BFHFNMINS设置为1时,必须采取以下措施来防止非安全软件重新进入安全状态:
- 禁用所有NSC区域属性。
- 禁用安全中断。
- 密封安全栈。

5.6 PSA认证

在安全固件开发中,除了审查代码以确保代码质量外,还应考虑PSA(平台安全架构)认证。“PSA认证” 是物联网时代的一种安全评估方案,为安全产品提供行业范围内的信任,并为物联网安全提供更高质量的定义。

综上所述,安全软件开发涉及多个方面的技术和实践,包括消息处理、指针检查、外设驱动、参数传递、栈管理等。通过遵循这些建议和最佳实践,可以提高软件的安全性和可靠性。在实际开发中,应根据具体的应用场景和需求,合理选择和应用这些技术,确保系统的安全稳定运行。同时,关注PSA认证等行业标准,有助于提升产品的市场竞争力。

安全软件开发的关键技术与实践指南

6. 技术要点总结与对比

为了更清晰地理解和应用上述安全软件开发的各项技术,下面通过表格对关键技术要点进行总结和对比:
| 技术要点 | 具体内容 | 操作步骤 | 优势 | 注意事项 |
| — | — | — | — | — |
| 消息打印与安全API | 使用 “puts” 等函数,添加自定义安全API处理整数等打印,调用 “puts” 前复制字符串到安全内存 | 1. 检查字符串长度范围;2. 检查源地址访问权限;3. 复制字符串到缓冲区;4. 检查字符串长度;5. 调用 “puts” 打印 | 确保字符串安全打印,避免非安全异常处理程序更改字符串带来的风险 | 注意复制字符串时的内存操作 |
| CMSE指针检查函数 | 安全API处理非安全数据时,用CMSE指针检查函数检测访问权限,考虑MPU权限级别,有不同标志组合使用 | 根据不同场景选择合适标志,处理函数指针有两种方法 | 防止非安全无特权软件攻击非安全特权软件,确保函数指针安全使用 | 不同处理器处理函数指针重入时方法不同 |
| 外设驱动 - 寄存器定义 | 定义数据结构表示外设寄存器,分别定义安全和非安全指针 | 1. 定义寄存器结构体;2. 定义安全和非安全基地址;3. 定义安全和非安全外设指针 | 方便管理外设,区分安全和非安全访问 | 注意基地址的正确定义 |
| 外设驱动 - 驱动代码 | 外设指针作为驱动函数参数,根据外设安全设置选择指针,可使用CMSE预定义宏有条件编译 | 1. 定义驱动函数;2. 设置外设控制寄存器;3. 选择合适外设指针;4. 判断外设安全设置;5. 有条件编译 | 同一函数可用于多个外设实例,代码可同时供安全和非安全开发者使用 | 注意指针选择和条件编译的逻辑 |
| 外设驱动 - 外设中断 | 通常非安全软件不生成安全异常,特定情况除外 | 配置外设为非安全时,用NVIC_ITNS寄存器配置其中断为非安全 | 避免安全攻击,特定情况允许通信和故障处理 | 了解可接受非安全软件生成安全异常的情况 |
| 参数传递 | 不确定工具链支持时,安全API参数仅用寄存器传递,大量参数可定义数据结构传递 | 确定参数传递方式,定义数据结构 | 提高性能,简化参数传递 | 确保工具链对参数传递方式的支持 |
| 定义XN属性区域 | 用MPU为安全SRAM中栈、堆和数据区域设置XN属性 | 设置MPU属性 | 降低代码注入攻击风险 | 注意XN属性的正确设置 |
| 主栈栈限制 | 利用栈限制检查功能,估计栈空间,避免过多异常优先级 | 1. 估计栈空间;2. 设置栈限制;3. 避免过多优先级 | 降低栈溢出风险 | 准确估计栈空间,避免影响故障处理 |
| 防止栈下溢攻击 | 预留栈内存,放置特殊值,进行栈密封 | 1. 预留内存;2. 设置特殊值;3. 进行栈密封 | 检测和阻止伪造操作导致的栈下溢攻击 | 注意特殊值的选择和栈密封时机 |
| 检查安全入口点 | 审查函数原型声明,确保安全入口属性不被错误复制 | 审查声明 | 避免安全入口点错误 | 仔细审查代码 |
| 限制安全库到无特权级别 | 设置CONTROL_S寄存器、安全MPU,根据处理器类型配置库代码MPU属性 | 1. 配置寄存器;2. 设置MPU权限;3. 配置库代码属性 | 确保安全库在无特权级别执行 | 不同处理器配置方式不同 |
| 安全函数可重入性 | 根据处理器类型采取措施防止安全API重入 | 1. 确定是否支持重入;2. 根据处理器类型处理 | 避免重入导致的软件问题 | 不同处理器处理方式不同 |
| 安全处理程序密封主栈 | 安全中断处理程序切换到无特权执行时,执行切换过程并密封主栈 | 1. 执行切换过程;2. 密封主栈 | 确保无特权执行时的安全 | 注意切换过程和密封时机 |
| 不使用安全世界时措施 | 禁用NSC区域属性、安全中断,密封安全栈 | 执行相应禁用和密封操作 | 防止非安全软件重新进入安全状态 | 确保各项操作执行到位 |
| PSA认证 | 安全固件开发考虑PSA认证 | 参与认证流程 | 提升产品安全性和市场竞争力 | 了解PSA认证要求 |

7. 安全软件开发流程示例

下面通过一个mermaid格式的流程图来展示一个简单的安全软件开发流程:

graph TD;
    A[需求分析] --> B[设计安全架构];
    B --> C[定义安全API和外设驱动];
    C --> D[实现代码,包括消息处理、指针检查等];
    D --> E[设置栈、堆和数据区域属性];
    E --> F[进行参数传递和函数可重入性处理];
    F --> G[配置外设中断和安全入口点];
    G --> H[代码审查和测试];
    H --> I[考虑PSA认证];
    I --> J[部署和维护];
8. 总结与展望

安全软件开发是一个复杂且关键的领域,涉及众多技术细节和实践要点。从消息处理到外设驱动,从参数传递到栈管理,每一个环节都对软件的安全性和可靠性有着重要影响。通过本文介绍的各项技术和实践建议,开发者可以构建更加安全稳定的软件系统。

在未来的安全软件开发中,随着物联网、人工智能等技术的不断发展,安全需求将更加多样化和复杂化。一方面,需要不断优化现有的安全技术,如进一步提高指针检查的准确性和效率,完善栈管理机制以应对更复杂的攻击场景。另一方面,要积极关注行业标准和认证,如PSA认证,不断提升产品在市场中的竞争力。同时,随着处理器技术的不断演进,如更新的架构和指令集的出现,安全软件开发也需要不断适应和创新,以保障系统的安全稳定运行。

总之,安全软件开发是一个持续发展的过程,开发者需要不断学习和实践,紧跟技术发展的步伐,才能在日益复杂的安全环境中开发出高质量的安全软件。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值