栈缓冲区溢出攻击相关疑问:栈存储、指令执行与栈帧机制
栈缓冲区溢出:栈执行指令的原理与栈帧疑问解答
为什么栈中的指令能被执行?
答案很直接:你编译时添加的-z execstack选项,开启了栈内存的可执行权限。
在x86架构的Linux系统中,默认栈内存是标记为不可执行的(NX保护机制),CPU会拒绝执行栈内的指令。但-z execstack会关闭这个保护,让CPU把栈区域当作可执行代码段处理——只要程序计数器(EIP)指向栈内的内存地址,就会逐条执行里面的机器码。
看你的代码:
void main(){ char buf[] = "\xeb\x0b\x31\xc0\xb0\x0b\x31\xd2\x31\xc9\x5b\xcd\x80\xe8\xf0\xff\xff\xff/bin/sh\x0"; int* p; p = (int*)&p + 2; *p = (int)buf; return; }
这里的buf是局部数组,存储在main函数的栈帧中。p = (int*)&p + 2是计算main函数返回地址的内存位置(i386架构下,局部变量p的地址向上偏移2个int长度,刚好指向返回地址的存储位置),随后你将返回地址替换成buf的地址。当main函数执行return时,CPU会把栈中的返回地址加载到EIP,此时EIP指向栈上的buf,于是开始执行buf中的shellcode机器码。
栈帧指针(fp/EBP)与返回地址(ra/EIP)的工作原理
你的理解基本正确,结合i386架构补充细节:
- 函数被调用时,CPU先将当前EIP(即调用者代码中下一条指令的地址,也就是返回地址)压入栈,然后跳转到函数入口。
- 函数入口通常执行
push %ebp; mov %esp, %ebp:把上一个栈帧的EBP(前栈帧指针)压栈,再将当前ESP的值设为新的EBP,作为当前栈帧的基准。 - 局部变量、临时数据存放在EBP下方(ESP向低地址增长的方向),返回地址存在
EBP+4位置,前栈帧指针存在EBP位置。 - 函数返回时执行
leave; ret:leave等价于mov %ebp, %esp; pop %ebp,恢复ESP和EBP到上一个栈帧的状态;ret则把栈顶的返回地址弹出到EIP,让CPU回到调用者代码继续执行。
在你的例子中,我们手动修改了返回地址,所以ret执行时,弹出的不是原调用者地址,而是buf的栈地址。此时原栈帧结构被修改,但你的shellcode是独立的:它通过jmp/call技巧获取/bin/sh的地址,直接调用int 0x80触发系统调用,不需要依赖原栈帧,因此能成功执行execve("/bin/sh", NULL, NULL)。
补充:你提供的shellcode对应汇编解析
.global main main: jmp strings start: xor %eax, %eax movb $0xb, %al # 将eax设为0xb,对应execve的系统调用号 xor %edx, %edx # edx置0,环境变量数组为NULL xor %ecx, %ecx # ecx置0,参数数组为NULL popl %ebx # 从栈中弹出/bin/sh的地址到ebx(call指令会把返回地址压栈,此处返回地址就是/bin/sh的地址) int $0x80 # 触发系统调用 strings: call start # call指令将下一条指令的地址(即/bin/sh的地址)压栈,然后跳转到start .string "/bin/sh"
这段汇编的核心是利用call指令的特性获取字符串地址,避免硬编码地址,在关闭ASLR的环境下能稳定运行。
内容的提问来源于stack exchange,提问作者yeongro
相关产品推荐
相关产品推荐

