在计算机硬件与低级语言(如 C/C++)中,数值以固定位宽的二进制形式存储。当计算结果超出了当前数据类型所能表示的极限时,硬件会直接舍弃最高位的进位/借位(Carry/Borrow Bit),从而导致正数变为负数、大数变成极小数,或者负数变成极大的正数。
整数溢出(Integer Overflow)与整数下溢(Integer Underflow)是底层开发(尤其 C/C++)以及涉及数值计算的业务系统(如智能合约、金融系统)中最常见的数值安全漏洞。
1. 漏洞原理与成因
整数溢出(Overflow)与下溢(Underflow)的本质,是计算机硬件存储数字的位数有限。当计算结果超出了该数据类型能表示的最大值(溢出)或最小值(下溢)时,数值会发生卷绕(Wrap-around),导致逻辑完全错乱。简单来说,其核心原理是固定位宽(Bit-width)下数值的二进制“卷绕”。
1.1 漏洞成因(根源)
计算机使用固定长度的比特(Bit)来存储整数,例如:
- 8 位无符号整数 (
uint8_t):范围是 0 ~ 255(2^8 – 1); - 8 位有符号整数 (
int8_t):范围是-128 ~ 127;
由于存储空间有限,数值的增加或减少不是无限延伸的,而是在边界处发生“回卷”:
【无符号8位整数的回卷机制】
255 (最大值)
/ \
(+1) 溢出 / \ (-1) 下溢
v v
0 (最小值)
1.2 整数溢出
整数溢出漏洞是指程序在执行算术运算时,计算结果超出了所用数据类型在内存中能够表示的物理极限,导致二进制高位被硬件直接丢弃、数值发生“回卷”(Wrap-around)的安全缺陷。
当运算结果大于该类型能表示的最大值时触发:
- 无符号数溢出示例:超上限归零(如 uint32 的 4,294,967,295 + 1 = 0);
- 有符号数示例(以补码形式):正数加成负数(如 int8 的 127 + 1 = -128)。在 C/C++ 标准中,有符号数溢出属于未定义行为,编译器可能会优化掉后续的安全检查代码;
溢出的二进制形式表示:
二进制:11111111 + 00000001 = 100000000(第 9 位被截断丢弃)
结果变成0
1.3 整数下溢
整数下溢漏洞是指当算术运算(主要是减法或递减)的结果小于该数据类型在内存中所能表示的最小值时,引发二进制“借位卷绕”(Borrow Wrap-around),从而导致数值变为极大正数或符号反转的安全缺陷。
与整数溢出向上突破上限不同,下溢是向下滑落越过下限。当运算结果小于该类型能表示的最小值时触发:
- 无符号数下溢:产生了最高位的物理借位(Borrow),数值直接从最小值卷绕(Wrap-around)变为了该类型能表示的最大值;例如uint32 的 0 – 1 = 4,294,967,295(负数减法变成了极大的正数);
- 有符号数下溢示例(以补码形式):超出负数下限后符号位翻转,变为极大的正数。在 C/C++ 中,有符号数下溢同样属于未定义行为;例如-128 – 1 -> 127;
下溢的二进制形式表示:
二进制:00000000 - 00000001(向高位借位)
结果:255(11111111)
2. 漏洞场景与危害
整数溢出/下溢本身通常只是一次错误的数学计算,但当计算结果被用于内存分配、长度校验、数组索引或余额扣减时,就会引发致命安全问题。
2.1 场景一:引发缓冲区溢出漏洞
这是 C/C++ 等语言中最经典的攻击向量。
void allocate_array(size_t num_elements) {
// 假设 num_elements 是攻击者传入的 0x40000001 (1073741825)
// 在 32 位系统中,4 * 0x40000001 = 0x100000004
// 发生了 32 位整数溢出,高位丢弃,实际大小变成了 4 字节!
size_t size = num_elements * sizeof(int);
int *buffer = (int *)malloc(size); // 仅分配了 4 字节内存
for (size_t i = 0; i < num_elements; i++) {
buffer[i] = 0; // 试图写入 10 亿个 int,引发严重的堆缓冲区溢出!
}
}
该代码可触发严重的堆缓冲区溢出,攻击者可利用其覆盖邻近堆块的结构指针,实现远程代码执行(RCE);
2.2 绕过边界安全校验
程序试图通过减法计算剩余空间,却触发了下溢:
void process_data(unsigned int user_len) {
unsigned int buf_size = 100;
// 如果攻击者传入 user_len = 105
// buf_size - user_len 计算得出 100 - 105 = -5
// 由于是无符号运算,-5 下溢卷绕变为 4,294,967,291
if (buf_size - user_len > 10) {
// 条件判断:4,294,967,291 > 10 为真!校验被成功绕过
memcpy(dest, src, user_len); // 拷贝 105 字节到 100 字节的缓冲区,溢出!
}
}
2.3 场景三:业务逻辑与金融越权
在电商、支付系统或区块链智能合约(如 Solidity 0.8 之前的版本)中非常常见:
- 转账/购买空手套白狼: 在购买商品时,总价 = 单价 * 数量。若单价为 100 元,攻击者购买 42,949,673 个商品(在 32 位无符号数下,100 * 42949673 = 4294967300,溢出后变成 4 元),从而以 4 元的价格购入上千万元的商品;
- 余额归零下溢: 账户余额为0,攻击者发起1元提现。如果缺乏严格下溢检查,0-1 卷绕变为账户拥有 2^64-1 元的天文数字;
3. 漏洞实例与演示
3.1 实例 1:C/C++ 内存分配中的整数溢出
这是 C/C++ 漏洞中最经典的模式,且广泛存在于图像解析库、媒体播放器和操作系统内核中。
存在漏洞的代码示例:
// 假设这是一个处理图像文件的函数,width 和 height 来自文件头数据
void process_image(unsigned int width, unsigned int height, unsigned char *src_pixels) {
// 漏洞点:两个 32 位无符号整数相乘,结果可能发生溢出
unsigned int bytes_needed = width * height * 4; // 每个像素 4 字节 (RGBA)
// 分配内存
unsigned char *buffer = (unsigned char *)malloc(bytes_needed);
if (!buffer) return;
// 拷贝像素数据
for (unsigned int i = 0; i < width * height; i++) {
buffer[i * 4 + 0] = src_pixels[i * 4 + 0]; // R
buffer[i * 4 + 1] = src_pixels[i * 4 + 1]; // G
buffer[i * 4 + 2] = src_pixels[i * 4 + 2]; // B
buffer[i * 4 + 3] = src_pixels[i * 4 + 3]; // A
}
}
假设攻击者构造了一个恶意图像文件,文件头中的尺寸参数为:width = 65,536 (2^16),height = 65,537 (2^16 + 1):
- 计算需要的内存大小:数学真值 = 65,536 * 65,537 * 4 = 17,180,131,328字节 (约16GB);
- 发生 32 位整数溢出: 在 32 位系统中,
unsigned int最大值为 4,294,967,295 (2^32-1); 17,180,131,328 (mod 2^32) = 262,144 字节 (仅 256KB); - 结果:
malloc仅成功分配了 256 KB 的堆内存;- 随后的
for循环将尝试向这块内存写入 16 GB 的数据; - 触发严重的堆缓冲区溢出,攻击者可利用其覆盖邻近堆块的结构指针,实现远程代码执行(RCE);
在相乘前使用除法检查溢出条件,做出健壮性判断,正确的修复代码如下:
// 安全校验:防止 width * height * 4 溢出
if (width > 0 && height > UINT_MAX / width / 4) {
// 拒绝处理,抛出异常
return;
}
3.2 以太坊美链 (BEC) 智能合约批量转账溢出
2018 年,BeautyChain (BEC) 智能合约因整数溢出漏洞导致代币价值瞬间归零。
存在漏洞的代码示例 (Solidity 0.4.x):
function batchTransfer(address[] _receivers, uint256 _value) public returns (bool) {
uint cnt = _receivers.length;
// 漏洞点:cnt * _value 可能会发生 256 位无符号整数溢出
uint256 amount = uint256(cnt) * _value;
require(cnt > 0 && cnt <= 20);
require(_value > 0 && balances[msg.sender] >= amount); // 余额校验
// 扣除发送者余额
balances[msg.sender] = balances[msg.sender] - amount;
// 给接收者发币
for (uint i = 0; i < cnt; i++) {
balances[_receivers[i]] = balances[_receivers[i]] + _value;
}
return true;
}
攻击者向 batchTransfer 传入了 2 个地址 (cnt = 2),并设置 _value 为:
_value = 2^255 = 0x8000000000000000000000000000000000000000000000000000000000000000
- 1. 计算转账总额amount:amount = 2 * 2^255 = 2^256;
- 2. 触发256位溢出:Solidity 中 256 位无符号数的最大存储上限为 2^256-1。2^256 溢出后最高位舍弃,amount 变为 0;
- 绕过安全校验:
require(balances[msg.sender] >= 0)轻松通过(哪怕账户只有0个代币); - 凭空增发:
- 攻击者的余额扣减:
balances[msg.sender] - 0(保持不变); - 两个接收者账户各增加了 2^255 个代币;
- 攻击者的余额扣减:
- 结果:攻击者瞬间凭空生成了天文数字的 BEC 代币并提现抛售,导致币值蒸发;
漏洞修复与建议:
- 使用 SafeMath 库进行算术运算(如
cnt.mul(_value)); - 升级至 Solidity >= 0.8.0(编译器底层默认内置了溢出检查,溢出时自动
revert回滚事务);
3.3 网络数据包解析中的整数下溢 (引发越界内存读取)
网络数据包解析时,处理“数据包头长度”与“总长度”的差值经常触发下溢。
存在漏洞的代码示例:
struct Packet {
uint16_t total_length; // 整个数据包声明的长度
uint16_t header_length; // 报头声明的长度
char data[512];
};
void parse_packet(struct Packet *pkt) {
// 漏洞点:未校验 total_length 是否 >= header_length
// 发生 16 位无符号数下溢
uint16_t payload_length = pkt->total_length - pkt->header_length;
char response_buf[1024];
// 如果 payload_length 变成了一个极大的数,这里会发生大内存拷贝
if (payload_length > 0) {
printf("Payload length: %u\n", payload_length);
memcpy(response_buf, pkt->data, payload_length); // 栈缓冲区溢出
}
}
假如攻击者精心构造了一个破损或恶意的网络包:
声明 total_length = 10 字节
声明 header_length = 20 字节(非法参数,报头比总长还长)
- 发生无符号整数下溢:
payload_length = 10 - 20 = -10- 在 16 位无符号整数 (
uint16_t) 中,-10 下溢卷绕表示为:65,536-10=65,526;
- 在 16 位无符号整数 (
- 逻辑失效与内存崩溃:
payload_length变成了巨大的正整数 65,526。memcpy尝试从pkt->data拷贝 65,526 字节到仅 1024 字节大小的response_buf中,直接引发栈缓冲区溢出并导致服务崩溃;
正确的修复代码示例,进行防御性断言与输入校验:
// 防御性校验:必须确保总长度大于等于头部长度
if (pkt->total_length < pkt->header_length) {
log_error("Invalid packet format!");
return;
}
uint16_t payload_length = pkt->total_length - pkt->header_length;
4. 漏洞现状与防御
4.1 主流语言的默认处理机制
不同编程语言对这类漏洞的防御机制完全不同:
- C / C++:默认不进行任何检查。有符号整数溢出属于“未定义行为”(Undefined Behavior),编译器可能会直接优化掉相关的检查代码,隐患极大;
- Java / C#:默认不报错,直接翻转。但 C# 支持使用
checked关键字,在发生溢出时抛出异常; - Python (3.x):整数对象会自动扩容为任意精度(长整数),因此原生不支持固定位数的整数溢出(但如果调用 C 语言底层扩展库仍需注意);
- Solidity (智能合约):在 0.8.0 版本之前默认会溢出(需使用
SafeMath库);0.8.0 及之后版本默认内置了溢出检查,发生溢出时会自动回滚交易;
4.2 运算前进行严格的边界校验
在执行加、减、乘运算前,先检查操作数是否会触发溢出,而不是运算后再检查:
加法防溢出示例(A + B):
if (A > MAX_UINT - B) {
// 触发溢出处理逻辑
}
减法防下溢示例(A – B):
if (A < B) {
// 触发下溢处理逻辑
}
4.3 使用内置安全函数或开发库
- GCC / Clang 内建函数:使用
__builtin_add_overflow、__builtin_sub_overflow、__builtin_mul_overflow,它们可以在硬件层检测溢出标志位; - C++:使用
Microsoft SafeInt库或std::clamp; - Java:选用高精度/防溢出库,比如
BigInteger/BigDecimal; - Rust:默认在 debug 模式下检测溢出并 panic,也可以显式使用
checked_add()、saturating_add()(饱和加法); - Solidity (以太坊):使用 SafeMath 库,或升级到 Solidity >=0.8.0(默认包含溢出检查);
4.4 编译器与动态监测
C/C++ 项目使用 C/C++ 编译器的 Sanitize 选项编译参数中增加 -fsanitize=undefined 或 -fsanitize=integer,在测试阶段捕获下溢行为。