数组越界漏洞(Buffer Overflow / Out-of-bounds Read/Write)在通用缺陷枚举中被编号为 CWE-119(及其子项 CWE-125 越界读 和 CWE-787 越界写)。它指的是程序在访问数组时,使用的索引超出了数组合法边界(小于 0 或大于等于数组长度),导致访问了相邻的内存区域。
由于 C/C++ 等低级语言不会自动进行数组边界检查,攻击者可以通过传入异常的索引值对内存进行非法读写,是内存安全中最典型的漏洞类型之一。
1. 漏洞原理与危害
数组越界漏洞的底层原理在于 C/C++ 等内存不安全语言将数组下标访问直接映射为指针算术运算,且编译器默认不会插入边界校验指令。
1.1 漏洞原理
在编译成二进制机器码后,CPU 访问数组元素并不是靠“数个数”,而是靠“基地址 + 偏移量”的数学公式来计算内存地址的;
在机器码层面,array[i] 的寻址公式为:目标地址 = 基地址 + (索引 * 元素大小)
CPU 并不感知“数组”这一高层概念,仅忠实执行上述地址计算并对该内存地址进行读写。当 Index 超出有效范围时,程序便直接访问了非预期的物理内存区域。
假设程序在栈上声明了一个结构:
int buffer[4]; // 假设首地址是 0x1000,每个 int 占 4 字节
int secret_key = 8888; // 紧随其后的另一个变量,地址在 0x1016
在正常的内存布局中,它们紧挨在一起:
buffer[0] → 0x1000
buffer[3] → 0x1012
secret_key → 0x1016 (本不属于 buffer)
如果程序允许你输入 buffer[4],公式计算出地址为 0x1000 + 4 * 4 = 0x1016。此时,程序就会直接把 secret_key 当作数组的第 5 个元素进行读写。
1.2 漏洞触发方向
数组越界根据操作行为,分为越界读(OOB Read)和越界写(OOB Write):
- 越界读(信息泄露)
- 原理:允许读取数组边界之外的内存;
- 结果:如果数组后面存放的是系统的敏感数据(如邻近变量、其他用户的 Session、内存指针、加密密钥等),攻击者就可以像“偷窥”一样将这些隐私数据读取出来;
- 典型案例:著名的“心脏出血(Heartbleed)”漏洞,本质上就是因为未校验用户传入的长度,导致程序把内存中其他用户的明文密码和私钥越界读取并返回给了攻击者;
- 越界写(内存破坏/控制流劫持)
- 原理:允许向数组边界之外的内存写入数据;
- 结果:这是二进制安全中最危险的漏洞类型之一。攻击者可以精准地“踩坏”或篡改邻近的关键内存结构;
- 篡改邻近变量:改变业务逻辑(例如把
is_admin变量从 0 强行改写为 1); - 篡改栈返回地址:在栈(Stack)上,函数的返回地址紧随局部变量之后。如果越界写覆盖了返回地址,当函数执行完毕准备退出时,CPU 就会跳转到攻击者指定的任意恶意代码地址(Shellcode)去执行,从而直接拿下系统最高权限;
- 篡改邻近变量:改变业务逻辑(例如把
1.3 漏洞危害
数组越界漏洞(Out-of-Bounds, OOB)是网络安全领域最具破坏力的“漏洞之王”之一。在 CWE 发布的全球最危险软件缺陷榜单中,数组越界相关的缓冲区溢出长年稳居前三。
根据攻击者操作的是越界写(OOB Write)还是越界读(OOB Read),其危害可以从“轻微闪退”一路飙升至“最高权限沦陷”。以下是其核心危害的深度解析:
1.3.1 远程代码执行(RCE)与系统最高权限沦陷(最严重)
这是越界写能带来的最致命后果。攻击者可以通过精准控制越界写入的数据,强行篡改程序底层的控制流结构:
- 劫持栈返回地址:在栈内存中,函数的返回地址(CPU 接下来要执行的指令位置)紧跟在局部数组后面。攻击者通过越界写,将该地址覆盖为恶意代码(Shellcode)的地址。当函数结束时,CPU 会直接跳转去执行攻击者的代码;
- 破坏虚函数表(vptr/vtable):在 C++ 等面向对象语言中,越界写可以覆盖对象的虚函数表指针,使其指向攻击者伪造的函数列表,从而在对象调用普通方法时触发恶意代码;
- 最终危害:攻击者无需任何密码,即可实现远程命令执行(Remote Code Execution),直接在受害者服务器上植入木马、开启后门或接管整台服务器;
1.3.2 敏感信息大范围泄露(“偷窥”内存)
这是越界读带来的核心危害。程序在读取数组时超过了合法范围,把邻近内存中的其他机密数据一起读取并发送给了用户:
- 泄露核心凭证:内存中往往实时缓存着系统的核心资产,如用户的明文密码、银行卡号、Session 会话令牌、API 密钥等;
- 绕过安全防御(破除 ASLR):现代系统有“地址空间布局随机化(ASLR)”防御,让攻击者找不到代码的具体位置。但攻击者可以利用越界读,把内存中的“函数指针地址”偷读出来,从而推算出整个系统的内存布局,为下一步的恶性攻击铺平道路;
- 历史翻车现场:大名鼎鼎的“心脏出血(Heartbleed)”漏洞(CVE-2014-0160)本质上就是严重的越界读,导致全球三分之二的网站服务器内存可被远程定期“抽水”偷窥;
1.3.3 权限提升
如果数组越界漏洞存在于操作系统内核(如 Windows 内核、Linux Kernel)或高权限服务(如系统的超级管理员组件)中,危害就会成倍放大:
- 本地提权:一个在服务器上只有最低权限的普通用户或恶意软件,可以通过触发内核驱动的数组越界漏洞,篡改当前进程的 Token(令牌)或安全凭证,将自己瞬间提升为 Root 或 SYSTEM 顶级管理员权限;
1.3.4 拒绝服务攻击(DoS / 系统崩溃)
这是最普遍、最保底的危害。即使攻击者无法完美控制内存布局来实现上述高难度攻击,也能轻松让系统瘫痪:
- 引发段错误(Segment Fault):在 C/C++ 中,如果越界访问的地址超出了操作系统分配给该进程的合法内存页边界,系统内核为了自我保护,会立刻向该程序发送崩溃信号;
- 最终危害:核心服务(如 Web 服务器、数据库、网关)瞬间死机闪退。如果是高并发业务,攻击者通过持续发送引发越界的数据包,就能让服务陷入无限死循环崩溃,彻底瘫痪企业业务;
1.3.5 业务逻辑被恶意篡改
如果越界写覆盖的是紧随其后的业务变量,会导致极其诡异的逻辑错误:
struct User {
char username[8];
int is_admin; // 0 表示普通用户,1 表示管理员
};
- 危害示例:当输入
username为 9 个系统字符时,由于没有边界检查,第 9 个字符会直接踩坏旁边的is_admin变量,将其从0变成非零值。攻击者借此直接非法获得了管理员操作权限;
漏洞危害矩阵一览表:
| 漏洞行为 | 内存区域 | 直接影响 | 最终危害 |
| 越界读 | 堆/栈/全局区域 | 内存数据外泄、绕过 ASLR | 数据资产窃取、隐私泄露、连环攻击 |
| 越界写 | 栈 | 覆盖返回地址、局部变量 | 远程控制服务器(RCE)、业务逻辑篡改 |
| 越界写 | 堆 | 破坏堆块元数据、覆盖结构体指针 | 沙箱逃逸、软件彻底崩溃、任意代码执行 |
2. 漏洞触发场景(成因)
在编写源代码时,以下几种逻辑缺陷最容易引发数组越界:
2.1 缺少显式边界检查
直接使用未经过滤的外部输入(如用户传参、网络数据包中的 Length 字段)作为数组下标:
int get_user_data(int index) {
// 恶意用户传入 index = 99999 或 index = -5
return global_array[index];
}
2.2 单字节偏离(Off-by-One)
循环边界条件书写错误,例如:误将循环判定条件 i < SIZE 写成了 i <= SIZE,导致额外多读写了一个元素:
int arr[10];
// 数组最大合法索引是 9,但循环会执行到 i = 10
for (int i = 0; i <= 10; i++) {
arr[i] = 0; // 当 i = 10 时,发生越界写
}
2.3 只检查上限,忽略下限(负数越界)
只记得检查索引不能太大,却忘记了索引还可能是负数:
void update_table(int index, int val) {
if (index >= 100) return; // 看似安全?
table[index] = val; // 如果传入 index = -10,地址向前偏移,直接篡改前方内存
}
2.4 整数溢出导致绕过检查
攻击者输入一个极大的数字,导致内部计算时发生溢出,从而绕过了边界检查:
void process(unsigned int length) {
// 假设攻击者传入 length = 4294967295 (即 0xFFFFFFFF)
// 内部计算 length + 1 时发生整数溢出,结果变成了 0
char *buf = malloc(length + 1); // 实际分配了 0 字节的内存!
// 随后向这个 0 字节的缓冲区写入大量数据,瞬间引发严重的越界写(堆溢出)
memcpy(buf, user_data, length);
}
3. 漏洞真实案例
近两年来,随着人工智能、云计算的爆发以及底层基础软件的演进,数组越界漏洞(Out-of-Bounds, OOB)依然是攻击者和安全研究员对抗的核心,涵盖了主流人工智能框架、顶级浏览器引擎、虚拟化管理器以及工业仿真软件。
3.1 CVE-2025-29834漏洞
CVE-2025-29834 是由微软官方于 2025 年 4 月 披露的 Microsoft Edge 浏览器(基于 Chromium 架构)的高危远程代码执行(RCE)与越界读取(Out-of-Bounds Read)漏洞。该漏洞在通用缺陷枚举中归类为 CWE-125(越界读),微软官方给予其的 CVSS 3.1 评分高达 7.5。作为一个经典的浏览器客户端漏洞,它是现代白帽黑客与浏览器安全沙箱对抗的典型产物。
3.1.1 漏洞原理解析
该漏洞的根本原因,在于 Microsoft Edge 浏览器内置的 V8 JavaScript 引擎在处理复杂的前端数据缓冲区时,其“边界检查(Bounds Checking)”逻辑出现了安全盲点:
- 类型化数组(TypedArray)的过度优化:在现代 JavaScript 引擎中,为了极速处理二进制数据流(如 WebGL、游戏音频、复杂图像),开发人员常使用
ArrayBuffer或Float32Array等类型化数组。V8 引擎内置的 TurboFan 实时编译器(JIT)在编译运行这些 JS 代码时,会尝试推算数组下标的范围; - 安全边界被消灭:如果 JIT 优化器通过数学推算,误判定某段循环或偏置读取代码“绝对不会超出数组长度”,为了追求极致性能,它就会在最终生成的机器码中把运行时的边界检查代码直接删掉;
- 引爆越界读(OOB Read):安全研究人员发现,攻击者可以通过特定的混淆位运算或极端边界条件欺骗 TurboFan 的范围分析器(Range Analysis),让其错误地删除了检查,但实际运行时索引却远远超出了数组原本分配的内存空间。这使得 JavaScript 脚本可以直接读取到该合法数组缓冲区之外的深层浏览器进程内存;
3.1.2 Exploit 攻击链路与现实危害
虽然该漏洞的本质是越界读,但在二进制安全领域,浏览器的越界读往往是远程代码执行(RCE)的关键敲门砖:
- 漏洞触发路径:攻击者无需入侵用户的电脑。他们只需精心构造一个包含恶意 JavaScript 脚本的 Web 网页,并诱导受害者使用受影响版本的 Edge 浏览器去访问该网页(通常通过钓鱼邮件、恶意广告投放等手法);
- 破除 ASLR 防御(内存大泄露):现代操作系统有 ASLR(地址空间布局随机化)保护,攻击者无法知道代码在内存中的具体位置。但利用 CVE-2025-29834,攻击者的 JS 脚本可以像“抽水机”一样在内存中探查,把邻近内存中的函数指针、对象虚函数表(vtable)地址、甚至是其他网站的 Session 会话凭证、加密密钥直接偷读出来;
- 实现远程代码执行(RCE):一旦通过越界读锁定了内存中关键组件的精准基地址,攻击者就可以借此绕过 ASLR,并通常结合另外一个越界写或释放后重用(UAF)漏洞组成“漏洞链(Exploit Chain)”,强行篡改控制流,彻底突破渲染器沙箱,在受害者电脑上直接静默下载并运行任意恶意木马程序;
3.1.3 POC伪代码逻辑
在真实的对抗中,利用 TypedArray 进行范围欺骗的 JavaScript 代码结构通常如下(以下为逻辑概念演示,不包含具体绕过最新沙箱的 ROP 载荷):
// 触发 JIT 优化的函数
function trigger_oob(index) {
// 1. 创建一个固定大小的类型化数组
let typed_array = new Uint32Array(10);
// 2. 精心设计的数学/位运算混淆逻辑(欺骗 TurboFan 的 Range Analysis)
// 让编译器误以为 x 的范围始终在 0~9,从而在编译时将边界检查优化掉
let x = index | 0;
if (x < 0) x = 0;
// 极其复杂的混淆条件...
// 3. 触发越界读写
// 由于检查被删除,当传入 index = 100 时,直接发生 OOB 访问
return typed_array[x];
}
// 强制 V8 引擎对函数进行高强度优化编译(JIT 介入)
for (let i = 0; i < 20000; i++) {
trigger_oob(0);
}
// 传入恶意越界索引,开始读取数组外的内存元数据(破除 ASLR)
let leaked_data = trigger_oob(12);
console.log("[+] 成功越界读取到邻近内存数据: 0x" + leaked_data.toString(16));
3.1.4 Exploit 核心技术链路(过程)
高级浏览器漏洞利用通常需要将“信息泄露”与“控制流篡改”组合成漏洞链(Exploit Chain)。攻击者利用 CVE-2025-29834 的典型 Exploit 构筑过程如下:
3.1.4.1 欺骗 JIT 编译器优化(触发越界)
V8 的 TurboFan 实时编译器在将 JavaScript 编译为底层机器码时,会运行范围分析算法:
- 攻击者编写包含复杂位运算或极端边界逻辑的 JavaScript 代码;
- 这段代码成功误导了优化器,使其得出结论:“该索引永远不可能超出数组长度”,进而为了追求极致性能,删除了最终机器码中的物理边界检查;
- 实际运行时,索引被故意赋予一个极大值,程序直接无视安全屏障,向后访问到了非法内存;
3.1.4.2 实现稳定的读写原语
这是 Exploit 成功的最关键一步。攻击者不会满足于一次性地随机读取,而是希望建立对任意内存的读写控制:
- 内存布局:攻击者在内存中创建大量的
ArrayBuffer(如Float64Array或Uint32Array),并利用堆喷射(Heap Spraying)技术让它们在内存中整齐紧密地排列; - 篡改长度字段:利用由于优化错误引发的首次越界读写,攻击者去精准覆盖紧随其后的第二个合法
ArrayBuffer的Length(长度)或Backing Store(后备存储指针)字段; - 无限蔓延:一旦第二个数组的长度字段被篡改成了
0xFFFFFFFF,攻击者就不再需要依赖原本的漏洞漏洞点,而是可以直接通过标准的 JavaScript 语法(如corrupted_array[index]),去任意读写整片浏览器进程的内存地址空间;
3.1.4.3 破除 ASLR(地址空间布局随机化)防线
现代系统开启了 ASLR 防护,导致内存中各种关键函数、动态库(如 ntdll.dll、kernel32.dll)的物理加载基地址每次启动都在变化,攻击者无法直接通过固定地址执行代码:
- 通过已经建立的任意内存读取原语,攻击者的 JS 脚本可以在内存中向后检索,读取
ArrayBuffer内部的引擎结构体指针; - 通过这些指针的偏移量,攻击者能反向推算出 V8 引擎、浏览器渲染进程以及核心系统组件在当前的精准基地址;
3.1.4.4 控制流劫持与代码执行(RCE)
- 一旦知道了精准的内存布局,攻击者通常会在堆区寻找一个合法的函数指针或 C++ 对象的虚函数表(vtable);
- 攻击者利用越界写将该指针替换为攻击者自己构造的恶意地址;
- 现代对抗:由于现代浏览器部署了 W^X(写不执行,执行不写) 和 V8 Sandbox(V8沙箱防护),攻击者现在很难直接将恶意的 Shellcode 写入内存执行。因此,攻击者通常会利用越界读收集的大量系统代码片段,拼接成 ROP(面向返回导向编程)攻击链,或者利用由于越界引出的类型混淆,强行突破沙箱,让浏览器进程去调用系统的
ShellExecute或WinExec接口,从而在受害者电脑上直接静默下载、运行恶意木马程序;
4. 漏洞防御与修复
防御和修复数组越界漏洞(Out-of-Bounds, OOB)的核心原则是“控制输入、卡死边界、依赖现代化工具”。由于这种漏洞在二进制底层具有毁灭性的破坏力,修复必须从代码开发、编译期防御以及运行时缓解三个维度建立纵深防御体系。
4.1 代码层面的黄金修复法则
4.1.1 前置双向边界检查(最核心)
任何使用变量(尤其是外部用户输入的变量)作为数组下标的地方,必须显式对上限和下限进行严格校验:
// ❌ 错误示例:只查上限没查负数,攻击者传入 -5 直接向前越界
void set_data(int index, int val) {
if (index >= MAX_SIZE) return;
my_array[index] = val;
}
// 现代安全写法:严格的双向检查
void set_data(int index, int val) {
if (index < 0 || index >= MAX_SIZE) {
// 记录日志并安全退出,拒绝执行
log_error("越界攻击拦截:非法索引 %d", index);
return;
}
my_array[index] = val;
}
4.1.2 严防“单字节偏离(Off-by-one)”
循环遍历数组时,必须确保边界条件不会多算一个元素:
- 长度为
N的数组,其合法索引范围是0到N-1; - 审查循环条件,优先使用
i < length,避免使用i <= length;
4.1.3 升级安全的标准内存函数
在 C/C++ 中,彻底弃用传统不检查长度的“危险函数”,一律替换为现代安全版本:
- 禁止使用:
strcpy、strcat、sprintf、memcpy; - 推荐替换:
strncpy、strncat、snprintf、memcpy_s(必须正确传入目标缓冲区大小);
4.1.4 防御整数溢出与符号转换缺陷
- 计算数组动态分配大小时,使用溢出安全函数(如
ckd_mul或标准库溢出检查)防止乘法溢出; - 声明数组索引与长度时,统一使用无符号类型(如
size_t),避免有符号数与无符号数混用导致负数隐式转换成巨大正数;
4.2 各语言现代化特性的安全替换
如果你使用的是支持现代化特性的语言,直接利用其自带的边界保护机制可以实现代码“天生免疫”:
- C++:彻底放弃原生 C 风格数组,全面改用标准库容器:
- 使用
std::vector或std::array; - 访问元素时,用
.at(index)替换[index]。.at()方法在越界时会强制抛出std::out_of_range异常,而不会任由程序去践踏内存;
- 使用
- Rust:由于其严格的借用检查器机制,Rust 默认禁止任何未受保护的内存访问。若必须动态提取元素,可以使用
.get()方法,它会返回一个Option结果,逼迫开发人员必须处理索引越界的失败分支;
4.3 编译器与操作系统层面的加固
在编译二进制程序时,务必开启编译器自带的“安全补丁”功能。即使程序员在代码中漏掉了边界检查,这些机制也能死死卡住漏洞,防止其演变为高危的远程代码执行(RCE):
- 启用栈金丝雀(Stack Canary):在 GCC/Clang 编译时加入参数
-fstack-protector-all。它会在数组和返回地址之间塞入一个随机数(Canary)。如果发生越界写,这个随机数会被率先踩碎,程序在跳转前发现随机数变了,会立即触发自杀机制(Aborted),使攻击者的 Shellcode 彻底失效; - 启用地址空间布局随机化(ASLR):确保操作系统和编译参数(如
-pie -fPIE)开启。这会让程序每次启动时的内存地址都不一样,让攻击者无法精准预测和覆盖目标控制流; - 开启边界检查器(如 ASan):在日常开发测试阶段,使用
-fsanitize=address编译代码。AddressSanitizer 会在程序运行时对每一次数组访问进行严格监控,一旦有任何越界行为,会在第一时间打印出极其详细的崩溃上下文,帮助开发人员在代码上线前掐灭隐患;
4.4 自动化安全发现机制
- 静态代码分析(SAST):在持续集成(CI/CD)流水线中配置 SonarQube、Coverity 等静态扫描工具,它们能通过数据流分析自动揪出那些“未进行边界检查就直接使用”的漏洞点;
- 模糊测试(Fuzzing):使用 AFL++ 或 libFuzzer 工具,针对接收外部网络包、解析复杂结构体文件的数组输入点进行极端的样本变异投喂。模糊测试在爆破数组边界、揪出深层越界写死机点方面具有无可替代的威力;
1. 一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。