C++安全编码手册:十大类39个漏洞点,一篇搞定代码审计(建议收藏)

admin 2026-03-18 20:09:36 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文档系统梳理了C++安全编码中十大类共39个常见漏洞点,涵盖内存安全、整数溢出、并发问题及注入攻击等核心领域。针对每项漏洞,文章不仅解析了漏洞原理与危害,还提供了具体的风险代码示例、审计检查要点及修复方案。这是一份兼具理论性与实操性的代码审计清单,有助于开发人员和安全从业者快速识别并修复代码隐患,提升软件安全性。 综合评分: 88 文章分类: 代码审计,安全开发,漏洞分析


cover_image

C++安全编码手册:十大类39个漏洞点,一篇搞定代码审计(建议收藏)

原创

ZKAFKA ZKAFKA

网络安全研究站

2026年3月10日 21:19 浙江

一、内存安全类

1

缓冲区溢出

向固定大小的缓冲区写入超出其容量的数据,覆盖相邻内存区域,可能导致程序崩溃或控制流劫持。常见于不安全的C风格字符串函数(strcpysprintf)或数组索引越界。

void vulnerable(const char* input) {    char buf[10];    strcpy(buf, input); // 若input长度>9,则溢出}
  • 检查要素

  • 搜索所有无边界检查的字符串/内存操作函数。

  • 检查数组索引是否在合法范围内。

  • 修复方法

  • 使用 strncpysnprintf 等并指定大小。

  • 改用 std::string 或 std::vector<char>

2

堆溢出

动态分配的内存(如 new/malloc)后,写入数据超出分配大小,破坏堆管理结构,可能导致任意代码执行。

char&nbsp;*p =&nbsp;new&nbsp;char[10];// 若user_input长度>10,则堆溢出memcpy(p, user_input,&nbsp;strlen(user_input));
  • 检查要素

  • 审查动态内存分配的大小与实际写入长度是否匹配。

  • 关注从外部输入获取长度的操作。

  • 修复方法

  • 始终校验写入长度 ≤ 分配大小。

  • 使用 std::vector 并借助其 push_back 或 resize 安全操作。

3

释放后使用

内存释放后仍通过悬垂指针访问,导致未定义行为。攻击者可控制释放后内存的内容实现利用。

int&nbsp;*p =&nbsp;new&nbsp;int(5);delete&nbsp;p;*p =&nbsp;10;&nbsp;// 访问已释放内存
  • 检查要素

  • 检查 delete/free 后指针是否立即置空。

  • 审查对象的生命周期管理。

  • 修复方法

  • 释放后立即将指针置 nullptr

  • 使用智能指针(unique_ptrshared_ptr)自动管理生命周期。

4

双重释放

对同一块内存多次调用 free 或 delete,破坏堆分配器的内部状态,可能引发程序崩溃或利用。

int&nbsp;*p =&nbsp;new&nbsp;int(5);delete&nbsp;p;delete&nbsp;p;&nbsp;// 第二次释放
  • 检查要素

  • 查找同一指针被多次释放的代码路径。

  • 特别关注异常处理或复杂控制流中的释放操作。

  • 修复方法

  • 释放后立即置空指针。

  • 使用智能指针避免手动管理。

5

内存泄漏

读取未初始化的变量,其值不确定,可能泄露敏感信息或导致不可预测行为。

void&nbsp;leak()&nbsp;{&nbsp; &nbsp;&nbsp;int&nbsp;*p =&nbsp;new&nbsp;int(100);&nbsp; &nbsp;&nbsp;// 忘记 delete p}
  • 检查要素

  • 确保每个 new/malloc 都有对应的 delete/free

  • 检查异常路径是否可能导致跳过释放。

  • 修复方法

  • 遵循RAII,使用容器或智能指针。

  • 使用 Valgrind、AddressSanitizer 检测泄漏。

6

未初始化内存

动态分配的内存未被释放,导致进程内存占用持续增长,最终耗尽资源。

int&nbsp;x;if&nbsp;(x ==&nbsp;0) { ... }&nbsp;// x 未初始化
  • 检查要素

  • 查找未赋值的局部变量。

  • 检查类成员是否在构造函数中全部初始化。

  • 修复方法

  • 始终初始化变量(声明时赋初值或构造函数初始化列表)。

  • 使用静态分析工具(如 Clang Static Analyzer)。

二、整数安全问题

1

整数溢出/下溢

整数运算结果超出类型表示范围,导致回绕或未定义行为,常被用于绕过边界检查。

size_t&nbsp;len =&nbsp;GetUntrustedSize();char&nbsp;*buf =&nbsp;new&nbsp;char[len +&nbsp;1];&nbsp;// 若len为SIZE_MAX,则len+1溢出为0
  • 检查要素

  • 审查作为内存大小、循环计数器的整数运算。

  • 检查外部可控的整数值参与运算处。

  • 修复方法

  • 进行溢出检查(如 if(a > SIZE_MAX - b))。

  • 使用安全整数库(如 SafeInt、Boost.SafeInt)。

2

符号错误

将有符号整数与无符号整数混用,导致意外比较结果或负数作为数组下标引发越界。

int&nbsp;index&nbsp;= -1;unsigned&nbsp;int&nbsp;size =&nbsp;10;if&nbsp;(index&nbsp;< size) {&nbsp;//&nbsp;隐式转换,-1&nbsp;变成很大的正数,条件成立&nbsp; &nbsp; arr[index] =&nbsp;0;&nbsp;//&nbsp;越界写入}
  • 检查要素

  • 查找有符号/无符号混合运算。

  • 检查用作数组下标的变量是否可能为负。

  • 修复方法

  • 避免混用,或显式转换前验证。

  • 使用 ssize_t 或 ptrdiff_t 等有符号类型表示大小/下标。

3

截断错误

将较大类型赋值给较小类型,导致高位数据丢失,可能引发逻辑错误或安全检查绕过。

unsigned&nbsp;long&nbsp;long&nbsp;big =&nbsp;0x100000000;unsigned&nbsp;int&nbsp;small = big;&nbsp;// 截断为0
  • 检查要素

  • 查找窄化转换(隐式或显式)。

  • 关注赋值前是否进行范围检查。

  • 修复方法

  • 使用 {} 初始化防止隐式窄化(C++11)。

  • 赋值前检查值是否在目标类型范围内。

三、字符串与格式化

1

不安全的字符串函数

使用 strcpystrcatsprintf 等缺乏边界检查的函数,即使改用 strncpysnprintf,也可能因参数错误(如目标大小计算错误)导致截断或未终止。

char&nbsp;dst[10];strncpy(dst, src,&nbsp;strlen(src));&nbsp;// 未指定目标大小,仍可能溢出
  • 检查要素

  • 搜索所有字符串函数,检查目标缓冲区大小是否正确传递。

  • 确认 strncpy 后是否手动添加 \0

  • 修复方法

  • 优先使用 std::string 及其方法。

  • 若必须用C函数,严格计算大小并确保终止。

2

格式化字符串漏洞

用户控制格式化字符串参数,通过 %x%n 等读取或修改栈内存。

printf(user_input);&nbsp;//&nbsp;用户输入包含&nbsp;%n&nbsp;可导致写入
  • 检查要素

  • 确保所有格式化函数的格式串为常量字符串。

  • 修复方法

  • 使用 printf("%s", user_input); 形式。

四、并发与多线程

1

数据竞争

多个线程同时访问同一内存且至少一个为写操作,无同步机制,导致数据不一致。

int&nbsp;counter =&nbsp;0;void&nbsp;thread()&nbsp;{&nbsp;for(int&nbsp;i=0;i<100000;++i) ++counter; }
  • 检查要素

  • 识别所有共享变量,检查访问是否加锁或使用原子操作。

  • 修复方法

  • 使用 std::mutex 保护共享数据。

  • 对简单变量用 std::atomic

2

死锁

两个或多个线程互相等待对方释放资源,导致所有线程永久阻塞。

std::mutex m1, m2;void&nbsp;t1() { m1.lock(); m2.lock(); ... }&nbsp;// t1 先锁 m1 再 m2void&nbsp;t2() { m2.lock(); m1.lock(); ... }&nbsp;// t2 先锁 m2 再 m1 -> 可能死锁
  • 检查要素

  • 审查锁的获取顺序是否一致。

  • 检查是否存在嵌套锁且顺序不同。

  • 修复方法

  • 固定锁获取顺序。

  • 使用 std::lock 一次性锁定多个互斥量。

3

锁滥用

锁的粒度过大导致性能下降和并发度降低,或粒度过小导致竞态条件未完全防护。

std::mutex m;void&nbsp;process()&nbsp;{&nbsp; &nbsp; m.lock();&nbsp; &nbsp;&nbsp;// 大量耗时操作,但只需保护一小部分数据&nbsp; &nbsp; m.unlock();}
  • 检查要素

  • 分析锁保护的范围是否合理,有无不必要的长时间持有。

  • 检查是否遗漏需要保护的操作。

  • 修复方法

  • 缩小临界区,只保护必要操作。

  • 使用读写锁(std::shared_mutex)优化读多写少场景。

4

条件变量虚假唤醒

条件变量可能在没有被通知的情况下被唤醒(虚假唤醒),若未在循环中检查条件,会导致逻辑错误。

std::condition_variable cv;std::mutex m;bool&nbsp;ready =&nbsp;false;void&nbsp;wait()&nbsp;{&nbsp; &nbsp;&nbsp;std::unique_lock&nbsp;lk(m);&nbsp; &nbsp;&nbsp;if&nbsp;(!ready) cv.wait(lk);&nbsp;// 错误:应使用 while 循环&nbsp; &nbsp;&nbsp;// 此时 ready 可能仍为 false}
  • 检查要素

  • 检查 wait() 是否在循环中(while(condition) wait())。

  • 修复方法

  • 总是使用 while(condition) cv.wait(lk); 或带谓词的 wait 重载。

五、异常安全

1

异常导致资源泄漏

异常抛出后,已获取的资源(内存、文件句柄、锁)未释放,造成泄漏。

void&nbsp;func() {&nbsp; &nbsp;&nbsp;FILE&nbsp;*f =&nbsp;fopen("file.txt",&nbsp;"r");&nbsp; &nbsp;&nbsp;// 可能抛出异常的操作&nbsp; &nbsp;&nbsp;fclose(f);&nbsp;// 若异常在之前抛出,则不会执行}
  • 检查要素

  • 查找资源获取与释放之间是否有异常抛出点。

  • 修复方法

  • 使用RAII包装资源(如 std::fstreamstd::unique_ptr)。

  • 在析构函数中释放资源。

2

状态不一致

异常发生在对象状态变更过程中,导致对象处于部分修改的无效状态。

class&nbsp;Data&nbsp;{&nbsp; &nbsp;&nbsp;int&nbsp;*p;&nbsp; &nbsp;&nbsp;int&nbsp;len;public:&nbsp; &nbsp;&nbsp;void&nbsp;resize(size_t&nbsp;n)&nbsp;{&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;delete[] p;&nbsp; &nbsp; &nbsp; &nbsp; p =&nbsp;new&nbsp;int[n];&nbsp;// 若此处 bad_alloc 抛出,则 p 指向已释放内存,len 未更新&nbsp; &nbsp; &nbsp; &nbsp; len = n;&nbsp; &nbsp; }};
  • 检查要素

  • 审查修改对象状态的操作序列是否可能抛出异常。

  • 修复方法

  • 使用“提交或回滚”策略:先进行可能失败的操作,成功后再更新状态。

  • 使用智能指针管理资源,简化异常安全。

六、输入验证与注入

1

命令注入

将用户输入拼接到系统命令中,攻击者可插入特殊字符执行任意命令。

system(std::string("ping "&nbsp;+ user_input).c_str());
  • 检查要素

  • 搜索 systempopenexec 族函数,检查命令字符串是否包含不可信输入。

  • 修复方法

  • 避免调用外部命令,或使用参数化API(如 execve 传参列表)。

  • 对输入进行白名单过滤。

2

SQL注入

SQL语句拼接用户输入,改变查询语义。

sprintf(query,&nbsp;"SELECT * FROM users WHERE name='%s'", user_input);
  • 检查要素

  • 查找SQL查询构建方式,确认是否使用参数化查询。

  • 修复方法

  • 强制使用参数化查询(预处理语句)。

  • 对输入进行转义(但不如参数化安全)。

3

跨站脚本(XSS)

C++后端输出HTML时,未对用户输入进行编码,导致恶意脚本注入浏览器。

std::cout <<&nbsp;"<div>"&nbsp;<< user_input <<&nbsp;"</div>";&nbsp;// 若输入包含 <script>
  • 检查要素

  • 检查所有输出到HTML的变量是否经过HTML转义。

  • 修复方法

  • 对输出进行HTML实体编码(如 & → &amp;, < → &lt;)。

  • 使用模板引擎自动转义。

七、加密与随机数

1

使用弱加密算法

使用已被破解的算法(如DES、MD5)或自定义加密算法,可被攻击者快速破解。

#include&nbsp;<openssl/md5.h>MD5(data, len, hash);&nbsp;// MD5 已不安全
  • 检查要素

  • 审查加密算法使用,识别弱算法。

  • 修复方法

  • 使用现代强算法(AES-256、SHA-256、SHA-3)。

  • 采用经过验证的加密库(OpenSSL、libsodium)。

2

硬编码密钥/凭证

密钥、密码直接写在代码中,易被逆向工程获取。

const&nbsp;char* api_key =&nbsp;"sk_live_1234567890abcdef";
  • 检查要素

  • 搜索代码中可能包含密钥的字符串(如 keysecretpassword)。

  • 修复方法

  • 将凭证移出代码,存储于安全配置、环境变量或密钥管理服务。

3

不安全的随机数

使用可预测的随机数生成器(如 rand())生成安全敏感值。

srand(time(NULL));int&nbsp;token =&nbsp;rand();&nbsp;//&nbsp;可预测
  • 检查要素

  • 搜索 randrandom 等,判断使用场景是否涉及安全。

  • 修复方法

  • 使用密码学安全随机数生成器(/dev/urandomRAND_bytes)。

4

证书验证不当

TLS/SSL通信时,未正确验证服务器证书,导致中间人攻击。

// 使用 libcurl 时设置 CURLOPT_SSL_VERIFYPEER = 0curl_easy_setopt(curl,&nbsp;CURLOPT_SSL_VERIFYPEER, 0L);
  • 检查要素

  • 检查所有TLS相关代码,确认证书验证被启用。

  • 修复方法

  • 始终启用证书验证,并检查主机名是否匹配。

  • 使用现代TLS库并遵循其安全指南。

八、权限与访问控制

1

权限提升

程序以过高权限运行,或未正确降权,被利用后攻击者获得高权限。

setuid&nbsp;程序未及时 drop 权限。
  • 检查要素

  • 分析程序启动时的权限,检查降权操作(如 setuidsetgid)。

  • 修复方法

  • 遵循最小权限原则,尽早降权。

  • 使用沙箱或容器隔离。

2

不安全的临时文件

在共享目录创建可预测名称的临时文件,攻击者可先创建符号链接指向关键文件,导致程序写入意外位置。

FILE&nbsp;*tmp =&nbsp;fopen("/tmp/tempfile.txt",&nbsp;"w");&nbsp;// 名称固定
  • 检查要素

  • 查找临时文件创建方式,是否使用安全函数。

  • 修复方法

  • 使用 mkstemptmpfile 或 std::filesystem::temp_directory_path 结合随机名称。

3

竞争条件(TOCTOU)

检查文件属性(如权限)与使用文件之间存在时间窗口,攻击者可替换文件。

if&nbsp;(access("file", R_OK) ==&nbsp;0) {&nbsp; &nbsp;&nbsp;//&nbsp;此时攻击者可替换文件为符号链接&nbsp; &nbsp;&nbsp;int&nbsp;fd =&nbsp;open("file", O_RDWR);}
  • 检查要素

  • 审查所有“检查-使用”模式的文件操作。

  • 修复方法

  • 尽量使用文件描述符而非路径进行后续操作。

  • 使用 open 结合 O_NOFOLLOW 等标志,避免符号链接。

九、第三方库与依赖

1

使用已知漏洞的库版本

项目依赖的库版本包含公开漏洞,可被攻击者利用。

  • 检查要素

  • 列出所有依赖库及版本,与CVE数据库比对。

  • 修复方法

  • 定期更新依赖到安全版本。

  • 使用依赖检查工具(如 OWASP Dependency-Check)。

2

库调用方式不安全

即使库本身安全,错误的使用方式(如未启用证书验证、使用不安全函数)也会引入漏洞。

  • 检查要素

  • 审查对第三方库的调用是否符合安全最佳实践。

  • 修复方法

  • 遵循库的安全使用指南。

  • 对关键库进行封装,强制安全配置。

十、配置与敏感信息

1

日志泄露敏感信息

在日志中输出密码、令牌、会话ID等敏感数据,导致信息泄露。

log("User login:&nbsp;%s, password:&nbsp;%s", username, password);
  • 检查要素

  • 搜索日志输出语句,确保不记录敏感字段。

  • 修复方法

  • 对敏感信息进行脱敏或禁止记录。

  • 制定日志安全规范。

2

调试代码残留

生产环境中残留调试接口、后门或详细错误信息,可能被攻击者利用。

#ifdef&nbsp;DEBUG&nbsp; &nbsp;&nbsp;system("debug_shell");&nbsp;// 调试后门#endif
  • 检查要素

  • 检查是否有调试宏或条件编译导致的功能残留。

  • 修复方法

  • 确保发布版本禁用所有调试功能。

  • 使用版本控制系统剥离调试代码。

3

不安全的编译选项

编译时未启用安全缓解措施(如栈保护、ASLR、DEP),降低漏洞利用难度。

  • 检查要素

  • 审查编译脚本,确认是否开启 -fstack-protector-strong-pie-z noexecstack 等。

  • 修复方法

  • 在发布版本中强制启用所有安全编译选项。

  • 使用工具(如 checksec)验证二进制安全属性。

C++ #信息安全 #代码审计



本站致力于做最深度、专业、前沿的网络安全知识分享平台,欢迎点赞、关注、推荐,为您持续更新深度好文。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:网络安全研究站 ZKAFKA ZKAFKA《C++安全编码手册:十大类39个漏洞点,一篇搞定代码审计(建议收藏)》

评论:0   参与:  0