用Visual C++ 2005 Express Edition构建安全代码

 
用Visual C++ 2005 Express Edition 构建安全代码
 
 
本文内容:
C运行时库的新安全特性
使用标准C++库
标准C++库的边界检查
编译器安全特性
新的C++编程语言
 
 
引言
       对编程爱好者来说,想要简单快速地生成安全可靠的程序,如今又多了一个新的选择,那就是微软公司刚刚发布不久的Visual C++ 2005 Express Edition,它是Microsoft Visual C++ 2005系列中最初级的版本,我们可以把它看作是个人版,可以从微软的官方网站免费下载使用。在Visual C++ 2005中,新的语言和新的库特性,可使编写安全可靠的程序变得比以往更加容易,同时,它也提供了标准C++语言强大的功能和高度的可伸缩性,而且,对 .NET Framework编程而言,它可能是最强大的语言了。
 
 
C 运行时库的安全特性
       如果正在使用C运行时库构建应用程序,那么应感到欣慰的是,Visual C++现在已经包含了大多数库函数更安全的版本。那些参数中有缓冲区的函数,现在已加入了长度参数,以确保数据不会超出缓冲区,同时也有更多的函数现在可以检查参数的有效性了,以保证在必要时,可调用非法参数处理程序。
       在C运行时库中,最靠不住的函数就是gets了,它用于从标准输入中读取一行,请看下列代码:
 
char buffer[10] = { 0 };
gets(buffer);
 
       第一行声明了缓冲区变量,并把缓冲区内的字符初始化为零;在声明变量时就把变量初始化为一个确定的值,这是一个非常好的习惯。接着,gets函数从标准输入流中读取一行,并存入到缓冲区中。那么错在哪里呢?C风格的数组在传递给函数时不是传值,而是传递了指向第一个数组元素的指针。因此,函数把char[]当成了char*,从而指针中就不再包含用于确认缓冲区大小的信息。那么gets将怎样做呢?它假定缓冲区为无穷大(实际上精确到UINT_MAX),只是简单地从输入流中一直把读取的数值复制到缓冲区中。攻击者很容易利用这一点,现在,这种类型的错误已经是一个“臭名昭著”的缓冲区溢出问题了。
       大多数的最初C运行时库函数都受类似问题的影响,原因是缺乏对参数的验证。而现在是一个“安全第一”的世界,所有此类的函数都应该被提供了更好安全性的相同函数所取代,当然,这要基于现有的代码在多大程度上使用了老式的库函数,可能要花上一点时间来移植到新的安全版本。这些新的函数有一个 _s 后缀,例如,gets函数被gets_s取代,而strcpy被strcpy_s取代,如下例:
 
char buffer[10] = { 0 };
gets_s(buffer, sizeof (buffer) / sizeof (buffer[0]));
 
       gets_s函数新增了一个参数,用于指示最大可写入的字符数(包含了一个空值结束符)。这儿使用的sizeof操作符,可使编译器在编译时确定数组的长度,注意,sizeof返回的是操作数的字节数,必须要除以数组中的第一个元素,从而得到数组中的元素个数。如果将来要移植到Unicode上,可使用以字符计量缓冲区大小的_getws_s函数。
       前面还提到,另一个需要安全检查的常用函数是非常类似的strcpy,像gets函数一样,它不能确定可用的缓冲区大小,只能假定有足够大的空间可容纳源字符串,这将在运行时导致不可预知的行为,下面是使用安全的strcpy_s函数的例子:
 
char source[] = "Hello world!";
char destination[20] = { 0 };
strcpy_s(destination, sizeof (destination) / sizeof (destination[0]), source);
 
       以上是调用新的strcpy_s函数时的大体样子,最明显的不同之处是新增的参数,它以字符计量来确定目的缓冲区的大小。这允许strcpy_s函数进行运行时检查,以保证字符不会超出目的缓冲区。另外,还有其他类型的检查可用于保证函数参数的有效性,在以debug构建程序时,有断言(assertion)检查,当条件未满足时,它会显示调试报告。而在debug和release构建时,如果一个特定的条件未满足,将会调用非法参数处理程序,默认情况下,会产生一个访问违例并终止当前程序。这样可保证程序不会有不可预知的行为,当然,只要确保strcpy_s之类的函数在调用时没有非法函数,就可防止此类问题的发生。
 
       通过使用新的_countof宏,可简化前一个例子,还能防止sizeof操作符的误用。宏_countof返回C风格数组中的元素数,但如果传递给它一个原始指针,将不能通过编译。
 
strcpy_s(destination, _countof(destination), source);
 
 
使用标准C++
       看过C运行时库的新增安全部分之后,让我们来看一下标准C++库是怎样有助于减少代码中错误产生的可能性的。
       从C运行时库转到标准C++库时,其中最有效的一个方法是使用库的vector类。Vector是标准C++库中的一个容器类,模拟了一个一维的T数组,而T可能是任何类型。大多数代码中使用缓冲区的地方,都能由vector类取而代之。让我们再看一下前一节两个例子,在第一个例子中,使用了gets_s函数来从标准输入中读取一行,请看下面的替代方法:
 
std::vector<char>buffer(10);
gets_s(&buffer[0], buffer.size());
 
       最大的不同之处是缓冲区变量现已是一个vector对象,vector对象初始大小为10个字符,并由其构造函数初始化为零;表达式&buffer[0]取vector对象中第一个元素的地址,这是传递vector给一个有缓冲区参数的C函数的正确方法。与sizeof操作符不同的是,所有的容器类计量都采用的是元素,而不是字节。例如,vector的size方法就返回容器类中的元素个数。
       前一节的第二个例子中,使用了strcpy_s函数把字符从源地址复制到目的地址,而vextor类也能被用于取代其中的C风格数组,在演示之前,先来看一下另一个非常有用的标准C++库容器类。
       Basic_string类可在C++中像使用普通类型那样来使用字符串,它也提供了多种重载操作符,以便为C++程序员提供一种更自然的编程模式。相对strcpy_s和其他字符串操作函数而言,更应该使用basic_string类;basic_string是一种类型T的字符容器类,只不过此处的T是字符类型。标准C++库为常用的字符类型都提供了类型定义,string和wstring是分别对应于char和wchar_t元素类型定义,下面的例子演示了basic_string类是多么的简单和安全:
 
std::string source = "Hello world!";
std::string destination = source;
 
       basic_string同时还提供了可用于其他常用字符串操作的方法和操作符,如字符串拼接和在字符串中进行字链查找。
       最后,标准C++库也提供了一个非常强大的I/O库,可用于与标准输入、标准输出和文件流的交互,虽然简单但是安全。尽管gets_s函数使用一个vector比使用C风格数组更好,但通过使用对类basic_istream和basic_ostream的类型定义,甚至能更进一步简化,这样写出的代码,不但简单,而且类型安全;不但能读取字符串,还能从流中读取任何类型的数据。
 
std::string word;
int number = 0;
 
std::cin >> word >> number;
std::cout << word << std::endl << number << std::endl;
 
       cin被定义为basic_istream,能从标准输入中读取键入的字符,而wcin是为wchar_t类型准备的。另一方面,cout被定义为basic_ostream,可用来写入到标准输出中。正如你所想的,这种模式可以无限扩展,而不只限于gets_s和puts函数,但在此的真正价值是,如今程序中想要有一丁点安全缺陷都很难喔。
 
 
       标准C++ 库的边界检查
       部分标准C++库的容器类与iterator在默认状态下并不提供边界检查,例如,vector的下标操作符在传统意义上来说,是一种快速、但有潜在不安全因素的访问个体元素的方法,如果想检查访问的安全性,可使用at方法。也许新增的安全性检查可能会导致性能降低,但在大多数时候,性能的损失是可以忽略不计的。请看如下的示例函数:
 
void PrintAll(const std::vector<int>&numbers)
{
    for (size_t index = 0; index < numbers.size(); ++index)
    {
        std::cout << numbers[index] << std::endl;
    }
}
 
void PrintN(const std::vector<int>& numbers, size_t index)
{
    std::cout << numbers.at(index) << std::endl;
}
 
       PrintAll函数使用下标操作符,而且函数控制的index在已知状态下是安全的。另一方面,PrintN函数没有保证index的有效性,但它使用了更安全的方法。当然,并不是所有的容器类存取都这么清晰明了。
       在Visual C++ 2005中,即便有了库提供的安全特性,但在保持标准C++库性能方面,也有长足的进步。其中最受欢迎的一个改进之处是,在debug构建时新增的边界检查,而且在release构建时,不会影响到程序性能。有了它,可使程序员捕捉到调试期间的越界错误,甚至是那些使用了传统非安全下标操作符的老代码。
       非安全的函数,如vector的下标操作符(还有其他,如它的front方法),如果不适当地使用,将导致不可预知的行为。幸运的话,它直接产生一个访问违例,导致程序崩溃;不幸运的话,程序将会继续运行,但会悄无声息地导致不可预知的后果,从而毁坏数据或被黑客利用。为保护程序的release构建,Visual C++ 2005引入_SECURE_SCL符号,可对非安全的函数加入运行时检查。如下所示在工程中简单地定义此符号,就可加入运行时检查,从而防止不可预知的程序行为。
 
#define _SECURE_SCL 1
 
       需要记住的是,定义此符号将会对代码有所影响,一些合法但有潜在不安全的动作,原本设计是在编译时导致错误,从而防止运行时的潜在bug,它们可能会受到此符号定义的影响。请看下面的示例:
 
std::copy(first, last, destination);
 
       first和last是定义为复制元素范围的输入iterator,destination是输出iterator,用于指示存放复制的第一个元素的目的缓冲区位置。此处的危险是,目的缓冲区或容器类可能没有足够的空间可以存放所有需复制的元素。如果destination是一个受检查的iterator,就可捕捉到这样的错误。但如果destination只是一个简单的指针,就没法保证复制算法的正确性了,而这正是_SECURE_SCL符号可派上用场的地方。在这种情况下,为避免运行时可能发生的错误,代码甚至不会通过编译。正如你所看到的,要在编程中防止此错误,通常需要编写完美且经过校验的代码,那么,这又是一个避免使用C风格数组,而多使用标准C++库容器类的极好理由。
 
 
       编译器安全特性
       虽然这对Visual C++ 2005来说,不是一个全新的概念,但多了解一点还是更好。Visual C++ 2005和前一个版本的最大不同之处在于,编译器安全检查选项,现在是默认打开的。下面来了解一下它们是如何有助于防止漏洞产生的。
       Visual C++编译器提供了一系列的选项,可进行严格的运行时检查,包括堆栈确认、上溢和下溢检查、对未初始化变量的识别等等,这些由选项/RTC来控制。虽然在开发阶段的早期,这有助于捕捉错误,但在release构建时,由此带来的性能损失却是不可接受的。
Microsoft Visual C++ .NET引入了/GS编译器选项,对release构建而言,它只是加入了一小点限制版本的运行时检查。选项/GS在编译后的代码中插入了一点代码,通过检查函数堆栈数据是否损坏,来探测一些基于堆栈的缓冲区溢出;如果探测到数据损坏,那么程序将异常终止。为了减轻这些运行时检查带来的性能损失,由编译器来决定哪此函数易受攻击,并只对这些函数加入安全检查。此类的安全检查,通常是在函数的堆栈上加入一个cookie,如果缓冲区溢出,那么此cookie将被改写,而此汇编指令一般会加在函数指令的前方或后方。函数cookie取自模块cookie,其是在函数执行之前计算得到的。当函数执行完毕,但在堆栈空间回收之前,将取回cookie的一个堆栈副本,并判断其是否改变。如果cookie没变,那么函数结束,程序中的指令继续执行;如果cookie改变了,将调用安全错误处理程序,并由它来终止主程序。
       要在Visual C++ 2005 Express Edition中控制这些编译器选项,请打开工程属性页,并单击C/C++项,在代码生成(Code Generation)属性页中,可找到此前叙述的两个属性。属性Basic Runtime Checks对应于选项/RTC,在debug构建时,应设置为Both;属性Buffer Security Check对应于选项/GS,在release构建时,应设为Yes。
       对使用Visual C++ 2005的开发者来说,这些编译器选项默认是打开的,这意味着不花什么力气,就可防止代码中的漏洞,但这并不是说,我们可以完全不用顾及安全问题了,一个合格的程序员通常为求正确而不断努力,并考虑可能遇到的各种各样的安全威胁;而编译器只能防止特定类型的错误。
       要记住的是,这些由编译器提供的特殊安全检查,只针对本地代码。幸运的是,托管代码不易导致此类错误影响,还有更好的消息:Visual C++ 2005中引入了C++/CLI,其是进行 .NET Framework编程最为强大的语言。
 
 
       新的C++ 编程语言
       Visual C++ 2005 Express Edition带来了C++/CLI语言设计的第一次实现,C++/CLI是为 .NET准备的系统级编译语言,在创建及使用 .NET模块和程序集方面,比其他语言提供了更多的控制;而对C++程序员来说,C++/CLI则显得更加优雅和自然。不管是C++或 .NET Framework编程的新手,都会发现,原来用C++编写托管代码,只是ANSI C++一个自然、优雅的扩展,而且很容易学会。
       还有一些其他的理由,驱使你在编写程序时,倾向于使用构建在本地C++之上的托管代码,其中最重要的两个理由就是 安全高效。通用语言运行时(CLR)提供了运行代码的一个安全环境,作为一个程序员,你就不需要考虑因为那些未初始化的变量,而导致的缓冲区溢出、堆栈损坏、未定义的行为等等。安全威胁当然不会就此消失,但许多常见的错误却可以通过使用托管代码来避免。
       另一个使用托管代码的理由,是丰富的 .NET Framework类库。尽管标准C++库更适合于常见的C++编程,但 .NET Framework包含了一系列功能强大的库,是标准C++库所不能比拟的。.NET Framework包含了大量的容器类、一此功能强大的数据访问类、和实现了从socket到HTTP再到Web Service的大多数流行通讯协议的类。尽管这些库对本地C++程序员来说,在多种形式上,都是可用的,但使用 .NET Framework,效率的提高却是显著的,这要归功于它的一致性和统一类型系统。不管你是使用System::Net::Sockets或System::Web命名空间,都可以用同一种类型来表示普遍可适用的概念,如流和字符串。这是 .NET Framework编程高效的一个主要因素,程序员一方面可生成更安全可靠的代码,另一方面可快速地编写功能强大的应用程序。
 
       Visual C++ 2005允许在单一工程中,自然地混合本地和托管代码。当你越来越多地使用 .NET Framework类库,或者编写自己的托管类时,也可以继续使用现有的本地代码函数和类;可以自由地把托管类型定义成一个引用类型或一个值类型,一般来说,值类型分配在堆栈上,而引用类型被分配在CLR的托管堆上。
       通过在类或结构定义之前加上关键字ref,可定义一个引用类型,取得或释放资源还是以普通的方式进行,使用如下所示的构造和析构函数:
 
ref class Connection
{
public:
 
    Connection(String^ server)
    {
        Server = server;
        Console::WriteLine("Aquiring connection to server.");
    }
 
    ~Connection()
    {
        Console::WriteLine("Disconnecting from server.");
    }
 
    property String^ Server;
};
 
       编译器负责对Connection引用类型IDisposable接口的实现,因此,程序员使用如C#、Visual Basic .NET之类的语言也能存取任何可用的资源。为简化资源管理和编写安全代码,可简单地在堆栈上声明Connection的对象,而实现了Dispose方法的析构函数,在对象超出范围时,将会被调用。以下是示例代码:
 
void UseStackConnection()
{
    Connection connection("sample.kennyandkarin.com");
    Console::WriteLine("Connection to {0} established!", connection.Server);
}
 
       上例中,函数返回之前,析构函数将会调用,以关闭对象connection,就像在C++中所做的一样。如果想自己控制对象的生存期,使用关键字gcnew就可以得到Connection的一个句柄,此句柄很像传统意义上的指针(但没有指针的那些缺陷),如果要调用析构函数,只需简单地使用delete操作符就可以了。以下是示例:
 
void UseHeapConnection()
{
    Connection^ connection = gcnew Connection("sample.kennyandkarin.com");
 
    try
    {
        Console::WriteLine("Connection to {0} established!", connection->Server);
    }
    finally
    {
        delete connection;
    }
}
 
       正如以上所叙述的,从本地C++代码到托管代码,Visual C++ 2005带来的是资源管理上的简单性和可伸缩性,而具有稳健资源管理的代码,是生成正确无误、安全高效的应用程序的关键。
 
 
 
       不论是构建小型程序或是大型应用,Visual C++ 2005 Express Edition都是功能强大的开发工具——更重要的是它是免费的;其附带的C运行时库和标准C++库都提供了一系列强大的工具集,并且还提供了以C++编写托管代码的第一类支持,毫无疑问,在Microsoft Windows平台上开发任何类型的应用程序,Visual C++ 2005绝对是无可匹敌的一员阵前大将!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值