注:文中代码除了最后两段来自泉哥的《漏洞战争:软件漏洞分析精要》,其余均来自姜晔大神的缓冲区溢出系列教程。
整数溢出基本原理
整数溢出,其实是缓冲区溢出的一种特殊形式,和其他类型的缓冲区溢出的基本原理相同的,整数溢出形成也是因为将数据放入了比它小的存储空间导致的。
在计算机中,整数分为有符号整数和无符号整数,其中有符号整数会在最高位用0表示正数,用1表示负数,而无符号整数就没有这个符号位,所有位都是数值位。在这些整数中常见的类型有8位的单字节型(byte)和布尔型(bool),16位的短整形(short int)和32位的长整形(int)。
下面就是一个较为简单的整数溢出的例子:
#include <stdio.h>
int main()
{
int InputTest;
unsigned short OutputTest;
printf("InputTest:");
scanf("%d", &InputTest);
OutputTest = InputTest; //造成整数溢出
printf("OutputTest:%d\n", OutputTest);
getchar();
return 0;
}
上面的这个示例程序实现的功能很简单,只是简单的将输入的长整型(int型)的变量复制给无符号的短整形变量(short int型),然后输出。因为将长整型数的长度是32位,放到比它本身小的的短整型数(16位)的存储空间中,因此就会构成溢出条件,那么也就会形成较为简单的整数溢出漏洞。
无符号的16位整数可以表示0~65535范围内的数值,那么我们就来测试一下对于这个程序分别输入65536、65536、65537,得出的结果都是什么:
可以看出,如果输入的数不超过65535时,程序都能正确的输出显示输入的数,但是当输入的数大于65535时,输出的结果就变得不正确,这就是溢出造成的结果,如果对溢出的数据进行精心构造,也就可以让程序按照我们的目的去运行。
动态调试初探溢出原理
还是上面的那个程序,动态调试一波,搞一波事情,看看溢出的流程具体是如何实现。
使用OD载入,进行调试后找到main()函数里面,如下图位置:
当程序步过地址为0x0040103E的位置时,程序会运行,提示需要执行输入命令,这也就是scanf()函数的地方,通过分析栈中的参数可以看出,输入的内容实际是存放在地址为0x0019FF3C的位置上的,输入十进制的65535,也就是十六进制的0x0000FFFF,然后再来看一下栈空间里面变化:
从上图可以看到,输入的数据65535已经存储到刚刚的eax寄存器中,再向下执行程序:
从上图中可以看到下面还有一些操作:先是将地址为0x0040103E中的数据复制给cx,再下面两条语句是将cx的内容辗转复制给edx寄存器,而edx寄存器则作为下面printf()函数的参数。但是通过源程序可知这个参数是无符号整形,也就是十六位,所以下面的操作就是将其与0xFFFF进行与运算,进而清空前十六位中的数据而保留后十六位中的数据,也就是将我们输入的数据的高十六位截去,只输出低十六位的数据。因为我们之前输入的是0xFFFF,只有后十六位的内容,所以对其并没有影响,下面将输入的数据变大进行第二次尝试,输入65536:
从上面图中可以看出来,输入65536后地址0x0040103E中存储的数据变成0x00010000,这样的话,在edx和0xFFFF进行与运算时,它的前十六位就会被舍去,只剩下0x0000,这也就是为什么会输出0。
这就是一个简单的整数溢出过程。
常见的整数溢出类型
常见的整数溢出主要由三类整数操作引起的:无符号整数的下溢和上溢、符号问题和截断问题。
无符号整数的下溢和上溢
无符号整数的下溢由于无符号整数没能识别负数造成的:
BOOL fun(size_t cbSize)
{
if( cbSize > 1024 )
{
return FALSE;
}
char *pBuf = new char[cbSize-1];
// 存在溢出隐患
memset(pBuf, 0x90, cbSize-1);
...
return TRUE;
}
从上述的示例代码中,在new分配内存后,程序没有检测调用结果的正确与否,这样的话,如果cbSize是0的时候,cbSize就是-1,但是memset的第三个参数是无符号的类型的,那么-1将被看作是正的0xFFFFFFFF,这样系统是无法操作这么大的空间,程序也就随之崩溃。
无符号的上溢其实额和下溢并不是十分对立的两个问题,上溢的话就是由于在计算超过0xFFFFFFFF的加法时,会发生进位,从而导致结果并不是我们想要的结果。
BOOL fun(char *s1, size_t len1, char *s2, size_t len2)
{
if(len1 + len2 + 1 > 1024)
{
return FALSE;
}
char pBuf = new char[len1 + len2 + 1];
if(pBuf == NULL)
{
return FALSE;
}
memcpy(pBuf, s1, len1);
// 存在溢出隐患
memcpy(pBuf + len1, s2, len2);
...
return TRUE;
}
从上面的代码中我们可以看出,在调用之前也进行了相应的检测,但是却仍然存在整数溢出漏洞。因为len1和len2都是无符号整数,如果len1=8,len2=0xFFFFFFFF,那么len1+len2+1就等于8,也就是我们只是在new时给pBuf分配了8个字节的空间,但是下面的memcpy(pBuf+len1,s2,len2)却要将0xFFFFFFFF长的字符串复制到这个空间里,这样也就会造成程序的崩溃。
符号问题
符号问题也有三种情况:1)有符号整数和无符号整数之间的转化;2)有符号整数的运算;3)有符号数之间的转化。
个人觉得这类问题也就是因为符号问题,使得整数操作不能够到达原来既定的目的,结果不是所应得的结果,进而就会导致溢出。比如上面的例子将负的有符号整数存放在无符号整数的存储空间中,这样系统会将这个数看作是一个很大的正的无符号整数,从而使得被操作的数能造成溢出并达到我们的目的。
截断问题
截断问题,也就是上面第一个例子中的情况,大多是因为高位数的整数要复制或者填充到低位数的整数中,进而造成的溢出。
因为当定义一个变量时,它在内存地址中的存储空间也就相应的固定,如果要将一个高位数存放在它的存储空间,这个内存空间肯定是不够用的,又因为这个变量的大小只有这么大,所以输出的只能是这个变量大小存储空间上的数据,这样高位数比这个变量多出来的几位就无法输出,操作数不是按照程序预计的进行,那么输出的结果也就会错误。
其实这三种分类归结到底还是因为将太大的数放到不够大的空间导致的溢出现象,这三种类别的问题也是相互联系的,无符号整数的下溢和上溢是因为经过整数运算后截断问题,符号问题则是有可能是由整数运算引发的截断问题,说到底,还是因为计算机中有符号整数和无符号整数之间的转化以及一些不同数据类型导致会有整数溢出的发生。
整数溢出引出的问题
上面也讲了一些整数溢出的原理性的东西,但是也不难看出,整数溢出很难发现,发现了造成溢出了却很难控制并为我们所利用,下面就讲一些能够运用整数溢出的地方。
整数溢出属于缓冲区溢出的一种,泉哥把它分为基于栈的整数溢出和基于堆的整数溢出,个人觉得分为引起栈溢出的整数溢出和引起堆溢出的整数溢出更好一些,虽说不是很学术,但是对于新手来说更容易理解一些。也就是我们往往就是会通过利用整数溢出来控制缓冲区溢出进而实现我们的目的。
总是感觉只是将整数溢出的原理有点不是很容易讲明白,下面就举两个泉哥书中的例子来说明一下这两种整数溢出的利用原理,通过实例来理清楚里面的运行机制。
下面是整数溢出引起栈溢出的示例代码:
#include "stdio.h"
#include "string.h"
int main(int argc,char *argv)
{
int i;
char buf[8]; //栈缓冲区
unsigned short int size; //无符号短整数取值范围:0~65535
char overflow[65550];
memset(overflow,65,sizeof(overflow)); //填充为“A”字符
printf("请输入数值:\n");
scanf("%d",&i);
size = i;
printf("size:%d\n",size); //输出系统识别出来的size数值
printf("i:%d\n",i); //输出系统识别出来的i数据
if(size>8) //边界检查
return -1;
memcpy(buf,overflow,i); //栈溢出
return 0;
}
上述的示例代码的主要功能是下面的memcpy()函数的实现,就是overflow数组中前i位复制给大小为8的buf数组中,在复制函数前面还进行了一次边界检查,但是并不能阻止溢出情况的发生。假如输入的i大于65535且小于65544时,因为size变量是无符号短整形数,它就会发生类似于第一个例子中的截断溢出情况,绕过看着很安全的边界检查,这样造成栈溢出,进而获取栈的控制权,以及程序的控制权。
下面是整数溢出引起堆溢出的示例代码:
#include "stdio.h"
#include "windows.h"
int main(int argc,char * argv)
{
int* heap;
unsigned short int size; 无符号短整数取值范围i:0~65535
char *pheap1,*pheap2;
HANDLE hHeap;
printf("输入size数值:\n");
scanf("&d",&size);
hHeap = HeapCreate(HEAP_GENRATE_EXCEPTIONS,0x100,0xfff);
if(size <= 0x50)
{
size -=5;
printf("size:%d\n",size);
pheap1 = HeapAlloc(hHeap,0,size);
pheap2 = HeapAlloc(hHeap,0,0x50);
}
HeapFree(hHeap,0,pheap1);
HeapFree(hHeap,0,pheap2);
return 0;
}
由于对于堆溢出的原理不太擅长,就将泉哥书上的理解重复一下,上述代码中的unsigned short int整数类型,当它小于5时,比如size=2,size减去5会得到负数,但是由于unsigned short int 取值范围的限制导致无法识别负数,反而得到整数65533,最后分配到过大的堆块,从而导致溢出覆盖到对管理结构。