文章总结: 本文档系统梳理了C++安全编码中十大类共39个常见漏洞点,涵盖内存安全、整数溢出、并发问题及注入攻击等核心领域。针对每项漏洞,文章不仅解析了漏洞原理与危害,还提供了具体的风险代码示例、审计检查要点及修复方案。这是一份兼具理论性与实操性的代码审计清单,有助于开发人员和安全从业者快速识别并修复代码隐患,提升软件安全性。 综合评分: 88 文章分类: 代码审计,安全开发,漏洞分析
C++安全编码手册:十大类39个漏洞点,一篇搞定代码审计(建议收藏)
原创
ZKAFKA ZKAFKA
网络安全研究站
2026年3月10日 21:19 浙江
一、内存安全类
1
缓冲区溢出
向固定大小的缓冲区写入超出其容量的数据,覆盖相邻内存区域,可能导致程序崩溃或控制流劫持。常见于不安全的C风格字符串函数(strcpy、sprintf)或数组索引越界。
void vulnerable(const char* input) { char buf[10]; strcpy(buf, input); // 若input长度>9,则溢出}
-
检查要素:
-
搜索所有无边界检查的字符串/内存操作函数。
-
检查数组索引是否在合法范围内。
-
修复方法:
-
使用
strncpy、snprintf等并指定大小。 -
改用
std::string或std::vector<char>。
2
堆溢出
动态分配的内存(如 new/malloc)后,写入数据超出分配大小,破坏堆管理结构,可能导致任意代码执行。
char *p = new char[10];// 若user_input长度>10,则堆溢出memcpy(p, user_input, strlen(user_input));
-
检查要素:
-
审查动态内存分配的大小与实际写入长度是否匹配。
-
关注从外部输入获取长度的操作。
-
修复方法:
-
始终校验写入长度 ≤ 分配大小。
-
使用
std::vector并借助其push_back或resize安全操作。
3
释放后使用
内存释放后仍通过悬垂指针访问,导致未定义行为。攻击者可控制释放后内存的内容实现利用。
int *p = new int(5);delete p;*p = 10; // 访问已释放内存
-
检查要素:
-
检查
delete/free后指针是否立即置空。 -
审查对象的生命周期管理。
-
修复方法:
-
释放后立即将指针置
nullptr。 -
使用智能指针(
unique_ptr、shared_ptr)自动管理生命周期。
4
双重释放
对同一块内存多次调用 free 或 delete,破坏堆分配器的内部状态,可能引发程序崩溃或利用。
int *p = new int(5);delete p;delete p; // 第二次释放
-
检查要素:
-
查找同一指针被多次释放的代码路径。
-
特别关注异常处理或复杂控制流中的释放操作。
-
修复方法:
-
释放后立即置空指针。
-
使用智能指针避免手动管理。
5
内存泄漏
读取未初始化的变量,其值不确定,可能泄露敏感信息或导致不可预测行为。
void leak() { int *p = new int(100); // 忘记 delete p}
-
检查要素:
-
确保每个
new/malloc都有对应的delete/free。 -
检查异常路径是否可能导致跳过释放。
-
修复方法:
-
遵循RAII,使用容器或智能指针。
-
使用 Valgrind、AddressSanitizer 检测泄漏。
6
未初始化内存
动态分配的内存未被释放,导致进程内存占用持续增长,最终耗尽资源。
int x;if (x == 0) { ... } // x 未初始化
-
检查要素:
-
查找未赋值的局部变量。
-
检查类成员是否在构造函数中全部初始化。
-
修复方法:
-
始终初始化变量(声明时赋初值或构造函数初始化列表)。
-
使用静态分析工具(如 Clang Static Analyzer)。
二、整数安全问题
1
整数溢出/下溢
整数运算结果超出类型表示范围,导致回绕或未定义行为,常被用于绕过边界检查。
size_t len = GetUntrustedSize();char *buf = new char[len + 1]; // 若len为SIZE_MAX,则len+1溢出为0
-
检查要素:
-
审查作为内存大小、循环计数器的整数运算。
-
检查外部可控的整数值参与运算处。
-
修复方法:
-
进行溢出检查(如
if(a > SIZE_MAX - b))。 -
使用安全整数库(如 SafeInt、Boost.SafeInt)。
2
符号错误
将有符号整数与无符号整数混用,导致意外比较结果或负数作为数组下标引发越界。
int index = -1;unsigned int size = 10;if (index < size) { // 隐式转换,-1 变成很大的正数,条件成立 arr[index] = 0; // 越界写入}
-
检查要素:
-
查找有符号/无符号混合运算。
-
检查用作数组下标的变量是否可能为负。
-
修复方法:
-
避免混用,或显式转换前验证。
-
使用
ssize_t或ptrdiff_t等有符号类型表示大小/下标。
3
截断错误
将较大类型赋值给较小类型,导致高位数据丢失,可能引发逻辑错误或安全检查绕过。
unsigned long long big = 0x100000000;unsigned int small = big; // 截断为0
-
检查要素:
-
查找窄化转换(隐式或显式)。
-
关注赋值前是否进行范围检查。
-
修复方法:
-
使用
{}初始化防止隐式窄化(C++11)。 -
赋值前检查值是否在目标类型范围内。
三、字符串与格式化
1
不安全的字符串函数
使用 strcpy、strcat、sprintf 等缺乏边界检查的函数,即使改用 strncpy、snprintf,也可能因参数错误(如目标大小计算错误)导致截断或未终止。
char dst[10];strncpy(dst, src, strlen(src)); // 未指定目标大小,仍可能溢出
-
检查要素:
-
搜索所有字符串函数,检查目标缓冲区大小是否正确传递。
-
确认
strncpy后是否手动添加\0。 -
修复方法:
-
优先使用
std::string及其方法。 -
若必须用C函数,严格计算大小并确保终止。
2
格式化字符串漏洞
用户控制格式化字符串参数,通过 %x、%n 等读取或修改栈内存。
printf(user_input); // 用户输入包含 %n 可导致写入
-
检查要素:
-
确保所有格式化函数的格式串为常量字符串。
-
修复方法:
-
使用
printf("%s", user_input);形式。
四、并发与多线程
1
数据竞争
多个线程同时访问同一内存且至少一个为写操作,无同步机制,导致数据不一致。
int counter = 0;void thread() { for(int i=0;i<100000;++i) ++counter; }
-
检查要素:
-
识别所有共享变量,检查访问是否加锁或使用原子操作。
-
修复方法:
-
使用
std::mutex保护共享数据。 -
对简单变量用
std::atomic。
2
死锁
两个或多个线程互相等待对方释放资源,导致所有线程永久阻塞。
std::mutex m1, m2;void t1() { m1.lock(); m2.lock(); ... } // t1 先锁 m1 再 m2void t2() { m2.lock(); m1.lock(); ... } // t2 先锁 m2 再 m1 -> 可能死锁
-
检查要素:
-
审查锁的获取顺序是否一致。
-
检查是否存在嵌套锁且顺序不同。
-
修复方法:
-
固定锁获取顺序。
-
使用
std::lock一次性锁定多个互斥量。
3
锁滥用
锁的粒度过大导致性能下降和并发度降低,或粒度过小导致竞态条件未完全防护。
std::mutex m;void process() { m.lock(); // 大量耗时操作,但只需保护一小部分数据 m.unlock();}
-
检查要素:
-
分析锁保护的范围是否合理,有无不必要的长时间持有。
-
检查是否遗漏需要保护的操作。
-
修复方法:
-
缩小临界区,只保护必要操作。
-
使用读写锁(
std::shared_mutex)优化读多写少场景。
4
条件变量虚假唤醒
条件变量可能在没有被通知的情况下被唤醒(虚假唤醒),若未在循环中检查条件,会导致逻辑错误。
std::condition_variable cv;std::mutex m;bool ready = false;void wait() { std::unique_lock lk(m); if (!ready) cv.wait(lk); // 错误:应使用 while 循环 // 此时 ready 可能仍为 false}
-
检查要素:
-
检查
wait()是否在循环中(while(condition) wait())。 -
修复方法:
-
总是使用
while(condition) cv.wait(lk);或带谓词的wait重载。
五、异常安全
1
异常导致资源泄漏
异常抛出后,已获取的资源(内存、文件句柄、锁)未释放,造成泄漏。
void func() { FILE *f = fopen("file.txt", "r"); // 可能抛出异常的操作 fclose(f); // 若异常在之前抛出,则不会执行}
-
检查要素:
-
查找资源获取与释放之间是否有异常抛出点。
-
修复方法:
-
使用RAII包装资源(如
std::fstream、std::unique_ptr)。 -
在析构函数中释放资源。
2
状态不一致
异常发生在对象状态变更过程中,导致对象处于部分修改的无效状态。
class Data { int *p; int len;public: void resize(size_t n) { delete[] p; p = new int[n]; // 若此处 bad_alloc 抛出,则 p 指向已释放内存,len 未更新 len = n; }};
-
检查要素:
-
审查修改对象状态的操作序列是否可能抛出异常。
-
修复方法:
-
使用“提交或回滚”策略:先进行可能失败的操作,成功后再更新状态。
-
使用智能指针管理资源,简化异常安全。
六、输入验证与注入
1
命令注入
将用户输入拼接到系统命令中,攻击者可插入特殊字符执行任意命令。
system(std::string("ping " + user_input).c_str());
-
检查要素:
-
搜索
system、popen、exec族函数,检查命令字符串是否包含不可信输入。 -
修复方法:
-
避免调用外部命令,或使用参数化API(如
execve传参列表)。 -
对输入进行白名单过滤。
2
SQL注入
SQL语句拼接用户输入,改变查询语义。
sprintf(query, "SELECT * FROM users WHERE name='%s'", user_input);
-
检查要素:
-
查找SQL查询构建方式,确认是否使用参数化查询。
-
修复方法:
-
强制使用参数化查询(预处理语句)。
-
对输入进行转义(但不如参数化安全)。
3
跨站脚本(XSS)
C++后端输出HTML时,未对用户输入进行编码,导致恶意脚本注入浏览器。
std::cout << "<div>" << user_input << "</div>"; // 若输入包含 <script>
-
检查要素:
-
检查所有输出到HTML的变量是否经过HTML转义。
-
修复方法:
-
对输出进行HTML实体编码(如
&→&,<→<)。 -
使用模板引擎自动转义。
七、加密与随机数
1
使用弱加密算法
使用已被破解的算法(如DES、MD5)或自定义加密算法,可被攻击者快速破解。
#include <openssl/md5.h>MD5(data, len, hash); // MD5 已不安全
-
检查要素:
-
审查加密算法使用,识别弱算法。
-
修复方法:
-
使用现代强算法(AES-256、SHA-256、SHA-3)。
-
采用经过验证的加密库(OpenSSL、libsodium)。
2
硬编码密钥/凭证
密钥、密码直接写在代码中,易被逆向工程获取。
const char* api_key = "sk_live_1234567890abcdef";
-
检查要素:
-
搜索代码中可能包含密钥的字符串(如
key、secret、password)。 -
修复方法:
-
将凭证移出代码,存储于安全配置、环境变量或密钥管理服务。
3
不安全的随机数
使用可预测的随机数生成器(如 rand())生成安全敏感值。
srand(time(NULL));int token = rand(); // 可预测
-
检查要素:
-
搜索
rand、random等,判断使用场景是否涉及安全。 -
修复方法:
-
使用密码学安全随机数生成器(
/dev/urandom、RAND_bytes)。
4
证书验证不当
TLS/SSL通信时,未正确验证服务器证书,导致中间人攻击。
// 使用 libcurl 时设置 CURLOPT_SSL_VERIFYPEER = 0curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
-
检查要素:
-
检查所有TLS相关代码,确认证书验证被启用。
-
修复方法:
-
始终启用证书验证,并检查主机名是否匹配。
-
使用现代TLS库并遵循其安全指南。
八、权限与访问控制
1
权限提升
程序以过高权限运行,或未正确降权,被利用后攻击者获得高权限。
setuid 程序未及时 drop 权限。
-
检查要素:
-
分析程序启动时的权限,检查降权操作(如
setuid、setgid)。 -
修复方法:
-
遵循最小权限原则,尽早降权。
-
使用沙箱或容器隔离。
2
不安全的临时文件
在共享目录创建可预测名称的临时文件,攻击者可先创建符号链接指向关键文件,导致程序写入意外位置。
FILE *tmp = fopen("/tmp/tempfile.txt", "w"); // 名称固定
-
检查要素:
-
查找临时文件创建方式,是否使用安全函数。
-
修复方法:
-
使用
mkstemp、tmpfile或std::filesystem::temp_directory_path结合随机名称。
3
竞争条件(TOCTOU)
检查文件属性(如权限)与使用文件之间存在时间窗口,攻击者可替换文件。
if (access("file", R_OK) == 0) { // 此时攻击者可替换文件为符号链接 int fd = open("file", O_RDWR);}
-
检查要素:
-
审查所有“检查-使用”模式的文件操作。
-
修复方法:
-
尽量使用文件描述符而非路径进行后续操作。
-
使用
open结合O_NOFOLLOW等标志,避免符号链接。
九、第三方库与依赖
1
使用已知漏洞的库版本
项目依赖的库版本包含公开漏洞,可被攻击者利用。
-
检查要素:
-
列出所有依赖库及版本,与CVE数据库比对。
-
修复方法:
-
定期更新依赖到安全版本。
-
使用依赖检查工具(如 OWASP Dependency-Check)。
2
库调用方式不安全
即使库本身安全,错误的使用方式(如未启用证书验证、使用不安全函数)也会引入漏洞。
-
检查要素:
-
审查对第三方库的调用是否符合安全最佳实践。
-
修复方法:
-
遵循库的安全使用指南。
-
对关键库进行封装,强制安全配置。
十、配置与敏感信息
1
日志泄露敏感信息
在日志中输出密码、令牌、会话ID等敏感数据,导致信息泄露。
log("User login: %s, password: %s", username, password);
-
检查要素:
-
搜索日志输出语句,确保不记录敏感字段。
-
修复方法:
-
对敏感信息进行脱敏或禁止记录。
-
制定日志安全规范。
2
调试代码残留
生产环境中残留调试接口、后门或详细错误信息,可能被攻击者利用。
#ifdef DEBUG system("debug_shell"); // 调试后门#endif
-
检查要素:
-
检查是否有调试宏或条件编译导致的功能残留。
-
修复方法:
-
确保发布版本禁用所有调试功能。
-
使用版本控制系统剥离调试代码。
3
不安全的编译选项
编译时未启用安全缓解措施(如栈保护、ASLR、DEP),降低漏洞利用难度。
-
检查要素:
-
审查编译脚本,确认是否开启
-fstack-protector-strong、-pie、-z noexecstack等。 -
修复方法:
-
在发布版本中强制启用所有安全编译选项。
-
使用工具(如 checksec)验证二进制安全属性。
C++ #信息安全 #代码审计
本站致力于做最深度、专业、前沿的网络安全知识分享平台,欢迎点赞、关注、推荐,为您持续更新深度好文。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:网络安全研究站 ZKAFKA ZKAFKA《C++安全编码手册:十大类39个漏洞点,一篇搞定代码审计(建议收藏)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论