You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

栈缓冲区溢出攻击相关疑问:栈存储、指令执行与栈帧机制

栈缓冲区溢出:栈执行指令的原理与栈帧疑问解答

为什么栈中的指令能被执行?

答案很直接:你编译时添加的-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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 18:45:48