8. 整数溢出实战案例:malloc(size)中的整数溢出,数组索引越界,长度计算错误,防御性编程技巧

大家好,我是你们的老朋友。今天咱们聊一个特别实在的话题——整数溢出。说实话,我在代码评审里见过最多的安全漏洞,不是缓冲区溢出,就是整数溢出引发的连锁反应。你想想看,一个看似无害的整数计算,怎么就搞崩了整个程序?

嗯,咱们今天就从三个最常见的实战场景入手:malloc(size)里的整数溢出数组索引越界长度计算错误。最后我会分享一些我这些年积累的防御性编程技巧。

核心观点:整数溢出不是“算错了”那么简单,它是通往任意代码执行的捷径。攻击者只要控制了一个整数,就可能控制整个进程。

8.1 malloc(size) 中的整数溢出

先看一个经典案例。我在项目中遇到过这样的代码:

// 危险版本
char *buffer = (char *)malloc(count * sizeof(char));
if (buffer == NULL) {
    // 处理错误
}

乍一看没问题,对吧?但你想过没有,如果 count 是攻击者可控的,会发生什么?

假设 count = 0x40000001(约 1GB),sizeof(char) = 1。乘积 0x40000001 * 1 = 0x40000001,没溢出。但如果 count = 0x40000001,而 sizeof(SomeStruct) = 64 呢?

计算一下:0x40000001 * 64 = 0x1000000040,这个值超过了 32 位整数的最大值 0xFFFFFFFF。结果呢?截断后变成 0x00000040,也就是只分配了 64 字节!

然后你往里面写数据,写个 1GB……嗯,堆溢出就发生了。

警告:malloc 的参数类型是 size_t,在 32 位系统上是 32 位无符号整数,在 64 位系统上是 64 位无符号整数。但即使是在 64 位系统上,如果乘积超过 SIZE_MAX,同样会溢出。

我个人习惯的做法是:

// 安全版本
if (count > SIZE_MAX / sizeof(SomeStruct)) {
    // 溢出,拒绝分配
    return NULL;
}
char *buffer = (char *)malloc(count * sizeof(SomeStruct));

说白了,就是先做除法检查,再做乘法。这个技巧叫“溢出前检查”,我几乎在每个涉及动态分配的项目里都用它。

8.2 数组索引越界

数组越界,听起来很基础对吧?但整数溢出能让它变得防不胜防。

我记得有一次审计第三方库,看到这样的代码:

int index = get_user_input();  // 用户可控
if (index < 0 || index >= MAX_ELEMENTS) {
    // 越界,拒绝
    return -1;
}
array[index] = value;

看起来没问题?等等,如果 index 是负数,检查能拦住。但如果 index 是一个很大的正数,比如 0x7FFFFFFF,而 MAX_ELEMENTS100,那检查也能拦住。

但问题出在另一种情况:索引计算中的整数溢出

int base = get_base_offset();  // 用户可控
int offset = get_offset();     // 用户可控
int index = base + offset;     // 可能溢出!
if (index < 0 || index >= MAX_ELEMENTS) {
    return -1;
}
array[index] = value;

如果 base = 0x7FFFFFFFoffset = 1,那么 base + offset 会变成负数(因为溢出)。然后检查 index < 0 为真,被拦住。但如果攻击者精心构造,让 base + offset 溢出后恰好落在一个合法的正数范围内呢?

举个例子:base = 0x7FFFFFFFoffset = 2,结果 0x7FFFFFFF + 2 = 0x80000001,这是负数,被拦住。但如果 base = 0x7FFFFFFEoffset = 3,结果 0x7FFFFFFE + 3 = 0x80000001,还是负数。

嗯,这里要注意:有符号整数溢出是未定义行为。编译器可以优化掉你的检查!

避坑指南:我曾经在一个嵌入式项目里,因为编译器优化把溢出检查给优化掉了,导致一个数组越界漏洞在生产环境里潜伏了半年。从那以后,我坚持用无符号整数做索引,并且用 unsigned 类型做边界检查。

正确的做法:

size_t base = (size_t)get_base_offset();
size_t offset = (size_t)get_offset();
if (base > SIZE_MAX - offset) {
    // 溢出,拒绝
    return -1;
}
size_t index = base + offset;
if (index >= MAX_ELEMENTS) {
    return -1;
}
array[index] = value;

8.3 长度计算错误

长度计算错误,说白了就是“你以为你算对了,其实没有”。

看一个我经常在代码里看到的模式:

// 计算需要分配的总大小
size_t header_size = sizeof(PacketHeader);
size_t payload_size = get_payload_size();  // 用户可控
size_t total_size = header_size + payload_size;

char *packet = (char *)malloc(total_size);
if (packet == NULL) {
    return -1;
}
memcpy(packet + header_size, payload_data, payload_size);

这里的问题在哪?如果 payload_size 非常大,比如 0xFFFFFFFF,而 header_size64,那么 total_size = 64 + 0xFFFFFFFF = 0x10000003F,截断后变成 0x0000003F,也就是 63 字节。

然后 malloc(63) 成功,但 memcpy 要复制 0xFFFFFFFF 字节……堆溢出,妥妥的。

我个人的修复方案:

if (payload_size > SIZE_MAX - header_size) {
    // 溢出,拒绝
    return -1;
}
size_t total_size = header_size + payload_size;

或者更简洁的:

size_t total_size = header_size + payload_size;
if (total_size < header_size || total_size < payload_size) {
    // 溢出,拒绝
    return -1;
}

这个技巧叫“回绕检查”——如果加法溢出,结果会小于任何一个加数。

8.4 防御性编程技巧

好了,讲了这么多案例,咱们总结一下我这些年积累的防御性编程技巧。

技巧 说明 示例
溢出前检查 在乘法或加法之前,先判断是否可能溢出 if (a > SIZE_MAX / b) { /* 溢出 */ }
使用无符号类型 无符号整数溢出是明确定义的(回绕),便于检查 size_tint 安全
避免有符号整数运算 有符号溢出是未定义行为,编译器可能优化掉你的检查 size_t 代替 int
使用安全函数 calloc 自带溢出检查 calloc(count, size)malloc(count * size) 安全
边界检查统一化 所有索引和长度计算,统一用 size_t 类型 避免混用 intsize_t

我个人最推荐的一条:能用 calloc 就别用 malloc。为什么?因为 calloc(count, size) 内部会检查 count * size 是否溢出,如果溢出会返回 NULL。而且它还会把分配的内存清零,一举两得。

另外,我建议在代码里显式地使用 __builtin_add_overflow 这类编译器内置函数(GCC/Clang 支持):

size_t result;
if (__builtin_add_overflow(a, b, &result)) {
    // 溢出处理
}

这些内置函数在溢出时返回 true,并且不会产生未定义行为。我在一些对安全性要求极高的项目里,会强制团队使用这些函数。

8.5 知识体系图

下面这张图总结了整数溢出在内存分配场景中的攻击路径和防御措施:

整数溢出攻击路径与防御 攻击者控制输入 malloc(size) 溢出 数组索引越界 长度计算错误 分配过小内存 访问非法内存 memcpy 溢出 堆溢出 → 任意代码执行 防御:溢出前检查 | 使用 calloc | 无符号类型 | 编译器内置函数 __builtin_add_overflow / __builtin_mul_overflow

最后说一句:整数溢出不是理论问题,它是实实在在的安全威胁。我见过太多因为少写一行检查代码,导致整个系统被攻破的案例。防御性编程不是“多此一举”,而是“保命符”。

好了,这一章的内容就到这里。记住:每一个整数都可能成为攻击者的跳板。写代码时多问自己一句:“这个值会不会溢出?”——这习惯能救你很多次。


公众号:蓝海资料掘金营,微信deep3321