第97章 安全编码:如何避免常见的安全漏洞?

说实话,做了这么多年嵌入式开发,我见过太多因为安全漏洞翻车的项目了。有的被黑客远程控制了设备,有的因为缓冲区溢出导致系统崩溃,还有的因为整数溢出让计费系统出了大问题。嗯,今天我们就来聊聊C语言中那些最容易踩的安全坑。

缓冲区溢出:老生常谈但依然致命

缓冲区溢出可以说是C语言最经典的安全问题。我在项目中遇到过一位同事,他用strcpy()复制用户输入的数据,结果输入长度超过了缓冲区大小,直接覆盖了返回地址。程序崩溃还算好的,更可怕的是攻击者可以精心构造输入数据,让程序跳转到恶意代码。

危险代码示例:
char buf[64];
strcpy(buf, user_input);  // 危险!没有长度检查

正确的做法是什么?我个人习惯用strncpy()或者snprintf(),并且一定要检查返回值。

安全代码示例:
char buf[64];
size_t len = strnlen(user_input, sizeof(buf));
if (len >= sizeof(buf)) {
    // 输入过长,记录日志并拒绝
    log_error("Input too long");
    return -1;
}
strncpy(buf, user_input, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';  // 确保以空字符结尾

整数溢出:看不见的陷阱

你想想看,一个int类型能表示的最大值是2147483647。如果在这个值上加1,会发生什么?会变成-2147483648。这就是整数溢出。

我曾经在做一个网络协议栈时,遇到过一个bug:计算数据包长度时,把两个16位整数相加,结果溢出了,导致分配了一个很小的缓冲区,但后续却往里面写入了大量数据。嗯,后果可想而知。

操作风险安全做法
加法结果可能超过最大值检查是否大于任一加数
乘法结果可能溢出使用更大类型或检查溢出
移位移位位数可能过大限制移位位数范围
安全整数运算示例:
int safe_add(int a, int b) {
    if (a > INT_MAX - b) {
        // 溢出,处理错误
        return -1;
    }
    return a + b;
}

格式化字符串漏洞:printf也能被攻击?

很多人觉得printf很安全,其实不然。如果你这样写:

printf(user_input);  // 危险!

攻击者可以在输入中包含%x%n等格式说明符,读取栈上的数据,甚至写入任意内存地址。我见过一个案例,攻击者通过这个漏洞读取了服务器的私钥。

正确的做法是:

printf("%s", user_input);  // 安全!指定格式字符串

空指针解引用:最基础的错误

这个错误太常见了。malloc返回NULL后直接使用,或者函数返回NULL指针没有检查。我记得有一次,一个同事在中断处理函数中调用了malloc,结果返回了NULL,但代码没有检查,直接解引用,系统直接挂掉。

避坑指南:

我曾经在代码审查中,要求所有malloc后面必须跟NULL检查。一开始大家觉得麻烦,但后来发现这个习惯至少避免了三次线上事故。

资源泄露:内存、文件描述符、锁

C语言没有垃圾回收,所有资源都得手动管理。我见过一个项目,每次请求都malloc一块内存,但只在正常路径下free了,异常路径下直接return了。运行了几天后,内存耗尽,系统重启。

我的习惯是:在函数开头就规划好所有资源的释放路径。用goto语句集中处理错误,虽然goto名声不好,但在资源管理上确实好用。

void process_data(void) {
    char *buf = malloc(1024);
    FILE *fp = fopen("data.txt", "r");
    if (!buf || !fp) {
        goto cleanup;
    }
    // 处理数据...
    
cleanup:
    if (buf) free(buf);
    if (fp) fclose(fp);
}

安全编码的核心逻辑

下面这张图总结了安全编码的核心思路:

安全编码核心原则 输入验证 长度检查、范围检查 内存安全 边界检查、释放管理 整数安全 溢出检查、类型转换 资源管理 RAII、错误路径清理 防御性编程 + 代码审查

实际项目中的安全编码清单

最后,我整理了一份安全编码检查清单,每次提交代码前对照检查一遍:

  1. 所有输入都经过验证:长度、类型、范围
  2. 字符串操作使用安全函数strncpysnprintfstrncat
  3. 整数运算检查溢出:特别是加法和乘法
  4. malloc/calloc后检查NULL:不要假设内存分配一定成功
  5. 所有资源都有对应的释放代码:内存、文件、锁、套接字
  6. 格式化字符串使用固定格式:不要直接把用户输入当格式串
  7. 数组访问检查边界:特别是循环中的索引
  8. 函数指针调用前检查是否为NULL

说实话,安全编码不是一朝一夕能练成的。我刚开始写C语言时也犯过很多低级错误。但只要你养成习惯,每次写代码时多问自己一句「这里会不会被攻击?」,慢慢地就会形成肌肉记忆。

记住,安全不是功能,而是一种思维方式。你写的每一行代码,都可能成为攻击者的突破口。嗯,今天就聊到这里,希望这些经验对你有帮助。


公众号:蓝海资料掘金营,微信deep3321