缓冲区溢出如何在栈上执行指令?栈不可执行时代码为何能运行?
缓冲区溢出如何在栈上执行指令?栈不可执行时代码为何能运行?
嘿,这个问题问得特别到位——刚接触缓冲区溢出的朋友几乎都会卡在这个点上!我来给你掰扯清楚:
先搞懂基础:栈不可执行是后来才有的“保护措施”
早期的操作系统(比如老版本的Linux、Windows XP之前的系统)根本没有栈不可执行的限制。那时候缓冲区溢出的玩法特别直接:把要执行的恶意代码(也就是shellcode)写到栈里,然后通过溢出覆盖函数的返回地址,让CPU跳转到栈上的shellcode位置,直接就能运行。这是最原始、最经典的缓冲区溢出利用方式。
现在有NX/DEP保护了,怎么绕?核心是“复用现有代码”
现在主流系统都开启了NX(No Execute,也叫DEP数据执行保护),栈和堆这类数据区域默认是不可执行的——这时候直接往栈里写代码肯定跑不起来,但安全研究者早就想出了绕过的办法,核心思路就是:不在栈上放新代码,而是复用程序里已经存在的可执行代码片段。
最典型的就是ROP(Return-Oriented Programming,返回导向编程):
- 程序的可执行段(比如
.text段)里本来就散落着很多短小的指令片段(比如pop ebx; ret、mov eax, 0; ret这种,我们叫它“gadget”) - 我们通过缓冲区溢出,在栈上构造一个“gadget链”:把这些gadget的地址按顺序放到栈里,让CPU执行完一个gadget后,通过
ret指令跳转到下一个gadget - 把这些零散的gadget拼接起来,就能完成复杂的操作(比如调用系统函数
execve来获取shell)。因为这些gadget本来就在可执行区域里,NX保护不会拦截。
还有几种常见的变种:
- ret2libc:这是ROP的简化版,直接跳转到系统库(比如libc)里的现成函数(比如
system),只要在栈上放好函数的参数(比如/bin/sh的地址),就能直接调用函数完成攻击 - ret2syscall:针对没有libc泄露的情况,直接拼接出调用系统调用的gadget链,触发内核执行我们要的操作
还有些特殊情况栈依然可执行
当然也不是所有场景都有NX保护:
- 一些嵌入式系统、老旧的软件可能刻意关闭了NX
- 有些程序(比如JIT编译的程序)会在栈上开辟可执行的内存区域,这时候也能直接用传统的shellcode方法
总结一下:栈不可执行确实挡住了最原始的缓冲区溢出,但通过复用现有可执行代码的思路,依然能完成攻击——这也是缓冲区溢出至今还是安全领域重点研究内容的原因之一。
备注:内容来源于stack exchange,提问作者FourierFlux
相关产品推荐
相关产品推荐

