版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1、缓冲区溢出攻击详细讲解缓冲区溢出(Buffer Overflow)是计算机安全领域内既经典而又古老的话题。随着计算机系统安全性的加强,传统的缓冲区溢出攻击方式可能变得不再奏效,相应的介绍缓冲区溢出原理的资料也变得“大众化”起来。其中看雪的0day安全:软件漏洞分析技术一书将缓冲区溢出攻击的原理阐述得简洁明了。本文参考该书对缓冲区溢出原理的讲解,并结合实际的代码实例进行验证。不过即便如此,完成一个简单的溢出代码也需要解决很多书中无法涉及的问题,尤其是面对较新的具有安全特性的编译器比如MS的Visual Studio2010。接下来,我们结合具体代码,按照对缓冲区溢出原理的循序渐进地理解方式去挖掘
2、缓冲区溢出背后的底层机制。一、代码 <=> 数据顾名思义,缓冲区溢出的含义是为缓冲区提供了多于其存储容量的数据,就像往杯子里倒入了过量的水一样。通常情况下,缓冲区溢出的数据只会破坏程序数据,造成意外终止。但是如果有人精心构造溢出数据的内容,那么就有可能获得系统的控制权!如果说用户(也可能是黑客)提供了水缓冲区溢出攻击的数据,那么系统提供了溢出的容器缓冲区。缓冲区在系统中的表现形式是多样的,高级语言定义的变量、数组、结构体等在运行时可以说都是保存在缓冲区内的,因此所谓缓冲区可以更抽象地理解为一段可读写的内存区域,缓冲区攻击的最终目的就是希望系统能执行这块可读写内存
3、中已经被蓄意设定好的恶意代码。按照冯·诺依曼存储程序原理,程序代码是作为二进制数据存储在内存的,同样程序的数据也在内存中,因此直接从内存的二进制形式上是无法区分哪些是数据哪些是代码的,这也为缓冲区溢出攻击提供了可能。图1 进程地址空间分布图1是进程地址空间分布的简单表示。代码存储了用户程序的所有可执行代码,在程序正常执行的情况下,程序计数器(PC指针)只会在代码段和操作系统地址空间(内核态)内寻址。数据段内存储了用户程序的全局变量,文字池等。栈空间存储了用户程序的函数栈帧(包括参数、局部数据等),实现函数调用机制,它的数据增长方向是低地址方向。堆空间存储了程序运行时动态申请
4、的内存数据等,数据增长方向是高地址方向。除了代码段和受操作系统保护的数据区域,其他的内存区域都可能作为缓冲区,因此缓冲区溢出的位置可能在数据段,也可能在堆、栈段。如果程序的代码有软件漏洞,恶意程序会“教唆”程序计数器从上述缓冲区内取指,执行恶意程序提供的数据代码!本文分析并实现栈溢出攻击方式。二、函数栈帧栈的主要功能是实现函数的调用。因此在介绍栈溢出原理之前,需要弄清函数调用时栈空间发生了怎样的变化。每次函数调用时,系统会把函数的返回地址(函数调用指令后紧跟指令的地址),一些关键的寄存器值保存在栈内,函数的实际参数和局部变量(包括数据、结构体、对象等)也会保存在栈内。这些数据统称为函数调用的栈
5、帧,而且是每次函数调用都会有个独立的栈帧,这也为递归函数的实现提供了可能。图2 函数栈帧如图所示,我们定义了一个简单的函数function,它接受一个整形参数,做一次乘法操作并返回。当调用function(0)时,arg参数记录了值0入栈,并将call function指令下一条指令的地址0x00bd16f0保存到栈内,然后跳转到function函数内部执行。每个函数定义都会有函数头和函数尾代码,如图绿框表示。因为函数内需要用ebp保存函数栈帧基址,因此先保存ebp原来的值到栈内,然后将栈指针esp内容保存到ebp。函数返回前需要做相反的操作将esp指针恢复,并弹出ebp。这样,函数
6、内正常情况下无论怎样使用栈,都不会使栈失去平衡。sub esp,44h指令为局部变量开辟了栈空间,比如ret变量的位置。理论上,function只需要再开辟4字节空间保存ret即可,但是编译器开辟了更多的空间(这个问题很诡异,你觉得呢?)。函数调用结束返回后,函数栈帧恢复到保存参数0时的状态,为了保持栈帧平衡,需要恢复esp的内容,使用add esp,4将压入的参数弹出。之所以会有缓冲区溢出的可能,主要是因为栈空间内保存了函数的返回地址。该地址保存了函数调用结束后后续执行的指令的位置,对于计算机安全来说,该信息是很敏感的。如果有人恶意修改了这个返回地址,并使该返回地址指向了一个新的代码位置,程
7、序便能从其它位置继续执行。三、栈溢出基本原理上边给出的代码是无法进行溢出操作的,因为用户没有“插足”的机会。但是实际上很多程序都会接受用户的外界输入,尤其是当函数内的一个数组缓冲区接受用户输入的时候,一旦程序代码未对输入的长度进行合法性检查的话,缓冲区溢出便有可能触发!比如下边的一个简单的函数。1. void fun(unsigned char *data)2. 3. unsigned char bufferBUF_LEN;4. strcpy(char*)buffer,(char*)data);/溢出点5. 这个函数没有做什么有“意义”的事情(
8、这里主要是为了简化问题),但是它是一个典型的栈溢出代码。在使用不安全的strcpy库函数时,系统会盲目地将data的全部数据拷贝到buffer指向的内存区域。buffer的长度是有限的,一旦data的数据长度超过BUF_LEN,便会产生缓冲区溢出。图3 缓冲区溢出由于栈是低地址方向增长的,因此局部数组buffer的指针在缓冲区的下方。当把data的数据拷贝到buffer内时,超过缓冲区区域的高地址部分数据会“淹没”原本的其他栈帧数据,根据淹没数据的内容不同,可能会有产生以下情况:1、淹没了其他的局部变量。如果被淹没的局部变量是条件变量,那么可能会改变函数原本的执行流程。这种方式可以用
9、于破解简单的软件验证。2、淹没了ebp的值。修改了函数执行结束后要恢复的栈指针,将会导致栈帧失去平衡。3、淹没了返回地址。这是栈溢出原理的核心所在,通过淹没的方式修改函数的返回地址,使程序代码执行“意外”的流程!4、淹没参数变量。修改函数的参数变量也可能改变当前函数的执行结果和流程。5、淹没上级函数的栈帧,情况与上述4点类似,只不过影响的是上级函数的执行。当然这里的前提是保证函数能正常返回,即函数地址不能被随意修改(这可能很麻烦!)。如果在data本身的数据内就保存了一系列的指令的二进制代码,一旦栈溢出修改了函数的返回地址,并将该地址指向这段二进制代码的其实位置,那么就完成了基本的溢出攻击行为
10、。图4 基本栈溢出攻击通过计算返回地址内存区域相对于buffer的偏移,并在对应位置构造新的地址指向buffer内部二进制代码的其实位置,便能执行用户的自定义代码!这段既是代码又是数据的二进制数据被称为shellcode,因为攻击者希望通过这段代码打开系统的shell,以执行任意的操作系统命令比如下载病毒,安装木马,开放端口,格式化磁盘等恶意操作。四、栈溢出攻击上述过程虽然理论上能完成栈溢出攻击行为,但是实际上很难实现。操作系统每次加载可执行文件到进程空间的位置都是无法预测的,因此栈的位置实际是不固定的,通过硬编码覆盖新返回地址的方式并不可靠。为了能准确定位shellcode的地址,
11、需要借助一些额外的操作,其中最经典的是借助跳板的栈溢出方式。根据前边所述,函数执行后,栈指针esp会恢复到压入参数时的状态,在图4中即data参数的地址。如果我们在函数的返回地址填入一个地址,该地址指向的内存保存了一条特殊的指令jmp esp跳板。那么函数返回后,会执行该指令并跳转到esp所在的位置即data的位置。我们可以将缓冲区再多溢出一部分,淹没data这样的函数参数,并在这里放上我们想要执行的代码!这样,不管程序被加载到哪个位置,最终都会回来执行栈内的代码。图5 借助跳板的栈溢出攻击借助于跳板的确可以很好的解决栈帧移位(栈加载地址不固定)的问题,但是跳板指令从哪找呢?“幸运”
12、的是,在Windows操作系统加载的大量dll中,包含了许多这样的指令,比如kernel32.dll,ntdll.dll,这两个动态链接库是Windows程序默认加载的。如果是图形化界面的Windows程序还会加载user32.dll,它也包含了大量的跳板指令!而且更“神奇”的是Windows操作系统加载dll时候一般都是固定地址,因此这些dll内的跳板指令的地址一般都是固定的。我们可以离线搜索出跳板执行在dll内的偏移,并加上dll的加载地址,便得到一个适用的跳板指令地址!/查询dll内第一个jmp esp指令的位置int findJmp(char*dll_name)ch
13、ar* handle=(char*)LoadLibraryA(dll_name);/获取dll加载地址for(int pos=0;pos+)/遍历dll代码空间if(handlepos=(char)0xff&&handlepos+1=(char)0xe4)/寻找0xffe4 = jmp espreturn (int)(handle+pos);这里简化了搜索算法,输出第一个跳板指令的地址,读者可以选取其他更合适位置。LoadLibraryA库函数返回值就是dll的加载地址,然后加上搜索到的跳板指令偏移pos便
14、是最终地址。jmp esp指令的二进制表示为0xffe4,因此搜索算法就是搜索dll内这样的字节数据即可。虽然如此,上述的攻击方式还不够好。因为在esp后继续追加shellcode代码会将上级函数的栈帧淹没,这样做并没有什么好处,甚至可能会带来运行时问题。既然被溢出的函数栈帧内提供了缓冲区,我们还是把核心的shellcode放在缓冲区内,而在esp之后放上跳转指令转移到原本的缓冲区位置。由于这样做使代码的位置在esp指针之前,如果shellcode中使用了push指令便会让esp指令与shellcode代码越来越近,甚至淹没自身的代码。这显然不是我们想要的结果,因此我们可以强制抬高esp指针,
15、使它在shellcode之前(低地址位置),这样就能在shellcode内正常使用push指令了。图6 调整shellcode与栈指针调整代码的内容很简单: add esp,-Xjmp esp第一条指令抬高了栈指针到shellcode之前。X代表shellcode起始地址与esp的偏移。如果shellcode从缓冲区起始位置开始,那么就是buffer的地址偏移。这里不使用sub esp,X指令主要是避免X的高位字节为0的问题,很多情况下缓冲区溢出是针对字符串缓冲区的,如果出现字节0会导致缓冲区截断,从而导致溢出失败。第二条指令就是跳转到shellcode
16、的起始位置继续执行。(又是jmp esp!)通过上述方式便能获得一个较为稳定的栈溢出攻击。五、shellcode构造shellcode实质是指溢出后执行的能开启系统shell的代码。但是在缓冲区溢出攻击时,也可以将整个触发缓冲区溢出攻击过程的代码统称为shellcode,按照这种定义可以把shellcode分为四部分:1、核心shellcode代码,包含了攻击者要执行的所有代码。2、溢出地址,是触发shellcode的关键所在。3、填充物,填充未使用的缓冲区,用于控制溢出地址的位置,一般使用nop指令填充0x90表示。4、结束符号0,对于符号串shellcode需要用0结尾,避免溢出时字符串异
17、常。前边一直在围绕溢出地址讨论,并解决了shellcode组织的问题,而最核心的代码如何构造并未提及即攻击成功后做的事情。其实一旦缓冲区溢出攻击成功后,如果被攻击的程序有系统的root权限比如系统服务程序,那么攻击者基本上可以为所欲为了!但是我们需要清楚的是,核心shellcode必须是二进制代码形式。而且shellcode执行时是在远程的计算机上,因此shellcode是否能通用是一个很复杂的问题。我们可以用一段简单的代码实例来说明这个问题。缓冲区溢出成功后,一般大家都会希望开启一个远程的shell控制被攻击的计算机。开启shell最直接的方式便是调用C语言的库函数system,该函数可以执
18、行操作系统的命令,就像我们在命令行下执行命令那样。假如我们执行cmd命令在远程计算机上启动一个命令提示终端(我们可能还不能和它交互,但是可以在这之前建立一个远程管道等),这里仅作为实例测试。为了使system函数调用成功,我们需要将“cmd”字符串内容压入栈空间,并将其地址压入作为system函数的参数,然后使用call指令调用system函数的地址,完成函数的执行。但是这样做还不够,如果被溢出的程序没有加载C语言库的话,我们还需要调用Windows的API Loadlibrary加载C语言的库msvcrt.dll,类似的我们也需要为字符串“msvcrt.dll”开辟栈空间。1. xor
19、60;ebx,ebx /ebx=02. 3. push 0x3f3f6c6c /ll?4. push 0x642e7472 /rt.d5. push 0x6376736d /msvc6. mov esp+10,ebx /'?'->'0'7. mov esp+11,ebx /'?'->'0'8. mov eax,esp /"msvcrt.dll"
20、地址9. push eax /"msvcrt.dll"10. mov eax,0x77b62864 /kernel32.dll:LoadLibraryA11. call eax /LoadLibraryA("msvcrt.dll")12. add esp,1613. push 0x3f646d63 /"cmd?"14. mov esp+3,ebx /'?'->'0'15. mov&
21、#160;eax,esp;/"cmd"地址16. push eax /"cmd"17. mov eax,0x774ab16f /msvcrt.dll:system18. call eax /system("cmd")19. add esp,8上述汇编代码实质上是如下两个函数调用语句:1. Loadlibrary(“msvcrt.dll”);2. system(“cmd”);3. 不过在构造这段汇编代码时需要注意不能出现字节0,为了填充字符串的结束字符,我
22、们使用已经初始化为0的ebx寄存器代替。另外,在对库函数调用的时候需要提前计算出函数的地址,如Loadlibrary函数的0x77b62864。计算方式如下:1. int findFunc(char*dll_name,char*func_name)2. 3. HINSTANCE handle=LoadLibraryA(dll_name);/获取dll加载地址4. return (int)GetProcAddress(handle,func_name);5. 这个函数地址是在本地计算的,如果被攻击计算机的操作系统版本差别较大的话,这个地址可能是错误的。不过在0day
23、安全:软件漏洞分析技术中,作者提供了一个更好的方式,感兴趣的读者可以参考该书提供的代码。因此构造一个通用的shellcode并非十分容易,如果想让攻击变得有效的话。六、汇编语言自动转换写出shellcode后(无论是简单的还是通用的),我们还需要将这段汇编代码转换为机器代码。如果读者对x86汇编十分熟悉的话,选择手工敲出二进制代码的话也未尝不可。不过我们都希望能让计算机帮助做完这些事,既然开发环境提供了编译器,用它们帮忙何乐而不为呢?既不用OllyDbg工具,也不适用其他的第三方工具,我们写一个简单的函数来完成这个工作。1. /将内嵌汇编的二进制指令dump到文件,style指定输出数组格式还
24、是二进制形式,返回代码长度2. int dumpCode(unsigned char*buffer)3. 4. goto END /略过汇编代码5. BEGIN:6. _asm7. 8. /在这里定义任意的合法汇编代码 END: /确定代码范围 UINT begin,end; _asm mov eax,BEGIN mov begin,eax mov eax,END mov end,eax /输出 int len=end-begin; memcpy(buffer,(void*)begin,len); /四字节对齐 int fill=(len-len%4)%4; while(fill-)bufferlen+fill=0x90; /返回长度 return len+fill; 因为C+是支持嵌入式汇编代码的,因此在函数内的汇编代码都会被整成编译为二进制代码。实现二进制转换的基本思想是读取编译器最终生成的二进制代码段数据,将数据导出到指定的缓冲区内。为了锁定嵌入式汇编代码的位置和长度,我们定义了两个标签BEGIN和END。这两个标签在汇编语言级别会被解析为实际的线性地址,但是在高级语言级是无法直接使
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2024年塔吊施工安全防护合同
- 2024年度互联网金融服务平台合作合同
- 2024年度广告位代理销售合同(新媒体广告)
- 胶带分配器机器市场发展预测和趋势分析
- 贵金属制钱包市场发展现状调查及供需格局分析预测报告
- 2024年度旅游活动赞助合同:旅游赛事赞助与合作协议
- 2024年度智能硬件产品代理销售合同
- 2024年度储藏室保险服务合同
- 洁厕凝胶市场发展预测和趋势分析
- 2024年度办公楼智能化升级合同:某智能化公司与某办公楼物业管理公司关于智能化升级的合同
- 2023-2024学年广东省深圳市南山区八年级(上)期末英语试卷
- 2023~2024学年第一学期高一期中考试数学试题含答案
- 非遗漆扇扇子科普宣传
- GB/T 15822.1-2024无损检测磁粉检测第1部分:总则
- 2024广西专业技术人员继续教育公需科目参考答案(100分)
- MOOC 马克思主义民族理论与政策-广西民族大学 中国大学慕课答案
- 苯氯苯连续精馏塔设计二设计正文
- 焊缝焊条用量的计算公式
- 人员编制及岗位调整表.doc
- (推荐)浅谈初中学生英语写作中存在的问题、原因及解决策略
- 七年级历史教案:林则徐的教学设计
评论
0/150
提交评论