C/C++ 缓冲区溢出漏洞实战:3类高危函数(strcpy/gets/scanf)代码审计与修复

发布时间:2026/7/9 21:32:16
C/C++ 缓冲区溢出漏洞实战:3类高危函数(strcpy/gets/scanf)代码审计与修复 C/C 缓冲区溢出漏洞实战3类高危函数strcpy/gets/scanf代码审计与修复在C/C开发领域缓冲区溢出漏洞长期占据安全威胁榜首。这类漏洞不仅可能导致程序崩溃更可能被攻击者利用来执行任意代码。本文将聚焦三种最危险的标准库函数——strcpy、gets和scanf通过真实漏洞案例演示如何识别、利用和修复这类安全隐患。1. 缓冲区溢出漏洞基础与高危函数分类缓冲区溢出发生在程序向固定大小的内存区域写入超过其容量的数据时。多余的数据会溢出到相邻内存区域可能覆盖关键数据或改变程序执行流程。根据发生位置可分为栈溢出发生在函数调用栈上堆溢出发生在动态分配的内存区域数据段溢出发生在全局/静态变量存储区在C标准库中以下三类函数因缺乏边界检查而成为高危源函数类别典型代表危险原因字符串操作strcpy, strcat无目标缓冲区大小检查输入函数gets, scanf无法限制输入长度格式化输出sprintf, vsprintf可能产生超长输出// 典型危险代码示例 char buffer[16]; strcpy(buffer, user_input); // 无长度检查 gets(buffer); // 完全无法控制输入长度 scanf(%s, buffer); // 等同于gets的危险2. strcpy类函数漏洞深度解析strcpy及其衍生函数(strcat, strncpy等)是最常见的漏洞来源。即使相对安全的strncpy也常被误用。2.1 strcpy漏洞原理strcpy的工作方式是从源地址逐字节复制到目标地址直到遇到NULL终止符完全不考虑目标缓冲区大小void vulnerable_function(char* input) { char buffer[32]; strcpy(buffer, input); // 危险操作 }当input长度超过31字节(需保留NULL终止符)时将发生栈溢出可能覆盖函数局部变量保存的基址指针(EBP)返回地址(RET)2.2 实战漏洞利用示例考虑以下含有strcpy漏洞的程序#include string.h #include stdio.h void auth_check(char* password) { int auth_flag 0; char buffer[16]; strcpy(buffer, password); if(auth_flag) { printf(Access Granted!\n); } else { printf(Access Denied!\n); } } int main(int argc, char** argv) { if(argc 2) { printf(Usage: %s password\n, argv[0]); return 1; } auth_check(argv[1]); return 0; }通过精心构造输入可以绕过认证$ ./vuln_program $(python -c print A*20 \x01) Access Granted!这里我们通过溢出buffer覆盖了auth_flag的值。在x86小端系统中连续20个A填满buffer(16字节)和EBP(4字节)最后的\x01覆盖了auth_flag。2.3 安全修复方案不安全替代方案strncpy(buffer, input, sizeof(buffer)); // 可能缺少NULL终止符推荐安全方案使用带边界检查的函数snprintf(buffer, sizeof(buffer), %s, input);C11新增的边界检查函数strcpy_s(buffer, sizeof(buffer), input);自定义安全包装函数void safe_strcpy(char* dest, size_t dest_size, const char* src) { if(dest NULL || src NULL || dest_size 0) return; size_t i; for(i 0; i dest_size - 1 src[i] ! \0; i) { dest[i] src[i]; } dest[i] \0; }3. gets函数漏洞分析与防御gets函数从标准输入读取一行到缓冲区直到遇到换行符或EOF完全不检查缓冲区大小堪称最危险的标准库函数。3.1 gets漏洞原理分析char buffer[32]; gets(buffer); // 无论输入多长都会写入buffer攻击者可以构造超长输入覆盖栈上的关键数据包括返回地址。现代编译器中gets已被标记为废弃但仍有大量遗留代码在使用。3.2 实际漏洞案例#include stdio.h void vulnerable() { char buffer[64]; printf(Enter your name: ); gets(buffer); printf(Hello, %s!\n, buffer); } int main() { vulnerable(); return 0; }利用方式$ python -c print A*80 \xef\xbe\xad\xde | ./gets_vuln这将覆盖返回地址为0xdeadbeef(小端表示)导致程序跳转到该地址执行。3.3 安全替代方案绝对避免使用gets替代方案包括使用fgetsfgets(buffer, sizeof(buffer), stdin); // 去除可能的换行符 buffer[strcspn(buffer, \n)] \0;使用getlinePOSIX标准char* line NULL; size_t len 0; ssize_t read getline(line, len, stdin); // 使用后记得free(line)自定义安全输入函数int safe_input(char* buf, size_t size) { if(fgets(buf, size, stdin) NULL) return -1; size_t len strlen(buf); if(len 0 buf[len-1] \n) buf[len-1] \0; else while(getchar() ! \n); // 清除输入缓冲区 return 0; }4. scanf系列函数的安全隐患scanf家族函数(scanf, fscanf, sscanf等)在不当使用时同样危险特别是%s格式符。4.1 scanf漏洞模式危险用法char buffer[32]; scanf(%s, buffer); // 等同于gets的危险相对安全的用法应指定最大宽度scanf(%31s, buffer); // 限制读取长度但仍有以下问题无法处理包含空格的输入可能留下未读取的输入在缓冲区宽度指定容易与缓冲区大小不同步4.2 漏洞利用示例#include stdio.h int main() { char name[32]; printf(Enter your name: ); scanf(%s, name); printf(Hello, %s!\n, name); return 0; }输入超过31字节将导致缓冲区溢出。即使使用%31s如果后续修改缓冲区大小而忘记更新格式字符串仍会产生漏洞。4.3 安全使用建议始终指定宽度且比缓冲区小1(为NULL保留空间)char buffer[32]; scanf(%31s, buffer);更好的替代方案char buffer[32]; fgets(buffer, sizeof(buffer), stdin);使用sscanf进行安全解析char input[128]; char buffer[32]; fgets(input, sizeof(input), stdin); sscanf(input, %31s, buffer);自定义安全扫描函数int safe_scan(char* buf, size_t size) { char format[16]; snprintf(format, sizeof(format), %%%zus, size-1); return scanf(format, buf); }5. 综合代码审计检查清单在进行代码审计时可使用以下检查表识别缓冲区溢出风险5.1 危险函数检查表函数类别危险函数安全替代方案字符串操作strcpy, strcatstrncpy, strncat, snprintf输入函数gets, getwdfgets, getline格式化输入scanf, fscanf, sscanf带宽度限制的scanf, fgetssscanf格式化输出sprintf, vsprintfsnprintf, vsnprintf内存操作memcpy, memmove带长度检查的版本5.2 审计要点识别所有缓冲区操作查找数组定义和内存分配跟踪数据流经过的所有函数验证边界检查检查所有数组访问是否越界确认字符串操作有正确的NULL终止特别注意循环中的缓冲区操作指针算术运算用户可控的输入源5.3 自动化检测工具静态分析工具FlawfinderRATS (Rough Auditing Tool for Security)Clang Static Analyzer动态分析工具Valgrind (Memcheck)AddressSanitizer (ASan)Fuzzing工具(AFL, libFuzzer)编译器保护GCC/Clang的-fstack-protector-D_FORTIFY_SOURCE2-Wformat-security等警告选项6. 现代防护技术与最佳实践除修复具体漏洞外还应采用纵深防御策略6.1 编译时防护栈保护(Stack Canaries)gcc -fstack-protector-all -o program program.c在返回地址前插入随机值溢出时会破坏该值位置无关可执行文件(PIE)gcc -fPIE -pie -o program program.c使代码地址随机化增加攻击难度不可执行栈(NX)gcc -z noexecstack -o program program.c防止栈上执行代码6.2 运行时防护地址空间布局随机化(ASLR)echo 2 | sudo tee /proc/sys/kernel/randomize_va_space随机化内存布局增加预测难度增强的malloc实现glibc的malloc检查OpenBSD的malloc实现6.3 开发最佳实践安全编码原则最小权限原则防御性编程深度防御代码审查重点// 不良模式 void unsafe_copy(char* input) { char buf[256]; strcpy(buf, input); } // 安全模式 void safe_copy(char* input, size_t input_len) { char buf[256]; size_t copy_len input_len sizeof(buf) ? input_len : sizeof(buf)-1; memcpy(buf, input, copy_len); buf[copy_len] \0; }测试策略边界值测试模糊测试静态分析集成到CI/CD7. 从漏洞到加固完整案例研究让我们通过一个完整案例演示从漏洞发现到修复的全过程。7.1 原始漏洞代码#include stdio.h #include string.h void process_data(char* username, char* password) { char userbuf[32]; char passbuf[32]; int authenticated 0; strcpy(userbuf, username); strcpy(passbuf, password); if(strcmp(userbuf, admin) 0 strcmp(passbuf, S3cr3t!) 0) { authenticated 1; } if(authenticated) { printf(Access granted to %s\n, userbuf); } else { printf(Access denied for %s\n, userbuf); } } int main(int argc, char** argv) { if(argc ! 3) { printf(Usage: %s username password\n, argv[0]); return 1; } process_data(argv[1], argv[2]); return 0; }7.2 漏洞分析使用不安全的strcpy缓冲区大小固定(32字节)认证标志在缓冲区之后可能被覆盖无输入长度验证7.3 漏洞利用构造超长用户名可覆盖authenticated标志$ ./auth $(python -c print A*40) test Access granted to AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7.4 加固后的代码#include stdio.h #include string.h #include stdbool.h #define MAX_INPUT_LEN 31 // 32 - 1 for NULL bool validate_input(const char* input) { size_t len strlen(input); return len MAX_INPUT_LEN; } void process_data(const char* username, const char* password) { char userbuf[MAX_INPUT_LEN 1] {0}; char passbuf[MAX_INPUT_LEN 1] {0}; bool authenticated false; if(!validate_input(username) || !validate_input(password)) { printf(Input too long\n); return; } strncpy(userbuf, username, MAX_INPUT_LEN); strncpy(passbuf, password, MAX_INPUT_LEN); userbuf[MAX_INPUT_LEN] \0; passbuf[MAX_INPUT_LEN] \0; if(strcmp(userbuf, admin) 0 strcmp(passbuf, S3cr3t!) 0) { authenticated true; } if(authenticated) { printf(Access granted to %s\n, userbuf); } else { printf(Access denied for %s\n, userbuf); } } int main(int argc, char** argv) { if(argc ! 3) { printf(Usage: %s username password\n, argv[0]); return 1; } process_data(argv[1], argv[2]); return 0; }7.5 加固措施总结使用strncpy替代strcpy明确定义最大长度常量添加输入验证显式NULL终止使用bool类型提高可读性使用const修饰不可变参数8. 进阶话题与延伸防护对于安全性要求更高的场景可考虑以下进阶防护措施8.1 安全字符串库使用专门设计的安全字符串库替代标准函数Libsafe提供有边界检查的函数替代品OpenBSD strlcpy/strlcat设计更安全的字符串函数Microsoft Secure CRT如strcpy_s等安全版本8.2 静态分析集成将静态分析工具集成到开发流程中# 使用Clang静态分析器 scan-build make # 使用flawfinder flawfinder --quiet --column --dataonly src/8.3 动态防护技术控制流完整性(CFI)clang -flto -fvisibilityhidden -fsanitizecfi -o program program.c防止非预期的控制流转移影子栈(Shadow Stack) 维护返回地址的副本防止覆盖内存标记 使用硬件特性(如ARM的MTE)检测内存安全问题8.4 安全开发生命周期威胁建模早期识别潜在威胁安全设计采用最小权限等原则安全编码遵循安全编码规范安全测试渗透测试、模糊测试安全响应建立漏洞响应流程9. 历史漏洞案例分析学习历史漏洞有助于理解实际风险9.1 Morris蠕虫(1988)利用gets()和fingerd的缓冲区溢出感染了约10%的互联网主机直接导致CERT/CC的建立9.2 Code Red(2001)利用IIS的索引服务缓冲区溢出感染超过35万台服务器造成约26亿美元损失9.3 Slammer(2003)利用SQL Server的堆溢出漏洞15分钟内感染7.5万台主机导致韩国全网断网等严重后果9.4 Heartbleed(2014)OpenSSL缓冲区越界读取影响约17%的HTTPS服务器可泄露服务器内存中的敏感数据10. 总结与持续学习缓冲区溢出作为最古老的安全问题之一至今仍在真实系统中出现。防御需要多层次策略代码层面避免危险函数严格的边界检查使用安全替代方案编译层面启用所有安全选项静态分析集成系统层面启用ASLR、NX等防护最小权限原则流程层面安全代码审查自动化安全测试持续安全培训推荐学习资源CERT C安全编码标准OWASP安全编码指南Microsoft安全开发生命周期MITRE CWE列表(特别是CWE-120)