整数溢出与下溢

在计算机硬件与低级语言(如 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. 计算转账总额amountamount = 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;
  • 逻辑失效与内存崩溃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,在测试阶段捕获下溢行为。

觉得有帮助可以赞赏本文哦~万分感谢!
文章:整数溢出与下溢
作者:沛旗
链接:https://www.peiqiblog.com/article/14301/
版权声明::本博客站点所有文章除特别声明外,均采用 CC BY-NC-SA 4.0协议
转载请注明文章地址及作者哦~
暂无评论

发送评论(禁止发表一切违反法律法规的敏感言论) 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇