一个简单的缓冲区溢出的思考

时间:2022-09-15 06:59:57

  从大二开始真正接触技术开始,从最早的HTML,PHP,WEB开发。一直以为以后可能会从事开发的工作,碰巧大三上的时候和同专业的郭子,邹豪参加了南京的一个信息安全技能大赛,才真正找到了兴趣的方向,也从懵懵懂懂开始懂了信安怎么学,像海贼王里面一样,感觉到了新世界了(好吧,我又YY了......)

  最近参加了ISCC2013的比赛,历时一个月的过程,虽然过程很辛苦,每天除了吃饭睡觉都在想题目(我不会告诉你我有一题是在睡觉的时候想出来的,做梦梦到单步调试....)。但是感觉收获颇丰,貌似比以前牛逼了一点点。也因此萌生了要开一个技术博客,写一点自己原创的东西,算是总结提高吧。

  在做溢出关第一题的时候,遇到了缓冲区溢出的问题,之前从没接触过这东西,汇编也不懂。于是各种查资料,看小甲鱼的汇编视频,总算是把原理搞明白了,希望在这里分享一些关于缓冲区溢出和汇编的基础知识,小白一个,大神路过不要拍砖啊。

  首先看一段代码:

#include<stdio.h>
void why_here(void) /*这个函数没有任何地方调用过*/
{
  printf("why u here ?!\n");
  _exit(0);
}
int main(int argc,char * argv[])
{
  int buff[1];
  buff[2]=(int)why_here;
  return 0;
}

用GCC或者VC编译运行一下,会发现why_here函数被运行了,地球人都知道这是缓冲区溢出,但是在底层代码到底发生了什么呢?

原因是是因为错误的越界溢出赋值导致了函数返回地址被错误的覆盖了,程序流执行到这里之后不是正常的返回而是执行了被覆盖的地址。这样说有点抽象,我们用VC进行debug一下,看看汇编代码到底做了什么。

发生溢出的C代码:

6:    int main(int argc,char * argv[])
7:    {
00401070   push        ebp
00401071   mov         ebp,esp
00401073   sub         esp,44h
00401076   push        ebx
00401077   push        esi
00401078   push        edi
00401079   lea         edi,[ebp-44h]
0040107C   mov         ecx,11h
00401081   mov         eax,0CCCCCCCCh
00401086   rep stos    dword ptr [edi]
8:        int buff[1];
9:        buff[2]=(int)why_here;
00401088   mov         dword ptr [ebp+4],offset @ILT+5(why_here) (0040100a)
10:       return 0;
0040108F   xor         eax,eax
11:   }
00401091   pop         edi
00401092   pop         esi
00401093   pop         ebx
00401094   mov         esp,ebp
00401096   pop         ebp
00401097   ret

这两行是函数调用的常用语句,EBP是基址寄存器,目的是在进入函数前将EBP保护起来,一遍出来的时候可以恢复现场(计算机组成原理上的知识)。

下面这句  sub   esp,44h 应该就是为新进入的函数预留栈空间。接下来就是经典的三个push为main函数传参。

............

我们重点看这行代码:

00401088   mov     dword ptr [ebp+4],offset @ILT+5(why_here) (0040100a)

将why_here的函数入口基地址放入[ebp+4]中,这样看也许看不出什么不妥,我们对比一下正常的情况。

下面是正规规范编写的C代码:

#include<stdio.h>
void why_here(void) /*这个函数没有任何地方调用过*/
{
  printf("why u here ?!\n");
  _exit(0);
}
int main(int argc,char * argv[])
{
  int buff[1];
  buff[0]=(int)why_here;(因为buff只申请了一个int型,也就是4字节的内存空间,所以只能只能保存一个32位的函数RVA地址)
  return 0;
}

对应的汇编

6:    int main(int argc,char * argv[])
7:    {
00401070   push        ebp
00401071   mov         ebp,esp
00401073   sub         esp,44h
00401076   push        ebx
00401077   push        esi
00401078   push        edi
00401079   lea         edi,[ebp-44h]
0040107C   mov         ecx,11h
00401081   mov         eax,0CCCCCCCCh
00401086   rep stos    dword ptr [edi]
8:        int buff[1];
9:        buff[0]=(int)why_here;
00401088   mov         dword ptr [ebp-4],offset @ILT+5(why_here) (0040100a)
10:       return 0;
0040108F   xor         eax,eax
11:   }
00401091   pop         edi
00401092   pop         esi
00401093   pop         ebx
00401094   mov         esp,ebp
00401096   pop         ebp
00401097   ret

  我看到这里的时候就恍然大悟了,嗖嘎斯内。因为溢出赋值覆盖了EIP(也就是指令寄存器,导致了函数返回后,程序的执行流被改变了)。

这里我们详细讨论一下:

进入main 函数后的栈内容下:
[ eip ][ ebp ][ buff[0] ]
高地址<---- 低地址

  因为在栈空间中,栈是向下生长的,而赋值是向上生长的。所以正常情况下[ebp-4]的赋值是正常的系统允许的,因为这个空间本来就系统为你分配好了。但是如果我们越界赋值[ebp+4]就会怎么?

  对!导致了EIP被覆盖,即发生了缓冲区溢出,可能有朋友要问了,你这么说有什么依据呢?我们还是回到汇编代码:

00401070   push        ebp
00401071   mov         ebp,esp
00401073   sub         esp,44h
00401076   push        ebx
00401077   push        esi
00401078   push        edi

在开头的4个push入栈操作对应结尾了4个pop出栈操作。

00401091   pop         edi
00401092   pop         esi
00401093   pop         ebx
00401094   mov         esp,ebp
00401096   pop         ebp

00401097   ret

EBP被完好的保存了,注意到这里有个汇编指令ret,这是返回的意思,它的原理实质上等于2条汇编指令的组合:

pop EIP

jmp

将栈上的最底下的4字节空间弹栈赋值给EIP,并跳转到EIP对应的那条指令执行。而这个所谓的栈底的4个空间的内容就是在进入main函数前入栈的,目的是为了等main函数执行完后可以继续执行main之后的指令,这也就是一般的函数调用的原理(因为这里只有一个main函数,所有有些特殊,不过本质上一样的)。

而所谓的溢出本质上是我们覆盖了这段EIP对应的栈空间,然后操作系统就傻乎乎的以为这就是真实的返回地址,pop并执行了,然后就.........所以说童话里都是骗人的

这个程序很简单,但是第一次分析的时候也花了我不少力气,以后希望能更多的研究一些原理性的东西,也继续和大家分享!

最后,希望这次的ISCC能玩的开心