游客发表
修复基于堆栈的基于缓冲区溢出,核心思路就一句话:让数据没机会越过边界。堆栈的缓但真做起来,冲区远没有这么轻巧。溢出
我先说个最直接的修复案例。2003年的基于SQL Slammer蠕虫,攻击者只用376字节的堆栈的缓溢出载荷,就让全球互联网瘫痪了十几分钟。冲区它利用的溢出正是微软SQL Server 2000里一个未检查长度的堆栈变量。那个漏洞代码看起来平平无奇——一个`sprintf`往固定大小的修复数组里写数据,没做长度限制。基于修复方式也简单到让人懊恼:把`sprintf`换成`snprintf`,堆栈的缓加上长度参数,冲区三行代码的溢出事。但就是修复这三行代码没写,酿成了当年最大的网络安全事件。
所以修复的第一层,永远是代码层面的硬修复。检查每一个往栈上拷贝数据的函数调用,`strcpy`换`strncpy`,`sprintf`换`snprintf`,`gets`直接拉黑。手动数一遍缓冲区大小和实际拷贝长度,别偷懒用`sizeof`去匹配用户输入,那玩意儿经常骗人。比如你声明了`char buf[64]`,但传进来的数据是UTF-16编码,实际字节数翻倍,照样溢出。这种细节,只有一行行读代码才能发现。
第二层修复,是编译器的防护机制。现代编译器都提供了栈保护选项,比如GCC的`-fstack-protector-strong`,它会在栈帧的局部变量和返回地址之间插入一个随机金丝雀值。函数返回前检查这个值是否被改动,一旦发现异常就立即终止程序。微软的`/GS`标志同理。开启这些选项,等于给缓冲区溢出加了道自动闸门。但注意,这玩意儿不是万能的——攻击者如果先泄露了金丝雀值,或者通过其他漏洞绕过检查,照样能打穿。所以它只是降低风险,不是消除风险。
第三层,把目光放到操作系统层面。现代系统默认开启了ASLR(地址空间布局随机化)和DEP(数据执行保护)。ASLR让栈地址每次运行都不同,攻击者没法硬编码跳转地址;DEP禁止栈上的数据当作代码执行。这两道防线配合编译器保护,能挡掉绝大多数溢出利用。但有个坑:如果你的程序用了`VirtualAlloc`之类的方式动态申请可执行内存,或者用了旧版加载器,ASLR可能被部分绕过。检查一下编译参数,确保`/DYNAMICBASE`和`/NXCOMPAT`都开了。
不过我得说句实在话——这些修复手段,本质上都是“堵”。真正的治本,是改变数据存储方式。堆栈溢出之所以危险,是因为局部变量和返回地址挨得太近,数据一冲就冲到控制流上。你要是把用户输入挪到堆上,或者干脆用动态数组(比如C++的`std::vector`),边界检查就变成了运行时行为,而不是程序员自觉。我见过不少老C代码,明明可以改用堆分配,偏要图省事在栈上开大数组,结果出了漏洞还得加班补丁。何必呢?
还有个小技巧容易被忽略:把敏感变量挪到栈帧的“低地址”方向。比如你有个认证标志`int auth_flag`,别放在缓冲区旁边,放在栈帧最底部,这样即使缓冲区溢出了,攻击者也得先填满整个数组才能碰到它。虽然不解决根本问题,但能显著提高利用难度。
最后说个我自己的经历。前几年审计一个嵌入式设备的网络服务,发现它把所有HTTP请求头都拼进一个栈数组,长度全靠`strlen`判断。我建议改成逐字段解析,每次读取前先算剩余空间。那哥们儿嫌麻烦,说“加个长度检查不就行了”。我让他看了一行代码——那个数组大小是1024,但请求头里一个`Cookie`字段就能塞2000字节。加检查?检查什么?检查到一半数据就溢出了。后来他老老实实重写了解析逻辑,用堆上动态增长的缓冲区分段处理。从此那设备再没出过栈溢出漏洞。
修复不是加个补丁就完事,你得理解数据怎么流动,边界在哪里,然后从架构上让边界变得多余。这才是真正的修复。
友情链接