菜鸟版Exploit编写指南之旧饭重炒【转】



我也很诧异我这个菜鸟能把这个系列写到第三话,但既然有人读,就只有继续献丑了。写稿的时候windows平台及其第三方软件似乎没有报道有新的漏洞,只 能用以前的一些重要漏洞作为例子,这样子继续的话,不知道会不会有一天写到古老的idq/ida啊,或许我应该换一下平台,写写*nix上面的东西?

Serv-U在国内的应用还是很广泛的,很多虚拟主机还有一些电影下载的网站都喜欢用这种FTP服务软件,所以从今年一月份开始公布的一系列Serv-U 的漏洞真是杀伤力超大。说起来虽然从site chmod到MDTM到后来畸形路径都是简单的栈溢出,但对shellcode的要求和对覆盖地址的要求还是比较高,外面有一些分析的文章,我们就用这些 文章来看看,shellcode怎么去构造。
关于溢出的触发方法和基本框架结构的搭建,这个系列的前面几个部分已经说了不少,这里先从最原始的FTP协议开始,简单说说怎样搭一个触发溢出的框架。
FTP协议标准,具体可以参见rfc959/2228/2640/2773,Serv-U漏洞的前面两个都是扩展协议出的问题,触发的流程可以看为连接到 ftp服务端口->登陆->畸形提交导致溢出。值得注意的是,很多ftp服务软件并不严格遵循rfc的定义,最好能够找到FTP服务器对应的 手册,按照手册的描述针对性的写出程序。对于Serv-U,如果用nc来连接到FTP的服务端口,你会发现一个奇怪的问题,那就是不管你输入了什么东西, Serv-U的守护进程始终不给你返回的信息。最开始的时候我也被这个问题困扰了很久,后来抓包的时候发现,原来nc把我们的回车作为一个单纯的""n" 发送了出去,而Serv-U要求的命令结束符是""r"n",这个结论很讨厌,其直接的后果是触发溢出漏洞的时候我们不能用方便的nc,而只能 telnet过去(顺带着说一句,telnet是可以有回显的,连接上后按ctrl+]然后输入set local_echo,然后再退回连接模式就可以看到,不过telnet是接受一个字节发送一个字节,很多方便的快捷键如上下左右就没有办法使用,倘若经 常使用到这些功能,不妨自己写一个类似nc的小工具)。
好,现在先来手工触发漏洞。按照ftp协议,连接以后应该有一个合法的身份可以登陆,然后我们通过一个畸形的命令让Serv-U挂掉,下面是一个流程。当 然,前提是Serv-U是4.x版本的,而且已经telnet了过去,值得注意的是site chmod需要一个有写权限的账号:

220 Hello~ This is F.Zh's FTP Server.
user test
331 User name okay, need password.
pass test
230 User logged in, proceed.
site chmod 777 namenamenamenamenamenamenamenamenamenamnamenamenamenamenamna
enamnamenamenamenamenamnamenamenamenamenamnamenamenamenamenamnamenamenam
enamenamnamenamenamenamenamnamenamenamenamenamnamenamenamenamenamnamen
enamnamenamenamenamenamnamenamenamenamenamnamenamenamenamenamnamenamen
namenamenamenamenamnamenamenamenamenamnamenamenamenamenamnamenamenamen
namenamenamenamnamenamenamenamenamnamenamenamenamenamnamenamenamenamen
namenamenamnamenamenamenamenamnamenamenamenamenamnamenamenamenamenamna
namenamnamenamenamenamenam

前面有数字的都是服务器给我们的提示,这类数字在FTP协议中有详细的描述,可以查阅一下rfc。我们提交的是user/pass/site开头的几行, 存在漏洞的服务器,一个超长的site chmod过去后,由于守护进程挂掉了,连接也会断掉。如果连接没有断掉的话,你可能要检查一下,是不是用户没有写的权限,或者是Serv-U的版本不 对。
把上面的动作自动化,写出程序来,也就搭好了最开始的架子,1.cpp(见光盘)。
那么开始运行这个最初的触发漏洞程序吧。之前呢,当然要先打开我们的利器——Ollydbg,选择附加到“ServUDaemon”这个进程,当程序运行 的时候,Ollydbg会在发生错误的地方停下来(见图一),这个我们已经看到很多次了,不是吗?接下来我们可以按Shift+F9直接看到出错的画面, 不过这一次,我们来看看这期间,Windows到底是怎样处理的(见动画1)。
看清楚了么?原来程序出错以后,Windows还给了它一个最后处理异常的机会,这就是一个所谓SEH结构化异常处理的机制。抛开个中细节不管,单看到最 后的地方,这个处理机制从内存中读了一个数据,然后准备跳转过去(动画结束的时候,那个call ecx)。是不是觉得这个地址很熟悉啊?0x73737373(ASCII码对应四个小写的s),刚好是我们提交的内容,再在内存中看看ebx指向的地 方,刚好也是我们提交的数据,我们可以控制跳转的方向,同时可以动态定位(ebx)我们的数据地址,非常符合一个典型的溢出情况。如果你看过这个系列的前 面几篇,我想你已经知道下一步该怎么做了。
在Ollydbg中按一下F9,果然执行到了0x73737373(见图二)。接下来就是两次触发异常来准确定位溢出点,然后覆盖跳转的地址为一个和 “call ebx”等价的内存地址,然后添上我们的shellcode,不是吗?流程没有错,但是shellcode一步有点问题,想想看我们提交的这个很长的字符 串其实是一个文件名(呵呵,请看看rfc),windows下对文件名有很多规定,比如不能出现问号、星号、冒号等等,如果包含在文件名中的 shellcode含有这些特殊字符,被Serv-U处理掉后,执行时就会出错。
从外面对漏洞的描述文章中可以知道,shellcode中不能含有的字符包括上面提到的非文件字符,空格还有三个连续"0xFF"等。如果你平时注意收集 shellcode的话,说不定能找到满足要求的拿来直接用,但是很不幸的,我用的这些shellcode全军覆没,主要问题都出在前面的decode部 分上。这样子,为了能够写出这个利用程序,我们不得不去设法改写现有的shellcode(另:这里的特殊字符非常有限,如果特殊字符更多一点, shellcode的改法可以参考这一期的另外一篇文章。)。
这里插上一句话,很多shellcode都是分成两段的,真正有意义的在第二段,第一段不过是起着解码的功能。我们找shellcode的时候,首先看看 它的第二部分是不是满足关于上述的字符限制,如果不是,我们有可能还要去重新书写这一部分。这个工作量太大以至于我们无法接受,所以我们还是多找找,总能 找到满足要求的。
这个漏洞对shellcode的要求比较讨厌,因为连续的三个0xFF不能出现,也就是说,传统的用call+pop来动态定位需要解码的真实 shellcode地址这条路是行不通的。Call的时候,由于是向低地址处做相对偏移的调用,所以一般都会出现连续的三个0xFF(负数),除非你的 decode部分长度超过了255个字节,而一般情况下这不可能。字符的限制迫使我们考虑其他的方法,前面几期上提到过在内存中搜索,要是直接搬过来用也 可以,如果有兴趣,可以参看前面的做法,这里我们换一下思路,直接计算地址,然后进行解码。
前面说到过,溢出的时候ebx刚好指向我们的shellcode开始前面的八个字节,如下图:


NOPs    jmp 6 jmp 6    jmp ebx    decode    real shellcode    

4 bytes 4 bytes ?? ebx +8 + ??

溢出发生并且成功跳转以后,ebx指向的地方是第一个jmp 6处,中间隔了两个jmp 6,一个jmp ebx的地址还有一个decode部分才是真正起作用的shellcode,也就是说中间隔了八个字节以及一个decode部分的长度。这个数字目前无法 确定,但是我们写出decode部分后就可以确定了,在这之前,我们把这个数字随便设定为一个比如50吧,因为写decode的部分需要用到,我们把 decode部分的长度确定下来了再回去改也可以。
下面开始写合乎编码要求的decode部分了,基本上这是一个费时间的工作,要注意到有没有出现异常的字符就得不停的编译不停地去看去修改。既然是体力 活,我也不废话了,给出一个模版。这个东西并不复杂,因为decode部分本来要做的工作就很简单,无非是获得真正的shellcode开始的地址,然后 解码(通常是与0x99做一下异或),先看代码,后面还会有解释的:

add ebx,32h
xor ecx,ecx
mov cx,155h
decode:
xor byte ptr [ebx+ecx],99h
loop decode

第一句当然是确定需要解码的shellcode的内存地址了,第二、三句话是确定解码的长度,长一点没有关系。第四、五句是简单的循环,依次把shellcode解码,解码完以后,就自然地执行过去了。
在VC中嵌入汇编可以很轻松地看到编译后的机器码,收集一下,是:

""x83"xC3"x32"x33"xC9"x66"xB9"x83"x01"x80"x34"x0B"x99"xE2"xFA"

这下子decode部分的长度也知道了,一共是十五个字节,所以可以计算溢出后执行到这里时,待解码的shellcode起始内存地址为ebx + 8 + 15 = ebx + 33 = ebx + 17h。考虑到循环条件是按照ecx的值来决定的,最后一次循环的时候,ecx的值为1(为0就跳出循环了),所以应该保证ebx+1是要解码的最后,也 就是待解码的第一字节。所以,解码部分的第一句,add ebx, 32h要改成add ebx, 16h,所以最后得到的decode是:

""x83"xC3"x16"x33"xC9"x66"xB9"x83"x01"x80"x34"x0B"x99"xE2"xFA"

把这一段替换掉原来shellcode的decode部分,就可以放心地使用了。
好,来构造好我们的最终版的溢出程序,呵呵~中间的步骤虽然看起来都省略了,但想想其实事情并不多,就是确定一下溢出点,我算过了403,你也算算?(见2.cpp)
运行一下试试看,成功没有?什么,没有成功?失败了?
我们拿出Ollydbg看看,跟进去一步一步的走,看看为什么,是因为decode出了问题码?(见动画2)
从 调试的过程来看,decode部分(就是call ecx -> jmp ebx ->jmp xxxxx后开始的地方)没有出错,解码也确实成功解码了,但是执行shellcode中似乎出了点问题。第一个直觉是shellcode被改掉了,在哪 里被改的呢,现在还不清楚究竟是shellcode本身含有特殊字符,还是在溢出发生之前被改掉了。第一种情况的话,可以换一个shellcode,第二 种情况就要挨着挨着去对比,工作量比较大,按道理应该先做第一个,但具体有些什么字符不能包含我们并不是很清楚,如果贸然去换shellcode的话,可 能问题越来越多,所以还是先做第二种情况的。有些时候,看似麻烦的路子可能反而容易走一些吧。
和上次的工作一样,运行到decode部分,在内存里面选择显示ebx处的内容,然后选定后面的一大段数据,导出到文件(图三)。我们来仔细对比一下内存中的内容和在程序中shellcode的内容是否一样。
经过长时间仔细的比较,我们发现在ebx+28的地方好像不一样。回去再看看图二,呵呵,我用红笔画出来的,有发现么,这个地方非常显眼,应该是s啊,却变成了q,被减小了2,同样的,对比文件中也发现是减小了2(放大的图二)。
既然这样,我们把ebx+28地方的值先加上2好了,到时候被减掉2,不久刚刚好么?(见源代码3.cpp)
执行一下程序,然后telnet 127.0.0.1 53,shell是不是已经出来了?很有成就感吧,呵呵。
对 于Serv-U的MDTM时区漏洞,我记得在最开始的那一篇文章里面有简要的提到过,这个漏洞的问题是如果时区超长反而就不会触发了,所以必须控制好时区 的长度。由于时区不能过长,所以一般来说都在这里只放上一个搜索真正shellcode的搜索代码,而把起作用的shellcode放到其他地方。这个 Serarch code和第一次的几乎一模一样,而且也没有什么特别需要注意的地方,有兴趣的朋友不妨动手做一做。
这个系列写到这里,有些话想 总结一下。对于栈溢出来说,其实要写一个Exploit并不是特别的困难,我们只要知道其中的原理,然后细心加耐心去调试就可以了。Exploit中真正 重点的地方几乎都在shellcode中,而shellcode中最让人头痛的功能部分,已经有很多大虾们写好并且无私地贴了出来,我们除了学习,就是直 接拿来使用。对于不同的漏洞,可能对shellcode的要求不一样,这是唯一的在调试每一个漏洞中能得到乐趣的地方,其他很多,像对溢出点的定位,溢出 程序的基本构造,都是很繁琐但是很机械的行为,如果不能在这些当中获得一些乐趣的话,跳过这些部分也无妨。
对于上面的溢出,还值得指出来的是,由 于可写的目录不一定是根目录,因而有可能我们需要对路径进行切换。单独的路径切换的话,仅仅是一个CWD命令就可以完成,但是路径对溢出点的定位是有影响 的,倘若要将程序做得完美,对于路径的考虑是非常必要的。另外,要做出通用的东西,还要考虑命令行的输入等,很多东西要给用户自己去指定,这些都是繁琐的 工作,在外面发布的一些利用源代码做得很好,看看就行,在这个地方就不多加赘述了。
关于这里写出来的一些调试方法,有些看起来似乎非常的取巧,然 而却是很有效果的。写出来纯粹是为了让读者也能感受一下编写Exploit的乐趣,并不是主张读者用相同的方法去研究漏洞。我想不管什么漏洞,最好都能够 看看反汇编的代码,这样子,比我们在这里有些东西只能靠瞎猜(比如前面的某一个地方被减去了2)要好很多,而且一旦对这个非常感兴趣的话,如果不去看反汇 编而仅仅是黑盒方式来测试的话,纵然有所提高,这提高也是非常有限的。
最后还是模仿Leven在Xfocus上面发的一篇Windows溢出随笔,写上一些随笔作为这篇文章的结束吧:

1、一般来说,栈溢出比堆溢出更容易利用、也更加通用
2、在考虑Exploit的通用性之前,最好能把软件在windows的各个版本下(win2k、xp、2003、不同的语言版本、不同的sp版本)调试一下。软件的兼容性也是一个问题,最好能够找到有漏洞的最高版本的软件来调试。
3、 不要试图在不同版本的windows里寻找不同跳转方式的相同地址,就是说,不要在win2ksp4简体中文版里的某个进程里搜索所有的jmp esp(ff,e4)的地址,在winxpsp1英文版里的某个进程里搜索所有的push esp,ret(54,c3)的地址,然后妄图比较出一个相同的地址。除非是加载在0x00400000位置的进程。另外,在同一种系统的各个语言版本 (如win2000 sp4), 在 msvcrt.dll 中找到的地址可以通用;在同一种语言的各个系统版本(如简体中文win2000, winxp, win2003), 在 0x7FFA0000 中找到的地址可以通用
4、如果用覆盖异常的方式接管指令(比如这篇文章中举到的例子),要注意到xp/2003和2k是不同的。如果要写通用程序,最好用pop,pop,ret的方式,xp/2003下面你常常会发现ebx在溢出的时候变成了0。
5、 涉及到搜索内存里的shellcode的时候,最好能够先接管异常,除非你确定在你开始搜索的地址附近一定能够找到。xp不允许执行栈中的代码,所以需要 在异常处理链里写入另一个跳转地址,再返回到栈中。或者之前就是通过覆盖异常的方式接管指令的话,现在可以直接利用了。
6、对于菜鸟而言,果断的放弃UNICODE,除非你有很多的时间而且你觉得你能够在不断的解决问题中得到乐趣。
7、指令跳转大多数情况下是靠指令:jmp、call或者ret。对于覆盖地址的情况,绝大部分漏洞都只能尝试寻找这些指令地址
8、除了esp,ebx,还有其他方法可以定位shellcode,分析漏洞时,不妨把当时的所有寄存器都看一遍(比如perl –e的漏洞),有时保存在堆栈里的地址也有用(比如Messenger)。把上下文的调用函数也看一遍,利用它们定位可能效果更好
9、栈溢出时,把shellcode放到覆盖返回地址或者异常处理链的前面也不失为一个好方法。
10、对于漏洞而言,写合适的shellcode往往是最难的,也是最有乐趣的一步,每个漏洞的利用都是耐心和创造力的考验(有些时候我们得去一个一个的比较,不是么?)
11、享受Exploit的乐趣,如果你不能够得到满足或者无法下手,不妨休息一下,事情往往会有戏剧性的转变,也许两三天后,你发现灵感又回来了。

posted on 2007-08-24 16:52  dhb133  阅读(671)  评论(0编辑  收藏  举报

导航