标 题 【技术专题】软件漏洞分析入门_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. 在汇编语言中调用这个函数需要获得这个函数的入口地址

在调用前需要向栈中按从右向左的顺序压入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较难的,进入简单,深造难。不过我相信大家能啃下来的。

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