- 论坛徽章:
- 0
|
整数溢出与程序安全
----[ 2.1 Widthness 溢出
所以整数溢出是尝试存储一个大数到一个变量中,由于这个变量太小不足以存储该大数导致的结
果.用最简单的例子来说明这个问题,存储一个大变量到一个小变量中去:
/* ex1.c - loss of precision */
#include <stdio.h>;
int main(void){
int l;
short s;
char c;
l = 0xdeadbeef;
s = l;
c = l;
printf("l = 0x%x (%d bits)\n", l, sizeof(l) * 8);
printf("s = 0x%x (%d bits)\n", s, sizeof(s) * 8);
printf("c = 0x%x (%d bits)\n", c, sizeof(c) * 8);
return 0;
} /* EOF */
让我们看看执行结果
nova:signed {48} ./ex1
l = 0xdeadbeef (32 bits)
s = 0xffffbeef (16 bits)
c = 0xffffffef (8 bits)
当我们把一个大的变量放入一个小变量的存储区域中,结果是只保留小变量能够存储的位,而其他的位
都被截短了.
有必要在这里提及整数进位.当一个计算包含大小不同的操作数时,通过计算较小的操作数会被进位到
较大的操作数.如果结果将被存储在一个较小的变量里,这个结果将会被重新减小,直到较小的操作数
可以容纳.
这个例子里:
int i;
short s;
s = i;
这里计算结果将被赋给一个不同尺寸的操作数,将发生的是变量s被提升为一个整型(32位),然后整数i的
内容被拷贝给新的提升后的s,接着,提升后的变量内容为了能存在s里面被降低回16位.如果超过了s能
存储的最大值降位将导致结果被截断..
------[ 2.1.1 Exploiting
整数溢出并不像普通的漏洞类型, 它们不允许直接的改写内存或者直接改变程序的控制流程.而是更加精巧.
程序的所有者面临的事实是没有办法在进程里面检查计算发生后的结果,所以有可能计算结果和正确
结果之间有一定的偏差.就因为这样,大多数的整数溢出不能被利用,即使这样,在一些情况下,我们还是有可能
强迫一个变量包含错误的值,从而在后面的代码中出现问题.
由于这些漏洞的精巧,导致有大量的地方能被利用,所以我就不尝试覆盖到所有能被利用的环境.相反
,我将提供一些能被利用的情况,希望读者能自己来探索.我将提供一些能被利用的情况的例子.
Example 1:
/* width1.c - exploiting a trivial widthness bug */
#include <stdio.h>;
#include <string.h>;
int main(int argc, char *argv[]){
unsigned short s;
int i;
char buf[80];
if(argc < 3){
return -1;
}
i = atoi(argv[1]);
s = i;
if(s >;= 80){ /* [w1] */
printf("Oh no you don't!\n");
return -1;
}
printf("s = %d\n", s);
memcpy(buf, argv[2], i);
buf[i] = '\0';
printf("%s\n", buf);
return 0;
}
然而像这种构造可能从来不会在真实的代码里面出现.这里只是一个简单的例子,让我们看看执行后
的输出:
nova:signed {100} ./width1 5 hello
s = 5
hello
nova:signed {101} ./width1 80 hello
Oh no you don't!
nova:signed {102} ./width1 65536 hello
s = 0
Segmentation fault (core dumped)
程序从命令行参数中得到一个整数值存放在整形变量i当中,当这个值被赋予unsigned short类型
的整数s,如果这个值大于unsigned short类型所能够存储的将被截短.(比如 这个值大于65535,
unsigned short存储的范围是0 - 65535),因次,可能绕过代码中的[w1]部分的边界检测,导致缓冲
区溢出.只要使用一般的栈溢出技术就能够溢出利用这个程序.
----[ 2.2 运算(Arithmetic) 溢出
在2.0章节中讲到,如果尝试存储一个大于整数变量最大值的整数到整数变量中,这个值将被截短.如
果存储值是一个运算操作,稍后使用这个结果的程序的任何一部分都将错误的运行,因为这个计算结
果是不正确的.这个可以完整的解释"环绕"的例子:
/* ex2.c - an integer overflow */
#include <stdio.h>;
int main(void){
unsigned int num = 0xffffffff;
printf("num is %d bits long\n", sizeof(num) * 8);
printf("num = 0x%x\n", num);
printf("num + 1 = 0x%x\n", num + 1);
return 0;
}
/* EOF */
程序执行的结果:
nova:signed {4} ./ex2
num is 32 bits long
num = 0xffffffff
num + 1 = 0x0
Note:
聪明的读者可能注意到了0xffffffff 是10进制中的-1,所以看起来我们只是做了以下操作1 + (-1) = 0
同时,这是一种方法可以看看它正在做了什么,可能会导致一些混淆,因为在这里这个变量是无符号的,
因此所有基于它的计算都是无符号的.当它发生了,很多整数溢出都是依赖符号运算的,正如下面这个
例子(2个操作数都是32位的变量):
-700 + 800 = 100
0xfffffd44 + 0x320 = 0x100000064
因为加出来的结果超出了整数变量的范围,最小的32位就会被当作结果来使用。这些最小
的32位是0x64,相当于十进制的100。
</note>;
如果一个整数缺省是有符号的,一个整数溢出能导致这个符号变化,这将对随后的代码发生有
趣的影响。正如下面这个例子:
/* ex3.c - change of signedness */
#include <stdio.h>;
int main(void){
int l;
l = 0x7fffffff;
printf("l = %d (0x%x)\n", l, l);
printf("l + 1 = %d (0x%x)\n", l + 1 , l + 1);
return 0;
}
/* EOF */
程序执行结果:
nova:signed {38} ./ex3
l = 2147483647 (0x7fffffff)
l + 1 = -2147483648 (0x80000000)
在这里整数被初始化为相当于最高的有符号的长整形的值,当它的值加1时,很重要的一个字节
(标志符号位的)被重新设置,这个整数将被解释成一个负数.加法不仅仅是一个运算方法,它能导
致一个整数溢出,几乎任何的改变变量的值的操作都会引发整数溢出,正如下面的这个例子:
/* ex4.c - various arithmetic overflows */
#include <stdio.h>;
int main(void){
int l, x;
l = 0x40000000;
printf("l = %d (0x%x)\n", l, l);
x = l + 0xc0000000;
printf("l + 0xc0000000 = %d (0x%x)\n", x, x);
x = l * 0x4;
printf("l * 0x4 = %d (0x%x)\n", x, x);
x = l - 0xffffffff;
printf("l - 0xffffffff = %d (0x%x)\n", x, x);
return 0;
}
/* EOF */
输出:
nova:signed {55} ./ex4
l = 1073741824 (0x40000000)
l + 0xc0000000 = 0 (0x0)
l * 0x4 = 0 (0x0)
l - 0xffffffff = 1073741825 (0x40000001)
相加导致一个整数溢出,这也恰恰和第一个例子相同,接下来是一个乘法操作,虽然看起来似乎不一样.
在这2个例子中,运算结果太大以至无法保存成一个整数,所以它被缩短了,就如前面表述的一样.减法稍许
有些不同,因为它引起的是下溢,而不是溢出.如果尝试去储存一个比整数可容纳的最小值更小的值,将会
引起一个"环绕".这样,我们可以强迫更改一个加法变成减法,一个乘法变成除法,或者一个减法变成加法.
------[ 2.2.1 Exploiting
数值溢出能被利用的最通常的方法是当计算结果给用来分配多大的缓冲区.通常一个程序必需分配空间
给一个数组对象,所以用malloc(3)或calloc(3)函数来保留空间并且通过用元素的数量乘以对象的尺寸来
计算需要多大的空间.前面已经提到,如果我们能控制这些操作数中的一个(元素的数量或对象的尺寸),
我们或许能够导致错误分配缓冲区,接下来的代码片段体现了这点:
int myfunction(int *array, int len){
int *myarray, i;
myarray = malloc(len * sizeof(int)); /* [1] */
if(myarray == NULL){
return -1;
}
for(i = 0; i < len; i++){ /* [2] */
myarray[i] = array[i];
}
return myarray;
}
这看起来无害的函数能带来系统的崩溃就因为没有检查len参数.在(1)处通过提供一个足够大的值
给len,乘法操作后能够被溢出,这样我们随意控制缓冲区的大小.通过选择一个合适的值给len,我们
能使得(2)处的循环写缓冲区的后面,这导致了一个heap溢出.通过改写malloc的控制结构能够在正
常的运行里面插入可执行的任意代码,但是这超出了本文的讨论范围.
另一个例子:
int catvars(char *buf1, char *buf2, unsigned int len1,
unsigned int len2){
char mybuf[256];
if((len1 + len2) >; 256){ /* [3] */
return -1;
}
memcpy(mybuf, buf1, len1); /* [4] */
memcpy(mybuf + len1, buf2, len2);
do_some_stuff(mybuf);
return 0;
}
在这个例子中,通过给合适的值给len1和len2能够欺骗过(3)处的检查,那样将导致加法的溢出并且
包含了一个低值.举个例子,看下面的数值:
len1 = 0x104
len2 = 0xfffffffc
当加在一起时将导致一个包含了0x100的结果(整数 256),这能通过(3)处的检查,然后在(4)处的
memcpy(3)将拷贝数据到缓冲区的后面. |
|