前言
上次我们已经讨论了缓冲区溢出的原理,而这次我们需要利用这个原理来构造条件,从而利用这个漏洞。
其实缓冲区溢出漏洞的利用主要是需要解决以下三个问题:
1、精确定位返回地址的位置
2、寻找一个合适的地址,用于覆盖原始返回地址
3、编写Shellcode到相应的缓冲区中
这次我们就结合实验程序,来解决上述三个问题,实现漏洞的利用。
精确定位返回地址的位置
缓冲区溢出利用的第一步,就是需要我们精确定位返回地址的位置。由于我们这次的程序比较简单,所以通过错误提示对话框,我们就能够判定返回地址的位置了。
我们首先运行一下OverrunTest_2.exe,经过上次的分析我们知道,由于返回地址被覆盖,而新地址是一个无效地址,所以使得main函数执行完后不知道该执行什么指令,于是就出现了错误对话框。一般来说,我们要么单击“发送错误报告”,要么单击“不发送”,但是这次我们选择单击以蓝色字体显示的“请单击此处”。然后再选择“要查看关于错误报告的技术信息,请单击此处”,就弹出了“错误报告内容”对话框:
图1
在这个报告中,有两个地方比较重要。一个是“Code”,即错误代码。这里是“0xc0000005”,表示出现了缓冲区溢出的错误。另外一个是“Address”,即错误发生的地址,这里是“0x00006579”,而结合上次的OD分析,我们知道由于这是一个不存在的地址,从而导致了程序出错。
借助于OD,我们可以很容易弄清楚返回地址的位置,如果没有OD该怎么办呢?对于这个实验程序来说,由于我第一次输入“jiangye”的时候,程序没出问题,而第二次输入“jiangyejiangye”时就报错了,报错内容是“0x00006579”这个地址出现了问题,而我们将这个地址翻译为英文字母,可以发现“0x65”代表的是“e”,而“0x79”则代表的是“y”。正好是我所输入的那一长串字符的最后两个字母。由于地址是由4个字节表示的,那么对于这个程序而言,如果我将全局变量name赋值为“jiangyejiangXXXX”,那么这最后的四个“X”就正好覆盖了返回地址,而前面的12个字符可以是任意字符。那么我们也就解决了缓冲区漏洞利用的第一个问题——精确定位返回地址的位置。
但是这里还有一个问题需要说明的是,我们这个程序中的局部变量,也就是buffer只有8个字节,因此很容易就能够被填充满,从而很容易就能够被定位,但是如果缓冲区空间很大,该如何定位呢?不能还是一直以“jiangyejiangye……”这样不断地测试下去吧。其实定位溢出位置的方法有很多,在此可以告诉大家一个便于初学者理解的方法,我们可以利用一长串各不相同的字符来进行测试,比如:
TestCode[]= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
利用26个英文大写字符与26个小写字符,一共是52个字母进行测试,这样一来,一次就可以验证52个字节的缓冲区空间。这里将我们的局部变量数组的大小修改为80个字节,然后利用这个方法进行验证。首先测试一段上述字符,可见程序没有什么变化,那么我们就再加上一段,变成104个字符进行验证,可见此时已经弹出了错误提示对话框:
图2
在Address后可以发现,其值为0x6a696867,注意我们的系统是小端显示,也就是说,实际的字符应该是0x67、0x68、0x69、0x6a。那么把它转换成字母,可以知道是g、h、i、j,由于我们这里是使用了两轮验证字符,第一轮是52个字符,加上第二轮的前26个大写字符,就是78个字符,然后小写字母g前面还有6个字符,那就是84个字符,注意这里还包含4个字节的EBP,所以我们所验证的缓冲区的大小就是80个字节的空间了。
关于其它的一些判断缓冲区空间大小的方法,我们会在以后的实际分析中再进行讲解。我觉得大家目前懂得这个办法就可以了。甚至还可以将键盘上的标点符号也加到TestCode里面,可以按照ASCII码表的顺序进行排列,这样一次就能够验证更多的空间了。
寻找一个合适的地址,用于覆盖原始返回地址
现在我们需要做的工作是确定“jiangyejiangXXXX”中的最后四个“X”应该是什么地址。这里我们不能凭空创造一个地址,而是应该基于一个合法地址的基础之上。当然我们通过在OD中的观察,确实能够找到很多合适的地址,但是这种方法不具有通用性,毕竟要找一个确切的地址还是不那么方便的。解决这个问题的方法有很多种,但是最为常用最为经典的,就是“jmp esp”方法,也就是利用跳板进行跳转。
这里的跳板是程序中原有的机器代码,它们都是能够跳转到一个寄存器内所存放的地址进行执行,如jmp esp、call esp、jmp ecx、call eax等等。如果在函数返回的时候,CPU内寄存器刚好直接或者间接指向ShellCode的开头,这样就可以把对栈内存放的返回地址的那一个元素覆盖为相应的跳板地址。
不妨使用实际的例子来说明一下。我们先研究一下正常的程序,也就是OverrunTest_1.exe,用OD载入,执行到main函数最后的位置,即retn语句处,此时我们关注一下esp寄存器所保存的值:
图3
可以发现,现在esp中保存的值是0x0012FF84这个地址,而在栈中这个地址对应的就是我们的返回地址0x00401699,即我们下一条语句的位置。然后我们此时再按一下F8,单步执行,那么此时main函数就会执行完毕:
图4
可见此时我们已经跳出了main函数,来到了0x00401699位置处的语句进行执行,我们这里不用管它的具体执行内容,主要关注esp的值。可以发现,esp的值由刚才的0x0012FF84变成了0x0012FF88,从栈空间来看,即刚才那个值的下一个位置。不要忘了,0x0012FF84位置正式我们想要修改的返回地址的位置。
总结一下,我们可以得知,当main函数执行完毕的时候,esp的值会自动变成返回地址的下一个位置,而esp的这种变化,一般是不受任何情况影响的。既然我们知道了这一个特性,那么其实就可以将返回地址修改为esp所保存的地址,也就是说,我们可以让程序跳转到esp所保存的地址中,去执行我们所构造的指令,以便让计算机执行。
当然了,以上我所讲的是正常的情况,也就是返回地址没有被破坏的情况,那么如果返回地址被破坏了,esp还具备这种特性吗?不妨再用OD载入OverrunTest_2.exe这个存在缓冲区溢出问题的程序,来研究一下,首先还是先来到main函数的retn的位置:
图5
可以看到,与正常情况相比,esp中所保存的地址是一样的,只是该地址中保存的值不同而已,然后按F8单步执行:
图6
可见除了此时反汇编代码窗口为空以外,其它位置还是没什么变化的,特别是esp的值,依旧是自增了4,在栈窗口来看,也就是来到了返回地址下方的位置。那么也就说明了,我们完全可以利用esp的这一特性来做文章。
那么如何让程序跳转到esp的位置呢?我们这里可以使用“jmp esp”这条指令。jmp esp的机器码是0xFFE4,那么我们可以编写一个程序,来在user32.dll中查找这条指令的地址(当然了,jmp esp在很多动态链接库中都存在,这里我只是以user32.dll作为例子):
#include <windows.h> #include <stdio.h> #include <stdlib.h> int main() { BYTE *ptr; int position; HINSTANCE handle; BOOL done_flag = FALSE; handle = LoadLibrary("user32.dll"); if(!handle) { printf("load dll error!"); exit(0); } ptr = (BYTE*)handle; for(position = 0; !done_flag; position++) { try { 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:
图7
可以看到,这里列出了非常多的结果,我们可以随便取出一个结果用于我们的实验,这里我选择的是倒数第二行的0x77e35b79。也就是说,我需要使用这个地址来覆盖程序的返回地址。这样,程序在返回时,就会执行jmp esp,从而跳到返回地址下一个位置去执行该地址处的语句。
请大家注意的是,有很多种方法可以获取jmpesp,而且不同的系统这个地址可能是不同的,但是有些地址是跨系统的,大家可以自行在网上搜索。
编写Shellcode到相应的缓冲区中
至此可以先总结一下我们即将要编写的“name”数组中的内容,经过分析可以知道,其形式为“jiangyejinagXXXXSSSS……SSSS”。其中前12个字符为任意字符,XXXX为返回地址,这里我使用的是0x77e35b79,而SSSS……SSSS则是我们具体想让计算机执行的代码。
由于ShellCode的编写涉及不少知识,所以我将这个内容留到下节课再进行论述。