跳转至

CERT C安全编码规范

学习目标

完成本模块后,你将能够: - 理解CERT C规范的目的和安全重点 - 掌握常见的C语言安全漏洞及防范方法 - 应用CERT C规则编写安全的医疗器械软件 - 识别和修复代码中的安全问题 - 结合MISRA C和CERT C提高代码质量

前置知识

  • C语言编程基础
  • 软件安全基础知识
  • 常见漏洞类型(缓冲区溢出、整数溢出等)
  • MISRA C编码规范(推荐)

内容

CERT C概述

CERT C安全编码标准由卡内基梅隆大学软件工程研究所(SEI)开发,专注于C语言的安全编程实践。与MISRA C侧重可靠性不同,CERT C更关注安全性。

CERT C的目标: 1. 防止安全漏洞:避免常见的安全缺陷 2. 提高安全意识:教育开发人员安全编程 3. 可验证性:提供可检查的安全规则 4. 实用性:提供实际可行的解决方案

CERT C与MISRA C的关系: - MISRA C:侧重可靠性和可维护性 - CERT C:侧重安全性和漏洞防范 - 两者互补,建议同时应用

规则分类

CERT C规则按主题分为以下类别:

  1. PRE - 预处理器
  2. DCL - 声明和初始化
  3. EXP - 表达式
  4. INT - 整数
  5. FLP - 浮点数
  6. ARR - 数组
  7. STR - 字符串
  8. MEM - 内存管理
  9. FIO - 文件I/O
  10. ENV - 环境
  11. SIG - 信号
  12. ERR - 错误处理
  13. API - API设计
  14. CON - 并发
  15. MSC - 杂项

关键安全规则

INT30-C:确保无符号整数运算不会回绕

// 违反规则:无符号整数溢出
void bad_example(void) {
    unsigned int ui1 = UINT_MAX;
    unsigned int ui2 = 1;
    unsigned int sum = ui1 + ui2;  // 回绕到0
}

// 符合规则:检查溢出
#include <limits.h>
bool safe_add(unsigned int a, unsigned int b, unsigned int* result) {
    if (a > UINT_MAX - b) {
        return false;  // 溢出
    }
    *result = a + b;
    return true;
}

void good_example(void) {
    unsigned int ui1 = UINT_MAX;
    unsigned int ui2 = 1;
    unsigned int sum;

    if (safe_add(ui1, ui2, &sum)) {
        // 使用sum
    } else {
        // 处理溢出
    }
}

INT32-C:确保有符号整数运算不会导致溢出

// 违反规则:有符号整数溢出(未定义行为)
void bad_example(void) {
    int si1 = INT_MAX;
    int si2 = 1;
    int sum = si1 + si2;  // 未定义行为
}

// 符合规则:检查溢出
#include <limits.h>
bool safe_signed_add(int a, int b, int* result) {
    if (((b > 0) && (a > INT_MAX - b)) ||
        ((b < 0) && (a < INT_MIN - b))) {
        return false;  // 溢出
    }
    *result = a + b;
    return true;
}

ARR30-C:不要形成或使用越界指针或数组下标

// 违反规则:数组越界
void bad_example(void) {
    int array[10];
    int* ptr = array + 11;  // 越界指针
    *ptr = 42;  // 未定义行为
}

// 符合规则:边界检查
void good_example(void) {
    int array[10];
    size_t index = 5;

    if (index < sizeof(array) / sizeof(array[0])) {
        array[index] = 42;  // 安全访问
    }
}

ARR38-C:保证库函数不会形成无效指针

// 违反规则:传递无效指针给库函数
void bad_example(void) {
    char src[] = "Hello";
    char dest[3];  // 太小
    strcpy(dest, src);  // 缓冲区溢出
}

// 符合规则:使用安全函数
void good_example(void) {
    char src[] = "Hello";
    char dest[10];

    strncpy(dest, src, sizeof(dest) - 1);
    dest[sizeof(dest) - 1] = '\0';  // 确保null终止
}

// 更好的方案:使用安全函数(C11)
#ifdef __STDC_LIB_EXT1__
void better_example(void) {
    char src[] = "Hello";
    char dest[10];

    strcpy_s(dest, sizeof(dest), src);
}
#endif

STR31-C:保证存储字符串的空间足够大

// 违反规则:缓冲区太小
void bad_example(void) {
    char buffer[5];
    sprintf(buffer, "Hello, World!");  // 缓冲区溢出
}

// 符合规则:使用安全函数
void good_example(void) {
    char buffer[20];
    snprintf(buffer, sizeof(buffer), "Hello, World!");
}

MEM30-C:不要访问已释放的内存

// 违反规则:使用已释放的内存
void bad_example(void) {
    int* ptr = malloc(sizeof(int));
    *ptr = 42;
    free(ptr);
    *ptr = 100;  // 使用已释放的内存
}

// 符合规则:释放后置NULL
void good_example(void) {
    int* ptr = malloc(sizeof(int));
    if (ptr != NULL) {
        *ptr = 42;
        free(ptr);
        ptr = NULL;  // 防止悬空指针
    }
}

MEM34-C:只释放动态分配的内存

// 违反规则:释放栈内存
void bad_example(void) {
    int stack_var = 42;
    int* ptr = &stack_var;
    free(ptr);  // 错误:释放栈内存
}

// 符合规则:只释放堆内存
void good_example(void) {
    int* ptr = malloc(sizeof(int));
    if (ptr != NULL) {
        *ptr = 42;
        free(ptr);  // 正确:释放堆内存
    }
}

FIO30-C:排除文件名中的用户输入

// 违反规则:直接使用用户输入作为文件名
void bad_example(const char* user_input) {
    FILE* file = fopen(user_input, "r");  // 路径遍历漏洞
    // ...
}

// 符合规则:验证和清理文件名
#include <string.h>
#include <ctype.h>

bool is_safe_filename(const char* filename) {
    // 检查路径遍历字符
    if (strstr(filename, "..") != NULL) {
        return false;
    }
    if (strchr(filename, '/') != NULL || strchr(filename, '\\') != NULL) {
        return false;
    }

    // 只允许字母数字和特定字符
    for (const char* p = filename; *p != '\0'; p++) {
        if (!isalnum(*p) && *p != '_' && *p != '-' && *p != '.') {
            return false;
        }
    }

    return true;
}

void good_example(const char* user_input) {
    if (is_safe_filename(user_input)) {
        char safe_path[256];
        snprintf(safe_path, sizeof(safe_path), "/safe/directory/%s", user_input);
        FILE* file = fopen(safe_path, "r");
        // ...
    }
}

ERR33-C:检测并处理标准库错误

// 违反规则:忽略错误返回值
void bad_example(void) {
    FILE* file = fopen("data.txt", "r");
    fread(buffer, 1, 100, file);  // 未检查fopen返回值
    fclose(file);
}

// 符合规则:检查所有错误
void good_example(void) {
    FILE* file = fopen("data.txt", "r");
    if (file == NULL) {
        // 处理文件打开错误
        perror("Failed to open file");
        return;
    }

    size_t bytes_read = fread(buffer, 1, 100, file);
    if (bytes_read < 100) {
        if (feof(file)) {
            // 到达文件末尾
        } else if (ferror(file)) {
            // 读取错误
            perror("Failed to read file");
        }
    }

    if (fclose(file) != 0) {
        perror("Failed to close file");
    }
}

安全编码模式

1. 输入验证

// 输入验证模板
typedef enum {
    VALIDATION_OK,
    VALIDATION_ERROR_NULL,
    VALIDATION_ERROR_RANGE,
    VALIDATION_ERROR_FORMAT
} ValidationResult_t;

ValidationResult_t validate_input(const char* input, size_t max_length) {
    // 空指针检查
    if (input == NULL) {
        return VALIDATION_ERROR_NULL;
    }

    // 长度检查
    size_t length = strnlen(input, max_length + 1);
    if (length > max_length) {
        return VALIDATION_ERROR_RANGE;
    }

    // 格式检查
    for (size_t i = 0; i < length; i++) {
        if (!isprint(input[i])) {
            return VALIDATION_ERROR_FORMAT;
        }
    }

    return VALIDATION_OK;
}

2. 安全的字符串操作

// 安全的字符串复制
int safe_string_copy(char* dest, size_t dest_size, const char* src) {
    if (dest == NULL || src == NULL || dest_size == 0) {
        return -1;
    }

    size_t src_len = strlen(src);
    if (src_len >= dest_size) {
        return -1;  // 目标缓冲区太小
    }

    memcpy(dest, src, src_len + 1);  // 包括null终止符
    return 0;
}

// 安全的字符串连接
int safe_string_concat(char* dest, size_t dest_size, const char* src) {
    if (dest == NULL || src == NULL || dest_size == 0) {
        return -1;
    }

    size_t dest_len = strnlen(dest, dest_size);
    size_t src_len = strlen(src);

    if (dest_len + src_len >= dest_size) {
        return -1;  // 缓冲区太小
    }

    memcpy(dest + dest_len, src, src_len + 1);
    return 0;
}

3. 安全的整数运算

// 安全的整数运算库
typedef struct {
    bool overflow;
    int result;
} SafeIntResult_t;

SafeIntResult_t safe_int_add(int a, int b) {
    SafeIntResult_t result = {false, 0};

    if (((b > 0) && (a > INT_MAX - b)) ||
        ((b < 0) && (a < INT_MIN - b))) {
        result.overflow = true;
    } else {
        result.result = a + b;
    }

    return result;
}

SafeIntResult_t safe_int_multiply(int a, int b) {
    SafeIntResult_t result = {false, 0};

    if (a > 0) {
        if (b > 0) {
            if (a > INT_MAX / b) {
                result.overflow = true;
            }
        } else {
            if (b < INT_MIN / a) {
                result.overflow = true;
            }
        }
    } else {
        if (b > 0) {
            if (a < INT_MIN / b) {
                result.overflow = true;
            }
        } else {
            if ((a != 0) && (b < INT_MAX / a)) {
                result.overflow = true;
            }
        }
    }

    if (!result.overflow) {
        result.result = a * b;
    }

    return result;
}

医疗器械软件中的应用

安全威胁模型

医疗器械软件面临的安全威胁:

  1. 数据完整性:患者数据被篡改
  2. 可用性:设备被拒绝服务攻击
  3. 机密性:患者隐私泄露
  4. 认证:未授权访问设备功能

CERT C在IEC 62304中的应用

  • 5.1.1 软件开发计划:包含安全编码标准
  • 5.5.3 软件单元验证:包含安全代码审查
  • 7.1 软件维护计划:包含安全补丁流程
  • 9 软件风险管理:识别和缓解安全风险

与IEC 81001-5-1的关系

IEC 81001-5-1(医疗器械网络安全)要求: - 安全编码实践 - 漏洞管理 - 安全测试

CERT C帮助满足这些要求。

最佳实践

CERT C应用最佳实践

  1. 结合MISRA C使用:CERT C关注安全,MISRA C关注可靠性
  2. 优先处理高风险规则:关注缓冲区溢出、整数溢出等
  3. 使用安全函数:优先使用C11 Annex K安全函数
  4. 输入验证:验证所有外部输入
  5. 错误处理:检查所有函数返回值
  6. 代码审查:定期进行安全代码审查
  7. 静态分析:使用工具检测安全问题
  8. 安全培训:培训团队安全编码实践

常见陷阱

注意事项

  1. 忽略整数溢出:整数溢出可导致严重安全问题
  2. 不安全的字符串函数:strcpy、sprintf等不检查边界
  3. 未检查返回值:忽略错误可能导致安全漏洞
  4. 悬空指针:使用已释放的内存
  5. 缓冲区溢出:数组越界访问
  6. 格式字符串漏洞:用户输入作为格式字符串
  7. 路径遍历:未验证文件路径
  8. 竞态条件:多线程访问共享资源

实践练习

  1. 漏洞识别练习:
  2. 识别代码中的安全漏洞
  3. 分类漏洞类型(缓冲区溢出、整数溢出等)

  4. 安全修复练习:

  5. 修复不安全的代码
  6. 应用CERT C规则

  7. 安全代码审查:

  8. 使用CERT C检查清单审查代码
  9. 编写审查报告

  10. 威胁建模练习:

  11. 为医疗设备识别安全威胁
  12. 应用CERT C规则缓解威胁

自测问题

CERT C和MISRA C有什么区别?应该如何选择?

两个标准有不同的侧重点。

答案

CERT C: - 侧重安全性 - 防止安全漏洞 - 关注缓冲区溢出、整数溢出等安全问题 - 适用于面临安全威胁的系统

MISRA C: - 侧重可靠性和可维护性 - 防止未定义行为 - 关注代码质量和一致性 - 适用于安全关键系统

选择建议: - 医疗器械软件:建议同时应用两者 - Class C设备:必须应用两者 - 联网设备:必须应用CERT C - 两者互补,不冲突

为什么整数溢出是安全问题?

整数溢出可能导致严重的安全漏洞。

答案

整数溢出的安全影响:

  1. 缓冲区分配错误:

    size_t size = user_input * sizeof(int);  // 可能溢出
    int* buffer = malloc(size);  // 分配的内存太小
    

  2. 数组越界:

    unsigned int index = user_input + offset;  // 可能回绕
    array[index] = value;  // 越界访问
    

  3. 逻辑错误:

    if (a + b > MAX) {  // 如果a+b溢出,检查失败
        // 错误处理
    }
    

防范措施: - 使用安全的整数运算函数 - 在运算前检查溢出 - 使用更大的数据类型 - 使用编译器内置函数(如__builtin_add_overflow)

如何安全地处理用户输入?

用户输入是主要的攻击向量。

答案

安全处理用户输入的步骤:

  1. 验证输入:
  2. 检查空指针
  3. 检查长度
  4. 检查格式
  5. 检查范围

  6. 清理输入:

  7. 移除危险字符
  8. 转义特殊字符
  9. 规范化路径

  10. 限制输入:

  11. 设置最大长度
  12. 使用白名单而非黑名单
  13. 限制允许的字符集

  14. 安全使用:

  15. 不要直接用于文件路径
  16. 不要直接用于SQL查询
  17. 不要直接用于系统命令
  18. 不要直接用于格式字符串

原则:永远不要信任用户输入

什么是格式字符串漏洞?如何防范?

格式字符串漏洞是严重的安全问题。

答案

格式字符串漏洞:

// 漏洞代码
char user_input[100];
gets(user_input);
printf(user_input);  // 危险!

攻击示例: - 用户输入"%s%s%s"可能导致崩溃 - 用户输入"%n"可能写入内存 - 用户输入"%x"可能泄露栈内容

防范方法:

// 正确做法
printf("%s", user_input);  // 使用格式字符串

// 或使用fputs
fputs(user_input, stdout);

规则:永远不要将用户输入直接用作格式字符串

在医疗器械软件中,哪些CERT C规则最重要?

不同规则的优先级不同。

答案

医疗器械软件的高优先级CERT C规则:

  1. INT30-C, INT32-C:整数溢出
  2. 影响剂量计算、时间计算等

  3. ARR30-C, ARR38-C:数组越界

  4. 影响数据完整性和系统稳定性

  5. STR31-C:字符串缓冲区

  6. 防止缓冲区溢出

  7. MEM30-C, MEM34-C:内存管理

  8. 防止内存泄漏和悬空指针

  9. FIO30-C:文件操作

  10. 防止路径遍历攻击

  11. ERR33-C:错误处理

  12. 确保错误被正确处理

  13. API00-C:API设计

  14. 确保接口安全

优先级基于对患者安全的潜在影响。

相关资源

参考文献

  1. SEI CERT C Coding Standard, Software Engineering Institute, Carnegie Mellon University, 2016
  2. Seacord, Robert C. "Secure Coding in C and C++, 2nd Edition." Addison-Wesley, 2013
  3. Seacord, Robert C. "The CERT C Secure Coding Standard." Addison-Wesley, 2008
  4. IEC 62304:2006+AMD1:2015 - Medical device software - Software life cycle processes
  5. IEC 81001-5-1:2021 - Health software and health IT systems safety, effectiveness and security - Part 5-1: Security
  6. FDA Guidance - Content of Premarket Submissions for Management of Cybersecurity in Medical Devices, 2014
  7. 《C和C++安全编码》,Robert C. Seacord,机械工业出版社,2014

💬 讨论区

欢迎在这里分享您的想法、提出问题或参与讨论。需要 GitHub 账号登录。