【每天5分钟,了解一个知识点】
有一种异常现象叫做缓冲区溢出。简单来说,就是当一段程序想把太多的数据放进一个缓冲区时,数据就会超出缓冲区本来的容量,然后溢出到别的内存空间,把其他地方的数据给覆盖了。这可不得了,攻击者就能利用这个漏洞去修改计算机的内存,破坏或者控制程序的执行,结果可能是数据损坏、程序崩溃,甚至让恶意代码跑起来。
一、缓冲区溢出攻击的类型
缓冲区溢出攻击呢,就是利用缓冲区溢出的漏洞来搞破坏。比如向程序的缓冲区写比它能装下的内容还多的东西,这样就能破坏程序的堆栈,让程序崩溃、系统关机,或者让程序去执行别的指令,达到攻击的目的。
栈溢出栈溢出就是在程序执行的时候,如果在栈上分配的内存超过了栈的大小,那就出事了。栈就像一个后进先出的箱子,用来放程序执行过程中的临时变量。栈溢出的时候,程序会马上停止,还会显示错误信息。这是最常见的缓冲区溢出攻击类型,发生的前提是程序得往栈上写数据,而且还不控制写的数据大小。
堆溢出堆溢出是程序在动态分配内存的时候,分的内存超过了堆的大小。堆呢,就像一个先进先出的箱子,放程序运行时长期要用的数据。堆溢出的时候,程序可能不会马上停止,但会变得不稳定,甚至崩溃。攻击者还能利用堆缓冲区溢出执行任意代码或者拿到敏感信息。
格式字符串溢出在编程语言里,用格式化字符串函数打印字符串的时候,如果格式串是用户自己定的,那攻击者就能随便伪造格式串。利用一些函数的特性,就能偷看堆栈空间的内容,输入太长还能引发传统的缓冲区溢出,或者用“%n”去覆盖指针、返回地址啥的。
整数溢出计算机语言里的整数都有个取值范围,两个整数做运算的时候,如果结果比最大值还大(上溢)或者比最小值还小(下溢),那就溢出了。比如最大值是 a,a+1 就等于 0 或者 0-1 等于 a,这就溢出了。大多数整数溢出不能直接利用,但如果这个整数决定内存分配等操作,就可能被间接利用来触发安全漏洞。
Unicode 溢出Unicode 溢出是把 Unicode 字符塞到只需要 ASCII 字符的输入里,造成缓冲区溢出。ASCII 和 Unicode 是让计算机显示文本的编码标准。因为 Unicode 里的字符比 ASCII 多,很多 Unicode 字符比最大的 ASCII 字符还大。一旦出现 Unicode 溢出,程序的工作方式就可能改变,引发更多安全问题。
二、攻击者如何利用缓冲区溢出
攻击者会把精心准备的数据输到程序里,程序想把这个数据存到缓冲区,结果数据就会覆盖跟缓冲区连着的部分内存。如果程序的内存布局很清楚,攻击者就能故意去覆盖那些已知有可执行代码的地方,然后用自己的可执行代码换掉原来的代码,改变程序的工作方式。
一般来说,缓冲区溢出攻击是这样的步骤:注入攻击代码→跳转到攻击代码→执行攻击代码。
下面介绍两种攻击者常用的方法:
利用栈溢出攻击破坏栈数据栈数据里有好多可能被攻击者利用的东西,比如函数调用时的实参、下一条要执行的操作指令的地址、调用函数前的栈帧状态值和函数里的本地变量。
常见的栈溢出攻击方法是改变下一条要执行的操作指令的地址的值,把已经注入到栈里的攻击代码的地址或者代码区里某些有特权的系统函数地址放到这个值里。要是成功改了这个值,等函数调用完,程序就会跳到攻击者设计好的地址去执行攻击者想让它执行的指令,这样攻击者就拿到系统控制权限了,后果很严重。
还有一种情况,攻击者会把调用函数前的栈帧状态值也当成攻击目标。攻击者先构建一个虚拟栈帧,让这个虚拟栈帧的下一条要执行的操作指令的地址指向攻击代码。然后通过溢出当前栈帧的栈帧状态值,把溢出后的栈帧状态值改成虚拟栈帧的地址。这样,执行完当前栈帧就会执行虚拟栈帧,执行完虚拟栈帧就会跳到虚拟栈帧的下一条要执行的操作指令的地址指向的地方,也就是攻击者设计好的地址,去执行攻击指令。
利用堆溢出攻击破坏堆数据因为堆里的内存是不连续地动态分配的,攻击者想预测地址就更难了,所以堆溢出攻击比栈溢出攻击难一些,但还是有办法攻击的。
Dword Shoot 攻击:Dword Shoot 就是能往内存的任意位置写任意数据,1WORD 等于 4 个字节。Linux 和 Windows 系统管理堆都是用双向链表的方式,每个分配的内存块有三部分:头指针、尾指针、内存数据。堆内存的管理主要有分配和释放。释放堆内存 M 的时候,会把 M 从链表上拿掉,会执行 M→head→tail=M→tail 操作。如果攻击者通过溢出 M 旁边的内存,把 M 的头指针、尾指针改掉,让头指针指向攻击者设计好的虚拟节点,尾指针指向攻击者设计好的位置,比如攻击代码。那执行 M→head→tail=M→tail 操作的时候,这个虚拟节点的尾指针就会指向攻击代码,调用这个尾指针就会转到攻击代码。摘链时的另一个操作 M→tail→head=M→head 也能用同样的原理攻击。
Heap Spray 攻击:Heap Spray 是在攻击代码前面加上很多无意义的指令,组成一个注入代码段。然后向系统申请很多内存,反复用这个代码段填满内存。再结合其他漏洞攻击技术控制程序流,让程序跳到堆上执行,这样攻击代码就能执行了,攻击者就拿到系统控制权限了。
三、如何防止缓冲区溢出攻击
缓冲区溢出攻击在远程网络攻击里占了很大一部分,一个匿名的网友都可能通过这个漏洞拿到一台主机的部分或者全部控制权。要是能把缓冲区溢出的漏洞都堵上,那很多安全威胁就都能解决了。有下面这些方法可以保护缓冲区:
完整性检查:在程序指针失效之前检查一下完整性。
随机化地址空间:把关键数据区的地址空间位置随机排列。这样的话,缓冲区溢出攻击就很难知道可执行代码的位置了。
防止数据执行:把内存的一些区域标记成可执行或者不可执行,这样就不能在不可执行的区域运行代码了,能阻止这种攻击。
编写安全的代码:用能识别不安全函数或者错误的编译器,利用编译器的边界检查来保护缓冲区。别用那些不检查缓冲区的函数,比如在 C 语言里,用 fgets() 代替 gets()。用有内置保护的语言写代码,或者在代码里加一些特殊的安全程序,预防缓冲区溢出漏洞。
虽然有这些预防措施,但还是可能会发现新的缓冲区溢出漏洞。一旦发现新漏洞,工程师就得赶紧修补受影响的软件,还得让软件的用户能及时拿到补丁。
【关联阅读】
关注公众号,回复【Java面试】,获取更多面试资料




