OWASPCheat Sheet
CS-C-Based_Toolchain_Hardening C-Based Toolchain Hardening Cheat Sheet
- Project
- OWASP Cheat Sheet Series
Introduction
C-Based Toolchain Hardening is a treatment of project settings that will help you deliver reliable and secure code when using C, C++ and Objective C languages in a number of development environments. This article will examine Microsoft and GCC toolchains for the C, C++ and Objective C languages. It will guide you through the steps you should take to create executables with firmer defensive postures and increased integration with the available platform security. Effectively configuring the toolchain also means your project will enjoy a number of benefits during development, including enhanced warnings and static analysis, and self-debugging code.
There are four areas to be examined when hardening the toolchain: configuration, preprocessor, compiler, and linker. Nearly all areas are overlooked or neglected when setting up a project. The neglect appears to be pandemic, and it applies to nearly all projects including Auto-configured projects, makefile-based, Eclipse-based, Visual Studio-based, and Xcode-based. It's important to address the gaps at configuration and build time because it's difficult to impossible to add hardening on a distributed executable after the fact on some platforms.
This is a prescriptive article, and it will not debate semantics or speculate on behavior. Some information, such as the C/C++ committee's motivation and pedigree for `program diagnostics`, `NDEBUG`, `assert`, and `abort()`, appears to be lost like a tale in the Lord of the Rings. As such, the article will specify semantics (for example, the philosophy of 'debug' and 'release' build configurations), assign behaviors (for example, what an assert should do in 'debug' and 'release' build configurations), and present a position. If you find the posture is too aggressive, then you should back off as required to suit your taste.
A secure toolchain is not a silver bullet. It is one piece of an overall strategy in the engineering process to help ensure success. It will complement existing processes such as static analysis, dynamic analysis, secure coding, negative test suites, and the like. Tools such as Valgrind and Helgrind will still be needed, and a project will still require solid designs and architectures.
The OWASP ESAPI C++ project eats its own dog food. Many of the examples you will see in this article come directly from the ESAPI C++ project.
Wisdom
Code must be correct. It should be secure. It can be efficient.
Dr. Jon Bentley: _"If it doesn't have to be correct, I can make it as fast as you'd like it to be"_.
Dr. Gary McGraw: _"Thou shalt not rely solely on security features and functions to build secure software as security is an emergent property of the entire system and thus relies on building and integrating all parts properly"_.
Configuration
Configuration is the first opportunity to configure your project for success. Not only do you have to configure your project to meet reliability and security goals, you must also configure integrated libraries properly. You typically have three choices. First, you can use auto-configuration utilities if on Linux or Unix. Second, you can write a makefile by hand. This is predominant on Linux, macOS, and Unix, but it applies to Windows, as well. Finally, you can use an integrated development environment or IDE.
Build Configurations
At this stage in the process, you should concentrate on configuring for two builds: Debug and Release. Debug will be used for development and include full instrumentation. Release will be configured for production. The difference between the two settings is usually _optimization level_ and _debug level_. A third build configuration is Test, and it's usually a special case of Release.
For debug and release builds, the settings are typically diametrically opposed. Debug configurations have no optimizations and full debug information, while Release builds have optimizations and minimal to moderate debug information. In addition, debug code has full assertions and additional library integration, such as mudflaps and malloc guards such as `dmalloc`.
The Test configuration is often a Release configuration that makes everything public for testing and builds a test harness. For example, all member functions that are public (C++ class) and all interfaces (library or shared object) should be made available for testing. Many Object Oriented purists oppose testing private interfaces, but this is not about object oriented-ness. This (_q.v._) is about building reliable and secure software.
GCC 4.8 introduced an optimization of `-Og`. Note that it is only an optimization, and still requires a customary debug level via `-g`.
Debug Builds
Debug builds are where developers spend most of their time when vetting problems, so this build should concentrate forces and tools or be a 'force multiplier'. Though many do not realize, debug code is more highly valued than release code because it's adorned with additional instrumentation. The debug instrumentation will cause a program to become nearly "self-debugging", and help you catch mistakes such as bad parameters, failed API calls, and memory problems.
Self-debugging code reduces your time during troubleshooting and debugging. Reducing time under the debugger means you have more time for development and feature requests. If code is checked in without debug instrumentation, it should be fixed by adding instrumentation or rejected.
For GCC, optimizations and debug symbolication are controlled through two switches: `-O` and `-g`. You should use the following as part of your `CFLAGS` and `CXXFLAGS` for a minimal debug session:
```text
-O0 -g3 -ggdb
```
`-O0` turns off optimizations and `-g3` ensures maximum debug information is available. You may need to use `-O1` so some analysis is performed. Otherwise, your debug build will be missing a number of warnings not present in release builds. `-g3` ensures maximum debugging information is available for the debug session, including symbolic constants and `#defines`. `-ggdb` includes extensions to help with a debug session under GDB. For completeness, Jan Krachtovil stated `-ggdb` currently has no effect in a private email.
For x86 builds, choose speculative-execution mitigations for the target CPU and operating system. GCC's `-mindirect-branch=thunk` replaces indirect calls and jumps with call-and-return thunks; `-mfunction-return=thunk` transforms function returns. Retpoline mitigates specific Spectre variant 2 attack paths, not speculative-execution vulnerabilities in general. Follow current platform guidance rather than assuming these flags protect against every Spectre variant or Meltdown.
Debug build should also define `DEBUG`, and ensure `NDEBUG` is not defined. `NDEBUG` removes "program diagnostics" and has undesirable behavior and side effects which are discussed below in more detail. The defines should be present for all code, and not just the program. You use it for all code (your program and included libraries) because you need to know how they fail, too (remember, you take the bug report - not the third party library).
In addition, you should use other relevant flags, such as `-fno-omit-frame-pointer`. Ensuring a frame pointer exists makes it easier to decode stack traces. Since debug builds are not shipped, it's OK to leave symbols in the executable. Programs with debug information do not suffer performance hits. See, for example, How does the gcc -g option affect performance?
Finally, you should ensure your project includes additional diagnostic libraries, such as `dmalloc` and Address Sanitizer. A comparison of some memory checking tools can be found at Comparison Of Memory Tools. If you don't include additional diagnostics in debug builds, then you should start using them since it's OK to find errors you are not looking for.
Release Builds
Release builds are what your customer receives. They are meant to be run on production hardware and servers, and they should be reliable, secure, and efficient. A stable release build is the product of the hard work and effort during development.
For release builds, you should use the following as part of `CFLAGS` and `CXXFLAGS` for release builds:
```text
-On -g2
```
`-O`_`n`_ sets optimizations for speed or size (for example, `-Os` or `-O2`), and `-g2` ensure debugging information is created.
Debugging information should be stripped and retained in case of symbolication for a crash report from the field. While not desired, debug information can be left in place without a performance penalty. See _How does the gcc -g option affect performance?_ for details.
Release builds should also define `NDEBUG`, and ensure `DEBUG` is not defined. The time for debugging and diagnostics is over, so users get production code with full optimizations, no "programming diagnostics", and other efficiencies. If you can't optimize or you are performing excessive logging, it usually means the program is not ready for production.
If you have been relying on an `assert` and then a subsequent `abort()`, you have been abusing "program diagnostics" since it has no place in production code. If you want a memory dump, create one so users don't have to worry about secrets and other sensitive information being written to the filesystem and emailed in plain text.
For Windows, you would use `/Od` for debug builds and `/Ox`, `/O2` or `/Os` for release builds. See Microsoft's /O Options (Optimize Code) for details.
Test Builds
Test builds are used to provide heuristic validation by way of positive and negative test suites. Under a test configuration, all interfaces are tested to ensure they perform to specification and satisfaction. "Satisfaction" is subjective, but it should include no crashing and no trashing of your memory arena, even when faced with negative tests.
Because all interfaces are tested (and not just the public ones), your `CFLAGS` and `CXXFLAGS` should include:
```text
-Dprotected=public -Dprivate=public
```
You should also change `__attribute__` `((visibility` `("hidden")))` to `__attribute__` `((visibility` `("default")))`.
Nearly everyone gets a positive test right, so no more needs to be said. The negative self tests are much more interesting, and you should concentrate on trying to make your program fail so you can verify it fails gracefully. Remember, a bad actor is not going to be courteous when they attempt to cause your program to fail, and it's your project that takes egg on the face by way of a bug report or guest appearance on Full Disclosure or Bugtraq - not `` you included.
Sanitizers and Valgrind
Run your tests with runtime checks enabled to catch bugs on paths the tests exercise. AddressSanitizer (ASan) can detect out-of-bounds accesses and use-after-free; UndefinedBehaviorSanitizer (UBSan) can detect undefined behavior such as signed integer overflow. With GCC or Clang, add these flags when compiling and linking a test build:
```text
-O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all
```
`-O1`, `-g`, and `-fno-omit-frame-pointer` help produce useful error reports. Most UBSan checks report errors and continue by default, so a test can pass despite a reported bug; `-fno-sanitize-recover=all` makes detected errors terminate the program with a nonzero exit status. Use this combined ASan/UBSan build during development and continuous integration (CI), but do not ship it: ASan's runtime is intended for testing and typically doubles execution time.
On a supported platform, run an executable built without ASan under Valgrind Memcheck when you cannot rebuild it or a library it uses. For example, `valgrind --leak-check=full --error-exitcode=1 ./test_suite` checks for memory errors and reports leaks in detail; `--error-exitcode` makes reported errors fail the test. Memcheck cannot detect overruns of static or stack arrays. For code you can rebuild, use ASan in a separate test run.
Autotools
Auto configuration tools are popular on many Linux and Unix based systems, and the tools include _Autoconf_, _Automake_, _config_, and _Configure_. The tools work together to produce project files from scripts and template files. After the process completes, your project should be set up and ready to be made with `make`.
When using auto configuration tools, there are a few files of interest worth mentioning. The files are part of the Autotools chain and include `m4` and the various `*.in`, `*.ac` (Autoconf), and `*.am` (Automake) files. At times, you will have to open them, or the resulting makefiles, to tune the "stock" configuration.
There are three downsides to the command-line configuration tools in the toolchain: (1) they often ignore user requests, (2) switching build flags may reuse objects from an earlier configuration, and (3) security is often not a goal.
To demonstrate the first issue, configure your project with the following: `configure` `CFLAGS="-Wall` `-fPIE"` `CXXFLAGS="-Wall` `-fPIE"` `LDFLAGS="-pie"`. You will probably find that Autotools ignored your request, which means the command below will not produce expected results. As a workaround, you will have to open an `m4` script, `makefile.in` or `makefile.am`, and fix the configuration.
```bash
$ configure CFLAGS="-Wall -Wextra -Wconversion -fPIE -Wno-unused-parameter
-Wformat=2 -Wformat-security -fstack-protector-all -Wstrict-overflow"
LDFLAGS="-pie -z,noexecstack -z,noexecheap -z,relro -z,now"
```
For the second point, use separate build directories for different configurations. Make checks modification times of prerequisites and targets, so changing `CFLAGS` or `CXXFLAGS` alone does not ensure that existing objects are rebuilt. Separate debug, test, and release build directories help prevent reuse of objects compiled with different hardening flags.
Finally, you will probably be disappointed to learn tools such as Autoconf and Automake miss many security related opportunities and ship insecure out of the box. There are a number of compiler switches and linker flags that improve the defensive posture of a program, but they are not 'on' by default. Tools like Autoconf - which are supposed to handle this situation - often provide settings to serve the lowest of all denominators.
A recent discussion on the Automake mailing list illuminates the issue: _Enabling compiler warning flags_. Attempts to improve default configurations were met with resistance and no action was taken. The resistance is often of the form, "` also produces false positives`" or "` does not support `". It's noteworthy that David Wheeler, the author of _Secure Programming for Linux and Unix HOWTO_, was one of the folks trying to improve the posture.
Makefiles
Make is one of the earliest build tools dating back to the 1970s. It's available on Linux, macOS and Unix, so you will frequently encounter projects using it. Unfortunately, Make has a number of shortcomings (_Recursive Make Considered Harmful_ and _What's Wrong With GNU make?_), and can cause some discomfort. Despite issues with Make, ESAPI C++ uses Make primarily for three reasons: first, it's omnipresent; second, it's easier to manage than the Autotools family; and third, `libtool` was out of the question.
Consider what happens when you: (1) type `make` `debug`, and then type `make` `release`. Each build would require different `CFLAGS` due to optimizations and level of debug support. In your makefile, you would extract the relevant target and set `CFLAGS` and `CXXFLAGS` similar to below (taken from ESAPI C++ makefile):
```text
## makefile
DEBUG_GOALS = $(filter $(MAKECMDGOALS), debug)
ifneq ($(DEBUG_GOALS),)
WANT_DEBUG := 1
WANT_TEST := 0
WANT_RELEASE := 0
endif
…
ifeq ($(WANT_DEBUG),1)
ESAPI_CFLAGS += -DDEBUG=1 -UNDEBUG -g3 -ggdb -O0
ESAPI_CXXFLAGS += -DDEBUG=1 -UNDEBUG -g3 -ggdb -O0
endif
ifeq ($(WANT_RELEASE),1)
ESAPI_CFLAGS += -DNDEBUG=1 -UDEBUG -g -O2
ESAPI_CXXFLAGS += -DNDEBUG=1 -UDEBUG -g -O2
endif
ifeq ($(WANT_TEST),1)
ESAPI_CFLAGS += -DESAPI_NO_ASSERT=1 -g2 -ggdb -O2 -Dprivate=public
-Dprotected=public
ESAPI_CXXFLAGS += -DESAPI_NO_ASSERT=1 -g2 -ggdb -O2 -Dprivate=public
-Dprotected=public
endif
…
## Merge ESAPI flags with user supplied flags. We perform the extra step to ensure
## user options follow our options, which should give user option's a preference.
override CFLAGS := $(ESAPI_CFLAGS) $(CFLAGS)
override CXXFLAGS := $(ESAPI_CXXFLAGS) $(CXXFLAGS)
override LDFLAGS := $(ESAPI_LDFLAGS) $(LDFLAGS)
…
```
Make will first build the program in a debug configuration for a session under the debugger using a rule similar to:
```text
%.cpp:%.o:
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $example.com:443
```
`nm` or `openssl` `s_client` will show that compression is enabled in the client. In fact, any symbol within the `OPENSSL_NO_COMP` preprocessor macro will bear witness since `-no-comp` is translated into a `CFLAGS` define.
```bash
$ nm /usr/local/ssl/iphoneos/lib/libcrypto.a 2>/dev/null | egrep -i "(COMP_CTX_new|COMP_CTX_free)"
0000000000000110 T COMP_CTX_free
0000000000000000 T COMP_CTX_new
```
Even more egregious is the answer given to auditors who specifically ask about configurations and protocols: "we don't use weak/wounded/broken ciphers" or "we follow best practices." The use of compression tells the auditor that you are using wounded protocols in an insecure configuration and you don't follow best practices. That will likely set off alarm bells, and ensure the auditor dives deeper on more items.
Preprocessor
The preprocessor is crucial to setting up a project for success. The C committee provided one macro - `NDEBUG` - and the macro can be used to derive a number of configurations and drive engineering processes. Unfortunately, the committee also left many related items to chance, which has resulted in programmers abusing built-in facilities. This section will help you set up your projects to integrate well with other projects and ensure reliability and security.
There are three topics to discuss when hardening the preprocessor. The first is well defined configurations which produce well-defined behaviors, the second is useful behavior from assert, and the third is proper use of macros when integrating vendor code and third party libraries.
Configurations
To remove ambiguity, you should recognize two configurations: Release and Debug. Release is for production code on live servers, and its behavior is requested via the C/C++ `NDEBUG` macro. It's also the only macro observed by the C and C++ Committees and Posix. Diametrically opposed to release is Debug. While there is a compelling argument for `!defined(NDEBUG)`, you should have an explicit macro for the configuration and that macro should be `DEBUG`. This is because vendors and outside libraries use the `DEBUG` (or similar) macro for their configuration. For example, Carnegie Mellon's Mach kernel uses `DEBUG`, Microsoft's CRT uses `_DEBUG`, and Wind River Workbench uses `DEBUG_MODE`.
In addition to `NDEBUG` (Release) and `DEBUG` (Debug), you have two additional cross products: both are defined or neither are defined. Defining both should be an error, and defining neither should default to a release configuration. Below is from ESAPI C++ EsapiCommon.h, which is the configuration file used by all source files:
```c
// Only one or the other, but not both
##if (defined(DEBUG) || defined(_DEBUG)) && (defined(NDEBUG)
|| defined(_NDEBUG))
## error Both DEBUG and NDEBUG are defined.
##endif
// The only time we switch to debug is when asked.
// NDEBUG or {nothing} results
// in release build (fewer surprises at runtime).
##if defined(DEBUG) || defined(_DEBUG)
## define ESAPI_BUILD_DEBUG 1
##else
## define ESAPI_BUILD_RELEASE 1
##endif
```
When `DEBUG` is in effect, your code should receive full debug instrumentation, including the full force of assertions.
ASSERT
Asserts will help you create self-debugging code by helping you find the point of first failure quickly and easily. Asserts should be used throughout your program, including parameter validation, return value checking and program state. The `assert` will silently guard your code through its lifetime. It will always be there, even when not debugging a specific component of a module. If you have thorough code coverage, you will spend less time debugging and more time developing because programs will debug themselves.
To use asserts effectively, you should assert everything. That includes parameters upon entering a function, return values from function calls, and any program state. Everywhere you place an `if` statement for validation or checking, you should have an assert. Everywhere you have an `assert` for validation or checking, you should have an `if` statement. They go hand-in-hand.
If you are still using `printf`'s, then you have an opportunity for improvement. In the time it takes for you to write a `printf` or `NSLog` statement, you could have written an `assert`. Unlike the `printf` or `NSLog` which are often removed when no longer needed, the `assert` stays active forever. Remember, this is all about finding the point of first failure quickly so you can spend your time doing other things.
There is one problem with using asserts - Posix states `assert` should call `abort()` if `NDEBUG` is not defined. When debugging, `NDEBUG` will never be defined since you want the "program diagnostics" (quote from the Posix description). The behavior makes `assert` and its accompanying `abort()` completely useless for development. The result of "program diagnostics" calling `abort()` due to standard C/C++ behavior is disuse - developers simply don't use them. It's incredibly bad for the development community because self-debugging programs can help eradicate so many stability problems.
Since self-debugging programs are so powerful, you will have to supply your own assert and signal handler with improved behavior. Your assert will exchange auto-aborting behavior for auto-debugging behavior. The auto-debugging facility will ensure the debugger snaps when a problem is detected, and you will find the point of first failure quickly and easily.
ESAPI C++ supplies its own assert with the behavior described above. In the code below, `ASSERT` raises `SIGTRAP` when in effect or it evaluates to `void` in other cases.
```c
// A debug assert which should be sprinkled liberally.
// This assert fires and then continues rather
// than calling abort(). Useful when examining negative
// test cases from the command-line.
##if (defined(ESAPI_BUILD_DEBUG) && defined(ESAPI_OS_STARNIX))
## define ESAPI_ASSERT1(exp) { \
if(!(exp)) { \
std::ostringstream oss; \
oss << "Assertion failed: " << (char*)(__FILE__) << "(" \
<< (int)__LINE__ << "): " << (char*)(__func__) \
<< std::endl; \
std::cerr << oss.str(); \
raise(SIGTRAP); \
} \
}
## define ESAPI_ASSERT2(exp, msg) { \
if(!(exp)) { \
std::ostringstream oss; \
oss << "Assertion failed: " << (char*)(__FILE__) << "(" \
<< (int)__LINE__ << "): " << (char*)(__func__) \
<< ": \"" << (msg) << "\"" << std::endl; \
std::cerr << oss.str(); \
raise(SIGTRAP); \
} \
}
##elif (defined(ESAPI_BUILD_DEBUG) && defined(ESAPI_OS_WINDOWS))
## define ESAPI_ASSERT1(exp) assert(exp)
## define ESAPI_ASSERT2(exp, msg) assert(exp)
##else
## define ESAPI_ASSERT1(exp) ((void)(exp))
## define ESAPI_ASSERT2(exp, msg) ((void)(exp))
##endif
##if !defined(ASSERT)
## define ASSERT(exp) ESAPI_ASSERT1(exp)
##endif
```
At program startup, a `SIGTRAP` handler will be installed if one is not provided by another component:
```c
struct DebugTrapHandler
{
DebugTrapHandler()
{
struct sigaction new_handler, old_handler;
do
{
int ret = 0;
ret = sigaction (SIGTRAP, NULL, &old_handler);
if (ret != 0) break; // Failed
// Don't step on another's handler
if (old_handler.sa_handler != NULL) break;
new_handler.sa_handler = &DebugTrapHandler::NullHandler;
new_handler.sa_flags = 0;
ret = sigemptyset (&new_handler.sa_mask);
if (ret != 0) break; // Failed
ret = sigaction (SIGTRAP, &new_handler, NULL);
if (ret != 0) break; // Failed
} while(0);
}
static void NullHandler(int /*unused*/) { }
};
// We specify a relatively low priority, to make sure we run before other CTORs
// http://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Attributes.html#C_002b_002b-Attributes
static const DebugTrapHandler g_dummyHandler __attribute__ ((init_priority (110)));
```
On a Windows platform, you would call `_set_invalid_parameter_handler` (and possibly `set_unexpected` or `set_terminate`) to install a new handler.
Live hosts running production code should always define `NDEBUG` (i.e., release configuration), which means they do not assert or auto-abort. Auto-abortion is not acceptable behavior, and anyone who asks for the behavior is completely abusing the functionality of "program diagnostics". If a program wants a core dump, then it should create the dump rather than crashing.
For more reading on asserting effectively, please see one of John Robbin's books, such as _Debugging Applications_. John is a legendary bug slayer in Windows circles, and he will show you how to do nearly everything, from debugging a simple program to bug slaying in multithreaded programs.
Additional Macros
Additional macros include any macros needed to integrate properly and securely. It includes integrating the program with the platform (for example MFC or Cocoa/CocoaTouch) and libraries (for example, Crypto++ or OpenSSL). It can be a challenge because you have to have proficiency with your platform and all included libraries and frameworks. The list below illustrates the level of detail you will need when integrating.
Though Boost is missing from the list, it appears to lack recommendations, additional debug diagnostics, and a hardening guide. See _BOOST Hardening Guide (Preprocessor Macros)_ for details. In addition, Tim Day points to _[\[boost.build\] should we not define \_SECURE_SCL=0 by default for all msvc toolsets](https://lists.boost.org/Archives/boost/2008/12/145749.php)_ for a recent discussion related to hardening (or lack thereof).
In addition to what you should define, defining some macros and undefining others should trigger a security related defect. For example, `-U_FORTIFY_SOURCES` on Linux and `_CRT_SECURE_NO_WARNINGS=1`, `_SCL_SECURE_NO_WARNINGS`, `_ATL_SECURE_NO_WARNINGS` or `STRSAFE_NO_DEPRECATE` on Windows.
a) Be careful with `_GLIBCXX_DEBUG` when using pre-compiled libraries such as Boost from a distribution. There are ABI incompatibilities, and the result will likely be a crash. You will have to compile Boost with `_GLIBCXX_DEBUG` or omit `_GLIBCXX_DEBUG`.
b) See Chapter 5, Diagnostics of the libstdc++ manual for details.
c) SQLite secure deletion zeroizes memory on destruction. Define as required, and always define in US Federal since zeroization is required for FIPS 140-2, Level 1.
d) _N_ is 0644 by default, which means everyone has some access.
e) Force temporary tables into memory (no unencrypted data to disk).
Compiler and Linker
Compiler warnings catch some common flaws, such as format string bugs and sign conversion mistakes, before code ships. Binary mitigation flags make memory safety bugs harder to exploit. They can be bypassed and do not replace fixing the bug.
The flags below are a subset of the OpenSSF Compiler Options Hardening Guide for C and C++ baseline. Start there for the full list, the rationale, and compiler and operating system version requirements.
GCC and Clang
GCC and Clang use mostly the same hardening flags. The `-Wl,-z,...` linker options below apply to ELF targets such as Linux and BSD, not macOS. See GCC Instrumentation Options, GCC Warning Options, and the Clang Command Line Reference.
Compile-Time Diagnostics and Warnings
• `-Wall -Wextra`: Enables baseline diagnostic warnings covering some undefined behavior, uninitialized variables, and common implementation flaws; does not catch deeper logic defects or architectural vulnerabilities.
• `-Wformat=2 -Wformat-security -Werror=format-security`: Warns about non-literal format strings and makes it an error when one is passed with no arguments, such as `printf(s)`; reduces but does not remove format string bugs, so always use a literal format string.
• `-Wconversion`: Warns on implicit type conversions that may alter values or signs; can produce noisy diagnostics on benign conversions that require explicit casting.
• `-Wimplicit-fallthrough`: Warns when a switch case falls through without a break, return, or fallthrough annotation; requires explicitly annotating intentional fallthrough cases.
• `-Werror` (or selective `-Werror=`): Treats compiler warnings as fatal errors to prevent ignoring compiler-diagnosed flaws; can break builds when updating toolchains that introduce new diagnostics.
Stack and Memory Protections
• `-fstack-protector-strong`: Inserts stack canaries into functions with vulnerable buffers and checks them when the function returns; detects linear overflows that reach the return address, but not heap overflows, overwrites of other local variables, or arbitrary writes.
• `-fstack-clash-protection`: Protects against stack-clash attacks by probing the stack page-by-page so dynamic stack allocations cannot bypass the guard page; does not prevent stack exhaustion or buffer overflows within an allocated frame.
• `-D_FORTIFY_SOURCE=3` (or `-D_FORTIFY_SOURCE=2`): Hardens standard C library memory and string functions by validating buffer bounds at compile time and runtime; requires optimization (`-O1` or higher) and cannot check buffers whose size the compiler cannot determine.
• `-D_GLIBCXX_ASSERTIONS` (libstdc++) or `-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST` (libc++): Enables runtime bounds checks for C++ standard library containers; adds runtime overhead.
Binary Layout and Control-Flow Mitigations
• `-fPIE` (compiler) and `-pie` (linker): Produces Position Independent Executables to enable full Address Space Layout Randomization (ASLR); applies only to executable binaries and does not prevent address leaks (information disclosure).
• `-Wl,-z,relro -Wl,-z,now`: Enables full Relocation Read-Only (RELRO) by resolving symbols at load time and marking relocation tables and the Global Offset Table read-only; increases program startup time for binaries with many dynamic symbols.
• `-Wl,-z,noexecstack`: Marks the stack as non-executable (NX), so code injected on the stack cannot run; does not prevent code-reuse attacks that repurpose existing executable memory.
• `-fcf-protection=full` (x86 only): Emits markers for Intel Control-flow Enforcement Technology (CET) shadow stacks and indirect branch tracking (IBT). In Linux user space only the shadow stack is enforced, which protects return addresses against return-oriented programming (ROP); IBT, which targets jump-oriented programming (JOP), is not. Requires CPU, kernel, and C library support.
Verification
Use a tool such as checksec to confirm which mitigations a Linux binary has (RELRO, stack canaries, NX, PIE, `_FORTIFY_SOURCE`).
Microsoft Visual C++ (MSVC)
Microsoft Visual C++ provides compiler and linker switches for security hardening on Windows platforms. Official reference documentation includes MSVC Compiler Options Listed by Category, MSVC Linker Options, and Microsoft Security Best Practices for C++.
Compiler Warnings and Static Analysis
• `/W4`: Enables a strict, practical warning level covering uninitialized variables, data truncation, and sign mismatches; does not enable all possible informational warnings provided by `/Wall`.
• `/WX`: Treats all compiler warnings as fatal errors to ensure diagnostics are resolved before deployment; can break builds when updating toolchains that introduce new warnings.
• `/sdl`: Enables additional Security Development Lifecycle checks by treating security-relevant warnings as errors and enforcing strict runtime buffer checks; limited to the specific set of warnings and checks defined by the compiler.
• `/analyze`: Runs compile-time static code analysis to detect memory safety issues, null pointer dereferences, and API misuse; increases build times.
Stack and Memory Protections
• `/GS` (Buffer Security Check): On by default; do not disable it. Places a security cookie before the return address (and, on x86, the exception handler address) of functions with vulnerable buffers; does not protect heap buffers or locals that the compiler does not treat as vulnerable.
Control-Flow Integrity and Binary Layout
• `/guard:cf` (Control Flow Guard): Mitigates control-flow hijacking by verifying that indirect call targets point to compiler-approved functions; must be enabled in both compiler and linker and does not protect return addresses on the stack.
• `/guard:ehcont` (Exception Handling (EH) Continuation Guard): Generates metadata listing valid exception handling continuation targets to prevent control-flow hijacking during exception unwinding. Pass it to both the compiler and the linker. Supported only for 64-bit processes and meant to work with CET shadow stacks; modules built without it are not protected.
• `/DYNAMICBASE` (Linker, on by default): Enables ASLR by instructing the operating system loader to randomize the binary load address; does not prevent memory disclosure attacks that reveal relocated base addresses.
• `/HIGHENTROPYVA` (Linker): Enables 64-bit high-entropy ASLR to increase address randomization space; applies only to 64-bit targets and requires `/DYNAMICBASE`.
• `/NXCOMPAT` (Linker, on by default): Marks the executable as compatible with Data Execution Prevention (DEP), preventing code execution from data pages; does not prevent code-reuse attacks executing from existing executable memory.
• `/CETCOMPAT` (Linker): Marks the executable image as compatible with CET shadow stack; x64 only, and requires a CET-capable processor and operating system.
Warning Suppression
Strict warning levels produce some false positives because the compiler cannot know every runtime invariant. Suppress individual false positives in the code. Do not turn warnings off globally.
Silencing spurious warnings lets you focus on the ones that matter. First is an unused parameter (for example, `argc` or `argv`). In modern C++ (C++17 and later), prefer the standard `[[maybe_unused]]` attribute; in C or earlier standards, define an unused parameter macro or cast the parameter to `void`:
```c
#define UNUSED_PARAMETER(x) ((void)(x))
…
int main(int argc, char* argv[])
{
UNUSED_PARAMETER(argc);
UNUSED_PARAMETER(argv);
…
}
```
A potential minefield lies near "comparing unsigned and signed" values, and `-Wconversion` will catch it for you. This is because C/C++ promotion rules state the signed value will be promoted to an unsigned value and then compared. That means `-1` `>` `1` after promotion! To fix this, you cannot blindly cast - you must first range test the value:
```c
int x = GetX();
unsigned int y = GetY();
ASSERT(x >= 0);
if(!(x >= 0))
throw runtime_error("WTF??? X is negative.");
if(static_cast(x) > y)
cout > t;
ASSERT(!(iss.fail()));
if(iss.fail())
throw runtime_error("WTF??? Failed to read port.");
// Should this be a port above the reserved range ([0-1024] on Unix)?
ASSERT(t > 0);
if(!(t > 0))
throw runtime_error("WTF??? Port is too small");
ASSERT(t (t);
…
```
Again, notice the code above will debug itself - you don't need to set a breakpoint to see if there is a problem with `port`. This code will continue checking conditions, years after being instrumented (assuming to write code to read a config file early in the project). There's no need to remove the `ASSERT`s as with `printf` since they are silent guardians.
Another useful suppression trick is to avoid ignoring return values. Not only is it useful to suppress the warning, it's required for correct code. For example, `snprintf` will alert you to truncations through its return value. You should not make them silent truncations by ignoring the warning or casting to `void`:
```c
char path[PATH_MAX];
…
int ret = snprintf(path, sizeof(path), "%s/%s", GetDirectory(), GetObjectName());
ASSERT(ret != -1);
ASSERT(!(ret >= sizeof(path)));
if(ret == -1 || ret >= sizeof(path))
throw runtime_error("WTF??? Unable to build full object name");
// OK to use path
…
```
The problem is pandemic, and not just boring user-land programs. Projects which offer high integrity code, such as SELinux, suffer silent truncations. The following is from an approved SELinux patch even though a comment was made that it suffered silent truncations in its `security_compute_create_name` function from `compute_create.c`.
```c
12 int security_compute_create_raw(security_context_t scon,
13 security_context_t tcon,
14 security_class_t tclass,
15 security_context_t * newcon)
16 {
17 char path[PATH_MAX];
18 char *buf;
19 size_t size;
20 int fd, ret;
21
22 if (!selinux_mnt) {
23 errno = ENOENT;
24 return -1;
25 }
26
27 snprintf(path, sizeof path, "%s/create", selinux_mnt);
28 fd = open(path, O_RDWR);
```
Unlike other examples, the above code will not debug itself, and you will have to set breakpoints and trace calls to determine the point of first failure. (And the code above gambles that the truncated file does not exist or is not under an adversary's control by blindly performing the `open`).
Runtime
The previous sections concentrated on setting up your project for success. This section will examine additional hints for running with increased diagnostics and defenses. Not all platforms are created equal - GNU Linux is difficult to impossible to add hardening to a program after compiling and static linking, while Windows allows post-build hardening through a download. Remember, the goal is to find the point of first failure quickly so you can improve the reliability and security of the code.
Xcode
Xcode offers additional Code Diagnostics that can help find memory errors and object use problems. Schemes can be managed through _Products_ menu item, _Scheme_ submenu item, and then _Edit_. From the editor, navigate to the _Diagnostics_ tab. In the figure below, four additional instruments are enabled for the debugging cycle: Scribble guards, Edge guards, Malloc guards, and Zombies.
There is one caveat with using some of the guards: Apple only provides them for the simulator, and not a device. In the past, the guards were available for both devices and simulators.
Windows
Visual Studio offers a number of debugging aids for use during development. The aids are called Managed Debugging Assistants (MDAs). You can find the MDAs on the _Debug_ menu, then _Exceptions_ submenu. MDAs allow you to tune your debugging experience by, for example, filtering exceptions for which the debugger should snap. For more details, see Stephen Toub's _Let The CLR Find Bugs For You With Managed Debugging Assistants_.
Finally, for runtime hardening, Microsoft provides Windows Defender Exploit Guard and the Process Mitigation Management Tool.
Windows Defender Exploit Guard replaces EMET and offers advanced exploit protection features.
Additionally, the Process Mitigation Management Tool (`ProcessMitigations` module) allows administrators to configure exploit mitigation policies via PowerShell and Group Policy.