利用VC++高效构建安全应用程序(英文)

Using Visual C++ Express to Build Secure Applications

 

Kenny Kerr
Microsoft Corporation

January 2005

Applies to:
   Microsoft Visual C++ 2005 Express Edition
   Microsoft .NET Framework

Summary: Review some of the new language and library features available in the Microsoft Visual C++ 2005 Express Edition that will help you to more efficiently produce secure and reliable code. (10 printed pages)

Contents

Introduction
Safety Features in the C Run-Time Library
Using the Standard C++ Library
Bounds Checking in the Standard C++ Library
Compiler Security Features
The New C++ Programming Language
Conclusion

Introduction

Microsoft Visual C++ 2005 Express Edition is the choice for hobbyist developers interested in writing safe and reliable applications quickly and easily. You heard right, the new language and library features in Visual C++ make developing safe and reliable applications easier than ever. It offers the power and flexibility of standard C++ as well as the most powerful language for .NET Framework programming.

In this article I explore some of the new language and library features available in the Visual C++ 2005 Express Edition that will help you to be more productive while producing secure and reliable code, whether for school projects or the next big thing!

Safety Features in the C Run-Time Library

If you are using Visual C++ to build applications using the C run-time library, you should take comfort in knowing that it now includes more secure versions of many of the library functions that you rely on. For functions that take one or more buffers as input, length arguments have been added to allow the functions to ensure that the buffer sizes are not exceeded. Many more functions now also check their arguments for validity, invoking the invalid parameter handler as necessary. Let's examine a few simple examples.

One of the most treacherous functions in the C run-time library is the gets function, which reads a line from standard input. Consider a simple example:

char buffer[10] = { 0 };
gets(buffer);

The first line declares the buffer variable and the initializer sets the characters in the buffer to zero. It is a good idea to initialize all variables to a well-known value to avoid surprises. Next, the innocent looking gets function reads a line from the standard input stream and writes it to the buffer. So what's wrong with this? C-style arrays are not passed by-value to functions. A pointer to the first element is passed instead. So the char[] is seen as a char* by the function and a raw pointer holds no additional information that could be used to determine the size of the buffer being pointed to. So what does gets do? It assumes the buffer is endless (UINT_MAX to be precise) and will simply continue copying characters from the input stream to the buffer. Attackers can easily exploit this vulnerability and this type of error is infamously known as a buffer overrun.

Many of the original C run-time library functions suffer from similar problems related to a lack of argument validation and are now deprecated. Keep in mind that these routines were written at a time when performance was at a premium, and we now live in a world where security takes precedence. Every deprecated function has been replaced by a corresponding function providing the same functionality but with safety and security features added. Of course, depending on how much existing code you have using the older library functions, it may take some time to migrate to the newer and safer alternatives. These new functions have an _s suffix. For example, the gets function is replaced by gets_s and the deprecated strcpy function is replaced by strcpy_s. Here is an example:

char buffer[10] = { 0 };

gets_s(buffer,
       sizeof (buffer) / sizeof (buffer[0]));

gets_s has an additional argument that indicates the maximum number of characters that can be written, including a null terminator. I use the sizeof operator, which is quite capable of determining the length of the array since the compiler determines the result of the sizeof operator at compile-time. Keep in mind that sizeof returns the size of its operand in bytes, so dividing this by the size of the first element in the array returns the number of elements in the array. This is a simple way of future-proofing the code in the event that it later needs to be ported to Unicode using the _getws_s function that expects the size of the buffer in characters.

As I mentioned, another common function that has received a safety check is the very familiar strcpy function. Like gets, it has no way of ensuring the available buffer size, so it just assumes that it is big enough to hold the source string. This can lead to unpredictable behavior at run-time and, as I mentioned, surprises are to be avoided for the sake of security and reliability. Here is an example using the safe strcpy_s function:

char source[] = "Hello world!";
char destination[20] = { 0 };

strcpy_s(destination,
         sizeof (destination) / sizeof (destination[0]),
         source);

There is a lot to like about the new strcpy_s function. The obvious difference is the additional argument that accepts the capacity of the destination buffer, measured in characters. This allows strcpy_s to perform run-time checks to ensure that characters aren't written beyond the end of the destination buffer. Other checks are also performed to make sure the function arguments are valid. In debug builds these checks include assertions that display debug reports if their conditions are not met. In both debug and release builds, if a particular condition is not met the invalid parameter handler is called, whose default behavior is to raise an access violation to stop the application. This is preferable to having your application continue with unpredictable results. Of course, this can be avoided by ensuring that functions like strcpy_s are not called with invalid parameters.

The previous example can be simplified even further using the new _countof macro. This macro removes the need for the error-prone use of the sizeof operator. _countof returns the number of elements in a C-style array. The macro itself resolves to a template that will fail to compile if passed a raw pointer. Here is an example:

strcpy_s(destination,
         _countof(destination),
         source);

Using the Standard C++ Library

Having looked at some of the new security enhancements in the C run-time library, let's take a look at how the Standard C++ Library can be used to further reduce the likelihood of errors in your code.

As you move from the C run-time library to the Standard C++ Library, one of the most effective ways that you can start benefiting from C++ is to use the library's vector class. Vector is one of the container classes in the Standard C++ Library and models a one-dimensional array of T, where T can be virtually any type. Many of the existing uses of buffers in your code can be replaced by vector objects. Let's consider the two examples from the previous section. In the first example we used the gets_s function to read a line from standard input. Consider this alternative:

std::vector<char> buffer(10);

gets_s(&buffer[0],
       buffer.size());

The most noticeable difference is that the buffer variable is now a vector object with useful methods and operators defined for it. The vector is initialized with an initial size of 10 characters and the constructor initializes all of them to zero. The expression &buffer[0] is used to get the address of the first element in the vector. This is the correct way of passing a vector to a C function expecting a simple buffer. Unlike the sizeof operator, all container measurements are based on elements, not bytes. For example, the vector's size method returns the number of elements in the container.

In the second example from the previous section, we used the strcpy_s function to copy the characters from the source to the destination buffer. It should be clear how the vector class can be used to replace raw C-style arrays, but rather than illustrating that, let's consider another extremely useful Standard C++ Library container.

The basic_string class is provided to enable the use of strings as normal types in C++. It provides various overloaded operators to provide a natural programming model for C++ programmers. Rather than using strcpy_s and the other string-manipulation routines, you should prefer to use basic_string. basic_string is a container of characters of type T, where T is a character type. The Standard C++ Library provides type definitions for common character types. string and wstring are defined for elements of type char and wchar_t, respectively. The following example illustrates just how simple and safe the basic_string class can be:

std::string source = "Hello world!";
std::string destination = source;

basic_string also provides the methods and operators you would expect for other common string operations, like concatenation and substring searching.

Finally, the Standard C++ Library also provides a very powerful I/O library to make interaction with standard input, output, and file streams simple and safe. Although using a vector with the gets_s function is better than using a C-style array, you can simplify this even further by using type definitions for the basic_istream and basic_ostream classes. You can write simple and type-safe code to read not only strings but virtually any type from a stream.

std::string word;
int number = 0;

std::cin >> word 
         >> number;

std::cout << word
          << std::endl
          << number
          << std::endl;

cin is defined as a basic_istream for extracting elements of type char from standard input. wcin is provided for wchar_t elements. cout, on the other hand, is defined as a basic_ostream and is used to write to standard output. As you can imagine, this model is infinitely more extensible than the gets_s and puts functions, but the real value here is that it is much harder to make a simple mistake that could lead to security flaws in your applications.

Bounds Checking in the Standard C++ Library

A number of Standard C++ Library containers and iterators do not provide range-checking by default. For example, the vector's subscript operator has traditionally been a fast, but potentially unsafe, method of accessing individual elements. If you were looking for checked access you could revert to the at method. With the added safety comes a performance penalty. Of course, the performance degradation is negligible most of the time, but for the most performance-critical code, this can be quite detrimental.

Consider the following simple functions:

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;
}

The PrintAll function uses the subscript operator, since the index is controlled by the function and is known to be safe. The PrintN function, on the other hand, does not ensure the validity of the index, so it employs the safer at method, instead. Of course, not all container access is this clear and concise.

Visual C++ 2005 goes to great lengths to maintain, and in many cases improve on, the performance characteristics of the Standard C++ Library, while providing features for tuning the safety features provided by the library. One welcome improvement is the addition of range-checking in debug builds. This does not affect your application performance for release builds, but it does make it possible for you to catch out-of-range errors during debugging, even in code that uses the traditionally unsafe subscript operator.

Unsafe functions, like vector's subscript operator, and others, like its front method, traditionally result in undefined behavior when called inappropriately. If you were lucky, it would quickly result in an access violation, which would crash your application. If you're not so lucky, it would continue on silently resulting in undesirable and unpredictable side effects that could corrupt data and be exploited by attackers. To protect release builds of your applications, Visual C++ 2005 introduces the _SECURE_SCL symbol that can be used to add runtime checks to these unsafe functions. Simply define this symbol as follows in your project to add extra runtime checks and prevent undefined behavior:

#define _SECURE_SCL 1

Keep in mind that defining this symbol has quite an impact on your code. A number of legal but potentially unsafe operations are designed to fail at compile-time to avoid potential bugs at runtime. Consider the following example using the copy algorithm:

std::copy(first,
          last,
          destination);

first and last are input iterators defining the range of elements to copy. destination is an output iterator indicating the position in the destination buffer to copy the first element in the range. The danger here is that the buffer or container that destination refers to may not be big enough to hold all the elements being copied. If destination is a checked iterator, such an error will be caught. If, however, destination is a simple pointer, there will be no way for the copy algorithm to ensure the correctness of the operation. This is exactly the type of scenario that the _SECURE_SCL symbol avoids. In this case, the code will not even compile, avoiding any possible error at runtime. As you can imagine, this would require perfectly valid code to be rewritten. So there's another good reason to avoid C-style arrays in favor of Standard C++ Library containers.

Compiler Security Features

Although not entirely new to Visual C++ 2005, a number of compiler security features are worth knowing about. A notable difference from previous versions is that the compiler security checks are now on by default. Let's explore some of the compiler features and how they help to prevent exploits in some cases.

The Visual C++ compiler has long offered options for controlling rigorous run-time checks, including stack verification, underflow and overflow checking, and the identification of variables that are used without being initialized. These run-time checks are controlled by the /RTC compiler option. Although useful for catching errors early in the development cycle, the performance penalty is not acceptable for release builds. Microsoft Visual C++ .NET introduced the /GS compiler switch that added a limited version of this run-time checking for release builds. The /GS switch inserts code in the compiled code to detect some common stack-based buffer overruns by checking a function's stack data has not been overrun. If corruption is detected the application is halted. To mitigate the performance impact of these run-time checks, the compiler determines which functions are vulnerable to attack and only adds security checks to those functions. The security check involves adding a cookie to the function's stack frame, which would be overwritten by a buffer overrun. Assembler instructions are added before and after a function's instructions. A function cookie that is derived from the module cookie is computed before the function executes. When the function completes, but before the stack space is reclaimed, the stack's copy of the cookie is retrieved to determine whether it has changed. If the cookie is unchanged, the function call ends and execution continues through the application. If the cookie was changed, however, the security error handler is called, which then terminates the application.

To control these compiler options from within Visual C++ 2005 Express Edition, open the project's Property Pages and click on the C/C++ folder. On the Code Generation property page you will find two properties that correspond to the features I just described. The Basic Runtime Checks property corresponds to the development time /RTC compiler option, and should be set to Both in debug builds. The Buffer Security Check property corresponds to the /GS compiler option, and should be set to Yes for release builds.

The good news for developers using Visual C++ 2005 is that these compiler features are turned on by default. This means that you can rest assured that the compiler is doing all it can possibly do to prevent exploits in your code. This does not, however, mean that we can simply forget about security. Programmers need to continue to strive for correct code and consider different security threats that can occur. The compiler can only prevent certain types of errors.

Keep in mind that these particular security checks that are provided by the compiler only apply to native code. Fortunately, managed code is far less susceptible to these types of errors. There's even more good news: Visual C++ 2005 introduces the C++/CLI language design, which provides the most powerful language for .NET Framework programming.

The New C++ Programming Language

Visual C++ 2005 Express Edition provides the first implementation of the C++/CLI language design. C++/CLI is the systems programming language for .NET, providing more control over the creation and consumption of .NET modules and assemblies than any other language. C++/CLI is also far more elegant and natural for C++ programmers. Whether you are new to C++ or the .NET Framework, you will find that writing managed code in C++ is a natural and elegant extension to ANSI C++ and will be easily learnt.

There are many compelling reasons to prefer managed code over native C++ for developing applications. Two of the most significant are safety and productivity. The Common Language Runtime (CLR) provides a safe environment for running your code. As a programmer, you need not concern yourself (as much) with things like buffer overruns, stack corruption, and undefined behavior due to variables you failed to initialize before use. Security threats do not go away entirely, but many common errors can be avoided simply by using managed code.

The other compelling reason to prefer managed code is the rich .NET Framework class library. Although the Standard C++ Library is more suited to the common C++ programming style, the .NET Framework includes a formidable library of functionality that cannot be matched by the Standard C++ Library. The .NET Framework includes a slew of useful collection classes, a powerful data access library, classes implementing many popular communication protocols from sockets to HTTP and Web Services and many more. Although all these services are available to the native C++ programmer in various forms, the productivity gained by using the .NET Framework is largely due to its consistency and unified type system. Whether you are using the System::Net::Sockets namespace or the System::Web namespace, you will come across the same types for representing widely applicable concepts like streams and strings. This is a major factor in the productivity attributed to the .NET Framework, and allows programmers to write more powerful applications rapidly while producing more reliable code in general.

Visual C++ 2005 naturally allows you to mix native and managed code in a single project. You can continue to use your existing native functions and classes while starting to use more and more of the .NET Framework class library, or even write your own managed types. You can define your managed type as either a reference type or a value type. Value types are allocated on the stack whereas reference types are allocated on the CLR's managed heap, although the Visual C++ compiler allows you to choose whether to employ stack semantics for convenience or to control resource management using traditional scoping rules.

A reference type is defined by adding ref to your class or struct definition to form a spaced keyword. Acquiring and releasing resources is done in the usual manner, by employing constructors and a destructor as illustrated here:

ref class Connection
{
public:

    Connection(String^ server)
    {
        Server = server;

        Console::WriteLine("Aquiring connection to server.");
    }

    ~Connection()
    {
        Console::WriteLine("Disconnecting from server.");
    }

    property String^ Server;
};

The compiler takes care of implementing the IDisposable interface for the Connection reference type, so that programmers using languages like C# or Visual Basic .NET can employ whatever resource management constructs are available to them. For the C++ programmer, the choices are the same as they have always been. To simplify resource management and write exception safe code, you can simply declare the Connection object on the stack. The destructor, implementing the Dispose method, will be called when the object goes out of scope. Here is an example of this:

void UseStackConnection()
{
    Connection connection("sample.kennyandkarin.com");

    Console::WriteLine("Connection to {0} established!",
                       connection.Server);
}

In this example the connection is "closed" by calling the destructor before the function returns to the caller, just as you would expect in C++. If you wish to control the lifetime of the object yourself, simply use the gcnew keyword to get a handle to a Connection object. The handle can be treated much like a traditional pointer (without many of the pitfalls) and the object destructor can be called simply by using the delete operator. Here is an example of this:

void UseHeapConnection()
{
    Connection^ connection = gcnew Connection("sample.kennyandkarin.com");

    try
    {
        Console::WriteLine("Connection to {0} established!",
                           connection->Server);
    }
    finally
    {
        delete connection;
    }
}

As you can see, Visual C++ 2005 brings the simplicity and flexibility of resource management in native C++ to managed code. Being able to write robust resource management code is critical to writing correct, exception-safe, and secure code.

Conclusion

Visual C++ 2005 Express Edition is a powerful development tool for building both small and large applications. The C run-time library and the Standard C++ Library provide a powerful toolset for delivering powerful and robust native applications, and with the first-class support for writing managed code in C++, Visual C++ 2005 is unmatched as the powerhouse for developing applications of any type for the Microsoft Windows platform.

For more information on using the .NET Framework with Visual C++ 2005, check out my article titled C++/CLI: The Most Powerful Language for .NET Framework Programming.

 


About the author

Kenny Kerr spends most of his time designing and building distributed applications for the Microsoft Windows platform. He also has a particular passion for C++ and security programming. Reach Kenny at http://weblogs.asp.net/kennykerr/ or visit his Web site: http://www.kennyandkarin.com/Kenny/.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值