当前位置:首页 > 百科

缓冲区溢出漏洞有哪些

2021年,缓冲一个叫Log4j的区溢Java日志库爆出漏洞,全球无数服务器被远程控制。出漏攻击者只需要在聊天框里输入一串特殊字符,缓冲就能让服务器执行任意命令。区溢这不是出漏魔法,是缓冲缓冲区溢出漏洞的现代变种——只不过这次溢出的是内存里的字符串拼接区域。

缓冲区溢出,区溢说白了就是出漏程序在内存里划了一块固定大小的区域存放数据,结果你硬塞了超过容量的缓冲内容。多余的区溢数据溢出去,覆盖了旁边存储指令的出漏区域。攻击者精心构造这些覆盖数据,缓冲就能让程序跳转到他们预设的区溢恶意代码上。这就像往一个只能装五升水的出漏桶里倒十升水,水溢出来流进了旁边的电路板,把电路板烧了。

最经典的案例是1988年的Morris蠕虫,它利用Unix系统finger服务的缓冲区溢出漏洞,在几小时内感染了当时互联网上约10%的机器。那时候互联网总共才六万多台主机。一个康奈尔大学的研究生写了个自复制程序想测试网络规模,结果因为代码里没控制好复制速度,直接把网络搞瘫了。

缓冲区溢出的根源,其实是个老掉牙的问题:C和C++语言里,程序员要手动管理内存边界。像`strcpy`、`sprintf`这些函数,根本不检查目标缓冲区够不够大。哪怕你写了三十年代码,一个不小心还是会把数据写到不该写的地方。Java、Python这些现代语言虽然自动管理内存,但底层库用C写的,照样逃不掉。

具体到漏洞类型,栈溢出是最常见的。程序调用函数时,局部变量和返回地址都压在栈上。你往局部变量数组里塞超长数据,就可能把返回地址盖掉。攻击者把返回地址改成自己shellcode的起始位置,程序一返回就执行了攻击代码。堆溢出稍微复杂点,发生在动态分配的内存区域,利用方式更绕,但危害一样大。

还有个变种叫整数溢出。程序里先算一个长度值,比如`int len = input_size + header_size`,如果这个加法结果超出了int能表示的范围变成负数,后面分配缓冲区时就分配了很小的空间,但拷贝数据时用的还是原始长度。这一小一大,数据就溢出了。

格式化字符串漏洞也算一类。程序员把用户输入直接当格式化参数传给`printf`,攻击者用`%x`、`%n`这些格式符读取甚至改写内存。2000年代初的很多远程漏洞都靠这个,现在少了,但老代码里还藏着不少。

防御手段这些年进步明显。操作系统有ASLR(地址空间布局随机化),每次运行程序时把栈、堆、库的地址随机打乱,让攻击者猜不准跳转目标。编译器加了栈保护,在返回地址前放个随机值,函数返回前检查这个值有没有被改。还有NX位(不可执行内存),把栈和堆标记为不可执行,shellcode就算进去了也跑不起来。

但道高一尺魔高一丈。攻击者改用return-to-libc,不往栈里塞shellcode,直接跳转到系统已有的libc函数。还发展出ROP(面向返回编程),把程序里已有的小指令片段拼接成恶意逻辑。就像不用自己买零件,而是把现成的乐高积木拆了重新拼。

现实里,很多系统还在跑着十年前的老代码。银行ATM、医院设备、工厂控制系统,这些地方用的操作系统补丁可能几年没更新过。2017年WannaCry勒索病毒利用的永恒之蓝漏洞,本质就是Windows SMB服务的缓冲区溢出。微软其实早三个月就发了补丁,但大量机构没打,结果全球150多个国家中招。

写代码的人最该记住一条:永远别信任输入长度。用`strncpy`代替`strcpy`,用`snprintf`代替`sprintf`,这些函数会限制拷贝字节数。但就算这样,还是得手动算清楚大小。真正靠谱的办法是直接用Rust、Go这类内存安全的语言,或者用C++的`std::string`和`std::vector`,让标准库替你管边界。

缓冲区溢出这问题,从1988年到现在三十多年了,还没绝迹。每次觉得补得差不多了,就有人换个角度挖出新坑。搞安全的和搞攻击的,永远在玩猫鼠游戏。但至少,知道漏洞长什么样,总比两眼一抹黑强。

分享到: