| 
		
			| 
	
	
		
			
		
				标 题
				: 
				【技术专题】软件漏洞分析入门_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较难的,进入简单,深造难。不过我相信大家能啃下来的。
 
 为了不至于在一节课中引入太多新东西,我在本节课中均采用现场调试确定的方法,并没有考虑通用性问题。在这里鼓励大家积极思考,有想法别忘了在跟贴中分享出来。
     |  | 
		
			| 公告常用链接留言簿(113)随笔分类随笔档案文章分类相册Link搜索最新评论
	阅读排行榜评论排行榜
 |  |