4、危险的字符串函数:strcpy、strcat、sprintf、gets等函数的安全隐患
说实话,C语言里这几个字符串函数,是我在代码审查中见到最多的"定时炸弹"。每次看到新人用它们,我都会心头一紧。今天咱们就来聊聊,为什么这些函数被业界"拉黑",以及该用什么来替代。
4.1 这些函数到底危险在哪?
先看一个我亲身经历的案例。几年前我接手一个嵌入式项目,前任工程师用 strcpy 处理用户输入的用户名。结果呢?测试时只要输入超过16个字符,系统就崩溃。后来发现,缓冲区只有16字节,而 strcpy 根本不管这些,它只管往目标地址写,直到遇到 '\0' 为止。
这就是核心问题——不检查目标缓冲区大小。说白了,这些函数就像没有刹车的跑车,跑得快,但停不下来。
4.1.1 strcpy 的问题
char buf[10];
strcpy(buf, "这是一个非常长的字符串,远超10个字节"); // 缓冲区溢出!
这段代码会直接覆盖 buf 后面的内存。如果后面恰好是返回地址或函数指针,恭喜你,你给攻击者留了一扇门。
4.1.2 strcat 的问题
strcat 更坑。它不光不检查目标大小,还会先找到目标字符串的末尾,然后从那里开始拼接。如果目标缓冲区已经满了,它照样往上写。
char path[32] = "/home/user/";
strcat(path, "一个非常长的文件名,远超剩余空间"); // 溢出!
我在项目中遇到过,有人用 strcat 拼接路径,结果路径一长,直接覆盖了相邻的变量。调试了整整两天才找到原因。
4.1.3 sprintf 的问题
sprintf 是格式化输出的"重灾区"。它把格式化的结果写入缓冲区,但同样不检查长度。
char buf[32];
sprintf(buf, "用户ID: %d, 用户名: %s", id, username); // 如果username很长,就炸了
你想想看,如果 username 是用户输入的,攻击者完全可以构造一个超长字符串,触发缓冲区溢出。
4.1.4 gets 的问题——最危险,没有之一
gets 函数在 C11 标准中已经被正式移除。为什么?因为它从标准输入读取一行,完全不限制长度。
char buf[100];
gets(buf); // 用户输入1000个字符?没问题,全给你写进去
嗯,这里要注意:gets 连目标缓冲区大小这个参数都没有,它根本不知道往哪写、写多少。这已经不是"不安全"了,这是"故意留后门"。
4.2 为什么不推荐使用?
总结一下,核心原因就三点:
- 缓冲区溢出:写入超过目标缓冲区大小的数据,破坏相邻内存
- 无法截断:即使知道数据太长,这些函数也不会帮你截断,而是继续写
- 安全漏洞入口:缓冲区溢出是栈溢出攻击、ROP攻击等高级攻击的基础
4.3 替代方案:安全版本的字符串函数
好消息是,C标准库提供了安全版本的替代函数。我个人习惯,在新项目中直接禁用 strcpy、strcat、sprintf、gets,强制使用安全版本。
4.3.1 strncpy 替代 strcpy
char dst[16];
strncpy(dst, src, sizeof(dst) - 1);
dst[sizeof(dst) - 1] = '\0'; // 手动确保以'\0'结尾
注意:strncpy 不会自动添加 '\0',如果源字符串长度 >= 目标大小,目标字符串不会以 '\0' 结尾。所以一定要手动加。
#define SAFE_STRCPY(dst, src) do { \
strncpy(dst, src, sizeof(dst) - 1); \
dst[sizeof(dst) - 1] = '\0'; \
} while(0)
4.3.2 strncat 替代 strcat
char path[32] = "/home/user/";
strncat(path, filename, sizeof(path) - strlen(path) - 1);
strncat 会检查剩余空间,最多追加指定数量的字符。它比 strcat 安全得多。
4.3.3 snprintf 替代 sprintf
char buf[32];
snprintf(buf, sizeof(buf), "用户ID: %d, 用户名: %s", id, username);
snprintf 会保证写入不超过 sizeof(buf) 个字符,并且自动添加 '\0'。这是我最推荐的格式化输出方式。
4.3.4 fgets 替代 gets
char buf[100];
if (fgets(buf, sizeof(buf), stdin)) {
// 去掉末尾的换行符
buf[strcspn(buf, "\n")] = '\0';
}
fgets 会限制读取长度,不会溢出缓冲区。而且它还能处理文件流,比 gets 灵活得多。
4.4 知识体系图
下面这张图展示了本章的核心逻辑:危险函数与安全替代方案的对应关系。
4.5 避坑指南
我曾经在代码审查中看到过这样的"骚操作":
strncpy(dst, src, strlen(src)); // 这跟strcpy有什么区别?!
嗯,这完全失去了 strncpy 的意义。正确的做法是用 sizeof(dst) 作为长度参数。
还有一次,我看到有人用 snprintf 但传错了大小:
snprintf(buf, strlen(buf), "数据: %s", data); // 应该用sizeof(buf)!
这会导致 snprintf 认为缓冲区只有当前字符串长度那么大,照样溢出。
- 永远用
sizeof()获取缓冲区大小,不要硬编码 - 永远手动确保字符串以 '\0' 结尾(尤其用
strncpy时) - 永远不要用
gets,哪怕是在玩具代码里 - 格式化输出优先用
snprintf,而不是sprintf
说白了,安全编程就是养成好习惯。这些替代函数用起来也就多打几个字符,但能避免90%的缓冲区溢出问题。何乐而不为呢?
cppcheck、flawfinder),自动拦截违规代码。这样从流程上杜绝隐患。
公众号:蓝海资料掘金营,微信deep3321