第7章缓冲区溢出攻击及防御技术张玉清国家计算机网络入侵防范中心2019/11/10网络入侵与防范技术2本章内容安排7.1缓冲区溢出概述7.2缓冲区溢出原理7.3缓冲区溢出的过程7.4代码植入技术7.5实例:ida溢出漏洞攻击7.6缓冲区溢出的防御7.7小结2019/11/10网络入侵与防范技术37.1缓冲区溢出概述什么是缓冲区?它是包含相同数据类型实例的一个连续的计算机内存块。是程序运行期间在内存中分配的一个连续的区域,用于保存包括字符数组在内的各种数据类型。所谓溢出,其实就是所填充的数据超出了原有的缓冲区边界。两者结合进来,所谓缓冲区溢出,就是向固定长度的缓冲区中写入超出其预告分配长度的内容,造成缓冲区中数据的溢出,从而覆盖了缓冲区周围的内存空间。黑客借此精心构造填充数据,导致原有流程的改变,让程序转而执行特殊的代码,最终获取控制权。2019/11/10网络入侵与防范技术47.1缓冲区溢出概述利用缓冲区溢出漏洞进行攻击最早可追溯到1988年Morris蠕虫,它所利用的就是fingerd程序的缓冲区溢出漏洞。1989年,Spafford提交了一份分析报告,描述了VAX机上BSD版Unix的Fingerd的缓冲区溢出程序的技术细节,引起了一部分安全人士对这个研究领域的重视。1996年,AlephOne发表了题为“Smashingthestackforfunandprofit”的文章后,首次详细地介绍了Unix/Linux下栈溢出攻击的原理、方法和步骤,揭示了缓冲区溢出攻击中的技术细节。1999年w00w00安全小组的MattConover写了基于堆缓冲区溢出专著,对堆溢出的机理进行了探索。7.1缓冲区溢出概述Windows系统中缓冲区溢出的事例更是层出不穷。2001年“红色代码”蠕虫利用微软IISWebServer中的缓冲区溢出漏洞使300000多台计算机受到攻击;2003年1月,Slammer蠕虫爆发,利用的是微软SQLServer2000中的缺陷;2004年5月爆发的“振荡波”利用了Windows系统的活动目录服务缓冲区溢出漏洞;2005年8月利用Windows即插即用缓冲区溢出漏洞的“狙击波”被称为历史上最快利用微软漏洞进行攻击的恶意代码。2008年底至2009年的Conficker蠕虫利用的是Windows处理远程RPC请求时的漏洞(MS08-067)。2019/11/10网络入侵与防范技术67.1缓冲区溢出概述目前,利用缓冲区溢出漏洞进行的攻击已经占所有系统攻击总数的80%以上。缓冲区溢出攻击之所以日益普遍,其原因在于各种操作系统和应用软件上存在的缓冲区溢出问题数不胜数,而其带来的影响不容小觑。对缓冲区溢出漏洞攻击,可以导致程序运行失败、系统崩溃以及重新启动等后果。更为严重的是,可以利用缓冲区溢出执行非授权指令,甚至取得系统特权,进而进行各种非法操作。如何防止和检测出利用缓冲区溢出漏洞进行的攻击,就成为防御网络入侵以及入侵检测的重点之一。7.1缓冲区溢出概述与其他的攻击类型相比,缓冲区溢出攻击不需要太多的先决条件杀伤力很强技术性强缓冲区溢出比其他一些黑客攻击手段更具有破坏力和隐蔽性。这也是利用缓冲区溢出漏洞进行攻击日益普遍的原因。2019/11/10网络入侵与防范技术72019/11/10网络入侵与防范技术87.1缓冲区溢出概述破坏性:它极容易使服务程序停止运行,服务器死机甚至删除服务器上的数据。隐蔽性:首先,漏洞被发现之前,程序员一般是不会意识到自己的程序存在漏洞的(事实上,漏洞的发现者往往并非编写者),于是疏于监测;其次,被植入的攻击代码一般都很短,执行时间也非常短,很难在执行过程中被发现,而且其执行并不一定会使系统报告错误,并可能不影响正常程序的运行;7.1缓冲区溢出概述隐蔽性:第三,由于漏洞存在于防火墙内部的主机上,攻击者可以在防火墙内部堂而皇之地取得本来不被允许或没有权限的控制权;第四,攻击的随机性和不可预测性使得防御变得异常艰难,没有攻击时,被攻击程序本身并不会有什么变化,也不会存在任何异常的表现;最后,缓冲区溢出漏洞的普遍存在,针对它的攻击让人防不胜防(各种补丁程序也可能存在着这种漏洞。7.2缓冲区溢出原理7.2.1栈溢出7.2.2堆溢出7.2.3BSS溢出7.2.4格式化串溢出2019/11/10网络入侵与防范技术117.2缓冲区溢出原理当程序运行时,计算机会在内存区域中开辟一段连续的内存块,包括代码段、数据段和堆栈段三部分。7.2缓冲区溢出原理程序在内存中的存放形式7.2缓冲区溢出原理代码段(.text),也称文本段(TextSegment),存放着程序的机器码和只读数据,可执行指令就是从这里取得的。如果可能,系统会安排好相同程序的多个运行实体共享这些实例代码。这个段在内存中一般被标记为只读,任何对该区的写操作都会导致段错误(SegmentationFault)。数据段,包括已初始化的数据段(.data)和未初始化的数据段(.bss),前者用来存放保存全局的和静态的已初始化变量,后者用来保存全局的和静态的未初始化变量。数据段在编译时分配。7.2缓冲区溢出原理堆栈段分为堆和栈。堆(Heap):位于BSS内存段的上边,用来存储程序运行时分配的变量。堆的大小并不固定,可动态扩张或缩减。其分配由malloc()、new()等这类实时内存分配函数来实现。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。堆的内存释放由应用程序去控制,通常一个new()就要对应一个delete(),如果程序员没有释放掉,那么在程序结束后操作系统会自动回收。7.2缓冲区溢出原理栈(Stack)是一种用来存储函数调用时的临时信息的结构,如函数调用所传递的参数、函数的返回地址、函数的局部变量等。在程序运行时由编译器在需要的时候分配,在不需要的时候自动清除。栈的特性:最后一个放入栈中的物体总是被最先拿出来,这个特性通常称为先进后出(FILO)队列。栈的基本操作:PUSH操作:向栈中添加数据,称为压栈,数据将放置在栈顶;POP操作:POP操作相反,在栈顶部移去一个元素,并将栈的大小减一,称为弹栈。堆和栈的区别分配和管理方式不同堆是动态分配的,其空间的分配和释放都由程序员控制。栈由编译器自动管理。栈有两种分配方式:静态分配和动态分配。静态分配由编译器完成,比如局部变量的分配。动态分配由alloca()函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无须手工控制。产生碎片不同对堆来说,频繁的new/delete或者malloc/free势必会造成内存空间的不连续,造成大量的碎片,使程序效率降低。对栈而言,则不存在碎片问题,因为栈是先进后出的队列,永远不可能有一个内存块从栈中间弹出。生长方向不同堆是向着内存地址增加的方向增长的,从内存的低地址向高地址方向增长。栈的生长方向与之相反,是向着内存地址减小的方向增长,由内存的高地址向低地址方向增长。2019/11/10网络入侵与防范技术177.2缓冲区溢出原理在这里,我们假设现在有一个程序,它的函数调用顺序如下。main()-;func_1()-;func_2()-;func_3()即:主函数main调用函数func_1;函数func_1调用函数func_2;函数func_2调用函数func_3。其详细结构图如下页图所示。2019/11/10网络入侵与防范技术18程序在内存中的影像随着函数调用层数的增加,函数栈帧是一块块地向内存低地址方向延伸的。随着进程中函数调用层数的减少,即各函数调用的返回,栈帧会一块块地被遗弃而向内存的高址方向回缩。各函数的栈帧大小随着函数的性质的不同而不等,由函数的局部变量的数目决定。在缓冲区溢出中,我们主要关注数据区和堆栈区。2019/11/10网络入侵与防范技术192019/11/10网络入侵与防范技术20程序所使用的栈在使用栈时,引用栈帧需要借助两个寄存器。一个是SP(ESP),即栈顶指针,它随着数据入栈出栈而发生变化。另一个是BP(EBP),即基地址指针,它用于标识栈中一个相对稳定的位置,通过BP,再加上偏移地址,可以方便地引用函数参数以及局部变量。2019/11/10网络入侵与防范技术21程序所使用的栈函数被调用的时候,栈中的压入情况如下:Func函数中的局部变量调用Func函数前的EBP退出Func函数后的返回地址传递给Func的实参内存低地址内存高地址最先压入栈最后压入栈2019/11/10网络入侵与防范技术22程序所使用的栈在局部变量的下面,是前一个调用函数的EBP,接下来就是返回地址。如果局部变量发生溢出,很有可能会覆盖掉EBP甚至RET(返回地址),这就是缓冲区溢出攻击的“奥秘”所在。2019/11/10网络入侵与防范技术237.2缓冲区溢出原理如果在堆栈中压入的数据超过预先给堆栈分配的容量时,就会出现堆栈溢出,从而使得程序运行失败;如果发生溢出的是大型程序还有可能会导致系统崩溃。7.2.1栈溢出程序中发生函数调用时,计算机做如下操作:首先把指令寄存器EIP(它指向当前CPU将要运行的下一条指令的地址)中的内容压入栈,作为程序的返回地址(下文中用RET表示);之后放入栈的是基址寄存器EBP,它指向当前函数栈帧(stackframe)的底部;然后把当前的栈指针ESP拷贝到EBP,作为新的基地址,最后为本地变量的动态存储分配留出一定空间,并把ESP减去适当的数值。7.2.1栈溢出实例我们来看一段简单程序的执行过程中对栈的操作和溢出的产生过程。#includestdio.hintmain(){charname[16];gets(name);for(inti=0;i16&&name[i];i++)printf(“%c”,name[i]);}2019/11/10网络入侵与防范技术267.2.1栈溢出实例编译上述代码,输入helloworld!结果会输出helloworld!在调用main()函数时,程序对栈的操作是这样的:先在栈底压入返回地址接着将栈指针EBP入栈,并把EBP修改为现在的ESP之后ESP减16,即向上增长16个字节,用来存放name[]数组2019/11/10网络入侵与防范技术277.2.1栈溢出实例现在栈的布局如图所示。2019/11/10网络入侵与防范技术287.2.1栈溢出实例执行完gets(name)之后,栈中的内容如下图所示2019/11/10网络入侵与防范技术297.2.1栈溢出实例接着执行for循环,逐个打印name[]数组中的字符,直到碰到0x00字符最后,从main返回,将ESP增加16以回收name[]数组占用的空间,此时ESP指向先前保存的EBP值。程序将这个值弹出并赋给EBP,使EBP重新指向main()函数调用者的栈的底部。然后再弹出现在位于栈顶的返回地址RET,赋给EIP,CPU继续执行EIP所指向的命令。说明1:EIP寄存器的内容表示将要执行的下一条指令地址。说明2:当调用函数时,Call指令会将返回地址(Call指令下一条指令地址)压入栈Ret指令会把压栈的返回地址弹给EIP2019/11/10网络入侵与防范技术307.2.1栈溢出实例如果输入的字符串长度超过16个字节,例如输入:helloworld!AAAAAAAA……,则当执行完gets(name)之后,栈的情况如图所示。内存低端堆栈顶部&nameNameEBPRet内存高端堆栈底部Helloworld!AAAAAAAAAAAAAAAAAAAA2019/11/10网络入侵与防范技术317.2.1栈溢出实例由于输入的字符串太长,name[]数组容纳不下,只好向栈的底部方向继续写‘A’。这些‘A’覆盖了堆栈的老的元素,从上页图