本节,我们来看一个简单的堆破坏示例,程序依旧来自前面的示例,Crash Me!按钮的消息函数如下:
void Cdump3Dlg::OnBnClickedButton1()
{
int* a = new int[1000];
for( int i = 0; i < 1005; i++ )
a[i] = i;
printf("%d\n", a[0]);
delete [] a;
}
我们让new分配的数组访问越界,看看会产生什么结果。本节采用程序的方式来采集DUMP,同时,堆分析需要比较多的信息,因此,MiniDumpWriteDump函数中的Minidump类型写为MiniDumpWithFullMemory,而不是原来的MiniDumpNormal。Visual Studio中的Debug模式下使用的堆为调试堆,具有额外的检查功能,可以检查出堆破坏。我们使用Release模式编译。运行程序,程序出错,并自动保存了mini.dmp文件:
拖入Windbg中,输入 !analyze -v 自动分析。先看看出错的位置及异常信息:
显示出错在RtlpAllocateHeap函数中,那必然是调用new或malloc时出错。异常代码为c0000005,非法访问,在RtlpAllocateHeap内部产生了非法访问?我们再来看看栈回溯:
显示是在OnBnClickedButton1中调用printf函数时出的错。看看我们的代码:
printf(“%d\n”, a[0]);
这句也能出错?没有任何越界访问啊?再看看栈回溯,大概是这个样子的:
也就是说,是printf内部调用了malloc导致出错。那么我们上一节说过,库函数使用VC运行时的堆_crtheap,任何时候这个堆被破坏,后续对该堆的操作都可能会出错,这个示例就是典型的这种情况。所以不要再怀疑Visual Studio为什么printf函数也会崩溃了。
使用Windbg的 !heap指令来查看一下:
Windbg检查到堆错误,错误的地址为0073c4b8,进程中有8个堆,出错的是第4个。你可以加 -h选项来列出所有的堆:
4号堆已经使用了两个段。查看4号堆,还可以获得更多的信息:
来看看错误提示消息,这很有意思:
PreviousSize field does not match Size field in previous entry
Entry->PreviousSize == 0x3e8
PreviousEntry->Size == 0x2a2
PreviousSize 和Size 均位于结构 HEAP_ENTRY中,每个小的堆块前面都是一个HEAP_ENTRY结构,用来保存该块的信息:
HEAP_ENTRY结构中有一些重要的字段:
0:000> dt _HEAP_ENTRY
ntdll!_HEAP_ENTRY
+0x000 Size : Uint2B // 以粒度表示的块大小,不包括本结构
+0x002 Flags : UChar // 堆块的状态
+0x003 SmallTagIndex : UChar
+0x000 SubSegmentCode : Ptr32 Void
+0x004 PreviousSize : Uint2B // 前一个堆块的大小
+0x006 SegmentOffset : UChar
+0x006 LFHFlags : UChar
+0x007 UnusedBytes : UChar // 用户数据区的未使用字节数
+0x000 FunctionIndex : Uint2B
+0x002 ContextValue : Uint2B
+0x000 InterceptorValue : Uint4B
+0x004 UnusedBytesLength : Uint2B
+0x006 EntryOffset : UChar
+0x007 ExtendedBlockSignature : UChar
+0x000 Code1 : Uint4B
+0x004 Code2 : Uint2B
+0x006 Code3 : UChar
+0x007 Code4 : UChar
+0x000 AgregateCode : Uint8B
那么错误的意思就是当前块的HEAP_ENTRY结构中保存的前一个堆块的大小(PreviousSize)与前一个堆块的实际大小(Size)不一致。这说明前一个堆块发生了溢出,修改了后一个堆块的HEAP_ENTRY结构。这实际上就是堆破坏的根本原因,堆通过一系列结构来管理,一旦对堆的操作越界,破坏了这些用来管理堆的结构,堆就无法再正常工作了。读者还可以继续跟踪实际的堆块(Chunk),我就不再继续了。