Security Features in MSVC

Andrew Pardoe

Shareable link: https://aka.ms/msvcsecurity 点这里看中文版

Every developer makes mistakes. No matter how careful you are when writing code, you will introduce bugs. And any bug can become a security vulnerability when software that runs in a connected environment or is used long past its initially planned lifespan. Code that isn’t correct is insecure code.

The Microsoft Visual C++ toolset offers numerous features that help you write secure, correct code from before you start typing code until after you’ve shipped it to your users.

For more information on specific security features in the MSVC toolset, make sure you review Security Best Practices for C++.

Before you write any code

Secure code starts before you write your first line of code. The compiler toolset can’t show you design defects that might lead to security exploits, but there are many resources in print and online that will help you think about potential exploits and how to design your code securely. For example, almost everyone who’s been at Microsoft for a while has read Michael Howard and David LeBlanc’s Writing Secure Code.

When you start writing code it’s important that you use modern C++ constructs to manage and access resources. One of the best resources available is the C++ Core Guidelines, a set of tried-and-true guidelines, rules, and best practices about coding in C++. Coding practices recommended in the C++ Core Guidelines help you write simpler, more modern, software. In doing so, you’ll avoid common pitfalls such as integer overflow or buffer overruns, making your code more secure. And many of the C++ Core Guidelines are enforceable with a static analysis code tool that’s included with Visual C++.

When you’re writing code

What can you do to help yourself when you’re writing code? First, get all the value you can from built-in compiler diagnostics by setting your warning levels properly. Run code analysis after you build to let the compiler toolset dive into a deeper analysis of your code. And don’t forget to do regular code reviews with your team!

Compiler Warnings

One of the most frequently-used security features is compiler warnings. The MSVC compiler provides many switches that allow you to control which warnings you will see in your code and whether they are kept as informational messages or cause your compile to fail.

Some compiler warnings are kept off-by-default because they are emitted too frequently in legacy code and most users don’t want to see them. But many of these warnings indicate real bugs in your program. For example, your code may have a valid reason to compare an unsigned value to a negative number but it could also be a bug. By enabling the off-by-default warnings you can catch potential errors.

To learn more about how you can adjust build settings to allow the compiler to find as many bugs in your code as possible, see the documentation on the compiler options warning level.

Static Code Analysis Security Features

We write frequently about C++ Code Analysis on this blog. We also keep you updated about the CppCoreCheck extension that checks your code for rules derived from the C++ Core Guidelines. But did you know that Microsoft has long considered PREfast, the engine at the core of our Code Analysis, a security tool? The tool was originally developed by a team that focused on software excellence and was later owned by the Secure Development Lifecycle team before making its way to the C++ team to be included with all versions of Visual Studio.

We now have a number of Code Analysis tools built upon the PREfast engine, including our base set of /analyze rules, the ESPC Concurrency Checker (pdf) and the CppCoreCheckers. We are also looking for ways to help you integrate Code Analysis more deeply into your daily development routine.

As the name implies, code analysis does a deeper analysis of your code to find possible errors. While compiler detects many potential errors in your code, code analysis looks through an entire function to determine whether there are some code paths that could result in an error. We call this kind of analysis a “path-sensitive” analysis.

While the compiler can do a lot of path-sensitive analysis, there are many cases it can’t identify. For example, compiling this code with all compiler warnings turned on (/Wall) and analysis (/analyze) shows that the compiler can only find one of three potential bugs:

void one()
{
    int a[4];
    a[4] = 1; // Buffer overrun, stack overflow
}

void two(int *p)
{
   bool isnull = false;
   if (p == nullptr)
      isnull = true;
   *p = 1;   // Null pointer dereference
}

int three(bool b)  
{  
   int i;  
   if (b)  
      i = 0;  
   return i; // i is unintialized if b is false  
}
C:\tmp>cl /c example.cpp /Wall /analyze
Microsoft (R) C/C++ Optimizing Compiler Version 19.10.25019 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

example.cpp
c:\tmp\example.cpp(4) : warning C6201: Index '4' is out of valid index range '0' to '3' for possibly stack allocated buffer 'a'.
c:\tmp\example.cpp(4) : warning C6386: Buffer overrun while writing to 'a':  the writable size is '16' bytes, but '20' bytes might be written.: Lines: 3, 4
c:\tmp\example.cpp(12) : warning C6011: Dereferencing NULL pointer 'p'. : Lines: 9, 10, 11, 12
c:\tmp\example.cpp(22) : warning C6001: Using uninitialized memory 'i'.: Lines: 17, 18, 22
c:\tmp\example.cpp(4) : warning C4789: buffer 'a' of size 16 bytes will be overrun; 4 bytes will be written starting at offset 16
c:\tmp\example.cpp(22) : warning C4701: potentially uninitialized local variable 'i' used

There’s an error in each function in the source above. The compiler misses one of these three errors and indirectly attributes another. As the parse information available to the compiler improves, the analysis in the compiler will improve and you’ll see more instances where diagnostics can be duplicated between tools.

  1. In function one code analysis tells us that we’re using the bounds of the array as an index, causing C6201. Both code analysis and the compiler pick up on the memory corruption, the former emitting C6386, the latter, C4789.
  2. In function two we dereference of a null pointer on one code path, causing C6011. The compiler misses this error entirely.
  3. The compiler and code analysis both pick up on the error in function three. The compiler issues off-by-default warning C4701; code analysis, C6001.

Code analysis also excels at finding code that doesn’t do what you may think it does. For example, warning C6268 finds code that contains an incorrect order of operations and suggests using parentheses to make the order clear. The example given in the documentation contains a potential buffer overrun.

You can read more about using C++ Code Analysis both inside VS and from the command line on the Microsoft Docs site.

Code Reviews

Code reviews can seem like a lot of overhead but they save time in the long run. Some teams do one-on-one code reviews, others send out all changes to a group of reviews, some bring the team together every Friday to look over all the week’s changes. It doesn’t matter how you do code reviews. They will turn out to be one of the most valuable techniques you can use to improve the quality of your code. Find a process that works for your team and use it.

Additional Security Checks

The /sdl compiler switch enables additional warnings focused on security issues as defined by the Microsoft Secure Development Lifecycle process. The /sdl switch is in many ways an expansion of off-by-default warning C4701 and is thus off by default.

CRT Secure Function Overloads

Security wasn’t an important design point for the C library—normally code was written and run inside of an organization instead of being exposed to a worldwide network of computers. The C “string” has no metadata associated with it that records its length. For example, functions that deal with strings, such as strcpy, must assume that the buffers supplied as parameters are an appropriate size for the requested operation. Several memory operations have similar limitations.

Over ten years ago Microsoft introduced a set of overloads to these functions that validate their parameters for security. If you are still using C-style functions you should consider moving to C++, which offers more safety in its objects and abstractions. If you can’t use C++, at least use the secure versions of the C runtime functions.

(NB: When we introduced this feature we incorrectly called the insecure C functions “deprecated”. This only means that Microsoft does not recommend use of the insecure functions, instead recommending you use the secure overloads. We’re aware that the term “deprecated” was used incorrectly.)

When you’re testing your code

The compiler toolset offers many options that help you when you’re testing your code. Most of these switches aren’t intended to be shipped with your final, retail builds of your program. They’re turned on for debug builds—either by default or opt-in—so that you can find more bugs during your testing.

CRT Debug Heap

The CRT Debug Heap is enabled when you compile your program in debug (non-release) mode. It finds common heap memory errors, including buffer overruns and leaks. The CRT Debug Heap will assert when it encounters any heap memory errors as you test your code. Various debug routines are enabled when you define the _DEBUG flag.

Runtime Checks

The CRT provides Runtime Checks enabled through use of the /RTC switch. These checks find real logic errors in your program such as data loss, initialization issues, and stack frame checking. Runtime Checks are only intended for when you are active testing your debug builds and are incompatible with optimizations. Because of these limitations they are off by default.

Checked Iterators

Checked iterators help ensure that your code doesn’t accidentally overwrite the bounds of iterable containers in your code. They can be used both in debug code (as debug iterators) and in release code (as checked iterators.)

After your code is compiled

The Windows team provides tools that help validate that your compiled binaries are secure. You can find these tools in the Debugging Tools for Windows and the Windows SDK.

GFlags and PageHeap

The GFlags and PageHeap tools enable heap allocation monitoring for Windows. When using these tools, Windows will reserve memory at the boundary of every allocation that allows it to detect memory accesses outside of the allocated memory.

Application Verifier

Application Verifier is a dynamic verification tool that subjects your binary to a number of stresses and tests as you exercise the code and generates a report of potential vulnerabilities.

Runtime protection for released code

The MSVC code generator and linker provide several security features that continue to offer protection long after you’ve built and deployed your code. Because the code generator can see all of your code at once–as opposed to just one source file at a time–it can often detect errors and vulnerabilities that can’t be found in an individual source file. And the code generator and linker work with the OS loader and runtime to provide even more security when your binary is loaded and executed in Windows.

Buffer Security Check

One of the oldest security features in the code generator is the Buffer Security Check, enabled by the /GS switch to the compiler. This feature is on-by-default as it protects against one of the most common security exploits. It creates a “security cookie” in functions that the compiler detects are vulnerable to buffer overruns. If an attacker writes past the end of the buffer over a return address, the address of an exception handler, or a vulnerable function parameter they will overwrite the security cookie. The runtime will check the integrity of the cookie before allowing execution to jump to this address or before returning these parameters.

Safe Exception Handlers

Safe Exception Handlers are another long-standing security feature. This feature is on-by-default but only applies to code generated for the x86 platform. When it is enabled, the linker will only produce an image if it can create a static table of the image’s safe exception handlers. This prevents an attacker from overwriting the target of exception handling control flow.

Dynamic Base and Address Space Layout Randomization

Address Space Layout Randomization (ASLR) is a technique that makes it harder for an attacker to predict target addresses for their attacks. When ASLR is enabled on your binary image the OS loader will load the image at a hard-to-predict base address. The /DYNAMICBASE switch to the linker, which is on by default, enables the image to use ASLR.

Data Execution Prevention

One common technique for attackers is to use data as executable code. Executing data that has been specially formatted as machine code is a powerful technique used by many languages, like .NET languages or JavaScript, in their Just-In-Time (JIT) compilers. But a C++ program shouldn’t normally need to execute data. Windows allows data sections to be marked as non-executable using a technique called Data Execution Protection (DEP). The /NXCOMPAT linker switch, on by default, specifies whether an image is compatible with DEP.

Control Flow Guard

In 2014 we announced an exciting new security feature called Control Flow Guard. The /guard:cf option instructs the compiler to analyze control flow for any indirect calls at compile time and records the results of that analysis in the compiled binary. It also puts a check in the binary before every indirect call that is checked by Windows when your code is run. Windows will call RaiseFastFailException if any of these checks fails at runtime.

Upcoming Security Features

We continue to innovate with new security features that benefit from our code generator’s program analysis. Security requires “defense in depth” because attackers will always find a way to work around the protections you have in place now. We constantly have to find new ways to help protect your code at all levels.

Is your code secure?

Good developer tools can do a lot to help you write solid and secure code but unfortunately, they can’t do everything for you. You need to start with a good design that includes security appropriate for the environment our code will run in—both when you deploy it and, potentially, for many years in the future, long after you might have expected your code to be rewritten, replaced, or just obsolete. Can your code be running in a connected environment? You need to plan for attacks, including those as simple as denial of service. Will your code handle sensitive user information? You need to plan for how your code will stand up to attackers who want to get at the data you handle.

Security isn’t a feature that can be bolted onto a finished product. But good tools–such as those provided in the Visual C++ toolset–can help you write solid, secure code.

Thank you!

Thank you for reading over this long list of security features provided at different points in your development process. And thanks to the hundreds of people who provide feedback and help us improve the C++ experience in Visual Studio.

If you have any feedback or suggestions for us, please reach out. We can be reached via the comments below, via email (visualcpp@microsoft.com) and you can provide feedback via Help > Report A Problem in the product, or via Developer Community. You can also find us on Twitter (@VisualC) and Facebook (msftvisualcpp).

0 comments

Discussion is closed.

Feedback usabilla icon