第27章:安全编码规范:MISRA C、CERT C编码标准核心规则,代码审查清单,安全编码流程
聊到C语言的安全编码,我不得不先提一个现实问题:为什么我们写了那么多年代码,漏洞还是层出不穷?
说白了,C语言给了程序员极大的自由——指针随便玩、内存随便操作、类型随便转换。这种自由,恰恰是安全问题的温床。我在嵌入式行业摸爬滚打十几年,见过太多因为“图省事”而埋下的雷。今天我们就来聊聊怎么用规范把这些雷一个个排掉。
27.1 MISRA C:汽车与嵌入式领域的“铁律”
MISRA C最早是为汽车行业设计的,但现在几乎成了嵌入式安全编码的通用标准。我个人习惯把它看作一套“约束清单”——它限制的不是你的能力,而是你的犯错空间。
27.1.1 核心规则速览
MISRA C:2012 有上百条规则,但真正高频踩坑的,其实就那么几条。我挑几个最关键的说说:
| 规则编号 | 核心要求 | 为什么重要 |
|---|---|---|
| Rule 1.1 | 所有代码必须符合C90/C99标准 | 避免编译器扩展带来的移植性问题 |
| Rule 8.2 | 函数必须有显式原型声明 | 防止隐式声明导致的类型不匹配 |
| Rule 10.1 | 禁止对布尔类型进行算术运算 | 逻辑与算术混用是经典bug来源 |
| Rule 11.3 | 指针类型转换必须显式且安全 | 隐式指针转换常导致内存越界 |
| Rule 17.2 | 禁止递归调用 | 递归在嵌入式环境中容易栈溢出 |
重点提醒:MISRA C不是“建议”,而是“必须”。在汽车功能安全标准ISO 26262中,违反MISRA规则直接关联到ASIL等级评定。我在做ADAS项目时,就因为一条指针转换规则没通过,整个模块被要求重写。
27.1.2 一个典型的MISRA违规案例
/* 违规写法:隐式指针转换 */
uint32_t *ptr;
void *p = &some_data;
ptr = p; /* MISRA Rule 11.3 违规 */
/* 合规写法:显式转换 */
uint32_t *ptr;
void *p = &some_data;
ptr = (uint32_t *)p; /* 显式转换,但需确保对齐 */
嗯,这里要注意:显式转换只是第一步。你还要保证目标类型的对齐要求。我曾经在一个ARM Cortex-M4的项目上,因为没检查对齐,直接导致总线错误——板子直接挂了。
27.2 CERT C:更贴近通用软件安全
如果说MISRA C是“嵌入式专用”,那CERT C就是“通用安全编码圣经”。它由卡内基梅隆大学的SEI实验室发布,覆盖了从内存管理到并发控制的方方面面。
27.2.1 CERT C的规则分类
CERT C把规则分成几个大类,我列一下最常出问题的:
- PRE(预处理):宏定义中的括号问题、头文件重复包含
- DEC(声明与初始化):未初始化变量、作用域混淆
- EXP(表达式):副作用顺序、未定义行为
- INT(整数):整数溢出、符号转换
- STR(字符串):缓冲区溢出、空终止符缺失
- MEM(内存管理):释放后使用、双重释放
- FIO(文件I/O):路径遍历、文件权限
我的经验:如果你时间有限,先啃INT和MEM这两类。我统计过自己经手的漏洞,大约60%都出在整数溢出和内存管理上。你想想看,这两个问题几乎贯穿所有C程序。
27.2.2 CERT C的“推荐”与“必须”
CERT C把规则分为“Rule”(必须遵守)和“Recommendation”(强烈建议)。举个例子:
/* Rule: 必须检查malloc返回值 */
int *p = (int *)malloc(sizeof(int) * 10);
if (p == NULL) {
/* 处理内存分配失败 */
return -1;
}
/* Recommendation: 使用size_t进行大小计算 */
size_t num_elements = 10;
int *p = (int *)malloc(sizeof(int) * num_elements);
为什么推荐用size_t?因为int可能是32位有符号,而size_t是无符号且与平台指针宽度一致。我在一个64位服务器项目上,就因为用了int来算大小,分配超过2GB内存时直接溢出——嗯,那是个线上事故,凌晨三点被叫起来修。
27.3 代码审查清单:我自己的“防呆”模板
光有标准还不够,你得有个可操作的清单。我这些年整理了一份审查清单,每次代码审查都按这个过一遍。分享给你:
27.3.1 内存安全审查项
- ☐ 所有
malloc/calloc/realloc返回值是否检查? - ☐ 每个
free后是否立即置NULL? - ☐ 数组下标是否在边界内?有没有可能被外部输入控制?
- ☐ 字符串操作是否使用
strncpy/snprintf等安全版本? - ☐ 结构体中的指针成员是否在拷贝时做了深拷贝?
27.3.2 整数安全审查项
- ☐ 整数运算前是否做了溢出检查?
- ☐ 有符号与无符号混用时是否显式转换?
- ☐ 移位操作是否检查了移位量?
- ☐ 枚举类型是否被当作整数直接运算?
27.3.3 控制流审查项
- ☐ switch语句是否有default分支?
- ☐ 所有if-else分支是否都覆盖了?
- ☐ 循环终止条件是否明确?会不会死循环?
- ☐ goto语句是否只用于错误处理?
注意:审查清单不是一次性写完就完事了。我每半年会更新一次,把新踩的坑加进去。比如最近我就加了一条:“检查所有回调函数指针是否被篡改”——这是从一次固件攻击事件中学到的。
27.4 安全编码流程:从需求到发布的闭环
标准有了,清单也有了,但怎么落地?我建议你建立这样一个流程:
这个流程看起来简单,但真正执行起来有几个关键点:
- 静态分析不能替代人工审查。工具只能发现模式化的问题,逻辑漏洞还得靠人眼。我见过一个项目,静态分析零告警,但审查时发现了一个条件竞争——工具根本扫不出来。
- 反馈闭环必须跑起来。每次线上事故或者漏洞发现,都要回溯到流程的哪个环节出了问题。是需求没识别威胁?还是审查清单漏了项?
- 工具链要统一。团队里有人用PC-lint,有人用Coverity,有人用Clang Static Analyzer——结果同一个问题,有人报有人不报。我建议统一工具,并且把规则集固定下来。
一个小技巧:在CI/CD流水线里加入安全编码检查。每次提交代码,自动跑MISRA和CERT C的规则检查。不过要注意,别把告警阈值设得太高,否则团队会麻木。我一般设成“error级别必须修复,warning级别必须人工确认”。
27.5 避坑指南:我踩过的那些坑
最后,分享几个真实案例。这些坑我踩过,希望你别再踩:
- 坑一:我曾经以为MISRA C只适用于汽车行业。直到一个医疗设备项目因为违反Rule 10.1(布尔运算)导致逻辑错误,差点造成临床事故。从那以后,我所有嵌入式项目都默认启用MISRA C。
- 坑二:CERT C的INT30-C规则说“确保无符号整数运算不环绕”。我一开始觉得“无符号环绕是定义行为,怕什么?”结果在一个网络协议栈里,无符号整数环绕导致序列号校验绕过——攻击者直接伪造了数据包。
- 坑三:代码审查清单列了50条,结果每次审查都流于形式。后来我改成“每次只重点查3条”,轮换着来。效果反而更好,因为审查者能集中注意力。
安全编码不是一朝一夕的事。它需要标准、工具、流程,更需要每个人的意识。你想想看,一个整数溢出可能只需要一行代码,但修复它可能花掉整个团队一周的时间。与其事后救火,不如事前防火——这就是安全编码规范的意义。
公众号:蓝海资料掘金营,微信deep3321