C/C++ 安全开发入门:缓冲区溢出、UAF、内存越界,漏洞成因与编码防护
法律声明:本文内容仅用于网络安全学习、C/C++ 安全开发自查、内存安全漏洞原理研究。文中漏洞代码、POC 载荷仅用于原理演示,严禁未经授权对线上系统进行漏洞利用。任何安全测试必须获得资产所有者书面授权,遵守《网络安全法》《数据安全法》等法律法规。
摘要
C/C++ 是系统级开发的主力语言,操作系统、驱动、中间件、音视频引擎、通信协议栈、嵌入式固件、游戏引擎大量使用 C/C++ 开发。C/C++ 语言直接管理内存,没有内置边界检查、没有垃圾回收,是内存安全漏洞的高发语言。缓冲区溢出、UAF(释放后使用)、内存越界、空指针解引用、整数溢出、堆溢出,构成了大量 CVE 漏洞的核心成因。
在 SRC 挖洞、二进制安全、漏洞挖掘(PWN/CTF)、安全开发(SDL/DevSecOps)领域,C/C++ 代码审计是基础中的基础。很多开发者熟悉 Web 漏洞,一看到 C/C++ 底层内存漏洞就发怵,实际上只要理解了内存布局、指针、堆栈机制,这些漏洞成因和编码防护是完全可以掌握的。
本文万字实战教程,聚焦 C/C++ 三大核心内存漏洞:缓冲区溢出(栈溢出 / 堆溢出)、UAF 释放后使用、内存越界(数组越界 / 越界读写),同时补充整数溢出、空指针、格式化字符串等高频漏洞,完整讲解漏洞成因、内存原理、可复现代码、利用思路、代码修复、编译防护(ASLR/PIE/Canary/NX)。适合 C/C++ 开发、嵌入式开发、安全工程师、二进制安全入门、SRC 白帽学习使用。
前言
学习 C/C++ 内存安全,首先要理解 C/C++ 和 Java、Python、Go 的本质差异:C/C++手动管理内存。开发者用malloc/new分配内存,用free/delete释放内存,用指针访问内存地址。一旦管理不当,就会产生内存安全漏洞。
内存安全三大核心问题
- 缓冲区溢出:写入的数据超过了缓冲区(数组 / 堆块)的边界,覆盖相邻内存;
- UAF(Use-After-Free):内存块被 free 之后,指针仍然指向这块已释放的内存,继续通过这个指针读写数据;
- 内存越界:通过指针 / 数组索引访问了不属于当前对象的内存区域。
为什么 C/C++ 漏洞危害大?
因为内存漏洞常被攻击者利用来实现任意代码执行(控制程序流、劫持返回地址、覆盖函数指针)或信息泄露(越界读取密钥、密码)。缓冲区溢出是最经典的 RCE 攻击手法(栈溢出覆盖返回地址),危害等级极高。
C/C++ 内存布局(理解漏洞的前提)
一个 C/C++ 程序进程的内存分为几大区域:
- 栈(Stack):存放局部变量、函数调用帧、返回地址,向下生长;
- 堆(Heap):
malloc/new动态分配的内存,向上生长; - 数据段(.data/.bss):全局变量、静态变量;
- 代码段(.text):程序机器码。
攻击者的视角:如何利用内存漏洞
- 栈溢出 → 覆盖栈上返回地址 → 劫持程序执行流 → 跳转到 shellcode 或 ROP 链 → 拿到 shell;
- 堆溢出 → 覆盖相邻堆块元数据(chunk header)/ 函数指针 → 内存破坏 → 任意代码执行;
- UAF → 释放后重新 malloc,分配相同大小,攻击者控制新内存内容,用旧指针触发函数指针 / 虚表劫持。
前置知识点:指针就是内存地址。C 语言中
int *p = &a;,p 保存的是变量 a 的内存地址。所有内存漏洞的核心,都是指针 / 索引访问了不该访问的内存区域。
一、C/C++ 安全开发前置知识
1.1 编译器与防护机制(现代系统默认开启)
理解漏洞,必须先理解现代系统提供的安全防护机制(这些机制会显著提高利用难度):
- ASLR(地址空间布局随机化):每次程序运行,栈、堆、库基址随机化,攻击者难以预测目标地址;
- NX(不可执行栈 / 堆):栈、堆数据区不可执行,攻击者不能直接执行 shellcode;
- Canary(栈金丝雀):栈溢出检测,在返回地址前插入随机值,溢出前先破坏 Canary,触发检测,程序终止;
- PIE(位置无关可执行文件):程序代码段基址随机化,配合 ASLR;
- RELRO(只读重定位):保护 GOT(全局偏移表)不被覆盖。
攻击者的对策:绕过这些防护,常见有 ROP(返回导向编程)、ret2libc、ret2plt、堆风水等,这是二进制漏洞利用(PWN)的核心技术。本文重点讲漏洞成因与编码防护,利用技术点到为止。
1.2 常用审计工具链(C/C++)
- gcc/g++/clang:编译器,开启编译防护选项;
- valgrind:内存错误检测器,检测内存泄漏、非法读写、UAF,动态检测神器;
- AddressSanitizer(ASan):编译时插入内存检测代码,定位缓冲区溢出、UAF、越界,C/C++ 代码审计最核心工具;
- gdb:调试器,查看内存布局、寄存器、栈帧;
- objdump/readelf:查看二进制反汇编、符号、段信息;
- RIPS(PHP 之外也支持)/Semgrep:静态扫描。
关键工具:ASan 编译选项
-fsanitize=address,可以在运行程序时实时检测并精确报告内存越界、UAF、堆溢出的位置和堆栈,是 C/C++ 漏洞复现的利器。
二、实战一:缓冲区溢出漏洞(栈溢出 / 堆溢出)
2.1 漏洞成因
缓冲区溢出:向固定大小的缓冲区写入超过其容量的数据,覆盖相邻内存区域。
最常见场景:
- 使用
strcpy、strcat、sprintf、gets等不安全的字符串函数,不检查目标缓冲区长度; memcpy复制数据时长度参数错误(来源长度大于目标容量);- 数组索引越界写入;
- 用户输入数据拷贝到固定长度数组,未限制长度。
- Source:用户可控输入长度 / 内容;
- Sink:strcpy、strcat、sprintf、memcpy、gets、scanf(% s)等不安全拷贝函数;
- 风险等级:严重;
- 危害:栈溢出可劫持返回地址执行任意代码,堆溢出可内存破坏导致 RCE,是最高危漏洞。
2.2 源码示例(漏洞代码)
栈溢出经典示例:strcpy 拷贝用户输入
#include <stdio.h>
#include <string.h>
void vulnerable(char *input){
// 固定大小缓冲区,栈上分配
char buf[16];
// 高危:strcpy不检查长度,直接拷贝
// 输入长度>16,覆盖栈上相邻内存(包括返回地址)
strcpy(buf, input);
printf("data:%s\n", buf);
}
int main(){
char input[256];
printf("请输入:");
// gets不检查长度,可输入任意长度数据
gets(input);
vulnerable(input);
return 0;
}
char buf[16]只分配 16 字节,但strcpy(buf,input)不检查 input 长度,输入超过 16 字节就发生栈溢出,覆盖 buf 之后的栈内存,包括函数返回地址。
注意:
gets()和scanf("%s")不检查输入长度,也是高危 Sink。现代编译器默认告警,但老代码大量存在。
sprintf 溢出示例
char buf[32];
// 高危:sprintf不检查目标缓冲区长度
sprintf(buf, "用户:%s 年龄:%d", name, age);
memcpy 长度错误示例
char dst[32];
char src[128];
// 高危:memcpy长度参数用了src的实际长度(128),超过dst容量(32),堆/栈溢出
memcpy(dst, src, strlen(src));
2.3 漏洞利用思路(原理层面)
栈溢出利用核心:覆盖函数返回地址。
- 攻击者输入超长数据;
- 数据溢出 buf,先覆盖局部变量,再覆盖栈帧上的返回地址;
- 攻击者把返回地址改写成自己构造的恶意代码地址(shellcode)或 ROP 链;
- 函数返回时,程序跳转到攻击者控制的位置,执行任意代码。
现代防护(Canary+ASLR+NX)会显著提高利用难度,但不代表漏洞不存在。只要存在缓冲区溢出,无论能否利用,都属于高危内存漏洞。
2.4 常见误报识别
SAST 静态扫描报缓冲区溢出,但实际可能:
- 虽然使用 strcpy,但传入的 input 来自服务端内部固定长度字符串,实际不会超长;
- 缓冲区大小足够容纳所有可能的输入;
- 输入长度已在上层校验。
人工复核重点:用户可控输入来源 + 目标缓冲区大小 + 是否真正可能超出。
2.5 漏洞修复代码
修复核心:使用安全函数,指定缓冲区容量,限制输入长度。
修复 1:使用带长度限制的安全函数
#include <stdio.h>
#include <string.h>
void vulnerable(char *input){
char buf[16];
// 安全:strncpy指定拷贝长度15,保证留\0终止符
// 注意strncpy可能不补\0,手动确保
strncpy(buf, input, sizeof(buf)-1);
buf[sizeof(buf)-1] = '\0'; // 手动补终止符
printf("data:%s\n", buf);
}
修复 2:改用 snprintf 替代 sprintf
char buf[32];
// 安全:snprintf指定缓冲区大小,防止溢出
snprintf(buf, sizeof(buf), "用户:%s 年龄:%d", name, age);
修复 3:fgets 替代 gets
char input[100];
// 安全:fgets限制最大读取长度
fgets(input, sizeof(input), stdin);
2.6 编译防护与纵深加固
编译时开启安全选项:
# 开启全部内存防护
gcc -fstack-protector-all -no-pie -z noexecstack -z relro -o prog vuln.c
# 检测使用ASan
gcc -fsanitize=address -g -o prog vuln.c
- 开启
-fstack-protector-all(栈 Canary); - 开启
-z noexecstack(栈不可执行 NX); - 开启 ASLR(系统层面);
- 使用 ASan 在开发阶段检测内存漏洞;
- 代码评审禁止使用 strcpy、gets、sprintf 等不安全函数,强制使用 strncpy、snprintf、fgets。
三、实战二:UAF(Use-After-Free,释放后使用)
3.1 漏洞成因
UAF:一块堆内存被free释放之后,代码仍然通过保存的指针继续访问这块内存。释放后这块内存可能被系统重新分配,或者内容被篡改,继续使用会造成内存破坏、逻辑错误、甚至代码执行。
- Source:指针指向已释放的内存;
- Sink:free 之后仍使用该指针读写;
- 风险等级:严重;
- 危害:堆内存破坏、虚表 / 函数指针劫持、任意代码执行、信息泄露。
3.2 源码示例(漏洞代码)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int id;
char name[32];
} User;
void vulnerable(){
// 堆上分配User对象
User *user = (User*)malloc(sizeof(User));
strcpy(user->name, "admin");
printf("before free: %s\n", user->name);
// 释放内存
free(user);
// 高危:free之后指针未置空,继续使用(UAF)
// user指向的内存已释放,读写是未定义行为
printf("after free: %s\n", user->name); // 读取已释放内存
user->id = 100; // 写入已释放内存
}
free(user)之后,user指针仍然有效(指向已释放的堆块),继续使用user->name、user->id,触发 UAF。
UAF 为什么高危:如果释放后,攻击者再次 malloc 相同大小的内存块,控制新分配内存的内容,再触发旧指针的使用,可能实现虚表劫持、函数指针覆盖,最终 RCE。
更危险的 UAF 场景:释放后重新分配再使用
typedef struct {
void (*func)(int); // 函数指针
int data;
} Object;
void vulnerable(){
Object *obj = (Object*)malloc(sizeof(Object));
obj->func = safe_handler; // 初始化函数指针
free(obj); // 释放
// 攻击者再次malloc相同大小,控制这块内存内容
Object *fake = (Object*)malloc(sizeof(Object));
fake->func = malicious_handler; // 攻击者伪造函数指针
// UAF:仍通过旧指针obj调用函数指针,实际调用的是fake伪造的函数
obj->func(obj->data);
}
经典利用:释放对象 → 攻击者 malloc 同大小块并填充恶意内容 → 旧指针触发虚函数 / 函数指针 → 跳转到恶意地址。
3.3 常见误报识别
- free 之后指针立即返回,后面没有继续使用;
- free 之后指针被置 NULL,后续有判空保护(
if(user!=NULL)); - 内存释放后程序立即退出,无利用价值。
3.4 漏洞修复代码
修复核心:free 之后立即将指针置为 NULL,使用前判空。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void safe(){
User *user = (User*)malloc(sizeof(User));
if(!user) return; // 分配失败检查
strcpy(user->name, "admin");
printf("before: %s\n", user->name);
free(user);
user = NULL; // 关键:释放后立即置空
// 使用前判空,防止UAF
if(user != NULL){
printf("after: %s\n", user->name);
}
}
补充:C++ 中,
delete之后同样要把指针置 NULL,delete[]匹配new[],delete匹配new。
3.5 纵深加固
- 释放后立即置空指针,使用前判空;
- 统一使用智能指针(C++11+
std::unique_ptr、std::shared_ptr),自动管理生命周期,从根上杜绝 UAF; - 使用 ASan 检测 UAF:
-fsanitize=address,UAF 会立即被检测并报告; - 对象生命周期管理规范化,明确所有权(谁分配谁释放)。
C++ 安全建议:现代 C++ 开发优先使用智能指针和 RAII,避免手动
new/delete,这是杜绝 UAF 和内存泄漏的最佳实践。
四、实战三:内存越界(数组越界 / 越界读写)
4.1 漏洞成因
内存越界:通过数组索引或指针访问了不属于当前对象 / 数组的内存区域。
常见场景:
- 数组索引越界(负数索引、超出 size 的索引);
- 指针偏移计算错误,越过对象边界;
- 循环遍历时边界条件错误(
for(int i=0;i<=n;i++)多一次); - 用户可控索引直接访问数组元素,未校验边界。
- Source:用户可控数组索引;
- Sink:
arr[index]、*(ptr+offset)越界访问; - 风险等级:中高危;
- 危害:越界读泄露敏感信息(信息泄露)、越界写覆盖内存(内存破坏 / RCE)。
4.2 源码示例(漏洞代码)
#include <stdio.h>
void array_access(int index){
int arr[10] = {0,1,2,3,4,5,6,7,8,9};
// 高危:用户可控index直接访问数组,未校验边界
// index<0 或 index>=10 造成越界访问
printf("arr[%d]=%d\n", index, arr[index]);
}
int main(){
int idx;
scanf("%d", &idx);
array_access(idx); // 用户可控索引
return 0;
}
arr[10]合法索引范围是 0~9。攻击者传入idx=-1或idx=100,访问 arr [-1] 或 arr [100],越界读写内存。
越界读:arr[100]读取数组外的内存数据,可能泄露其他变量、密钥等敏感信息(信息泄露)。
越界写:arr[100] = value写入数组外内存,可能覆盖其他变量、函数指针(内存破坏)。
4.3 常见误报识别
- 索引来自服务端内部固定值,始终在合法范围内;
- 索引经过严格边界校验后才使用;
- 数组本身动态分配,容量足够。
4.4 漏洞修复代码
修复核心:访问数组前,严格校验索引边界。
#include <stdio.h>
#define ARR_SIZE 10
void array_access(int index){
int arr[ARR_SIZE] = {0,1,2,3,4,5,6,7,8,9};
// 安全:先校验索引范围
if(index < 0 || index >= ARR_SIZE){
printf("索引越界,拒绝访问\n");
return;
}
printf("arr[%d]=%d\n", index, arr[index]);
}
int main(){
int idx;
scanf("%d", &idx);
array_access(idx);
return 0;
}
4.5 纵深加固
- 所有数组访问前校验索引范围(下界 0,上界 size-1);
- 优先使用
std::vector(C++)替代裸数组,.at(index)带边界检查; - 循环边界写清楚,避免
i<=n多访问一次; - 使用 ASan 检测越界读写;
- 禁止用户可控索引直接访问数组,先白名单校验。
五、C/C++ 其他高频内存漏洞补充
5.1 整数溢出
整数溢出:算术运算结果超出类型范围,发生回绕。整数溢出常被利用绕过长度检查,间接导致缓冲区溢出。
unsigned int len = userLen;
// 如果len非常大,len*2发生整数溢出变成小值
char *buf = malloc(len * 2); // 分配的内存过小
// 后续按len写入,造成堆溢出
修复:运算前检查是否溢出,使用大类型(size_t)、限制长度范围、使用安全数学运算。
5.2 空指针解引用
malloc失败返回 NULL,直接解引用造成崩溃。修复:分配后判空。
5.3 格式化字符串漏洞
printf(userInput),用户输入直接作为格式化字符串,可以泄露栈数据、任意读写。
// 高危:用户输入作为格式化字符串
printf(user_control_str);
修复:永远用
printf("%s", userInput),不要把用户输入直接当格式串。
5.4 双重释放(Double Free)
同一块内存 free 两次,造成堆破坏。修复:释放后置空,使用前判空。
5.5 内存泄漏(Memory Leak)
分配后未释放,长期运行内存耗尽。修复:RAII / 智能指针,释放逻辑规范。
六、C/C++ 审计难点:指针、堆布局、编译器差异
6.1 指针算术与越界
ptr+offset容易越界,尤其 offset 用户可控时。审计重点:检查所有指针偏移计算,边界校验是否完整。
6.2 编译器差异与优化
不同编译器、不同优化等级(-O0/-O2),漏洞表现可能不同。审计和复现时注意编译选项一致性。
6.3 真实项目大规模代码
真实系统(浏览器、引擎、OS)代码量巨大,人工审计难,主要靠:
- ASan/Fuzzing(模糊测试)动态检测;
- 静态扫描(CodeQL、Coverity、Clang Static Analyzer);
- Fuzzing 工具:libFuzzer、AFL++,自动生成大量输入触发崩溃,定位漏洞。
6.4 缓解措施评估
现代系统有 ASLR/NX/Canary,但内核、驱动、嵌入式场景防护弱。审计漏洞时评估实际防护,判断可利用性。
七、SAST/ASan 检测结果复核(区分真漏洞 / 误报)
C/C++ 漏洞检测结果复核:
- 确认污点源:输入是否来自外部用户可控(网络、文件、命令行);
- 追踪数据流:用户输入是否传入不安全拷贝 / 索引 / 指针操作;
- 确认 Sink:strcpy、memcpy 长度、数组索引、free 后使用;
- 用 ASan 实际运行验证:是否触发内存错误,报错位置和调用栈;
- 判断可利用性:能否被外部输入触发、能否造成实际危害。
ASan 运行检测示例:
gcc -fsanitize=address -g -o vuln vuln.c
./vuln
# ASan会输出具体越界/溢出的地址和调用栈
常见误报场景:
- 输入来自内部固定长度数据,不可能超长;
- 缓冲区容量足够容纳所有输入;
- 索引范围始终合法;
- free 后指针未再使用,或已置空。
八、SRC 漏洞报告撰写要点(C/C++ 内存漏洞)
提交 C/C++ 内存漏洞(如针对开源组件、固件、中间件的 CVE 挖掘),报告写清楚:
- 漏洞名称(CWE 分类)、漏洞等级;
- 源码文件、函数、代码行号;
- 漏洞成因分析(内存布局、溢出 / 越界路径);
- POC / 触发输入、复现步骤;
- ASan 报错截图、调用栈;
- 漏洞影响(可能 RCE / 信息泄露);
- 修复代码、编译加固建议。
重点:内存漏洞报告必须有可复现的触发输入 + ASan 报错证据,纯代码分析容易被判无效,动态证据(ASan 崩溃、gdb 调用栈)是提高评级关键。
九、C/C++ 安全开发自查清单
开发提交代码前,对照自查清单:
- 是否使用不安全函数:strcpy/strcat/sprintf/gets/scanf (“% s”),全部替换为 strncpy/snprintf/fgets;
- 所有 memcpy/memmove/memset,长度参数是否校验不超过目标容量;
- 数组访问索引,是否校验边界(0 <= index < size);
- free/delete 之后,指针是否置空,使用前是否判空;
- malloc / 分配后是否判空,防止空指针解引用;
- 是否使用智能指针(C++)替代裸指针 new/delete;
- 用户可控输入是否可能超长、索引是否可控,追踪完整数据流;
- 算术运算是否可能整数溢出,长度计算前是否校验;
- printf 是否误把用户输入当格式化字符串;
- 编译是否开启防护:-fstack-protector-all、-z noexecstack、-z relro;
- 开发阶段是否用 ASan 跑测试,检测内存错误;
- 是否使用 Fuzzing 模糊测试补充检测。
十、总结
C/C++ 内存安全漏洞(缓冲区溢出、UAF、内存越界)是最底层、危害最大的漏洞类型,也是 PWN/CTF、SRC 二进制挖洞、系统安全的核心领域。和 Web 漏洞不同,C/C++ 漏洞的直接后果往往是任意代码执行、系统沦陷。
核心要点总结:
- 缓冲区溢出:strcpy/sprintf/memcpy 长度不当,覆盖返回地址 / 相邻堆块,可 RCE。修复用安全函数 + 限制长度 + 编译防护。
- UAF 释放后使用:free 之后指针继续使用,堆破坏、函数指针劫持。修复释放后置空、判空、用智能指针。
- 内存越界:数组索引 / 指针越界,越界读泄露信息、越界写破坏内存。修复严格校验索引边界、用 std::vector。
- 现代防护 ASLR/NX/Canary 提高利用难度,但漏洞存在即高危,审计重点在于确认是否可被外部输入触发。
- ASan 是 C/C++ 漏洞复现和定位的最佳工具,配合 gdb、Fuzzing,可以高效确认漏洞真实性。
- 安全编码最佳实践:安全函数、智能指针、边界校验、编译防护、ASan 测试。
C/C++ 安全开发是 “代码审计 + 二进制安全 + SDL” 综合能力的体现。掌握了内存漏洞成因和编码防护,既能写出更安全的系统级代码,也能为后续 PWN 二进制漏洞挖掘、SRC 挖掘奠定坚实基础。
免责声明:本文漏洞代码仅用于安全学习,禁止未经授权对任何系统进行漏洞利用。所有安全测试必须获得资产所有者授权,遵守《网络安全法》《数据安全法》等法律法规。
以上就是今天的分享,觉得有帮助,记得点赞+收藏,以上所说到的漏洞扫描工具都整理成压缩包了,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】


同时还为准备学习网络安全(黑客)或者正在学习,整理了一套360独家内部资料,包含以下内容,如有需要也可文末获取:
①网络安全学习路线
②20份渗透测试电子书
③安全攻防357页笔记
④50份安全攻防面试指南
⑤安全红队渗透工具包
⑥网络安全必备书籍
⑦100个漏洞实战案例
⑧安全大厂内部视频资源
⑨历年CTF夺旗赛题解析
一、网络安全(黑客)学习路线
网络安全(黑客)学习路线,形成网络安全领域所有的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。

二、网络安全教程视频
我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了。

三、网络安全CTF实战案例
光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这里带来的是CTF&SRC资料&HW资料,毕竟实战是检验真理的唯一标准嘛~

四、网络安全面试题
最后,我们所有的作为都是为就业服务的,所以关键的临门一脚就是咱们的面试题内容,所以面试题板块是咱们不可或缺的部分,这里我给大家准备的就是我在面试期间准备的资料。

网安其实不难,难的是坚持和相信自己,我的经验是既然已经选定网安你就要相信它,相信它能成为你日后进阶的高效渠道,这样自己才会更有信念去学习,才能在碰到困难的时候坚持下去。
机会属于有准备的人,这是一个实力的时代。人和人之间的差距不在于智商,而在于如何利用业余时间,只要你想学习,什么时候开始都不晚,不要担心这担心那,你只需努力,剩下的交给时间!
这份完整版的网络安全学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)