9.4 漏洞详述
异常处理有几个特点。一般在几种语言中实现try-catch块;Windows操作系统有结构化的异常处理(与Objective C++(www.cppentry.com)相同),它包含3类代码块:try、except和finally;基于UNIX的操作系统(包括Linux和Mac OS)都可以利用信号处理。Windows也实现了信号处理的一个非常小的集合,但由于所支持的信号非常不完整,所以Windows上的程序很少使用信号--完成这一任务还有许多其他方式。
9.4.1 有漏洞的C++(www.cppentry.com)异常
C++(www.cppentry.com)异常的基本概念是相当简单的。把可能出错的代码放在try块中,就可以在catch块中处理错误。下面是使用C++(www.cppentry.com)异常的示例:
- void Sample(size_t count)
- {
- try
- {
- char* pSz = new char[count];
- }
- catch(...)
- {
- cout << "Out of memory\n";
- }
- }
首先在try块中执行可能失败的代码,然后在catch块中捕获异常。如果想创建异常,可以使用throw关键字。Try-catch块可以嵌套,所以如果作用域中的第一个catch块没有捕获某个异常,它就可以由下一个catch块捕获。
前面的示例也说明了问题是如何出现的。使用catch(…)时,这是一个特殊的结构,它告诉编译器处理这个catch块中的所有C++(www.cppentry.com)异常。我们一般不希望在C++(www.cppentry.com)的catch块中处理操作系统异常或信号,例如访问违例(也称为分隔故障)。稍后讨论这个问题。在前面的小示例中,唯一可能导致出错的是内存分配失败。但在实际中,我们要执行许多操作,导致出错的绝不仅仅是内存分配--应对它们一视同仁。下面是一个略微复杂的示例,看看它是如何工作的。
- void Sample(const char* szIn, size_t count)
- {
- try
- {
- char* pSz = new char[count];
- size_t cchIn = strnlen( szIn, count );
- // Now check for potential overflow
- if( cchIn == count )
- throw FatalError(5);
- // Or put the string in the buffer
- }
- catch( ... )
- {
- cout << "Out of memory\n";
- }
- }
如果仅执行了这些操作,就已经对错误的输入和超出内存同等对待。正确的方法是使用如下catch语句:
- catch( std::bad_alloc& err )
这个catch语句仅捕获了new语句抛出的异常std::bad_alloc。接着是另一个更高一级的try-catch块,它会捕获FatalError异常,或者记录它们,然后再次抛出异常,让应用程序继续执行,并退出。
当然,事情并不总是这么简单,因为在一些情况下,operator::new也许会抛出另一个异常。例如,在Microsoft Foundation Classes中,失败的new运算符可能会抛出一个CMemoryException异常,在许多现代的C++(www.cppentry.com)编译器(例如Microsoft Visual C++(www.cppentry.com)和gcc)中,可以使用std::nothrow防止new运算符抛出异常。下面的两个示例都未能捕获正确的异常,因为第一个示例没有抛出异常,第二个示例没有抛出std::bad_alloc异常,而是抛出了CMemoryException异常。
- try
- {
- struct BigThing { double _d[16999];};
- BigThing *p = new (std::nothrow) BigThing[14999];
- // Use p
- }
- catch(std::bad_alloc& err)
- {
- // handle error
- }
- And
- try
- {
- CString str = new CString(szSomeReallyLongString);
- // use str
- }
- catch(std::bad_alloc& err)
- {
- // handle error
- }
如果仔细查看,您或许会问:"代码导致了一个内存泄漏!这些作者写不出真正的代码吗?"非常好--举这些示例有一个原因。处理异常的正确代码必须是不会出现异常的。正确的方法是使用某种类型的指针存储器,一旦退出try块,就释放内存--即使退出机制是一个抛出的异常,也释放内存。如果决定在应用程序中使用异常,这种方法就比较合适--它会大大提高代码的效率;如果使用正确,就能更好地在处理器中使用预报渠道,错误处理也可以更干净利索。还可以编写出不出错的代码。如果查看得比较仔细,就会注意到我们是按引用捕获异常的,而不是按值捕获,也不是按指向异常的指针来捕获异常--这么做有很好的理由。如果需要这方面的解释,请参阅Scott编著的Effective C++(www.cppentry.com)一书。
其实,这个示例有好几个值得注意的地方。另一个常见的错误是编写如下代码:
- catch(...)
- {
- delete[] pSz;
- }
这段代码假定,pSz已经在try块的外部正确初始化和声明。如果pSz没有初始化,就变成一个可以被攻击者利用的机会。Richard vab Eeden在代码审查过程中发现一个更糟糕的灾难性错误:
- catch(...)
- {
- // Geez pSz is out of scope - add it here
- char* pSz;
- delete pSz;
- }
程序员在try块的内部声明pSz,发现它不编译,于是用相同的名字创建了一个新的未初始化的变量--这非常容易造成漏洞。正确的做法是把指针包含在指针存储器中,该指针会正确初始化为null,当它超出作用域时,会释放内存,并把指针设置回null。