0x00 整数
在 C 语言中,整数的基本数据类型分为短整型 (short),整型 (int),长整型 (long),这三个数据类型还分为有符号和无符号,每种数据类型都有各自的大小范围(编译器版本为64位gcc-5.4),如下所示:
类型 | 字节 | 范围 |
short int | 2byte(word) | 0~32767(0~0x7fff) -32768~-1(0x8000~0xffff) |
unsigned short int | 2byte(word) | 0~65535(0~0xffff) |
int | 4byte(dword) | 0~2147483647(0~0x7fffffff) -2147483648~-1(0x80000000~0xffffffff) |
unsigned int | 4byte(dword) | 0~4294967295(0~0xffffffff) |
long int | 8byte(qword) | 正: 0~0x7fffffffffffffff 负: 0x8000000000000000~0xffffffffffffffff |
unsigned long int | 8byte(qword) | 0~0xffffffffffffffff |
当程序中的数据超过其数据类型的范围,则会造成溢出,整数类型的溢出被称为整数溢出。
0x01 上界溢出
计算机底层指令是不区分有符号和无符号的,数据都是以二进制形式存在 (编译器的层面才对有符号和无符号进行区分,产生不同的汇编指令)。
# 伪代码
short int a;
a = a + 1;
# 对应的汇编
movzx eax, word ptr [rbp - 0x1c]
add eax, 1
mov word ptr [rbp - 0x1c], ax
unsigned short int b;
b = b + 1;
# assembly code
add word ptr [rbp - 0x1a], 1
add 0x7fff, 1 == 0x8000
,对无符号整型没有影响,但是在有符号短整型中,0x7fff
表示的是 32767
,但是 0x8000
表示的是 -32768
,用数学表达式来表示就是在有符号短整型中 32767+1 == -32768
。
add eax, 1 == 0x10000
,无符号的汇编代码是对内存进行加法运算 add word ptr [rbp - 0x1a], 1 == 0x0000
。在有符号的加法中,虽然 eax
的结果为 0x10000,但是只把 ax=0x0000
的值储存到了内存中,从结果看和无符号是一样的。
再从数字层面看看这种溢出的结果,在有符号短整型中,0xffff==-1,-1 + 1 == 0
,从有符号看这种计算没问题,但是在无符号短整型中,0xffff == 65535, 65535 + 1 == 0
。
0x02 下界溢出
下界溢出的道理和上界溢出一样,在汇编代码中,只是把 add
替换成了 sub
。
第一种是 sub 0x0000, 1 == 0xffff
,对于有符号来说 0 - 1 == -1
没问题,但是对于无符号来说就成了 0 - 1 == 65535
。
第二种是 sub 0x8000, 1 == 0x7fff
,对于无符号来说是 32768 - 1 == 32767
是正确的,但是对于有符号来说就变成了 -32768 - 1 = 32767
。
0x03 未限制范围
假设有一个固定大小的桶,往里面倒水,如果你没有限制倒入多少水,那么水则会从桶中溢出来。
一个有固定大小的东西,你没有对其进行约束,就会造成不可预期的后果。
示例:
#include<stddef.h>
int main(void)
{
int len;
int data_len;
int header_len;
char *buf;
header_len = 0x10;
scanf("%uld", &data_len);
len = data_len+header_len
buf = malloc(len);
read(0, buf, data_len);
return 0;
}
gdb调试输入-1,发现malloc申请了0xF大小的堆,但是可以输入0xFFFFFFFF长度的数据,即通过整型溢出发生堆溢出
0x04 错误的类型转换
void check(int n)
{
if (!n)
printf("vuln");
else
printf("OK");
}
int main(void)
{
long int a;
scanf("%ld", &a);
if (a == 0)
printf("Bad");
else
check(a);
return 0;
}
输入一个大于int大小的值,程序会返回vuln,这是由于长整型a传入check函数后变成了范围小的整型变量n,造成了数据溢出。已经长整型的占有 8 byte 的内存空间,而整型只有 4 byte 的内存空间,所以当 long -> int,将会造成截断,只把长整型的低 4byte 的值传给整型变量。
而范围小的变量把值传给范围更大的变量时,就不会造成数据丢失。
有的函数会对输入的参数进行强制类型转换,比如read()的第三个参数指定了输入的长度,这个参数的类型为 size_t ,相当于 unsigned long int ,属于无符号长整型,如果这个参数被定义成有符号整型,输入-1会导致read可以读入很长一段数据。例题:
$ cat test3.c
int main(void)
{
int len, l;
char buf[11];
scanf("%d", &len);
if (len < 10) {
l = read(0, buf, len);
*(buf+l) = 0;
puts(buf);
} else
printf("Please len < 10");
}
$ gcc test3.c
$ ./a.out
-1
aaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaa
题目来源:整数溢出 - CTF Wiki