标 题 :  【技术专题】软件漏洞分析入门_5_初级栈溢出D_植入任意代码
作 者 :  failwest
时 间 :  2007 - 12 - 16 , 17 : 06
链 接 :  http : //bbs.pediy.com/showthread.php?t=56656

第 5 讲  初级栈溢出D——植入任意代码

To be the apostrophe which changed “Impossible” into “I’m possible”
—— failwest

麻雀虽小,五脏俱全

如果您顺利的学完了前面 4 讲的内容,并成功的完成了第 2 讲和第 4 讲中的实验,那么今天请跟我来一起挑战一下劫持有漏洞的进程,并向其植入恶意代码的实验,相信您成功完成这个实验后,学习的兴趣和自信心都会暴增。

开始之前,先简要的回答一下前几讲跟贴中提出的问题

代码编译少头文件问题:可能是个人习惯问题,哪怕几行长的程序我也会丢到project里去build,而不是用cl,所以没有注意细节。如果你们嫌麻烦,不如和我一样用project来build,应该没有问题的。否则的话,实验用的程序实在太简单了,这么一点小问题自己决绝吧。另外,看到几个同学说为了实验,专门恢复了古老的VC6 .0 ,我也感动不已啊,呵呵。

地址问题:溢出使用的地址一般都要在调试中重新确定,尤其是本节课中的哦。所以照抄我的实验指导,很可能会出现地址错误。特别是本节课中有若干个地址都需要在调试中重新确定,请大家务必注意。能够屏蔽地址差异的通用溢出方法将会在后续课程中逐一讲解。

还有就是抱歉周末中断了一天的讲座——无私奉献也要过周末啊,大家体谅一下了。另外就是下周项目很紧张,估计不能每天都发贴了,争取两到三天发一次,请大家体谅。

如果有什么问题,欢迎在跟贴中提出来,一起讨论,实验成功完成的同学记住要吱——吱——吱啊,呵呵

在基础知识方面,本节没有新的东西。但是这个想法实践起来还是要费点周折的。我设计的实验是最最简单的情况,为了防止一开始难度高,刻意的去掉了真正的漏洞利用中的一些步骤,为的是让初学者理解起来更加清晰,自然。

本节将涉及极少量的汇编语言编程,不过不要怕,非常简单,我会给于详细的解释,不用专门去学汇编语言也能扛下来

另外本节需要最基本的使用OllyDbg进行调试,并配合一些其他工具以确认一些内存地址。当然这些地址的确认方法有很多,我只给出一种解决方案,如果大家在实验的时候有什么心得,不妨在跟贴中拿出来和大家一起分享,一起进步。

开始前简单回顾上节的内容:

password . txt 文件中的超长畸形密码读入内存后,会淹没verify_password函数的返回地址,将其改写为密码验证正确分支的指令地址

函数返回时,错误的返回到被修改的内存地址处取指执行,从而打印出密码正确字样

试想一下,如果我们把buffer [ 44 ] 中填入一段可执行的机器指令(写在password . txt文件中即可),再把这个返回地址更改成buffer [ 44 ] 的位置,那么函数返回时不就正好跳去buffer里取指执行了么——那里恰好布置着一段用心险恶的机器代码!

本节实验的内容就用来实践这一构想——通过缓冲去溢出,让进程去执行布置在缓冲区中的一段任意代码。



图 1
  


   如上图所示,在本节实验中,我们准备向password . txt文件里植入二进制的机器码,并用这段机器码来调用windows的一个API函数 MessageBoxA,最终在桌面上弹出一个消息框并显示“failwest”字样。事实上,您可以用这段代码来做任何事情,我们这里只是为了证明技术的可行性。

为了完成在栈区植入代码并执行,我们在上节的密码验证程序的基础上稍加修改,使用如下的实验代码:

#include  < stdio . h >
#include  < windows . h >
#define  PASSWORD  "1234567"
int  verify_password  ( char  * password )
{
   int  authenticated ;
   char  buffer [ 44 ];
   authenticated = strcmp ( password , PASSWORD );
   strcpy ( buffer , password ); //over flowed here!  
   return  authenticated ;
}
main ()
{
   int  valid_flag = 0 ;
   char  password [ 1024 ];
   FILE  *  fp ;
   LoadLibrary ( "user32.dll" ); //prepare for messagebox
   if (!( fp = fopen ( "password.txt" , "rw+" )))
  {
     exit ( 0 );
  }
   fscanf ( fp , "%s" , password );
   valid_flag  =  verify_password ( password );
   if ( valid_flag )
  {
     printf ( "incorrect password!\n" );
  }
   else
   {
     printf ( "Congratulation! You have passed the verification!\n" );
  }
   fclose ( fp );
}

这段代码在底 4 讲中使用的代码的基础上修改了三处:

增加了头文件windows . h,以便程序能够顺利调用LoadLibrary函数去装载user32 . dll

verify_password函数的局部变量buffer由 8 字节增加到 44 字节,这样做是为了有足够的空间来“承载”我们植入的代码

main函数中增加了LoadLibrary ( "user32.dll" ) 用于初始化装载user32 . dll,以便在植入代码中调用MessageBox

用VC6 .0 将上述代码编译(默认编译选项,编译成debug版本),得到有栈溢出的可执行文件。在同目录下创建password . txt文件用于程序调试。


我们准备在password . txt文件中植入二进制的机器码,在password . txt攻击成功时,密码验证程序应该执行植入的代码,并在桌面上弹出一个消息框显示“failwest”字样。
  
让我们在动手之前回顾一下我们需要完成的几项工作:

1 :分析并调试漏洞程序,获得淹没返回地址的偏移——在password . txt的第几个字节填伪造的返回地址

2 :获得buffer的起始地址,并将其写入password . txt的相应偏移处,用来冲刷返回地址——填什么值

3 :向password . txt中写入可执行的机器代码,用来调用API弹出一个消息框——编写能够成功运行的机器代码(二进制级别的哦)

这三个步骤也是漏洞利用过程中最基本的三个问题——淹到哪里,淹成什么以及开发shellcode

首先来看淹到什么位置和把返回地址改成什么值的问题

本节验证程序里verify_password中的缓冲区为 44 个字节,按照前边实验中对栈结构的分析,我们不难得出栈帧中的状态如下图所示:

 

图 2


如果在password . txt中写入恰好 44 个字符,那么第 45 个隐藏的截断符null将冲掉authenticated低字节中的 1 ,从而突破密码验证的限制。我们不妨就用 44 个字节做为输入来进行动态调试。

  出于字节对齐、容易辨认的目的,我们把“ 4321 ”作为一个输入单元。
  buffer [ 44 ] 共需要 11 个这样的单元
  第 12 个输入单元将authenticated覆盖
  第 13 个输入单元将前栈帧EBP值覆盖
  第 14 个输入单元将返回地址覆盖

分析过后我们需要进行调试验证分析的正确性。首先在password . txt中写入 11 组“ 4321 ”共 44 个字符:


  

图 3


如我们所料,authenticated被冲刷后程序将进入验证通过的分支:
 

图 4

用OllyDbg加载这个生成的PE文件进行动态调试,字符串拷贝函数过后的栈状态如图:

 

图 5

   此时的栈区内存如下表所示

局部变量名  内存地址  偏移 3 处的值  偏移 2 处的值  偏移 1 处的值  偏移 0 处的值
buffer [ 0 ~ 3 ]   0x0012FAF0  0x31  ( ‘ 1 ’ )   0x32  ( ‘ 2 ’ )   0x33  ( ‘ 3 ’ )   0x34  ( ‘ 4 ’ )
……  ( 9 个双字)   0x31  ( ‘ 1 ’ )   0x32  ( ‘ 2 ’ )   0x33  ( ‘ 3 ’ )   0x34  ( ‘ 4 ’ )
buffer [ 40 ~ 43 ]   0x0012FB18  0x31  ( ‘ 1 ’ )   0x32  ( ‘ 2 ’ )   0x33  ( ‘ 3 ’ )   0x34  ( ‘ 4 ’ )
authenticated
(被覆盖前)   0x0012FB1C  0x00  0x00  0x00  0x31  ( ‘ 1 ’ )
authenticated
(被覆盖后)   0x0012FB1C  0x00  0x00  0x00  0x00  ( NULL )
前栈帧EBP   0x0012FB20  0x00  0x12  0xFF  0x80
返回地址   0x0012FB24  0x00  0x40  0x11  0x18

   动态调试的结果证明了前边分析的正确性。从这次调试中我们可以得到以下信息:

buffer数组的起始地址为 0x0012FAF0 ——注意这个值只是我调试的结果,您需要在自己机器上重新确定!

password . txt文件中第 53 到第 56 个字符的ASCII码值将写入栈帧中的返回地址,成为函数返回后执行的指令地址

也就是说将buffer的起始地址 0x0012FAF0 写入password . txt文件中的第 53 到第 56 个字节,在verify_password函数返回时会跳到我们输入的字串开始出取指执行。


我们下面还需要给password . txt中植入机器代码。

让程序弹出一个消息框只需要调用windows的API函数MessageBox。MSDN对这个函数的解释如下:

int  MessageBox (
   HWND hWnd ,           // handle to owner window
   LPCTSTR lpText ,      // text in message box
   LPCTSTR lpCaption ,   // message box title
   UINT uType           // message box style
);

hWnd 
[ in ]  消息框所属窗口的句柄,如果为NULL的话,消息框则不属于任何窗口 
lpText 
[ in ]  字符串指针,所指字符串会在消息框中显示 
lpCaption 
[ in ]  字符串指针,所指字符串将成为消息框的标题 
uType 
[ in ]  消息框的风格(单按钮,多按钮等),NULL代表默认风格 


虽然只是调一个API,在高级语言中也就一行代码,但是要我们直接用二进制指令的形式写出来也并不是一件容易的事。这个貌似简单的问题解决起来还要用一点小心思。不要怕,我会给我的解决办法,不一定是最好的,但是能解决问题。

  我们将写出调用这个API的汇编代码,然后翻译成机器代码,用 16 进制编辑工具填入password . txt文件。

注意:熟悉MFC的程序员一定知道,其实系统中并不存在真正的MessagBox函数,对MessageBox这类API的调用最终都将由系统按照参数中字符串的类型选择“A”类函数(ASCII)或者“W”类函数(UNICODE)调用。因此我们在汇编语言中调用的函数应该是MessageBoxA。多说一句,其实MessageBoxA的实现只是在设置了几个不常用参数后直接调用MessageBoxExA。探究API的细节超出了本书所讨论的范围,有兴趣的读者可以参阅其他书籍。

用汇编语言调用MessageboxA需要三个步骤:

1. 装载动态链接库user32 . dll。MessageBoxA是动态链接库user32 . dll的导出函数。虽然大多数有图形化操作界面的程序都已经装载了这个库,但是我们用来实验的consol版并没有默认加载它

2. 在汇编语言中调用这个函数需要获得这个函数的入口地址

3  在调用前需要向栈中按从右向左的顺序压入MessageBoxA的四个参数。当然,我肯定压如failwest啦,哈哈

对于第一个问题,为了让植入的机器代码更加简洁明了,我们在实验准备中构造漏洞程序的时候已经人工加载了user32 . dll这个库,所以第一步操作不用在汇编语言中考虑。

对于第二个问题,我们准备直接调用这个API的入口地址,这个地址需要在您的实验机器上重新确定,因为user32 . dll中导出函数的地址和操作系统版本和补丁号有关,您的地址和我的地址不一定一样。

MessageBoxA的入口参数可以通过user32 . dll在系统中加载的基址和MessageBoxA在库中的偏移相加得到。为啥?看下看雪老大《软件加密与解密》中关于虚拟地址这些基础知识的论述吧,相信版内也有很多相关资料。

这里简单解释下,MessageBoxA是user32 . dll的一个导出函数,要确定它首先要知道user32 . dll在虚拟内存中的装载地址(与操作系统版本有关),然后从这个基地址算起,找到MessageBoxA这个导出函数的偏移,两者相加,就是这个API的虚拟内存地址。

具体的我们可以使用VC6 .0 自带的小工具“Dependency Walker”获得这些信息。您可以在VC6 .0 安装目录下的Tools下找到它:
 

图 6

   运行Depends后,随便拖拽一个有图形界面的PE文件进去,就可以看到它所使用的库文件了。在左栏中找到并选中user32 . dll后,右栏中会列出这个库文件的所有导出函数及偏移地址;下栏中则列出了PE文件用到的所有的库的基地址。

 

图 7


   如上图示,user32 . dll的基地址为 0x77D40000 ,MessageBoxA的偏移地址为 0x000404EA 。基地址加上偏移地址就得到了MessageBoxA在内存中的入口地址: 0x77D804EA


   有了这个入口地址,就可以编写进行函数调用的汇编代码了。这里我们先把字符串“failwest”压入栈区,消息框的文本和标题都显示为 “failwest”,只要重复压入指向这个字符串的指针即可;第一个和第四个参数这里都将设置为NULL。写出的汇编代码和指令所对应的机器代码如下:

           

机器代码( 16 进制)  汇编指令  注释
33  DB  XOR EBX , EBX  压入NULL结尾的”failwest”字符串。之所以用EBX清零后入栈做为字符串的截断符,是为了避免“PUSH  0 ”中的NULL,否则植入的机器码会被strcpy函数截断。
53                   PUSH EBX  
68 77 65 73 74   PUSH  74736577  
68 66 61 69 6C   PUSH  6C696166  
8B  C4                MOV EAX , ESP  EAX里是字符串指针
53                   PUSH EBX  四个参数按照从右向左的顺序入栈,分别为 :
                                                 ( 0 , failwest , failwest , 0 )
                                                   消息框为默认风格,文本区和标题都是“failwest”
50                    PUSH EAX  
50                    PUSH EAX  
53                    PUSH EBX  
B8 EA  04  D8  77   MOV EAX ,  0x77D804EA   调用MessageBoxA。注意不同的机器这里的                                    
                                                                    函数入口地址可能不同,请按实际值填入 !
FF D0                 CALL EAX  


从汇编指令到机器码的转换可以有很多种方法。调试汇编指令,从汇编指令中提取出二进制机器代码的方法将在后面逐一介绍。由于这里仅仅用了 11 条指令和对应的 26 个字节的机器代码,如果您一定要现在就弄明白指令到机器码是如何对应的话,直接查阅Intel的指令集手工翻译也不是不可以。

  将上述汇编指令对应的机器代码按照上一节介绍的方法以 16 进制形式逐字抄入password . txt,第 53 到 56 字节填入buffer的起址 0x0012FAF0 ,其余的字节用 0x90 ( nop指令 ) 填充,如图:

 

图 8


换回文本模式可以看到这些机器代码所对应的字符:
 


图 9

这样构造了password . txt之后在运行验证程序,程序执行的流程将按下图所示:




图 10


程序运行情况如图:
 

图 11


成功的弹出了我们植入的代码!

您成功了吗?如果成功的唤出了藏在password . txt中的消息框,请在跟贴中吱一下,和大家一起分享您喜悦的心情,这是我们学习技术的源动力。

最后总结一下本节实验的几个要点:
确认函数返回地址与buffer数组的距离——淹哪里
确认buffer数组的内存地址——把返回地址淹成什么(需要调试确定,与机器有关)
编制调用消息框的二进制代码,关键是确定MessageBoxA的虚拟内存地址(与机器有关)

我实验用的PE和password . txt在这里:

想要PE的请点这里:stack_overflow_exec . rar
想要Passwrd . txt的请点这里:password . txt


这节课的题目是麻雀虽小,五脏俱全。这是因为这节课第一次把漏洞利用的全国程展现给了大家:
密码验证程序读入一个畸形的密码文件,竟然蹦出了一个消息框!
Word在解析doc文档时,不知有多少个内存复制和操作的函数调用,如果哪一个有溢出漏洞,那么office读入一个畸形的word文档时,会不会弹出个消息框,开个后门,起个木马啥的?
IIS和APACHE在解析WEB请求的时候,也不知道有多少内存复制操作,如果存在溢出漏洞,那么攻击者发送一个畸形的WEB请求,会不会导致server做出点奇怪的事情?
RPC调用中如果出现……

上面说的并不是危言耸听,全都是真实世界中曾经出现过的漏洞攻击案例。本节的例子是现实中的漏洞利用案例的精简版,用来阐述基本概念并验证技术可行性。随着后面的深入讨论,您会发现漏洞研究是多么有趣的一门技术。



在本节最后,我给出一个课后作业和几个思考题——因为下一讲可能会稍微隔几天,大家不妨自己动手练习练习,记住光听课是没有的,动手非常重要!

课后作业:如果您细心的话,在点击上面的ok按钮之后,程序会崩溃:


 图 12

   这是因为MessageBoxA调用的代码执行完成之后,我们没有写安全退出的代码的缘故。您能把我给出的二进制代码稍微修改下,使之能够在点击之后干净利落的退出进程么?

如果你能做到这一点,不妨把你的解决方案也拿出来和大家一起分享,一起进步。

思考题:

1 :我反复强调,buffer的位置在实验中需要自己在调试中确定,不同机器环境可能不一样。
大家都知道,程序运行中,栈的位置是动态变化的,也就是说buffer的内存地址可能每次都不一样,在真实的漏洞利用中,尤其是遇到多线程的程序,每次的缓冲区位置都是不同的。那么我们怎么保证在函数返回时总能够准确的跳回buffer,找到植入的代码呢 ?

比较通用的定位植入代码(shellcode)的方法我会在后面的讲座中系统介绍,这里先提一下,大家可以思考思考

2 :我也反复强调,API的地址需要自己确定,不同环境会有不同。这样植入代码的通用性还是会大打折扣。有没有通用的定位windows API的方法呢?

以上两个问题是影响windows平台下漏洞利用稳定性的两个很关键的问题。我选择了windows平台来讲解,是为了照顾初学者对linux的进入门槛和windows下美轮美奂的调试工具。但windows的溢出是相对linux较难的,进入简单,深造难。不过我相信大家能啃下来的。

为了不至于在一节课中引入太多新东西,我在本节课中均采用现场调试确定的方法,并没有考虑通用性问题。在这里鼓励大家积极思考,有想法别忘了在跟贴中分享出来。