这是一个相当简单的错误。使用字节计数而不是字符计数调用了宽字符串函数,从而导致缓冲区溢出。找到问题后,修复方法很简单,只需将sizeof更改为_countof,很容易的。但像这样的BUG浪费时间。由于崩溃,playtest被取消了,而且由于缓冲区溢出破坏了堆栈,因此找到错误代码并非易事。我知道这种类型的错误是可以避免的,我知道还有很多工作要做。

我所做的工作包括:

尽量早点诊断

如果程序在发出错误的宽字符函数调用的函数内部崩溃了,那么找到错误将是微不足道的——代码检查会很快发现它。但是,一旦执行从该函数返回,垃圾堆就模糊了bug的位置,使其调查变得更加棘手。

事实证明,有一个VC++编译器开关可以防止在破坏堆栈后返回。所以,我做的第一件事就是打开开关。这个开关告诉VC++在堆栈上添加一个标志,并在返回之前进行检查。当这个开关打开的时候,溢出的功能被抓住了,找到它花了几秒钟。GS开关是作为一种安全特性,用于防止恶意的缓冲区溢出,但它也可以作为开发人员的生产力工具很好地工作。它确实有一些运行时开销,但这种权衡通常是值得的,尤其是在内部构建上,推荐使用。

修改bug

一旦我打开/GS并重新调试了这个bug,找到这个bug就很简单了,所以下一步就是修复它,如前所述。

保护证据

当缓冲区溢出破坏堆栈时,buggy函数返回到一个垃圾地址,这个地址正好在堆中。抹掉bug所在位置的证据已经够糟糕的了,但更糟糕的是,在返回堆中的地址后,会将堆中的数据作为指令执行。这扰乱了寄存器和堆栈,通常会使事情变得混乱。

还有,一个可执行堆?真正地?这是一个首要的安全漏洞。因此,下一个任务是将链接器设置更改为/NXCOMPAT。这将告诉Windows使堆和堆栈不可执行。这大大提高了安全性,还可以简化调试。而且,这个选项没有运行时成本,推荐使用。实际上,这应该被认为是必要的。

 

我还打开了我们的发布分支中的/DYNAMICBASE链接器开关,以进一步提高安全性,同时也没有运行时开销。

规避崩溃风险

在这一点上,我已经修复了这个错误,使以后的此类错误更容易调查,并提高了安全性。但还有很多事情要做。原来这种错误很容易犯。当开发人员传递缓冲区和大小时,传递错误大小的方法至少有六种。将来避免这些错误的最好方法是避免传递大小。创建以数组为参数并以100%的精度推断大小的模板函数非常容易。与其写这个:

mywprintf(buffer, _countof(buffer), …); // Verbose and dangerous

你可以写成下面的:

mywprintf_safe(buffer, …); // Compact and safe

它打字少,阅读少,而且保证是正确的。接受数组引用的模板化函数的语法有点粗糙,但您只需要在几个地方正确地使用它。如果有一个原始指针,模板技术就不起作用,但是对于任何其他目标,您应该能够创建一个重载来处理它。手动传递的大小应该很少。因此,我的下一个任务是为所有字符串函数添加模板重写,并鼓励每个人在所有新代码中使用它们,从而使避免再次编写此类错误变得非常简单。

避免过去的风险

虽然safe模板函数可以让我们在将来避免编写这种类型的bug,但它们对现有的数百万行代码却无能为力。可以肯定的是,有更多的尺寸不匹配等着咬我们。所以我在字符串函数中添加了SAL注释,启动了visualstudio的/analyze,并开始编译。到目前为止,这是最大的任务。任何没有运行静态分析的大型代码库都会有很多可检测的bug。缓冲区溢出、逻辑错误、格式字符串不匹配、释放后使用等等。分析了上千个项目,修复了5个主要的机器故障。这是几个月的工作分散了几年,但时间是值得花的。今天它仍在发现新的编码错误。

标签: none

添加新评论