整数溢出与程序安全转载自网络|=--------------------=[BasicIntegerOverflows]=----------------------=||=-----------------------------------------------------------------------=||=-------------------=[bybleximblexim@hush.com]=-------------------=|中文翻译整理:Sam@sstSam@vertarmy.orgairsupply@sstairsupply@vertarmy.org1:目录1.1什么是整数?1.2什么是整数溢出?1.3为什么那是危险的?2:整数溢出2.1Widthness溢出2.1.1Exploiting2.2运算(Arithmetic)溢出2.2.1Exploiting3:符号类型的问题3.1它们看起来像什么?3.1.1Exploiting3.2符号类型的问题导致的整数溢出4:真实的例子4.1整数溢出类4.2符号问题类--[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位),它能存储的最大值是固定的,当尝试去存储一个大于这个固定的最大值时,将会导致一个整数溢出.在ISOC99的标准中讲到整数溢出将会导致不能确定的行为,也就是说编译器遵从了这个的规则,那就是完全忽略溢出而退出这个程序.很多编译器似乎忽略了这个溢出,结果是一个意想不到的错误值被存储.----[1.3为什么那是危险的?整数溢出是不能被立即察觉,因此没有办法去用一个应用程序来判断先前计算的结果是否实际上也是正确的.如果是用来计算缓冲区的大小或者计算数组索引排列的距离,这会变的危险.当然很多整数溢出并不是都是可利用的,因为并没有直接改写内存,但是有时,他们可导致其他类型的bugs,缓冲区溢出等.而且,整数溢出很难被发现,因此,就算是审核过的代码也会产生意外。--[2.0整数溢出所以当一个整数溢出已经发生时会发生什么呢?ISOC99是这样说的:Acomputationinvolvingunsignedoperandscanneveroverflow,becausearesultthatcannotberepresentedbytheresultingunsignedintegertypeisreducedmodulothenumberthatisonegreaterthanthelargestvaluethatcanberepresentedbytheresultingtype.译者注:大致的意思是:涉及到无符号操作数计算的时候从不会溢出,因为结果不能被无符号类型表示的时候,就会对比该类型能表示的最大值还大的数求余.这样就能用该结果来表示这种类型了.NB:取模的运算方法是2个数相除取余数的值例子:10modulo5=011modulo5=1所以在减轻体重法里面,一个大数被和(最大的int值+1)取模,在C语言中,取模操作的符号是%./NB这里有一个字节是多余的,可能是一个很好的象征性例子证明我们说的导致不确定的行为.我们有2个无符号的整数,a和b,2个数都是32位字节长,我们赋值给a一个32为整数的最大值,b被赋值为1.然后我们让a和b相加然后存储结果到第3个无符号32位的整数r:a=0xffffffffffb=0x1r=a+b现在,当相加起来的结果不能用32位的的值来表示,结果,为了和ISO标准一致,被和0x100000000取模.r=(0xffffffff+0x1)%0x100000000r=(0x100000000)%0x100000000=0减轻体重法的取模算法只能计算低于32位的计算结果,所以整数溢出导致结果被截断到一个范围,通常用一个变量来存储这个结果。这个经常被称作一个环绕(译者注:类似成语中否极泰来的意思,在这篇文章中我们理解为一个正数大到了极点就会变成负数,负数小到了极点就会变成正数),作为这里的结果,就出现了环绕到0.---[2.1Widthness溢出所以整数溢出是尝试存储一个大数到一个变量中,由于这个变量太小不足以存储该大数导致的结果.用最简单的例子来说明这个问题,存储一个大变量到一个小变量中去:#includestdio.hintmain(void){intl;shorts;charc;l=0xdeadbeef;s=l;c=l;printf(l=0x%x(%dbits)\n,l,sizeof(l)*8);printf(s=0x%x(%dbits)\n,s,sizeof(s)*8);printf(c=0x%x(%dbits)\n,c,sizeof©*8);return0;}让我们看看执行结果nova:signed{48}./ex1l=0xdeadbeef(32bits)s=0xffffbeef(16bits)c=0xffffffef(8bits)当我们把一个大的变量放入一个小变量的存储区域中,结果是只保留小变量能够存储的位,而其他的位都被截短了.有必要在这里提及整数进位.当一个计算包含大小不同的操作数时,通过计算较小的操作数会被进位到较大的操作数.如果结果将被存储在一个较小的变量里,这个结果将会被重新减小,直到较小的操作数可以容纳.这个例子里:inti;shorts;s=i;这里计算结果将被赋给一个不同尺寸的操作数,将发生的是变量s被提升为一个整型(32位),然后整数i的内容被拷贝给新的提升后的s,接着,提升后的变量内容为了能存在s里面被降低回16位.如果超过了s能存储的最大值降位将导致结果被截断..------[2.1.1Exploiting整数溢出并不像普通的漏洞类型,它们不允许直接的改写内存或者直接改变程序的控制流程.而是更加精巧.程序的所有者面临的事实是没有办法在进程里面检查计算发生后的结果,所以有可能计算结果和正确结果之间有一定的偏差.就因为这样,大多数的整数溢出不能被利用,即使这样,在一些情况下,我们还是有可能强迫一个变量包含错误的值,从而在后面的代码中出现问题.由于这些漏洞的精巧,导致有大量的地方能被利用,所以我就不尝试覆盖到所有能被利用的环境.相反,我将提供一些能被利用的情况,希望读者能自己来探索.我将提供一些能被利用的情况的例子.Example1:#includestdio.h#includestring.hintmain(intargc,char*argv[]){unsignedshorts;inti;charbuf[80];if(argc3){return-1;}i=atoi(argv[1]);s=i;if(s=80){printf(Ohnoyoudon't!\n);return-1;}printf(s=%d\n,s);memcpy(buf,argv[2],i);buf[i]='\0';printf(%s\n,buf);return0;}然而像这种构造可能从来不会在真实的代码里面出现.这里只是一个简单的例子,让我们看看执行后的输出:nova:signed{100}./width15hellos=5hellonova:signed{101}./width180helloOhnoyoudon't!nova:signed{102}./width165536hellos=0Segmentationfault(coredumped)程序从命令行参数中得到一个整数值存放在整形变量i当中,当这个值被赋予unsignedshort类型的整数s,如果这个值大于unsignedshort类型所能够存储的将被截短.(比如这个值大于65535,unsignedshort存储的范围是0-65535),因次,可能绕过代码中的[w1]部分的边界检测,导致缓冲区溢出.只要使用一般的栈溢出技术就能够溢出利用这个程序.----[2.2运算(Arithmetic)溢出在2.0章节中讲到,如果尝试存储一个大于整数变量最大值的整数到整数变量中,这个值将被截短.如果存储值是一个运算操作,稍后使用这个结果的程序的任何一部分都将错误的运行,因为这个计算结果是不正确的.这个可以完整的解释环绕的例子:#includestdio.hintmain(void){unsignedintnum=0xffffffff;printf(numis%dbitslong\n,sizeof(num)*8);printf(num=0x%x\n,num);printf(num+1=0x%x\n,num+1);return0;}程序执行的结果:nova:signed{4}./ex2numis32bitslongnum=0xffffffffnum+1=0x0Note:聪明的读者可能注意到了0xffffffff是10进制中的-1,所以看起来我们只是做了以下操作1+(-1)=0同时,这是一种方法可以看看它正在做了什么,可能会导致一些混淆,因为在这里这个变量是无符号的,因此所有基于它的计算都是无符号的.当它发生了,很多整数溢出都是依赖符号运算的,正如下面这个例子(2个操作数都是32位的变量):-700+800=1000xfffffd44+0x320=0x100000064因为加出来的结果超出了整数变量的范围,最小的32位就会被当作结果来使用。这些最小的32位是0x64,相当于十进制的100。/note如果一个整数缺省是有符号的,一个整数溢出能导致这个符号变化,这将对随后的代码发生有趣的影响。正如下面这个例子:#includestdio.hintmain(void){intl;l=0x7fffffff;printf(l=%d(0x%x)\n,l,l);printf(l+1=%d(0x%x)\n,l+1,l+1);return0;}程序执行结果:nova:signed{38}./ex3l=2147483647(0x7fffffff)l+1=-2147483648(0x80000000)在这里整数被初始化为相当于最高的有符号的长整形的值,当它的值加1时,很重要的