告别以前的方法( Writing Secure Code 的作者之一Michael Howard)

摘要: Michael Howard 介绍了这一工作,该工作旨在使 C 运行库在面对恶意代码的威胁时,更加可靠。这项变更适用于 Visual Studio 2005,它还将同时对 C 运行库和 C++ 标准模板库产生影响。

*

注意:本文中的内容适用于发布前的软件版本。发布产品时,可能会更改本文档中的部分内容。

本页内容
新版 CRT 的新增功能新版 CRT 的新增功能
C++ 中的情况C++ 中的情况
标准的变化标准的变化
小结小结
发现安全漏洞发现安全漏洞

无论是居室还是代码,我们总是有必要不时对其进行认真的清理。不幸的是,在我们开始清理时,总会产生这样的疑惑,那就是这些东西到底来自何方?为什么我们从未注意到它的存在?尽管能够清理部分内容,但总有一些会保留下来。如果您在某些方面与我相像,那么可能还会导致出现更明显、更新奇的问题。

我们看一下问题出在什么地方。C 运行库亟待进行有效的改进,这里的改进不是指一般意义上的完善,而是使它具有稳固的结构,使它完全脱胎换骨!

请认真考虑一下此问题。请问人们在什么情况下写出了诸如 strcpystrcat 之类的函数?是很久以前 Kernighan 和 Ritchie 刚刚开发出 C 语言的那段美好时光,那时代码面临的威胁远没有现在严重,网络的互连也远没有现在普及。而现在的情况则让我摸不清头脑,那就是您仍然能够使用诸如 strcpy 的函数写出安全的代码。这就是数据在起作用。但诸如 strcpy 之类的函数本身无法帮助您写出安全的代码,并且在调用这类函数时可能出现灾难性的错误。正如 'Nuff 所说,是的,gets 只是一般的错误!

那么,针对这种情况,我们能够采取什么措施呢?您可能听说过 strsafe.h 文件,该文件中包含了一组一致的、更安全的字符串处理函数,于 2002 年 Windows Security Push 活动期间开发,适用于 Visual Studio® .NET 2003 和 Platform SDK。可以查阅 Strsafe.h:Safer String Handling in C,了解有关 strsafe 的更多信息。

也可以在许多开放源代码的操作系统中使用其他函数(比如,strlcpystrlcat)。可以在 David Wheeler 撰写的 Secure Programming for Linux and Unix HOWTO 一文中查阅这两个函数的有关信息。

尽管开发 strl*strsafe 是一种有效的措施,我们仍然需要在核心的 C 运行库中创建更加可靠的功能,这正是经过更新的 CRT 发挥作用的地方。Microsoft Visual C++® 库开发小组决定跟踪并认真检查 CRT 中的每一个函数,以确定其中是否存在安全性方面的缺陷,找出可能的解决方法。众所周知,为了提高安全性,也为了有助于用户写出更加安全的代码,已经重新编写了许多函数(大约有 400 个左右)。

新版 CRT 的新增功能

首先,本文所介绍的新增加的 CRT 函数将出现在 Visual Studio 2005 中,但最终发行的版本可能与目前的版本有所不同。其次要指出的是,仅仅通过改变编译器的某个参数,新的库不会奇迹般地使得不安全的代码变为安全的代码,但肯定有助于增加代码的安全性。

更安全的可供选择的方法不会取代已有的功能。换言之, strcpy 仍将是 strcpy。它的更加安全的版本具有一个新名称,即 strcpy_s。但是,如果在编译时使用新的库,那么旧版本的函数将失效。所以需要说明的是,编译器会立即向您发出警告信息。也就是说,与修复安全性缺陷相比,改正编译器警告信息所指出的错误要更加容易。请认真考虑我就此问题提出的建议!

某些函数(比如,calloc)仅仅加强了检查参数的工作,其功能与以前的版本完全相同,所以不存在 calloc_s 函数。稍后将介绍有关 calloc 函数的更多信息。

我最赞同的更改是使用 strncat_s 函数代替了 strncat 函数。strncat 函数的问题在于它的最后一个参数不表示目标缓冲区的总的大小,它指示目标缓冲区中剩余的最小缓冲区的大小,以及需要复制的数目。这可能导致各种类型的 off-by-one(差 1)错误,甚至导致更严重的 off-by-lots(差多) 错误。请看下面的例子:

if (szURL != NULL) { 
  char   szTmp[MAX_PATH]; 
  char  *szExtSrc, *szExtDst; 

  strncpy(szTmp, szURL, MAX_PATH); 

  szExtSrc = strchr(szURL, '.'); 
  szExtDst = strchr(szTmp, '.'); 

  if(szExtDst) { 
    szExtDst[0] = 0; 

    if (fValid)  
      strncat(szTmp, szExtSrc, MAX_PATH);  
  }
}

调用 strncat 函数时出现错误-严重错误。实际上,这时将发生缓冲区溢出。无法将 MAX_PATH 字符串安全地复制到 szTemp 中,因为在调用 strncpy 函数时,已经将 szURL 添加至该字符串,这实际上减少了 szTmp 中剩余的空间。

以下是一个较为简单的例子:

char szTarget[12];
char *s = "Hello, World";

strncpy(szTarget,s,sizeof(szTarget));
strncat(szTarget,s,sizeof(szTarget));

如果在 Visual C++ 2003 中编译此程序,将出现一个错误,指示 szTarget 附近的数据已被破坏。这是因为编译器参数 /GS 在起作用。它检测到一个基于堆栈的缓冲区溢出,并中止了应用程序。

可以使用以下代码来解决这个问题:

char szTarget[12];
char *s = "Hello, World";

strncpy(szTarget,s,sizeof(szTarget));
strncat(szTarget,s,strlen(szTarget) - strlen(s));

但程序中仍然存在一个顽固的错误。如果目标缓冲区的长度正好等于源缓冲区的长度,那么许多 n 版本的函数不会使目标缓冲区以空字符结束,这使得 strlen(szTarget) 有可能返回一个大于目标缓冲区长度的值,因为没有末尾的“”字符。这样的话,程序会变得混乱不堪!

以下是一个以更加灵活的方式使用新运行库的程序:

char szTarget[12];
char *s = "Hello, World";

size_t cSource = strlen_s(s,20);
strncpy_s(temp,sizeof(szTarget),s,cSource);
strncat_s(temp,sizeof(szTarget),s,cSource);

其中的两个新增加的函数 strncpy_sstrncat_s 具有类似的特征:

它们都返回错误代码 (errno_t),而不返回指针。

目标缓冲区 (char *)。

目标缓冲区的总的字符计数 (size_t)。

源缓冲区 (const char *)。

源缓冲区总的字符计数 (size_t)。

记录两个缓冲区的计数,分别用于每个缓冲区。没有必要跟踪处于变化状态的目标缓冲区计数,虽然这一任务肯定较容易完成。此外还有其他引人入胜的特性。这两个函数都是以空字符来结束字符串,但以下功能是我特别看重的。请查看一下我在前面“发现安全漏洞”部分中所提供的代码示例:

void noOverflow(char *str)
{
  char buffer[10];
  strncpy(buffer,str,(sizeof(buffer)-1));
  buffer[(sizeof(buffer)-1)]=0;
  /* 上面两行代码用于避免缓冲区溢出 */
}

我在 2003 年 12 月发布的一篇文档中发现了这些代码,这篇文档来自一个大型的跨国软件公司(但不是 Microsoft),它用来向开发人员说明编写安全代码的优点。这段代码的问题是,它存在一个很明显的安全漏洞。如果 *str 指向 NULL,那么 strncpy 在复制 NULL 指针时将出现错误!在各种开放源代码的软件中所使用的 strlcat 存在同样的问题,但 strncat_s 不是这样。

strncat_s 不会出现错误的原因在于,所有更新的运行库函数都会对输入的参数执行更为严格的检查。以下是 strncat_s 函数中参数有效性验证部分的内容:

/* 验证部分 */
_VALIDATE_RETURN_ERRCODE(front != NULL, EINVAL);
_VALIDATE_RETURN_ERRCODE(sizeInTChars > 0, EINVAL);
_VALIDATE_RETURN_ERRCODE(back != NULL || count == 0, EINVAL);

验证宏语句为:

#define _VALIDATE_RETURN_ERRCODE( expr, errorcode /    
{                                                 /                         
      _ASSERTE( ( expr ) );                       /     
      if ( !( expr ) )                            /
      {                                            /
        errno = errorcode;                        /   
        _INVALID_PARAMETER(expr);                 /          
        return ( errorcode );                     /           
      }                                           / 
}

_INVALID_PARAMETER 用于在出错后进行的调试中提供文件的有关信息,以帮助用户调试代码。

在学校,老师总是教导我们要检查函数参数。现在,这项工作将由 CRT 来完成。实现这一飞跃仅仅用了二十年。

您应该清楚的一点是,strsafe 函数(比如,StringCchCopyStringCchCat)所执行的操作与 strncpy_sstrncat_s 函数是不同的。strncat_s 函数在检测到错误之后,会将字符串设置为 NULL。但是在默认情况下, strsafe 函数将向目标填充尽可能多的数据,然后以 NULL 结束此字符串。可以在 strsafe 函数中加入以下代码来模仿这一操作:

StringCchCatEx(dst,sizeof(dst)/sizeof(dst[0]),src,NULL,NULL,STRSAFE_NULL_ON_FAILURE)

其他用于操作缓冲区的函数也具有同样的行为,这些函数包括各种 printfscanf 函数、mbstowcsstrerror_strdate_strtimeasctime 以及 ctime 函数等。对于缓冲区操作函数以外的函数也进行了更新,这些函数包括 _makepath_splitpathgetenvrand 以及许多其他函数。

calloc 函数也是一个有趣的函数。如果 size * num 超出了 2^32,很容易导致出现整数溢出的错误,更新后的 calloc 函数验证计算是否未溢出:

/* 确保 (size * num) 未溢出 */
if (num > 0 && (_HEAP_MAXREQ / num) <= size)
{
   errno=ENOMEM;
   return NULL;
}

C++ 中的情况

Visual C++ 小组并未停止使用 C 运行库。对于标准模板库 (STL),有许多众所周知的事实。您知道使用迭代程序能够使缓冲区溢出吗?许多安全性方面的风险都与不恰当地使用了迭代程序有关,使用当迭代超出有效范围时中止运行(或出现异常)的迭代程序可以消除这一风险。下面是一个示例:

#include <vector>

vector<int> v(10);      // 向量大小为 10
v[20] = 10;             // 出现缓冲区溢出
vector<int>::iterator it = v.end();

// 超出界限后导致缓冲区溢出
++it;                   

#define _SECURE_SCL (1) 编译这段代码后,可以使所有的迭代程序进行检查范围的操作。

也可以不使用新添加的 #define,而使用新功能来达到相同的目的。例如,以下代码将不会导致溢出:

vector<int> v(10);      // 向量大小为 10
stdext::checked_iterator<vector<int> > ck_it(v, v.end());

// 超出界限后将中止程序
++ck_it;                

其他升级的类和方法有 operator[]vectorstringdequebitset classes 等), frontbackvectorqueuelist 类等)。此外还更新了各种算法,包括 copycopy_backward*_copytransformfillset_* 等等。

标准的变化

看到这里,您可能会想,标准有什么变化吗?C 和 C++ 不符合标准吗?不,它们已经标准化,并且 Microsoft 已将最新的草案提交给标准化委员会。可以转到 http://std.dkuug.dk/jtc1/sc22/wg14/www/docs/n1031.pdf 查看此草案。

小结

我确实很高兴使用新的运行库。虽然它不能显著提高代码的安全性,但它却是防止缓冲区溢出的又一个工具。在即将结束之际,我要感谢 Visual C++ 库开发小组出色地完成了这一任务,尤其要感谢 Martyn Lovell 对写作本文档提供的帮助。

“以往的 CRT 大势已去,愿新的 CRT 焕发生机!”

发现安全漏洞

许多人帮助我最后改正了错误。早在这篇文章之前,答案就已明确。所谓“安全的” strncpy 并不检查参数是否为 NULL,它能够导致您的应用程序死机或非法访问。

好,看一下本月的问题。这段代码有什么问题?

void ReadDataFromFile(char *szFilename,
LPOVERLAPPED_COMPLETION_ROUTINE func) {

   HANDLE hFile = CreateFile(szFilename,
                  FILE_ALL_ACCESS,
                  FILE_SHARE_READ,
                  NULL,
                  OPEN_EXISTING,
                  FILE_ATTRIBUTE_NORMAL |         
                  FILE_FLAG_OVERLAPPED,
                  NULL); 
   OVERLAPPED io;
   memset(&io,0,sizeof OVERLAPPED);
   DWORD dwWritten=0, dwRes=0;

   // 执行读取操作
   const size_t cBuff = 1024;
   char buff[cBuff];
   if (!ReadFileEx(hFile,buff,cBuff,&io,func)){
      // 哦!保证!
   }

   // 其他代码

}

Michael Howard 是 Microsoft Secure Engineering 小组的高级安全程序经理,是 Writing Secure Code 的作者之一,现在正在进行该书新版本的写作,他还是《Designing Secure Web-based Applications for Windows 2000》的主要作者。他的主要工作就是确保人们设计、构建、测试和介绍无缺陷的安全系统。他最喜欢的话是“尺有所短,寸有所长”。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值