栈溢出原理:

栈溢出漏洞是利用各种不安全的,不限制输入长度的输入方法来导致缓冲区溢出的漏洞,根据溢出发生的位置,可以分成栈溢出和堆溢出,其中由于栈上保存着局部变量和一些状态信息(寄存器的值、返回地址等),通过溢出可以任意修改栈上的值,也可以通过溢出来覆写返回地址从而执行任意地址的代码,利用方法包括shellcode注入,ret2libc、ROP等。为了应对这样的漏洞,也发展出了很多防守方法,例如Stack Canaries。

想要学会栈溢出,首先要了解函数调用栈。

函数调用栈是一块连续的用来保存函数运行状态的内存区域,调用函数和被调用函数根据调用关系堆叠起来,从内存的高地址向低地址增长。

x86与x86-64的情况略有不同,首先看x86的情况

调用函数时,首先会存储当前的ebp,将其压入栈中,然后更新esp为ebp,开辟出新的栈空间。最后按照从右至左的顺序依次将所有参数入栈。压栈结束后call调用函数。调用后同样先将ebp保存,压入栈中,然后更新esp为当前ebp。开始执行函数功能,函数返回时则相反,通过leave指令将esp恢复为当前的ebp,并从栈中将调用者的ebp弹出,最后ret指令弹出返回地址作为eip,程序回到调用函数中,最后抬高esp清理被调用函数的参数,一次函数的调用过程就结束了

以一个简单的程序作为例子

  int func(int arg1,int arg2,int arg3,int arg4,int arg5,int arg6,int arg 7,int arg8)
  {
    int c1 = arg1 + 1;
    int loc8 = arg8 + 8;
    return loc1 +loc8;
  }
  int main()
  {
      return func(11,22,33,44,55,66,77,88);
  }

使用gcc -m32命令编译后,用gdb调试查看具体过程

  pwndbg> disassemble main
Dump of assembler code for function main:
   0x565561b5 <+0>:     push   ebp       #将栈底ebp压栈(esp-=4)
   0x565561b6 <+1>:     mov    ebp,esp   #更新ebp为当前栈顶esp
   0x565561b8 <+3>:     call   0x565561dc <__x86.get_pc_thunk.ax>
   0x565561bd <+8>:     add    eax,0x2e43  
   0x565561c2 <+13>:    push   0x58      #将arg8压栈(esp-=4)
   0x565561c4 <+15>:    push   0x4d
   0x565561c6 <+17>:    push   0x42
   0x565561c8 <+19>:    push   0x37
   0x565561ca <+21>:    push   0x2c
   0x565561cc <+23>:    push   0x21
   0x565561ce <+25>:    push   0x16
   0x565561d0 <+27>:    push   0xb
   0x565561d2 <+29>:    call   0x56556189 
   0x565561d7 <+34>:    add    esp,0x20
   0x565561da <+37>:    leave  
   0x565561db <+38>:    ret    
End of assembler dump.
pwndbg> disassemble func
Dump of assembler code for function func:
0x56556189 <+0>: push ebp 将栈底ebp压栈
0x5655618a <+1>: mov ebp,esp 更新ebp为当前栈顶
0x5655618c <+3>: sub esp,0x10 为局部变量开辟栈空间
0x5655618f <+6>: call 0x565561dc <__x86.get_pc_thunk.ax>
0x56556194 <+11>: add eax,0x2e6c
0x56556199 <+16>: mov eax,DWORD PTR [ebp+0x8]
0x5655619c <+19>: add eax,0x1
0x5655619f <+22>: mov DWORD PTR [ebp-0x4],eax
0x565561a2 <+25>: mov eax,DWORD PTR [ebp+0x24]
0x565561a5 <+28>: add eax,0x8
0x565561a8 <+31>: mov DWORD PTR [ebp-0x8],eax
0x565561ab <+34>: mov edx,DWORD PTR [ebp-0x4]
0x565561ae <+37>: mov eax,DWORD PTR [ebp-0x8]
0x565561b1 <+40>: add eax,edx
0x565561b3 <+42>: leave
0x565561b4 <+43>: ret

End of assembler dump.


64位的程序,略有不同的地方在于,传递的前六个参数(从左向右的六个)会使用寄存器进行,剩下的参数会和32位相同,从右至左依次入栈。除此之外,调用函数时,rsp不会下移开辟空间,同时rsp下的128个字节会作被用来保存临时数据。

导致栈溢出漏洞的函数


在语言设计之初,因为很多原因,导致出现了一些存在漏洞的函数,如果不经注意,很容易就会导致溢出。

这样的函数大致分为两类,第一类为输入读取函数,例如scanf。

char buf[10];
scanf("%s",buf);

这样的使用,因为没有限制scanf的读取长度,很容易超出buf的存储限度导致溢出。
但有时即使限制了长度也会存在溢出问题,比如下面的写法

sacnf("%10s",buf)

这样的写法看起来很安全,但依然存在溢出问题,因为scanf会在输入的字符串结尾自动添加一个结束符,从而再次出现溢出的危险


第二类危险的函数是strcpy,strcat,sprintf等拷贝函数

char srcbuf[20];
char destbuf[10];

read(0,srcbuf,19);
strcpy(destbuf,srcbuf);


此时read函数读取是十分安全的,但是由于没有考虑到destbuf的长度小于srcbuf,所以也会造成溢出问题。