第五章MemoryCorruption.docx
- 文档编号:9396084
- 上传时间:2023-02-04
- 格式:DOCX
- 页数:69
- 大小:210.44KB
第五章MemoryCorruption.docx
《第五章MemoryCorruption.docx》由会员分享,可在线阅读,更多相关《第五章MemoryCorruption.docx(69页珍藏版)》请在冰豆网上搜索。
第五章MemoryCorruption
第五章MemoryCorruption
第一部分:
堆栈
有两个原因导致内存破坏成为最棘手的程序出错类型之一。
首先,破坏的源头和现象可能相隔很远,很难将原因和结果关联起来。
其次是由于只有在比较罕见的条件下症状才显露出来,使得要一致的重现错误比较困难。
原则上只要满足以下两个条件中的任意一个,就会发生内存破坏。
⏹线程对一块不属于它的内存进行写操作。
⏹线程对属于它自己的内存进行写操作时,破坏了该内存的状态。
这里有个小程序可以作为展示第一个条件的例子。
#include
#defineBAD_ADDRESS0xBAADF00D
int__cdeclwmain(intargc,wchar_t*pArgs[])
{
char*p=(char*)BAD_ADDRESS;
*p=’A’;
return0;
}
上面的小程序先声明了一个char型指针,然后对其初始化,赋给其一个不可访问的地址(0xBAADF00D)。
运行该程序的最终结果就是程序崩溃,紧接着弹出可怕的Dr.Watson(译注)。
很明显这是因为这个小程序中执行了一次无效的内存访问导致,但是在很复杂的系统中要指出错误很麻烦。
例如应用程序分配了一块内存,并且计划了其生命周期。
如果过早的释放了它,失效的指针访问就会导致内存破坏。
应用程序对不属于自己的内存进行写访问会导致程序崩溃,这是最好的情况。
等等!
!
读者看到这里可能会问:
你是说程序崩溃是最好的情况?
!
没错,对于内存破坏来说,发生崩溃的话也许能立即指明发生内存破坏的原因。
就像上面的小程序一样,由于被写的内存无效,所以立即发生了崩溃。
这是个好消息,因为我们很轻松的就看到了错误原因:
一个指针指向了无效的内存地址。
再看看第2种情况,如果无效指针指向的是属于程序中别的部分分配的内存的话,可能出现如下几种症状:
⏹程序崩溃:
跟前面的程序崩溃的主要区别在于发生的时间会延后一些。
上面的示例程序因为尝试写一块被操作系统认为无效的内存导致了崩溃。
第2种情况下,应用程序尝试写入的是操作系统认为有效的内存,所以允许其写入,没有错误发生。
随后应用程序可能会试着使用被错误改写过的内存,也许就会崩溃(依赖于内存访问的性质)。
⏹不会崩溃,但是有意料之外的行为:
由于之前写了无效数据到其他部分所拥有的内存中,不一定程序就会崩溃。
这种情况相当多。
应用程序的其他部分会继续使用被写入异常数据的内存,甚至内存的状态都已经被修改过(通常情况下不会发生这种状况)。
看个例子,假设有个线程池的类,除了能对线程池的请求排队外,还有个方法用于设置一个标志以控制流程结束。
线程池周期性的检查该标志,一旦发现该标志为TRUE则停止工作。
应用程序会初始化出该线程池的一个单体对象来使用。
现在假设线程池正在处理200个请求(信用卡授权)时,一个线程错误的将标志设置为TRUE了。
于是线程池突然间就停止工作,客户在用信用卡交易时发生错误,电话铺天盖地的响起来……这是典型的内存破坏的例子:
线程破坏内存最终导致发生不可预料的行为。
由于修改内存的线程已经对内存数据造成了损害,随后使用这块内存有时(通常总是)无法预料。
要找到这些类型的内存破坏的根源那是相当的难啊。
译注:
如果你发现没有弹出Dr.Watson对话框也不要太惊讶。
实际上依赖于注册表中的一个键值:
HKEY_LOCAL_MECHINE\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\AeDebug。
其中包括Auto,Debugger和UserDebuggerHotKey三个值。
默认情况下Debugger的内容为"drwtsn32-p%ld-e%ld",指出系统的默认调试器为Dr.Watson。
Auto键值为0或1。
为0时表示系统不自动处理,当有应用程序崩溃时会弹出一个对话框通知用户;当Auto为1时,系统就自动调用调试器记录发生错误的程序的相关信息然后退出,不通知用户。
UserDebuggerHotKey是设置一个快捷键用来发送一个DebugBreak()调用,就好像进断点了一样,前提是程序是由调试器加载起来的。
内存破坏诊断流程
本节讲述一下内存破坏问题的处理流程。
下面的流程图简单描述了每一个步骤。
有一点很重要:
要找到内存破坏问题的根源,对于不同的状况,可能需要反复执行Figure5.1中的流程才行。
Figure5.1内存破坏分析过程
步骤1:
状态分析
在开始研究内存破坏的问题前,首先应该确保你当前发现到的错误确实是因为内存破坏的缘故导致的。
这个步骤还可以进一步分解,如Figure5.2
Figure5.2状态分析过程
如同前面提到的,内存破坏的特征无非就是这两种:
程序崩溃或者不崩溃但是行为异常。
最开始可以通过分析被破坏的内存的状态来对其行为进行初步的分析。
那么我们如何知道要分析哪些状态呢?
随着程序崩溃,寻找起点就(译注:
这里的起点应该是指代码中程序崩溃的位置吧)十分简单了。
由于一些未预期的状态导致程序中的代码执行崩溃,而在崩溃的时候的代码是已知的。
通过观察程序发生崩溃时的内存状态以及对相关代码的检视,我们可以对原始状态作出准确的判断。
“没问题”,尽管问题多多,但是代码执行路径可以产生当前状态。
如果你遇到的是这种情况,就其本身而言,这不是内存破坏问题,但仍可能是由于未预期的代码执行路径写内存错误。
然而,如果没有代码执行路径能令内存变成当前状态,唯一貌似可能的解释就是发生了内存破坏,内存被改写了。
如果你遇到的不是应用程序崩溃,而是发生周期性的奇特行为的话,要找到被偷偷破坏的内存可就没程序崩溃时那么简单明了。
一般来说,当程序发生异常行为时,你最好用调试器中断程序以进行些初步的分析。
看个例子,如果客户在进行信用卡认证时不断的发生错误,你应该赶紧检查线程池(线程池处理所有的信用卡认证)的状态,看看为啥会失败。
如果你注意到线程池已经停止工作,根本不接受请求的话,那就进行第2步:
源代码分析,确认一条“合法”的(译注:
这里的合法是指线程池合法停止工作)代码执行路径或者(如果不存在这样的路径的话)做结论:
发生了内存破坏。
步骤2:
源代码分析
根据步骤一确认自己可能面临一个内存破坏问题后,接下来就是进行代码分析,看是否能找到其根源。
当线程对不属于自己的内存块写数据时肯能会发生内存破坏。
照这么说,我们就有了一个重要的方向。
线程向内存块写入数据,可以推测写入的数据应该与某个特定的线程相关。
就这点来说,如果我们能够分析出这些数据的含义,那么就能进一步缩小可怀疑的范围。
来看个例子,Listing5.1是一个非常简单的控制台程序,该程序给用户提供两个选项:
1.显示应用程序信息(比如全名和版本信息),2.模拟内存破坏。
读者可以试着不看完整源代码,只看看下表列出的部分。
Listing5.1模拟内存破坏的简单控制台程序
int__cdeclwmain(intargc,wchar_t*pArgs[])
{
wint_tiChar=0;
g_AppInfo=newCAppInfo(L”Simpleconsoleapplication”,L”1.0”);
if(!
g_AppInfo)
{
return1;
}
wprintf(L”Press:
\n”);
wprintf(L”1Todisplayapplicationinformation\n”);
wprintf(L”2Tosimulatememorycorruption\n”);
wprintf(L”3Toexit\n”);
wprintf(L”\n\n>“);
while((iChar=_getwche())!
=’3’)
{
if(iChar==‘1’)
{
g_AppInfo->PrintAppInfo();
}
elseif(iChar==’2’)
{
SimulateMemoryCorruption();
wprintf(L”\nmemorycorruptioncompleted\n”);
}
else
{
wprintf(L”\nInvalidoption\n”);
}
wprintf(L”\n\n>“);
}
deleteg_AppInfo;
return0;
}
Listing5.1完整的源代码和执行程序存放在下列位置:
SourceCode:
C:
\AWD\Chanpter5\MemCorrupt
Binary:
C:
\AWDBIN\WinXP.x86.chk\05MemCorrupt.exe
通过下面的命令行执行该程序:
C:
\AWDBIN\WinXP.x86.chk\05MemCorrupt.exe
此程序由一个封装了应用自定义信息(包括完整的应用名称和版本信息)的类组成。
主函数允许用户输出自定义信息、模拟内存破坏或者退出。
Press:
1Forapplicationinformation
2Forsimulatedmemorycorruption
3Toexit
按1的话,显示如下:
>1
FullapplicationName:
Simpleconsoleapplication
Version:
1.0
按2的话,显示如下:
>2
Memorycorruptioncompleted
如果你再按1的话,不必惊讶,程序会崩溃的。
现在开始变得有趣了。
我们要如何才能找出是程序中哪部分导致的崩溃呢(withoutsteppingthroughthecodeforstep2)?
最开始,在调试器下运行该程序,选择跟刚才相同的选项序列。
当你第二次选择选项1时,调试器会因为访问为例而中断程序。
…
…
…
0:
000>g
ModLoad:
5cb700005cb96000C:
\WINDOWS\system32\ShimEng.dll
Press:
1Todisplayapplicationinformation
2Tosimulatememorycorruption
3Toexit
>1
FullapplicationName:
Simpleconsoleapplication
Version:
1.0
>2
Memorycorruptioncompleted
>1(bdc.8d8):
Accessviolation-codec0000005(firstchance)
Firstchanceexceptionsarereportedbeforeanyexceptionhandling.
Thisexceptionmaybeexpectedandhandled.
eax=72726f43ebx=7ffd0073ecx=00000007edx=7ffffffeesi=00000020edi=00000002
eip=77c43869esp=0007fa68ebp=0007fed8iopl=0nvupeiplnznaponc
cs=001bss=0023ds=0023es=0023fs=003bgs=0000efl=00010202
msvcrt!
_woutput+0x695:
77c4386966833800cmpwordptr[eax],0ds:
0023:
72726f43=?
?
?
?
0:
000>kb
ChildEBPRetAddrArgstoChild
0007fed877c4229077c5fca0010012080007ff28msvcrt!
_woutput+0x695
0007ff1c010014480100120872726f4300032cb0msvcrt!
wprintf+0x35
0007ff30010013b200032cb000032cb07ffd0031memcorrupt!
CAppInfo:
:
PrintAppInfo+0x18
0007ff44010015fa0000000100032bf00003688005memcorrupt!
wmain+0xb2
0007ffc07c816fd7000119707c9118f17ffdf00005memcorrupt!
wmainCRTStartup+0x12f
0007fff000000000010014cb0000000078746341kernel32!
BaseProcessStart+0x23
我们从堆栈中能看到主函数调用了CAppInfo:
:
PrintAppInfo函数,该函数再调用了wprintf函数。
结合源代码以及调试器中看到的,看起来非常合理。
接下来的问题是为什么wprintf函数会出错。
看看源代码中给wprintf函数传入了什么:
VOIDPrintAppInfo()
{
wprintf(L"\nFullapplicationName:
%s\n",m_wszAppName);
wprintf(L"Version:
%s\n",m_wszVersion);
}
一个合理的解释就是我们传入的两个指针(m_wszAppNme和m_wszVersion)必定无效。
wsprintf函数假定我们传入的指针(这里是字符串类型)指向的是以NULL结尾的宽字符串。
如果这个假定部成立,那么函数就可能会崩溃。
现在我们再将注意力转过来分析一下有问题的对象的状态。
具体的看一下CAppInfo的状态:
0:
000>X05memcorrupt!
g_*
0100200805memcorrupt!
g_AppInfo=0x00032cb0
0:
000>dtCAppInfo0x00032cb0
+0x000m_wszAppName:
0x72726f43->?
?
+0x004m_wszVersion:
0x01747075->?
?
看看我们感兴趣的两个指针m_wsaAppName和m_wszVersion各自指向什么内容:
0:
000>dd0x72726f43
72726f43?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
72726f53?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
72726f63?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
72726f73?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
72726f83?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
72726f93?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
72726fa3?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
72726fb3?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
0:
000>dd0x01747075
01747075?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
01747085?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
01747095?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
017470a5?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
017470b5?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
017470c5?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
017470d5?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
017470e5?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
上面的问号表明这块内存不可访问。
有趣吧~~第一次让程序显示信息,一切都挺正常的。
而现在这些指针都指向了不可访问的位置。
不知为啥CAppInfo对象的内容被破坏了。
简单C++类对象的内存布局由其成员变量构成,在上面的例子中就是两个指针。
如果内存对象被改写,我们就陷入了拥有两个被破坏的指针的情形。
在此基础上,这两指针指向什么就值得看看了:
0:
000>x05memcorrupt!
g_*
0100200805memcorrupt!
g_AppInfo=0x00032cb0
0:
000>dd0x00032cb0
00032cb072726f4301747075abababababababab
00032cc0000000000000000000040012001c07f2
00032cd0005000410044005000540041003d0041
00032ce0003a00430044005c0063006f006d0075
00032cf0006e006500730074006100200064006e
00032d000053002000740065006900740067006e
00032d10005c00730061006d006900720068006f
00032d200041005c007000700069006c00610063
这次内存dump看到的仍然是我们之前看到的指针值。
不用dd命名了,尝试将指针(0x00032cb0)当成字符串指针看看:
0:
000>da0x00032cb0
00032cb0“Corrupt.........”
越来越有趣了!
看起来CAppInfo类对象的成员指针被改写为“Corrupt”。
现在我们通过检视代码来看看程序中有没有代码维护一个内容为“Corrupt”的字符串。
正如你猜想的一样,在我们选择第2个选项(模拟内存破坏)时,程序强制将CAppInfo对象的成员指针的内容改写为了(“Corrupt”)。
那么我们如何知道要用何种形式来dump数据才能弄清其含义呢?
没有明显的规则可循,只有大致方向。
下面的几个策略在进行内存分析时可以试试:
1.使用dc命令转储出指针指向的内存数据。
此命令以Double-word的形式转储数据,同时也显示对应的ASCII。
如果看到输出中有字符串,那就用da或者du命令显示这些字符串。
2.使用!
address扩展命令来收集关于内存的数据。
此命令能为你提供内存的类型(比如私有)、保护级别(比如读或写)、状态(比如提交或保留)以及用途(比如堆或堆栈)。
3.使用dds命令以双字(double-word)的形式转储内存数据以及符号,帮助我们将内存和符号对应起来。
4.使用dpp命令来解引用给定的指针,以双字的形式显示内存数据。
如果有任何一个双字匹配上一个符号,那么符号也会相应的显示出来。
这个技巧在内存指针包含虚函数表时很有用。
5.使用dpa或dpu命令将内存以ASCII或者UNICODE的形式转储出来。
6.如果内存数据是个比较小的数字(是4的倍数),这可能是一个句柄。
可以用!
handle命令来转储该句柄的信息。
7.如果前面的步骤都没有得到信息,可以尝试搜索整个地址空间来寻找对这个内存地址的引用。
这种在被破坏的内存中识别数据的技巧在试图找出破坏内存的错误代码时非常有用。
但是,再说一遍,这个技巧不可能总会找到罪犯(offender)。
下一步是使用内存破坏诊断工具,这样你就轻松多了。
步骤3:
使用内存破坏诊断工具
在开始讲这些工具前,明白这些工具不会对捕获内存破坏提供保证是很重要的。
这些工具只是能够捕获一些很常见的内存破坏情况。
针对不同的内存破坏类型,使用的工具也不同。
对于堆栈的破坏,最好的工具就是编译器。
它会在你的程序中插入一些堆栈校验的代码。
对于堆被破坏的情况,最好的工具是ApplicationVerifier(见第一章工具介绍)。
ApplicationVerifier有大量关于内存破坏的测试设置。
所有这些工具的共同之处是企图在内存破坏刚发生时,立即捕获常规的内存相关的程序错误,而不是等到发生很多棘手的副作用之后。
在本章后面将会看到编译器在堆栈破坏方面是如何帮助我的。
在第6章"内存破坏PartII-堆"中再使用ApplicationVerifier来分析基于堆的破坏。
步骤4:
InstrumentSourceCode
如果前面几步都没能帮到你,那你就得干点苦活了。
接下来你要把前面几步获得的信息以及可能的推测都收集起来,当你想到一点可能时,就自己写代码来验证正确与否。
Instrumentation技术是跟操作系统支持的追踪完全不同的一种简单的跟踪方式。
步骤4:
定义回避策略
最后,可以说是最重要的,利用你学到的知识定义一个深入的回避策略。
回避策略可以引入在整个开发过程中使用工具帮助捕获一般内存破坏问题的形式,同时正在编写的代码中有明显的步骤用于最小化潜在内存破坏的风险。
本章余下的部分挨个儿看看一些常见的内存破坏场景。
展示如何应用这些处理流程来找出隐藏在内存破坏背后的原因。
本章中的场景集中基于堆栈的破坏,第6章关注基于堆破坏的情况。
堆栈破坏
堆栈大概是大家最常用的,也是大家都熟悉的数据结构之一。
很多算法入门课都以学习堆栈数据结构作为开始。
这确实是一个非常简单而又直观的数据结构,就好像一堆纸。
你每放(对应push)一张纸到纸堆都是放在纸堆的顶部,而每次取走(对应pop)一张纸也都是从纸堆的顶部拿走。
同样的,对堆栈执行的两个基本操作(push和pop)也总是在顶部。
因为往纸堆放一张纸或者从纸堆取走一张纸都是从顶部开始,所以说这个算法有后进先出(LIFO)的语义。
堆栈对于Windows系统中的执行代码而言很简单,就是操作系统分配给执行线程的一块内存。
除了其他的目的外,堆栈用于跟踪函数调用链(为局部变量分配空间,参数传递等等)。
任何时候有函数被调用时,就有一个堆栈帧(译注:
包括传入的参数,返回地址等)被创建并保存在堆栈中。
随着线程调用越来越多的函数,堆栈增长的也越来越大。
Figure5.3分析了一个函数调用时的堆栈。
在接下来的例子中我们会精确的看到堆栈中的每个元素是如何形成的。
现在暂时看看Figure5.3中说明的在x86结构的系统中一个函数调用过程中堆栈的一般布局。
Figure5.3函数调用时的堆栈分析
注意如果你编译本章的代码,需要先确认在你的编译环境中已将BUFFER_OVERFLOW_CHECKS环境变量设置为0来禁用缓存区溢出检查。
(译注:
没见过这个环境变量,google后发现大多是跟驱动有关,猜测作者可能是DDK的编译器。
)
为了更好的理解堆栈是如何工作以及如何被破坏,我们来看个例子。
Listing5.2的应用展示了一个会进行一些嵌套的函数调用的新线程的启动点,在每个函数中都声明了些局部变量。
Listing5.2展示线程创建的应用示例
#include
#include
#include
DWORDWINAPIThreadProcedure(LPVOIDlpParameter);
VOIDProcA();
VO
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 第五 MemoryCorruption
![提示](https://static.bdocx.com/images/bang_tan.gif)