-
-
[原创]软件调试基础--08堆破坏之完全页堆
-
发表于:
2016-1-27 15:10
4656
-
在软件中,有一种bug,其产生原理,非常简单,但解决起来却十分困难。这个bug的名字叫“堆破坏”。这个问题并没有一个唯一的解决方案。有时候,我们只能一点一点分析出破坏的代码到底是哪里,但显然Microsoft的大神们,早就意识到了这个问题的复杂性,所以,他们给我们提供了各种解决此问题的方法,本节要讲的是,其中最有效,最简单的一种:完全页堆!
完全页堆的结构比较复杂,我把其原理的核心讲一下,知道这个原理足够应付我们日常使用完全页堆进行调试了。如果你想追根问题,请百度+《软件调试》详细了解。
基本上就是这个样子,用户所使用的每一个堆块,操作系统为我们构建如上的结构,可以看到,即便我们的多块只有几个字节大小,但操作系统还是为我们分配了8K的空间,所以这个方法,是很耗内存的。
为什么要占用这么大空间呢?因为要保证我们的代码当访问到用户数据范围后面的数据时,及时产生异常,并通知调试器处理该异常,也就是第一时间给了开发者来解决这个错误的机会。因为操作系统对内存的管理是内存页机制,最小粒度为4K,所以。。。也就是说,我们不可能针对几个字节的内存设置其访问属性为不可访问,想改变内存的属性,必须改变整个页的或多个页的。前面的数据页中的内容,我们发现用户数据在尾部,后面紧跟着的是栅栏页,所以一旦我们访问到用户数据之后的地方,就会立马产生异常。这个结构其实就这么简单。
下面我们搞个简单的例子来试试。代码如下:
void test()
{
char *psz = new char[16];
psz[16] = 'A';
}
int _tmain(int argc, _TCHAR* argv[])
{
test();
system("pause");
return 0;
}
明显越界访问了堆内存,但是debug下不会崩溃,release下也不会崩溃,然后我们开启完全页堆。再次执行,发现debug下不会崩溃,但release崩溃了。想想这是为什么呢?
因为,debug下,我们的数据区并不是完全是16个字节,其尾部还有4个fd,用来做堆尾检查的。所以,访问psz[16]并未跨越栅栏边界,访问到栅栏页。所以没有崩溃。
但release下,psz[16]已经跨越了栅栏边界,访问到了栅栏页,立马就会崩溃,此时我们就抓到了堆破坏的第一现场。这个第一现场是非常重要的,否则一个堆破坏产生之后,可能好长时间之后,才会访问到这个已经被破坏掉的堆,然后才会崩溃,到那时,基本无力回天了。
还有,这个结构中,如果发生了如下破坏,明显是检测不到的。
所以,完全页堆也并不是检查堆破坏的万能方法,只是增加找到问题根源的几率,真要碰上如上图这种情况,还得靠我们自己细心根据线索去查找。而且一些内存使用密集的应用,开启了完全页堆,未必能启动的起来。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)