You want to minimize the exposure of data such as passwords and cryptographic keys to local attacks.
You can only guarantee that memory is erased if you declare it to be
volatile
at the point where you write over it.
In addition, you must not use an operation such as realloc(
)
that may silently move sensitive data. In any event, you
might also need to worry about data being swapped to disk; see Recipe
13.3.
Securely erasing data from memory is a lot easier in C and C++ than it is in languages where all memory is managed behind the programmer's back. There are still some nonobvious pitfalls, however.
One pitfall, particularly in
C++, is that some API
functions may silently move data behind the
programmer's back, leaving behind a copy of the data
in a different part of memory. The most prominent example in the
C realm is realloc(
)
, which will sometimes move a piece of memory, updating
the programmer's pointer. Yet the old memory
location will generally still have the unaltered data, up until the
point where the memory manager reallocates the data and the program
overwrites the value.
Another pitfall is that functions like memset(
)
may fail to wipe
data because of compiler optimizations.
Compiler writers have worked hard to implement optimizations into their compilers to help make code run faster (or compile to smaller machine code). Some of these optimizations can realize significant performance gains, but sometimes they also come at a cost. One such optimization is dead-code elimination, where the optimizer attempts to identify code that does nothing and eliminate it. Only relatively new compilers seem to implement this optimization; these include the current versions of GCC and Microsoft's Visual C++ compiler, as well as some other less commonly used compilers.
Unfortunately, this optimization can cause problems when writing secure code. Most commonly, code that "erases" a piece of memory that contains sensitive information such as a password or passphrase in plaintext is often eliminated by this optimization. As a result, the sensitive information is left in memory, providing an attacker a temptation that can be difficult to ignore.
Functions like memset(
)
do
useful work, so why would dead-code elimination passes remove them?
Many compilers implement such functions as built-ins, which means
that the compiler has knowledge of what the function does. In
addition, situations in which such calls would be eliminated are
restricted to times when the compiler can be sure that the data
written by these functions is never read again. For example:
int get_and_verify_password(char *real_password) { int result; char *user_password[64]; /* WARNING * WARNING * WARNING * WARNING * WARNING * WARNING * WARNING * * This is an example of unsafe code. In particular, note the use of memset( ), * which is exactly what we are discussing as being a problem in this recipe. */ get_password_from_user_somehow(user_password, sizeof(user_password)); result = !strcmp(user_password, real_password); memset(user_password, 0, strlen(user_password)); return result; }
In this example, the variable user_password
exists
solely within the function get_and_verify_password(
)
. After the memset( )
,
it's never used again, and because memset(
)
only writes the data, the compiler can
"safely" remove it.
Several solutions to this particular problem exist, but the code that
we've provided here is the most correct when used
with a compiler that conforms to at least the ANSI/ISO 9899-1990
standard, which includes any modern C compiler. The key is the use of
the volatile
keyword, which essentially instructs
the compiler not to optimize out expressions that involve the
variable because there may be side effects unknown to the compiler. A
commonly cited example of this is a variable that may be modified by
hardware, such as a real-time clock.
It's proper to declare any variable containing
sensitive information as volatile
. Unfortunately,
many programmers are unaware of what this keyword means, so it is
frequently omitted. In addition, simply declaring a variable as
volatile
may not be enough. Whether or not it is
enough often depends on how aggressive a particular compiler is in
performing dead-code elimination. Early implementations of dead-code
elimination optimizations were probably far less aggressive than
current ones, and logically you can safely assume that they will
perhaps get more aggressive in the future. It is best to protect code
from any current optimizing compiler, as well as any that may be used
in the future.
If simply declaring a variable as
volatile
may not be enough, what more must be
done? The answer is to replace calls to functions like
memcpy( )
, memmove( )
, and
memset( )
with handwritten versions. These
versions may perform less well, but they will ensure their expected
behavior. The solution we have provided above does just that. Notice
the use of the volatile
keyword on each
function's argument list. An important difference
between these functions and typical implementations is the use of
that keyword. When memset( )
is called, the
volatile
qualifier on the buffer passed into it is
lost. Further, many compilers have built-in implementations of these
functions so that the compiler may perform heavier optimizing because
it knows exactly what the functions do.
Here is code that implements three different methods of writing data
to a buffer that a compiler may try to optimize away. The first is
spc_memset( )
, which acts just like the standard
memset( )
function, except that it guarantees the
write will not be optimized away if the destination is never used.
Then we implement spc_memcpy(
)
and spc_memmove( )
, which are also analogs of the appropriate
standard library functions.
#include <stddef.h> volatile void *spc_memset(volatile void *dst, int c, size_t len) { volatile char *buf; for (buf = (volatile char *)dst; len; buf[--len] = c); return dst; } volatile void *spc_memcpy(volatile void *dst, volatile void *src, size_t len) { volatile char *cdst, *csrc; cdst = (volatile char *)dst; csrc = (volatile char *)src; while (len--) cdst[len] = csrc[len]; return dst; } volatile void *spc_memmove(volatile void *dst, volatile void *src, size_t len) { size_t i; volatile char *cdst, *csrc; cdst = (volatile char *)dst; csrc = (volatile char *)src; if (csrc > cdst && csrc < cdst + len) for (i = 0; i < len; i++) cdst[i] = csrc[i]; else while (len--) cdst[len] = csrc[len]; return dst; }
If you're writing code for Windows using the latest
Platform SDK, you can use SecureZeroMemory(
)
instead of spc_memset( )
to zero memory. SecureZeroMemory( )
is actually
implemented as a macro to RtlSecureMemory(
)
, which is implemented as an inline function
in the same way that spc_memset( )
is implemented,
except that it only allows a buffer to be filled with zero bytes
instead of a value of the caller's choosing as
spc_memset( )
does.