|
程序崩溃的检测那一刻,调试器弹出一行字:Stack smashing detected。到基你盯着屏幕,于堆心里明白——有人往栈上写了超出预期的缓冲数据,把返回地址给覆盖了。区溢这不是出办什么罕见事故,C/C++项目里每隔几周就会撞上一次。检测缓冲区溢出攻击的到基本质,就是于堆利用程序对输入长度缺乏校验,往栈上写入恶意数据,缓冲劫持程序执行流。区溢检测到它,出办说明你的检测代码里有个地方在"敞开大门迎客"。 先别急着改代码。到基第一步是于堆定位溢出点。Linux下用GDB跑一遍,看崩溃时的backtrace,注意RIP或EIP寄存器的值——如果它指向了某个奇怪的地址(比如0x41414141,也就是"AAAA"),基本可以确认返回地址被覆盖了。Windows上可以用WinDbg,看栈回溯里有没有异常跳转。我遇到过最隐蔽的一次,溢出发生在日志模块,因为格式化字符串没限制长度,用户输入被直接拼进栈缓冲区,结果攻击者用一串精心构造的路径就拿到了shell。 找到溢出点后,判断它是"栈溢出"还是"堆溢出"。栈溢出通常发生在char buf[64]这种局部数组上,函数返回时ret指令从栈上弹出被污染的地址。堆溢出则涉及malloc分配的块,覆盖的是堆管理结构或相邻对象。两者修复思路不同:栈溢出优先考虑替换危险函数,堆溢出要检查分配大小和边界。 修复手段分三层。最直接的是代码层:把strcpy、sprintf、gets换成strncpy、snprintf、fgets,并且显式传入缓冲区大小。但别以为换了函数就万事大吉——strncpy不保证结尾有\0,后续strlen可能越界读。我习惯在拷贝后手动补一个buf[sizeof(buf)-1] = '\0',虽然丑,但管用。 编译层加防护是第二道防线。GCC的-fstack-protector-strong会在栈帧里插入canary值,函数返回前检查它是否被改写。一旦检测到溢出,程序直接abort,而不是跳转到攻击者指定的地址。这招不能阻止溢出发生,但能阻止它变成可利用的漏洞。再配合-D_FORTIFY_SOURCE=2,编译时就能对已知危险函数做长度检查。不过这些选项有性能开销,嵌入式环境要权衡。 系统层启用ASLR和DEP是第三道防线。ASLR让栈地址随机化,攻击者很难猜中返回地址;DEP让栈内存不可执行,就算跳过去也跑不了shellcode。但别依赖它们——ASLR可以被暴力破解,DEP可以被ROP绕过。我见过一个CTF题目,开了ASLR和DEP,选手用ret2libc照样拿下了。防护是纵深,不是替代。 如果溢出已经发生了,比如线上服务被攻击,先隔离进程,抓core dump,别急着重启。用gdb加载core,看栈上的数据模式——如果有一串0x90(NOP sled)或者重复的地址,基本能还原攻击载荷。然后查日志,找异常输入。有一次我们线上服务被打了,日志里全是超长URL参数,直接定位到某个接口没做长度校验。修完上线,加了个WAF规则,才算消停。 最后说个容易忽略的点:栈溢出检测工具本身也可能被绕过。比如stack-protector只保护有数组的函数,纯指针操作它不管。所以别把工具当保险,写代码时对每个memcpy、read、recv都问一句:长度从哪来?可信吗?缓冲区够吗?这三个问题答不上来,就别提交。 |