[ Team LiB ] |
13.2 Erasing Data from Memory Securely13.2.1 ProblemYou want to minimize the exposure of data such as passwords and cryptographic keys to local attacks. 13.2.2 SolutionYou 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. 13.2.3 DiscussionSecurely 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. |
[ Team LiB ] |