转载时请以超链接形式标明文章原始出处和作者信息及本声明
http://att2.blogbus.com/logs/5134302.html

 

在正式开始我们的Exploit学习之前,首先让我们做一个必备的工作:安装虚拟机,因为毕竟是两个系统对照嘛,所以虚拟机当然是最好的选择。VMWare是一个虚拟机软件工具,利用它可以在一台机器上同时运行二个或更多的操作系统,即在一个主操作系统平台运行(例如Windows 2000)的时候,可以像打开一个程序那样简便地打开另一个或多个操作系统平台(例如Windows 98或Linux)。对我们初学Linux下操作的人来说,有诸多的好处,后面我们将一一体会到这样的美妙。
第一个好处是便于学习未知的软件和操作。比如Linux下的文本编辑器VI,初学者首次进入时,多半是发觉按什么键都没有用,想退却怎么也退不出来,换来的是一阵机箱喇叭的狂响,把人气个半死。现在用虚拟机,轻松的Ctrl+Alt退到Windows环境,然后再去Google搜索解决的办法后,解决后再进去,嘿嘿,你会有发现有Google真好、有虚拟机真好!这也是推荐给新手们学习其它操作系统的一个经典方法。同样,我的GCC,GDB,Objdump的学习都是这样的,如图1所示。

第二个好处是VMWare是窗口模式的,初学者会感到很熟悉,想抓图也很方便,少了学习Linux的恐惧感。
第三个好处就是节约硬盘空间了,我们可以安装多个不同的系统进行学习,又不至于像真实的系统占用的空间那么大。比如我就在Windows 2000 sp4的系统上安装了Windows 2000 sp0、RedHat 7.2、RedHat 8.0,哈哈,想学习什么样的漏洞都有条件了!

TIPS:虚拟机系统的选择是比较重要的,一般为了研究漏洞,建议使用非最新版的OS,比如Windows下的漏洞不要打上最新的HOTFIX,而Red Hat最好也不要用Red Hat9。

好处这么多,或许新手会觉得这样的软件安装起来一定是复杂异常,实际上VMWare安装起来非常方便,随着提示点下一步就完成了。然后安装操作系统,在VMWare的菜单下选择File->New->New VIrtual Machine,选择要安装系统的类型,然后就会提示你插入安装光盘,以后的安装就如同单一系统的真实安装一样了,一点都不会感觉陌生。最后运行安装好后的系统,点Power on就开机了,熟悉的开机画面就出现在我们面前了。简单的安装后我们就有了一个很好的环境,可以在Windows下学习Linux下的编程和调试,以及缓冲区溢出了。下面进入今天的正题!

Linux和Windows堆栈溢出条件对比
我们看一个很简单的漏洞作为例子,在Windows和Linux系统下,溢出有什么相同和不同。程序如下:
#include<stdio.h>
#include<string.h>
char name[] = "ww0830";
int main()
{
char output[12];
strcpy(output, name);
printf("%s/n",output);
return 0;
}
程序很简单,就是把Name里面装的字符拷贝给Output,然后把它Printf出来。在Windows下,编译、链接、执行,其结果如图2所示。就是把ww0830输出到屏幕后,安全退出。

TIPS:很多读者朋友觉得溢出很神秘,需要对无数代码细致的分析才有收获。当然,如果你要去发现溢出漏洞这点是必须的。其实C里面的很多函数都可以给我们做溢出的练习,比如上文的例子代码,非常简单吧?而它就是一个典型的溢出!所以说,溢出并不是大家想得那么复杂!

那在Linux下呢?我们用VI敲一个同样的程序Vul.c,然后用命令“GCC –o vul vul.c”编译生成可执行文件Vul,执行它“./vul”。其结果如下图3所示。也是成功运行后,安全的退出。

TIPS:如果你的Linux是装的个人工作站,默认是没有GCC和一些开发工具的,需要重新安装,依次


接下来我们增加Name数组的长度,在Windows下改成“abcdefghijklmnopqrstuvwx”,然后运行,结果如下图4,溢出了!

仔细看显示的 “0x74737271”就是“qrst”,“q”是第17位字母,看来就是第17个数覆盖到了EIP。原因嘛,大家只要看过以前杂志的内容,都应该很清楚:Windows为Char output[12]分配了12字节的空间,后面是4字节的EBP,再后面就是4字节的EIP了。如果传递给Output的值超过了16个字节,那就会进入EIP,把保存的返回地址覆盖了,这样程序在返回的时候就会出错了。其结构如下图5所示:


而在Linux下呢?我们先从a到x,居然没有反应?如图6所示。

好,我们继续加长,看它溢出不溢出!加到“yz12”的时候,终于出错了,Linux下的出错提示信息是“Segmentation fault”,如图7所示。看来给Output分配的空间不仅仅是12个字节,要多得多。

这样,我们可以总结出:Linux和Windows堆栈溢出条件是相似的,都是字符串超过数组的大小而溢出的;产生溢出后,它们都会报错,可以方便我们定位溢出点。但它们也有些许不同,在Linux下,不像Windows恰好给数组分配定义大小的这么多,Linux下用GCC编译的时候会随机填充一定数目的无用数据。另一个区别是Windows报错时,会给出EIP当时的值,可以方便我们利用(盖茨真好!),而Linux只会提示段错误,不会报出错的数据。

Linux和Windows堆栈溢出利用对比
好了,对比了溢出的产生后,我们再看看溢出的利用有什么异同吧。堆栈溢出的利用,有三大条件,一是返回点的定位,二是ShellCode的编写,三是跳转到ShellCode。如果大家对这三个条件本身的意义还不清楚,就多翻翻以前的杂志吧。
我们就由这三个条件开始,继续由刚才那个程序,对比着学习Windows和Linux的缓冲区溢出。
1.返回点的定位
在Windows下,F.ZH的在它经典的《菜鸟版Exploit编写指南之一》的文章中,详细的讲述了利用报错对话框精确定位溢出返回点的方法。刚才那个程序溢出的时候大家也看到了,Windows下弹出报错对话框,显示“0x74737271”指令引用“0x74737271”内存,该地址不能为“read”(如图8所示)。

里面隐含的意思就是返回点EIP被覆盖成了“0x74737271”,去执行的时候该地址是不能读的,就有错误了。“0x74737271”是什么呢?前面说了,就是字符串“qrst”,离我们填充的第一个字符“a”正好距离是“q”–“a”=0x71 – 0x61=0x10=16,所以在报错对话框的帮助下,我们可以轻松的知道,在覆盖了16个字符之后,就到达返回点的位置了。
在Linux下,还是像Windows下类似会报错,但没有Windows下那么直观和友好,它只会显示“Segmentation fault”,但也足够了。我们可以逐渐增长得到刚刚报错时的填充长度。回过来看刚才那个程序,在Linux下,我们填充 “abc……z12”的时候,就是填充26个字母+2个数字=28个字符,刚好报错。Linux下刚好报错的时候是覆盖到了EBP,后面4个字节就是EIP的位置。所以就是在填充了28个字符之后,达到了返回点EIP的位置。这可比Windows分配的缓冲区多多了。
在知道了返回点EIP的位置后,我们就可以覆盖返回点为任意值,让程序运行到一个错误的地方,从而出现报错提示。那如果我们特意把EIP覆盖成我们想去的程序的地方呢?当然就可以运行我们想要的程序了,而一般的,我们想要的程序称为ShellCode!看到胜利的曙光了吧?!

2.ShellCode的编写方法
Shell最先指人机交互的界面,而这里Shellcode不仅仅指交互,它还指可以实现任意功能的代码,可以涉及到很多方面。我们想要的程序功能最好是能够开一个DOS窗口,那我们可以做很多事情了。
在Windows下,开DOS窗口的程序如下:
#include<Windows.h>
int main()
{
LoadLibrary(“msvcrt.dll”);
system(“command.com”);
return 0;
}
Windows下执行一个Command.com就可以获得一个DOS窗口,在C库函数里面,语句System(“command.com”);将完成我们需要的功能。Windows是通过动态链接库DLL来提供系统函数的。System函数由Msvcrt.dll(The Microsoft VIsual C++ Runtime library)提供,所以要想执行System,我们必须首先使用LoadLibrary(“msvcrt.dll”);装载动态链接库,svcrt.dll之后才能调用System函数。
我们执行上面的代码,看看效果吧:弹出来了一个DOS对话框!可以执行DIR,COPY等任意DOS命令,如图9所示。

我们要得到机器码形式的ShellCode,可以在VC中按F10调试,然后在Debug工具栏中,点最后一个按钮Disassemble,这样出现了源程序的汇编代码,再在代码窗口上点右键,在弹出菜单中选择Code Bytes,这样就出现了源程序的机器码。如图10所示。

原理上我们抄下来就可以了,但一般情况下,需要用汇编优化一下,并且处理好调用函数地址的问题,这样我们就可以得到Windows2000 SPN下,开DOS窗口的ShellCode了,没你想象中那么神秘吧?代码如下:
char shellcode[] =
{0x8B,0xE5, 0x55,0x8B,0xEC,0x83,0xEC,0x0C,0xB8,0x63,0x6F,0x6D,0x6D,6D,6D,6F,63,0x89,0x45,0xF4,0xB8,0x61,0x6E,0x64,0x2E,0x89,0x45,0xF8,0xB8,0x63,0x6F,0x6D,0x22,0x89,0x45,0xFC, 0x33,0xD2, 0x88,0x55,0xFF, 0x8D,0x45,0xF4, 0x50, 0xB8,0x24,0x98,0x01,0x78, 0xFF,0xD0 };
真正要把它构造成可以实战使用的ShellCode还有很多问题要解决,比如通用性、编码、功能等,需要很高深的技术功底,但在本文这就不是重点了,也就不深入下去了(详细内容请看本期“新手学溢出”栏目中另一个文章《定制自己的ShellCode》)。
我们的重点还是放在对比上吧,开一个Shell窗口的Linux代码如下:
#include<stdio.h>
int main ( )
{
char * name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve( name[0], name, NULL );
return 0;
}
就是用Execve函数来执行“/bin/sh”,我们在Linux下编译执行,得到一个新的Shell了,在新的Shell下,我们也可以执行各种命令。如图11所示。

然后还是把它变成机器码吧,和Windows系统不同,Linux的函数执行的原理是使用用系统调用,即执行函数的时候,是把参数赋给寄存器,然后调用中断Int 80来执行相应的函数。
刚才那个Execve函数的执行过程是:将0xb拷贝到寄存器EAX中(Execve的代码号),将“/bin/sh”的地址拷贝到寄存器EBX中,将“/bin/sh”的地址拷贝到寄存器ECX中,将NULL拷贝到寄存器EDX中,然后执行中断指令Int $0x80,所以Linux下我们的ShellCode就是:
char ShellCode[] ={
"/x31/xc0" // xor %eax,%eax
"/x50" // push %eax
"/x68/x2f/x2f/x73/x68" // push $0x68732f2f
"/x68/x2f/x62/x69/x6e" // push $0x6e69622f
"/x89/xe3" // mov %esp,%ebx
"/x8d/x54/x24/x08" // lea 0x8(%esp,1),%edx
"/x50" // push %eax
"/x53" // push %ebx
"/x8d/x0c/x24" // lea (%esp,1),%ecx
"/xb0/x0b" // mov $0xb,%al
"/xcd/x80" // int $0x80
}
这就是Windows和Linux下ShellCode的来源。在实际编写Exploit过程中,Windows和Linux都有很多现成的ShellCode,我们直接拿来使用就可以了,但明白了原理总要好很多,如果有需要,可以自己改改功能,变换变换形式以符合编码要求,以后我们有机会再讨论,这涉及到很多有意思的东西。

Linux和Windows跳转到ShellCode方法对比
现在分析一下我们现在拥有的资源:
1.我们知道有问题程序返回点的精确位置,意思就是我们可以把它覆盖成任意地址,让计算机执行这个地址的代码。
2.我们有了ShellCode,一个可以给我们提供DOS窗口或新Shell的代码。
3.那接下来……就应该是把程序的返回点地址覆盖为ShellCode的地址了,这样在程序返回时,就可以执行我们的ShellCode了,意思就是我们就能得到一个Shell了!
分别看看Windows和Linux是如何实现跳转的吧。
在Windows下,ShellCode的地址一般最高位是0x00,这样就会把覆盖的字符串截断。所以以前很多人都提出了很多办法来定位ShellCode,但都不精确。随着技术的发展,1999年Dark Spyrit AKA Barnaby Jack提出了一个天才的想法:使用系统核心DLL里的指令来完成跳转!这一技巧开创了一个崭新的Windows缓冲区溢出的思路,并一直沿用到今天!
Windows的系统核心DLL包括Kernel32.dll、User32.dll、Gdi32.dll,这些DLL是一直位于内存中而且对应于固定的版本Windows加载的位置是固定的,而且最高位一般不是0x00。我们用系统核心DLL中的Jmp esp来覆盖返回地址,而把ShellCode紧跟在后面,这样就可以完成跳转到我们的ShellCode中的要求了。覆盖后的缓冲区是这个样子(如图12所示):

其实现跳转的原理是这样的:当程序返回前,ESP指向原EIP的地方,函数返回就是执行RET=Pop EIP,这样EIP就是Jmp esp地址;而ESP会往下移,指向ShellCode的第一个字节——上图中的S0。
计算机不知道我们做了手脚,继续往下执行:EIP指向的指令Jmp esp,而此时的ESP正好指向了我们的ShellCode,那当然下一步就是执行我们的ShellCode了!这样间接的完成了向我们ShellCode的跳转。
我们把三步综合起来吧!对于程序,我们把缓冲区覆盖成这个样子,如图13所示。

ShellCode如下图14所示:

我们在程序中把Name赋成这样的值,它看起来很奇怪,但会得到很奇妙的结果。我们执行它就会弹出我们想要的DOS对话框啦,哈哈!下图15是实验效果。

写Main函数的程序员,他只会去负责读Name并赋给Output数组,根本不会感觉到Name数组中会隐藏这样精心构造的恶意代码,对于任何的Name,他都会把它读入并作为正常的东西处理,但就有可能会出现他想不到的结果。
再看看Linux下的跳转吧。对于Linux来说,程序中数组的地址最高位不是0x00,那要实现跳转就简单多了。我们定义一个ShellCode数组,把ShellCode的地址读出来,直接把返回点覆盖成地址就可以了!对于那个程序,其构造的结构如下图16所示。

我们先读出ShellCode数组的地址。在程序中添上ShellCode数组的定义,并打印出它的地址,即Printf(“%p/n”,shellcode);如下。
#include<stdio.h>
#include<string.h>
char shellcode[] = “12345”;
char name[] = "ww0830";
int main()
{
char output[12];
strcpy(output, name);
printf("%s/n",output);
printf(“%p/n”,shellcode);
return 0;
}
执行后,程序打印出ShellCode的地址,在我的机器上为0x8049580,哈哈,果然没有0x00的字节啊!这样我们得到了在Linux下,Name数组的构造结构。如图17所示。
 
然后把ShellCode数组替换成真正打开Shell的代码,这样总的程序代码如下图18:

好,编译、链接、执行!哈哈,弹出新Shell来了!


小结
从上面的对照分析,我们可以看出系统底层的东西是可以融会贯通的,所以只要深入的掌握了某个系统的底层,那就可以很容易学习领会其它的系统,并且注意力会放在两者相同和不同的地方,更利于理解。
Windows下堆栈的溢出已经有了很多经典的文章,而Linux对初学者来说,不太直观,所以用窗口模式和Windows的作对比,希望至少能从方法上帮助初学者了解,有任何问题请和编辑部联系,架起我们之间互相联系的桥梁!谢谢观赏!
h;http://att2.blogbus.com/logs/5134302.html

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐