整数溢出
整数溢出
本文适合
能读 C/反编译代码,但容易把长度检查、类型转换和内存操作割裂看的 Pwn 学习者。学完你能:识别有符号/无符号混用、加乘溢出和截断,把异常数值转化为越界读写、分配不足、负索引或权限绕过
整数溢出不是“数字变大了”这么简单。它发生在整数运算结果超出类型能表示的范围,导致结果回绕、截断或符号解释错误。Pwn 中它经常把一次看似安全的长度检查变成越界读写。
整数类型有边界
不同整数类型有不同范围。
例如 8 位无符号整数范围是 0 到 255。
如果再加 1,结果可能回到 0。
有符号整数还涉及正负范围和符号位。
CTF 中要关注变量类型,而不是只看数学表达式。
回绕和截断
回绕:结果超过最大值后从最小值继续。
截断:大类型赋值给小类型,高位被丢掉。
例如一个长度先用 int 计算,再转成 short 或 char,就可能出现逻辑不一致。
这种不一致常常出现在内存分配、复制长度、数组索引和循环边界中。
有符号和无符号混用
有符号数可以表示负数,无符号数不能。
如果负数被当成无符号数解释,可能变成一个非常大的正数。
如果无符号数和有符号数比较,编译器的类型提升规则可能让检查结果和直觉不同。
这类问题常导致:
- 负数绕过上界检查。
- 大数变小导致分配不足。
- 小数变大导致越界复制。
- 索引检查失效。
典型危险模式
先检查后计算:
if (len < 100) malloc(len + header_size)如果 len + header_size 溢出,分配大小可能变小。
计算使用一种类型,复制使用另一种类型。
用户控制数组长度、元素数量、单个元素大小时,乘法溢出尤其常见。
整数溢出如何变成利用
整数溢出本身只是逻辑错误。
它通常要进一步造成:
- buffer overflow。
- heap overflow。
- OOB read。
- OOB write。
- 类型混淆。
- 权限检查绕过。
所以分析时要追踪溢出后的值被用在哪里。
有符号与无符号混淆详解
C 语言中,有符号和无符号比较时的隐式转换规则:
int len = -1;
unsigned int max = 100;
if (len < max) {
// len 会被转换为无符号数
// -1 变成 0xFFFFFFFF(4294967295)
// 0xFFFFFFFF > 100,条件为 false
// 检查被绕过!
}常见的危险模式:
// 模式 1:有符号检查,无符号使用
int check_len(int len) {
if (len < 0 || len > 100) return -1; // 有符号检查
return 0;
}
void use_len(unsigned int len) { // 无符号使用
memcpy(buf, src, len); // len 很大时溢出
}
// 模式 2:隐式类型提升
void vuln(int idx) {
if (idx < 10) { // 有符号比较
// idx 可以是负数
array[idx] = value; // 负数索引,越界访问
}
}
// 模式 3:size_t 和 int 混用
void vuln(int len) {
size_t size = len; // int -> size_t 转换
if (size > 0x1000) return; // size 是无符号,负数变大数
char *buf = malloc(size);
read(0, buf, len); // len 是有符号,可能为负数
}size_t 利用
size_t 是无符号整数,通常等于指针大小(32 位系统 4 字节,64 位系统 8 字节):
// size_t 溢出示例
void vuln(char *input, size_t len) {
// len 是无符号数
// 如果 len 非常大,len + 1 可能溢出变成小数
size_t total = len + sizeof(header); // 溢出!
if (total < len) {
// 溢出发生,total 变小
printf("Overflow detected\n"); // 但检查可能被跳过
}
char *buf = malloc(total); // 分配了很小的缓冲区
memcpy(buf, input, len); // 复制了大量数据 -> 堆溢出
}# size_t 溢出利用
from pwn import *
# 64 位系统,size_t 是 8 字节
# 0xFFFFFFFFFFFFFFFF + 1 = 0
# 如果 len = 0xFFFFFFFFFFFFFFFF
# len + 0x10 = 0x0F
# 构造溢出
payload = b'A' * 0xFFFFFFFFFFFFFFFF # 实际输入长度
# 或者利用程序逻辑:
# len = -1 被转为 size_t = 0xFFFFFFFFFFFFFFFF整数溢出导致堆溢出
// 典型漏洞代码
struct chunk {
int size;
char data[0];
};
void vuln() {
int count;
printf("count: ");
scanf("%d", &count);
// 整数溢出:count * sizeof(int)
// 如果 count = 0x40000001
// count * 4 = 0x100000004 -> 溢出为 4
int *arr = malloc(count * sizeof(int));
for (int i = 0; i < count; i++) {
scanf("%d", &arr[i]); // 写入 count 个 int,但只分配了 4 字节
}
}# 利用整数溢出实现堆溢出
from pwn import *
p = process('./vuln')
# 触发 count * 4 溢出
count = 0x40000001 # 1073741825
p.sendline(str(count).encode())
# 现在可以写入大量数据,但堆空间很小
# 造成堆溢出
payload = b'A' * 0x100 # 覆盖相邻 chunk
p.sendline(payload)编译器相关行为
符号扩展
// 不同编译器对符号扩展的处理可能不同
char c = 0xFF; // c = -1(有符号)或 255(无符号)
int i = c; // 符号扩展:0xFFFFFFFF 或 0x000000FF
// GCC 和 Clang 默认 char 是 signed
// 某些 ARM 编译器默认 char 是 unsigned整数提升规则
// C 标准的整数提升规则
// 1. 比 int 小的类型会被提升为 int
// 2. 有符号和无符号混合时,转为无符号
unsigned short a = 0xFFFF;
unsigned short b = 0x0001;
int c = a + b; // a 和 b 先提升为 int
// 结果是 0x10000(65536),不是 0整数溢出检测
# Python 中整数不会溢出,需要手动检测
def check_overflow_add(a, b, bits=32):
result = a + b
if result >= (1 << bits):
return True # 溢出
return False
def check_overflow_mul(a, b, bits=32):
result = a * b
if result >= (1 << bits):
return True
return False
# C 语言中可以用 __builtin_add_overflow 检测
# if (__builtin_add_overflow(a, b, &result)) {
# // 溢出发生
# }整数溢出 CTF 实战
from pwn import *
# 步骤 1:识别变量类型
# 查看反编译代码中的变量类型
# int, unsigned int, size_t, short, unsigned short
# 步骤 2:找到检查和使用的类型不一致
# if (len > 100) return; // 有符号检查
# memcpy(buf, src, len); // 可能无符号使用
# 步骤 3:构造溢出值
# 目标:绕过检查,但实际使用时值很大
# 或者:乘法溢出,使分配大小变小
# 步骤 4:验证溢出
# 使用 GDB 观察溢出后的变量值
# 步骤 5:利用溢出
# 通常转化为堆溢出或栈溢出常见整数溢出题目模式
乘法溢出
// count * size 溢出
int count = 0x40000000;
int size = 4;
int total = count * size; // 溢出为 0
char *buf = malloc(total); // malloc(0)加法溢出
// len + header 溢出
int len = 0x7FFFFFFF;
int header = 10;
int total = len + header; // 溢出为负数减法下溢
// unsigned 减法下溢
unsigned int idx = 0;
idx = idx - 1; // 变成 0xFFFFFFFF截断
// long 赋值给 int
long big = 0x100000000;
int small = big; // small = 0常见误区
- 只看输入是否大,不看变量类型。
- 忽略乘法溢出。
- 不区分 signed 和 unsigned。
- 看到检查
len < max就认为安全。 - 没有追踪溢出结果如何影响内存操作。
一句话判断
当用户能控制长度、数量、索引、价格、大小或循环边界,并且代码里有类型转换、加乘计算或 signed/unsigned 混用时,就按整数溢出排查。
整数溢出本身不是最终利用,必须继续追踪它是否导致分配不足、越界复制、负索引、越界写或逻辑绕过。
题目中常见信号
scanf("%d")读入长度或索引。malloc(count * size)、len + header、idx - 1。- 反编译里变量类型出现
char、short、int、size_t混用。 - 上界检查存在,但没有下界检查。
- 负数输入表现异常。
- 超大数导致分配很小,但后续循环仍写很多。
- Web/业务题中数量乘价格变负,Pwn 题中长度变成越界读写。
核心概念
整数漏洞常见三段链:
用户输入 -> 类型/计算异常 -> 内存操作或逻辑判断出错要同时记录两个值:
检查时的值:是否通过 if
使用时的值:malloc/read/memcpy/array index 实际看到什么如果检查和使用的类型不同,安全判断就可能失效。
最小分析流程
- 找所有用户控制的数字:长度、数量、索引、大小。
- 标注变量类型和位宽:signed/unsigned、32/64 位。
- 找计算点:加、减、乘、强制转换、赋值给小类型。
- 找检查点:上界、下界、是否允许负数。
- 找使用点:malloc、read、memcpy、数组访问、循环。
- 用 Python 或 GDB 计算边界值。
- 验证异常值是否改变内存操作范围。
- 再把越界读写接到 堆基础、栈、返回地址与控制流 或任意写路线。
最小验证示例
验证乘法溢出:
def u32(x):
return x & 0xffffffff
count = 0x40000001
alloc_size = u32(count * 4)
print(hex(alloc_size)) # 0x4GDB 中确认:
b *vuln+120
r
p/x count
p/x count*4
x/gx $rax成功标准:能说明“检查通过时看到的值”和“内存操作实际使用的值”不同,并能观察到越界读写或分配不足。
常见利用 / 解题路线
路线总览:
路线一:乘法溢出导致堆溢出
malloc(count * size) 分配很小 -> 循环写 count 次 -> 覆盖相邻 chunk关联:堆基础。
路线二:负索引 OOB
只检查 idx < n 没检查 idx >= 0,负数索引可写数组前面的指针或函数表。
路线三:size_t 转换
负数转无符号变超大,或者超大数加 header 回绕成小数。
路线四:截断
64 位长度存到 16 位/8 位字段后变小,检查小值,复制大值。
路线五:逻辑绕过
数量、价格、积分等溢出成负数或小数,绕过余额/权限限制,再进入后续漏洞。
常见失败原因
- 只构造大数不看位宽:32 位和 64 位回绕点不同。
- Python 默认不溢出:需要手动
& 0xffffffff或按类型模拟。 - 忽略编译器提升:
short + short可能先提升为 int。 - 触发了检查但没到使用点:必须让异常值流到
memcpy/malloc/array。 - 崩溃不等于利用:还要判断覆盖目标是否可控。
- 负数被前置过滤:需要找其他截断或乘法边界。
迷你案例
代码:
int count;
scanf("%d", &count);
buf = malloc(count * 4);
for (int i = 0; i < count; i++) {
read_int(&buf[i]);
}输入:
count = 0x40000001count * 4 在 32 位 int 中回绕为 4,只分配 4 字节,但循环写入大量元素。后续利用方向取决于相邻对象:如果覆盖函数指针,走控制流劫持;如果覆盖 chunk 元数据,转 堆基础。