标 题
:
【技术专题】软件漏洞分析入门_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
搜索
最新评论
阅读排行榜
评论排行榜
|
|