彻底杜绝可恶的缓冲区溢出错误!

缓冲区溢出是困扰程序员多年的痼疾,特别是使用C语言的程序员,面对毫无保证可言的内存指针,既不能实时检查内存区域的真实大小,也不能避免内存使用者有意或无意的缓冲区溢出操作,实在是非常的无奈。就算是C++的程序员,其实境遇也好不到哪里去,比如STL里的vector,在使用operator []和迭代器来访问数据成员时同样是得不到保证的,一切只能靠自觉。
那么,在数据安全性要求非常高的情况下,有没有办法彻底杜绝这种缓冲区溢出问题呢?值得欣慰的是,Windows 2000及以上操作系统提供的内存访问控制机制能帮忙解决问题。
以下,我实现了一个简单地C++类来封装一段绝对安全的内存,由于其中并没有真正依赖任何的C++特性,改装成C函数也是轻而易举的事情,所以实际上,这是Windows平台上的一种通用解决方案。

首先我们来回顾一下Windows的内存保护机制。
Windows采用虚拟地址的方式把真实的物理内存空间划分成很多的虚拟的段,每一个段的大小固定(典型值为0×1000),并可以为其设置相应的访问权限。默认的内存访问权限是可读写不可执行,不过我们可以使用VirtualAlloc或VirtualProtect来改变这种权限,例如改成完全不能访问或可执行等等,具体可以参考MSDN中对这两个函数的说明。如果将内存的访问权限设为PAGE_NOACCESS,那么任何针对这个区域的读写操作都会抛出异常,也就是大家常见的“无法访问”异常。这个异常可以用C++的try…catch(…)…捕捉,也可以用__try…__except(…)…捕捉(这个是推荐用法,但是用起来麻烦),在C语言里还可以用setjmp()/longjmp()来检测,所以只要捕到这个异常也就检测出缓冲区溢出问题,接下来如何处理就完全由程序员自己控制了。
特别值得一提的是VirtualAlloc的一些用法和注意事项。通过改变第三个参数flAllocationType,它不但可以用来申请内存,还可以保留一段内存为未来使用。不过VirtualAlloc并不是万能的,它申请空间时会自动根据段的大小进行对齐,而且访问权限也只能针对一个段来设定,而不能给更小的内存区域设定访问权限。

所以为了使用不可访问的内存作为缓冲区的保护区域,在实际使用时,不可访问区域大小为段的大小,权限设置为PAGE_NOACCESS,而中间一段区域权限设置为PAGE_READWRITE,其中可读写缓冲区的尾部与不可访问区域相接,头部则用0xDD填充,作为保护。将可读写的缓冲区尾部与不可访问区域相接是考虑到缓冲区溢出多发生在尾部,这也意味着,头部的缓冲区溢出问题必须用手动检查的方式来验证。

在这里,不能不说一下这种方案的缺陷:会造成较大的内存损耗。在最坏的情况下,且段的大小为0×1000(4K),这种方案将多消耗约12KB的内存用于保护可读写区域,所以它只适合于保护最为核心、最为关键的内存区域,而不能像new一样随意使用。

我将这些功能封装在一个SafeMemory类中,它的使用方法如下:

SafeMemory buffer(3000); // Alloc 3000 bytes safe memory.

try
{
foo(buffer.Get());
// Some function use the buffer.
buffer.Validate(); // Validate the header.
}

catch (…)
{
// Buffer over-run!
}

以下是完整的源代码。
首先是SafeMemory.h

#ifndef _SAFE_MEMORY_H_
#define _SAFE_MEMORY_H_

/**
Manage the memory and make no-access memory gaps before
and after the buffer.
*/

class SafeMemory
{
static const BYTE DangerousDataValue = 0xDD; /**< The old value in dangerous data area,
which is writable but invalid.
*/


public:
explicit SafeMemory(size_t size);
~SafeMemory();

/**
Get safe memory.
@return The pointer to safe memory.
*/

void * Get()
{
return buffer_ + pageSize_ + offset_;
}


void Validate() throw (…);

private:
BYTE
* buffer_; /**< Virtual memory buffer. */
DWORD offset_;
/**< Offset from the beginning of a page. */
DWORD totalPageCnt_;
/**< Total page number of read-writable buffer. */
DWORD pageSize_;
/**< System page size. */
}
;

#endif // ifndef _SAFE_MEMORY_H_

然后是SafeMemory.cpp

#include <windows.h>
#include <tchar.h>

#include
SafeMemory.h

SafeMemory::SafeMemory(size_t size)
{
SYSTEM_INFO info;
::GetSystemInfo(
&info);
pageSize_
= info.dwPageSize;
totalPageCnt_
= size / pageSize_ + 1;
offset_
= totalPageCnt_ * pageSize_ - size;

buffer_
= reinterpret_cast<BYTE*>(
::VirtualAlloc(NULL,
pageSize_
* (totalPageCnt_ + 2),
MEM_RESERVE,
PAGE_NOACCESS));

::VirtualAlloc(buffer_
+ pageSize_, pageSize_ * totalPageCnt_,
MEM_COMMIT, PAGE_READWRITE);

::FillMemory(buffer_
+ pageSize_,
offset_,
DangerousDataValue);
}


SafeMemory::
~SafeMemory()
{
::VirtualFree(buffer_, pageSize_
* (totalPageCnt_ + 2), MEM_RELEASE);
}


/**
Validate the memory. If the dangerous data is modified, throw exception.
*/

void SafeMemory::Validate() throw (…)
{
BYTE
* buffer = buffer_ + pageSize_;

for (DWORD i = 0; i < offset_; ++i)
{
if (DangerousDataValue != buffer[i])
{
// This code will generate a memory access exception.
i = static_cast<DWORD>(buffer_[0]);
break;
}

}

}

所有代码在Windows XP SP2 + Visual Studio.Net 2003下面调试通过。

相关阅读

有话想说?请留下评论吧~~如果喜欢我的blog,欢迎订阅~~

评论

还没有任何评论。

留下评论

(必需)

(必需)