--[ 1.0 目录
在这篇文章中我将会讲述2种由于不安全编程引发的问题,导致一些恶意的用户修改受影响的进程
改变程序执行流程.这2种类型的问题都是由于某一程序变量包含一个不可预料的值,因此这种类型
的问题不同于那些程序内存被改写的问题,比如:缓冲区溢出,格式化溢出.所有文章中给出的程序例
子都是用C语言编写,所以需要读者熟悉C语言.一些整数在内存中存储方法的知识也是会有所帮助
的,但不是全部.
----[ 1.1 什么是整数?
一个整数, 在计算范围内, 是一个变量可能是一个没有小数部分的实数的.在系统上被编译处理后,
整型和指针的尺寸一般是相同的(比如: 在32位的系统中,例如i386, 一个整数是32字节长,在64位的
系统中,例如SPARC,一个整数是64字节长).然而一些编译器不使整型和指针为同样尺寸 ,所以为了
通俗易懂,所有这里谈到的例子是在32位的系统环境和32位的整数,长度和指针.
整数,如同所有的变量只是内存的一个区域, 当我们谈及关于整数,我们通常用10进制来表示它们.
换句话说也就是人们经常使用的一种编码方式.计算机是基于数字的,不能直接处理10进制,所以在
计算机中整数是以2进制的方式存储的.2进制是另一种编码方式,它们只有2个数字,1和0,与之不同
的10进制是用10个数字来表示的.2进制和10进制,16进制是广泛的被使用的在电脑中能够很简单的
转换2进制和16进制.
因为存储负数通常是必要的,这样就需要一种机制仅仅用位来代表负数,这种方法已经完成了,通过
一个变量的最高为来决定正负.如果最高位置1,这个变量就被解释为负数; 如果置0,这个变量就解释
为整数.这会导致一些混淆,这可以说明一些符号类型问题的概念,因为不是所有的变量都是有符号
之分的,意思就是说并不是所有的类型都需要使用MSB来区分正负.这些变量被定义为无符号,它只能
被赋予正数值.如果变量可正可负,可以被称做是无正负的,
整数溢出
。----[ 1.2 什么是整数溢出?
既然一个整数是一个固定的长度 (在本篇文章中使用32位),它能存储的最大值是固定的,当
尝试去存储一个大于这个固定的最大值时,将会导致一个整数溢出.在ISO C99的标准中讲
到整数溢出将会导致"不能确定的行为",也就是说编译器遵从了这个的规则,那就是完全忽略
溢出而退出这个程序.很多编译器似乎忽略了这个溢出,结果是一个意想不到的错误值被存储.
----[ 1.3 为什么那是危险的?
整数溢出是不能被立即察觉,因此没有办法去用一个应用程序来判断先前计算的结果是否实
际上也是正确的.如果是用来计算缓冲区的大小或者计算数组索引排列的距离,这会变的危险.
当然很多整数溢出并不是都是可利用的,因为并没有直接改写内存,但是有时,他们可导致其他
类型的bugs,缓冲区溢出等.而且,整数溢出很难被发现,因此,就算是审核过的代码也会产生意外。
--[ 2.0 整数溢出
所以当一个整数溢出已经发生时会发生什么呢? ISO C99 是这样说的:
"A computation involving unsigned operands can never overflow,
because a result that cannot be represented by the resulting unsigned
integer type is reduced modulo the number that is one greater than
the largest value that can be represented by the resulting type."
译者注:
大致的意思是:
涉及到无符号操作数计算的时候从不会溢出,因为结果不能被无符号类型表示的时候,
就会对比该类型能表示的最大值还大的数求余.这样就能用该结果来表示这种类型了.
NB:取模的运算方法是2个数相除取余数的值
例子:
10 modulo 5 = 0
11 modulo 5 = 1
所以在减轻体重法里面,一个大数被和(最大的int值 + 1)取模,在C语言中,取模操作的符号是%.
这里有一个字节是多余的,可能是一个很好的象征性例子证明我们说的"导致不确定的行为".
我们有2个无符号的整数,a和b, 2个数都是32位字节长,我们赋值给a 一个32为整数的最大值,
b被赋值为1.然后我们让a和b相加然后存储结果到第3个无符号32位的整数r:
a = 0xffffffffff
b = 0x1
r = a + b
现在,当相加起来的结果不能用32位的的值来表示,结果,为了和ISO 标准一致,被和0x100000000
取模.
r = (0xffffffff + 0x1) % 0x100000000
r = (0x100000000) % 0x100000000 = 0
减轻体重法的取模算法只能计算低于32位的计算结果,所以整数溢出导致结果被截断到一个范围,
通常用一个变量来存储这个结果。这个经常被称作一个"环绕"(译者注:类似成语中"否极泰来"的
意思,在这篇文章中我们理解为一个正数大到了极点就会变成负数,负数小到了极点就会变成正数),
作为这里的结果,就出现了环绕到0.
----[ 2.1 Widthness 溢出
所以整数溢出是尝试存储一个大数到一个变量中,由于这个变量太小不足以存储该大数导致的结
果.用最简单的例子来说明这个问题,存储一个大变量到一个小变量中去:
/* ex1.c - loss of precision */
#include
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
#include
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]部分的边界检测,导致缓冲
区溢出.只要使用一般的栈溢出技术就能够溢出利用这个程序.
[1] [2] 下一页