『初究VC++2008中缓冲区保护机制』

本文排版环境1366*768,如因考虑不周引起版面不协调还请见谅!

『初究VC++2008中缓冲区保护机制』

目前,操作系统(Windows、Linux、UNIX)、数据库以及应用软件主要采用c/c++语言开发,但c/c++语言存在一个巨大缺陷——缺乏数组边界检查。因此,这些软件不可避免的存在缓冲区溢出漏洞,成为安全隐患。众所周知,当年风靡的SQL Slammer蠕虫就是利用Microsoft SQL Server 2000中的缓冲区漏洞进行攻击的。

简单的说缓冲区就是指一块特定的内存区域——连续的、定长的。一般情况下编译器能够为我们分配足够用的内存空间,反汇编一下程序笔者发现VC++6.0给我们分配的缓冲区最小是40h字节(VC++2008是C0h),如果在我们的程序中定义了变量则根据我们定义的变量实际分配,笔者多次观察总结了个这样的公式:【缓冲区大小=未声明任何变量时大小+8*连续区域数目+变量占有空间之和】,连续空间是指如数组这样连在一起的内存区域,关于公式中的那个“8*连续区域数目”的由来是因为笔者发现对每个连续区域的前后都捆绑了4字节的保护区域,不过这公式也不精确,因为还有内存对其方式问题,当然笔者感觉缓冲区大小还跟其他的因素有关。(这个公式是VC++2008的,VC++6.0笔者没有去观察)。因此当我们在程序中有如下定义 char str[10];并要求用户输入,但用户他并不知道(或者是有意攻击)应该输入的最大长度。因此当他输入的数据超过10,而我们的程序又没有检查数组越界问题,这就很有可能将我们堆栈以下的数据覆盖,一般情况这会导致我们的程序出错或者运行中止。但是攻击者利用这个漏洞,精心设计出一段入侵程序代码,覆盖缓冲区以外的内存单元,这些程序代码就有可能被CPU执行,从而代替了我们的程序而得到了系统的控制权。

基于上面的原理,笔者编了个程序模拟缓冲区溢出攻击:

//程序清单:internal.cpp(程序内部缓冲区溢出)

#include <windows.h>

#include <stdio.h>

#include <iostream>

#include <string.h>

using namespace std;

void copyString(char *s)

{

     char buf[10];

         strcpy(buf,s); //请在此设置个断点;

}//请在此设置个断点;

void hacked(void )

{

     cout<<"This program is hacked!";

     while(1);

}

int main(int argc,char *argv[])

{

     char badStr[]="000011112222333344445555";

     DWORD * pEIP=(DWORD*)&badStr[16];

     *pEIP=(DWORD)hacked; //将我们想要非法调用的函数地址覆盖掉“4444“,对于为什么要这样覆盖后面笔者会解释。

     copyString(badStr); //对于正常情况函数调用完毕就会回到这里,但现在我们就是利用缓冲区溢出漏洞让它回不来,而是执行了我们根本就没有调用的函数hacked;

     return 0;

}

下面我们来分析程序的执行情况,将两个断点设置好,按F5调试运行:(请注意观察截图)

程序停留在断点一处,Strcpy函数执行之前的内存布局如下:

下面笔者解释为什么要将函数入口地址覆盖4444的问题:如上图所视的红矩形框内的12个字节就是编译器为我们安排的用于存放buf[10]的内存区域,因为这里采用4字节对其方式故而实际安排了12个字节来存放10字节的字符数组,后面蓝色矩形框内的内存区域正常情况是不会使用的,因此strcpy函数将从偏移量0012FEFCh开始依次将我们的badStr复制到此后的内存单元直到遇到\0为止,所以从badStr[16]开始恰好就是返回地址00401240h所在之处。

程序停留在断点二处,执行strcpy函数后的内存布局如下:

到此我们的模拟实验就算成功了,按F5继续运行程序就会在控制台输出This program is hacked!

问题的解决

分析到这里,笔者相信大家一定知道如何去更改我们的程序了吧。笔者的做法就是在调用strcpy之前首先检查数组边界,如果大于缓冲区大小则返回或者提示出错。

       【VC++2008是否弥补了这个问题呢

由于上面的解决方法要求程序员特别的仔细,但事实上这很难做到(老一辈程序员不是不知道这个漏洞出现的原因,只是有时候粗心了才让今天的攻击者有机可乘),可以想象当程序的规模很大时漏洞出现的概率有多大。于是,笔者自然就想去看看VC++2008是否弥补了这个问题,将那段程序在VC++2008做了个同样的实验。编译、连接都能够通过,但运行时会出现如下的错误:Run-Time Check Failure #2 - Stack around the variable 'buf' was corrupted.显然,VC++2008对同样的问题做了防范,但到底是如何做的呢(笔者没有安装vc++2005,有兴趣的朋友可以自己去探究一下):

首先,笔者想到是不是我们调用的strcpy对实参进行了检查。表面上看这很有可能,但仔细一想就发现这种想法的不可行性。我们知道strcpy中的两个形参仅仅是两个内存地址,没有再提供更多的信息。而c/c++检查字符串长度的做法是从给定的地址出发一直走,直到遇到‘\0’为止,因此当我们用strcpy中第一参数作为strlen的实参调用时,结果没人能预知(除非先去看了内存情况)。为了证实这种说法,读者可以用一个未初始化的字符数组首地址去调用strcpy 函数试试。(笔者查阅了MSDN文档,发现有这样的一个函数LPTSTR StrCpyN( LPTSTR psz1,LPCTSTR psz2, int cchMax); 这个可以指定复制的最大长度,因此能够帮助我们防范缓冲区溢出漏洞。)

说到这里,不知大家是否还记得笔者前面提到的“VC++2008编译器在每个缓冲区前后都添加了4字节的保护区域”。下面笔者将解释这个保护区域是如何保护我们的缓冲区。为简单起见,笔者用如下程序做实验:

void main()

{//在此设置断点;

  char a[4]="abc";

}

这段程序虽是短小,但它足以帮助笔者向大家解释VC++2008对缓冲区的保护策略。设置好断点并按F5调试运行:在源文件区域左击->选择Go To Disassmebly(转到反汇编):在此,笔者发现与VC++6.0有很多的不同,其中有以下几条语句:

004113C6  xor         eax,eax

004113C8  push        edx 

004113C9  mov         ecx,ebp

004113CB  push        eax 

004113CC  lea         edx,[ (4113E0h)]

004113D2  call        @ILT+130(@_RTC_CheckStackVars@8) (411087h)

显然,这是个子程序调用。而且从命名上我们不难推想这个子程序就是我们所寻求的。笔者按F11单步进入此子程序,发现VC++2008给缓冲区前后加的保护区域在这里派上用处了。在这个子程序中检查了缓冲区两端的数据是否与0cccch相等。为了证实这种说法,请看笔者做的如下实验:

为节省页面,笔者只给出改了右边保护区域的情况,其实左右这8字节大小都一样,改了就会被检查出来。

【注释】如上图所示:蓝色矩形框中的00411978H就是子程序返回地址,绿色矩形框中的0012FFB8就是主调函数(调用这个函数的函数)堆栈基地址ebp。

笔者分别更改了这两个返回地址和堆栈基地址,发现程序运行并没有报错。于是就想把攻击字符串相应位置改为0CCCCH,按照上面想法的程序如下所示:

#include <windows.h>

#include <stdio.h>

#include <iostream>

#include <string.h>

using namespace std;

void copyString(char *s)

{

     char buf[10];

     strcpy(buf,s);//

}

void hacked(void )

{

     cout<<"This is hacked!";

     while(1);

}

int main(int argc,char *argv[])

{

     char badStr[24];//

     DWORD * pEIP=(DWORD*)&badStr[20];

     *pEIP=(DWORD)hacked;

              pEIP=(DWORD*)&badStr[12];

     *pEIP=(DWORD)0xcccccccc; //笔者只更改了需要复制的字符串;

     copyString(badStr);

     return 0;

}

调试运行,失败!!笔者反汇编程序,查看了内存布局,这才恍然大悟。哦,原来微软对这种攻击早就做了防范,请看如下截图:

    那么,究竟是在什么条件满足的情况下,编译器才为我们堆栈保护区域做标记,难道是因为我们调用了strcpy库函数吗?下面的程序中笔者重写了strcpy库函数:

void nlqStrcpy(char *d,char *s) //此函数即为笔者自定义的字符串复制函数;

{

  while(*s) *d++=*s++;

  *d=’\0’;

}

void main()

{

  char d[4], s[10]="aaabbbccc";

  nlqStrcpy(d,s);

}

调试运行,发现现在的保护区域也做了标记,内存布局如下:

0x0012FF4E  cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc e5 13 30 89 b8 ff 12 00 68 1a 41 00

笔者猜想:是不是当我们对定义的缓冲区进行更新时,编译器就会做如上的标志呢?带着这个问题,笔者又做了个实验:

#include <iostream>

using namespace std;

void main()

{

  char a[4];

  cin>>a;

}

笔者输入了:asv;

    查看内存片段如下,笔者惊奇的发现,编译器在此没有对保护区域做任何标记:

0x0012FF58  cc cc cc cc cc cc cc cc 61 73 76 00 cc cc cc cc b8 ff 12 00 b8 19 41 00

所以如果我们不去对数组下标进行检查,同样会出现缓冲区溢出漏洞,笔者认为,如果VC++2008编译器能够在每次对缓冲区更新时标记缓冲区保护区域,这该多好啊!当然,有可能他们是出于其他考虑,只是笔者没有想到吧了。

关于VC++2008对保护区域进行标记的条件笔者没有能力帮大家找出源程序,因为笔者没有参加VC++的编写(哈,说说大话!)。不过,这两个实验应该能有助于大家的猜想吧。

现在我们还有一个问题没有解决,如果编译器对缓冲区进行了标记我们就真的无可奈何了吗?回过头去仔细分析以下刚才产生随机数据的并将它放入缓冲区保护区的汇编语句:

设置指令:

004114CE  mov         eax,dword ptr [___security_cookie (419004h)]

004114D3  xor         eax,ebp

004114D5  mov         dword ptr [ebp-4],eax ;此语句将eax内容写入堆栈基地址前4个字节区域;也就是堆栈的最后一道防线

检查指令:

0041144F  mov         ecx,dword ptr [ebp-4]

00411452  xor         ecx,ebp

00411454  call        @ILT+25(@__security_check_cookie@4) (41101Eh)

从上面的程序片段我们不难发现,如果在eax寄存器被重写之前我们能够得到它的内容那也就算大功告成了,但在实际中这也很难做到,不过作为学习,写写也无妨:

#include <windows.h>

#include <stdio.h>

#include <iostream>

#include <string.h>

using namespace std;

void copyString(char *s)

{

  DWORD addr;

  _asm mov addr ,eax //关键在此添加了汇编程序;

  DWORD * pEIP=(DWORD*)&s[4];

  *pEIP=addr;

  char buf[4];

  strcpy(buf+24,s);

}

void hacked(void )

{

  cout<<"This is hacked!";

  while(1);

}

int main(int argc,char *argv[])

{

  char badStr[16];

  DWORD * pEIP=(DWORD*)&badStr[12];

  *pEIP=(DWORD)hacked;

  copyString(badStr);

  return 0;

}

另外,还有几条对堆栈基地址与栈顶进行检查的指令:

00411459  add         esp,0D8h

0041145F  cmp         ebp,esp

00411461  call        @ILT+315(__RTC_CheckEsp) (411140h)

;与之对应的上文指令是:

00411401  mov         ebp,esp

00411403  sub         esp,0D8h

笔者简单解释一下这几条指令的意思:进入子程序时,就以主调程序堆栈栈顶做为当前堆栈基地址,(声明一下,这个栈顶与主调程序压栈栈顶相差4bytes,因为子程序在设置自己的缓冲区前已经先压入了主调程序基地址。)然后向低地址移动缓冲区所需字节,因此在子程序设置好堆栈以后压入数据的又全部被弹出后,再将栈顶减去缓冲区大小,如果能够回到栈底则表示没有问题,否则有问题。

分析到此,相信大家对VC++2008的缓冲区保护机制有了一定的了解了吧,笔者水平有限,也快黔驴技穷了,想了解更多的朋友们可以查阅微软相关资料。今天写这篇文章不为别的,只做学习、只做学习!另外,笔者认为缓冲区溢出安全隐患虽说微软帮我们较完美的防范了。不过我们在编程中最好还是自己在意点。否则,一旦因出现问题而弹出那些出错信息,对用户来说那可很不友好啊(很容易降低用户对我们的信任度,觉得我们的程序不可靠)。小弟初学编程快半年,用了近两天写下自己的一些”谬论”,以与朋友们交流交流,限于在下的经验与水平,其中的错误和不当之处在所难免,敬请读者朋友批评指正。

                                                                                                                                                                                                                                                            笔名:SageMiner

                                                                                                                                                                                                                                                            QQ: 496565825

                                                                                                                                                                                                                                                            Email: nlq_417@163.com

                                                                                                                                                                                                                                                            Date: 2009-11-10

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值