david wong

Hey! I'm David, cofounder of zkSecurity and the author of the Real-World Cryptography book. I was previously a crypto architect at O(1) Labs (working on the Mina cryptocurrency), before that I was the security lead for Diem (formerly Libra) at Novi (Facebook), and a security consultant for the Cryptography Services of NCC Group. This is my blog about cryptography and security and other related topics that I find interesting.

Zero'ing memory, compiler optimizations and memset_s posted August 2017

tl;dr: use this code

When a program uses a secret key for some cryptographic operation, it will store it somewhere in memory. This is a problem because it is trivial to read what has been previously stored in memory from a different program, just create something like this:

#include <stdio.h>

int main(){
    unsigned char a[5000];
    for(int i = 0; i < 10000; i++) {
        printf("x", a[i]);
    }
    printf("\n");
}

This will print out whatever was previously there in memory, because the buffer a is not initialized to zeros. Actually, C seldom initializes things to zeros, it can if you specifically use something like calloc instead of malloc or static in front of a global variable/struct/...

EDIT: as Fred Akalin pointed to me, it looks like this is fixed in most modern OS. Colin Perceval notes that there are other issues with not zero'ing memory:

if someone is able to exploit an unrelated problem — a vulnerability which yields remote code execution, or a feature which allows uninitialized memory to be read remotely, for example — then ensuring that sensitive data (e.g., cryptographic keys) is no longer accessible will reduce the impact of the attack. In short, zeroing buffers which contained sensitive information is an exploit mitigation technique.

This is a problem.

To remove a key from memory, developers tend to write something like this:

memset(private_key, 0, sizeof(*private_key));

Unfortunately, when the compiler sees something like this, it will remove it. Indeed, this code is useless since the variable is not used anymore after, and the compiler will optimize it out.

How to fix this issue?

A memset_s function was proposed and introduced in C11. It is basically a safe memset (you need to pass in the size of the pointer you're zero'ing as argument) that will not get optimized out. Unfortunately as Martin Sebor notes:

memset_s is an optional feature of the C11 standard and as such isn't really portable. (AFAIK, there also are no conforming C11 implementations that provide the optional Annex K in which the function is defined.)

To use it, a #define at the right place can be used, and another #define is used as a notice that you can now use the memset_s function.

#define __STDC_WANT_LIB_EXT1__ 1
#include <string.h>
#include <stdlib.h>

// ...

#ifdef __STDC_LIB_EXT1__
memset_s(pointer, size_data, 0, size_to_remove);

Unfortunately you cannot rely on this for portability. For example on macOS the two #define are not used and you need to use memset_s directly.

Martin Sebor adds in the same comment:

The GCC -fno-builtin-memset option can be used to prevent compatible compilers from optimizing away calls to memset that aren't strictly speaking necessary.

Unfortunately, it seems like macOS' gcc (which is really clang) ignores this argument.

What else can we do?

I asked Robert Seacord who always have all the answers, here's what he gave me in return:

void *erase_from_memory(void *pointer, size_t size_data, size_t size_to_remove) {
    if(size_to_remove > size_data) size_to_remove = size_data;
    volatile unsigned char *p = pointer;
    while (size_to_remove--){
       *p++ = 0;
    }
    return pointer;
}

Does this volatile keyword works?

Time to open gdb (or lldb) to verify what the compiler has done. (This can be done after compiling with or without -O1, -O2, -O3 (different levels of optimization).)

Let's write a small program that uses this code and debug it:

int main(){
    char a[6] = "hello";
    printf("%s\n", a);
    erase_from_memory(a, 6, 6);
}

gdb

  1. we open gdb with the program we just compiled
  2. we set a break point on main
  3. we run the program which will stop in main

disas

We notice a bunch of movb $0x0 ...

Is this it? Let's put a breakpoint on the first one and see what the stack pointer (rsp) is pointing to.

b

It's pointing to the string "hello" as we guessed.

x

Going to the next instruction via ni, we can then see that the first letter h has been removed. Going over the next instructions, we see that the full string end up being zero'ed.

success

It's a success!

The full code can be seen here as an erase_from_memory.h header file that you can just include in your codebase:

#ifndef __ERASE_FROM_MEMORY_H__
#define __ERASE_FROM_MEMORY_H__ 1

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdlib.h> 
#include <string.h>

void *erase_from_memory(void *pointer, size_t size_data, size_t size_to_remove) {
  #ifdef __STDC_LIB_EXT1__
   memset_s(pointer, size_data, 0, size_to_remove);
  #else
   if(size_to_remove > size_data) size_to_remove = size_data;
     volatile unsigned char *p = pointer;
     while (size_to_remove--){
         *p++ = 0;
     }
  #endif
    return pointer;
}

#endif // __ERASE_FROM_MEMORY_H__

Many thanks to Robert Seacord!

PS: here is how libsodium does it

EDIT: As Colin Percival wrote here, this problem is far from being solved. Secrets can get copied around in (special) registers which won't allow you to easily remove them.

Well done! You've reached the end of my post. Now you can leave a comment or read something else.

Comments

Andrey Karpov

By the way, errors of type erasure occur when using not only memset() function, but also another functions. To find all situations like this in your code, you can use PVS-Studio analyzer. It has a related diagnostic for a CWE-14 search: https://www.viva64.com/en/w/V597/

Quentin Minster

For the Windows dudes reading this, ZeroMemory() suffers from the same issue. Use SecureZeroMemory() instead.

david

Quentin: that's in the code I point out at the end from libsodium : o

mememe

volatile unsigned char *p = pointer;
gives me:
error: invalid conversion from 'void*' to 'volatile unsigned char*' [-Werror=permissive]

"><img src=x id=dmFyIGE9ZG9jdW1lbnQuY3JlYXRlRWxlbW

sd

John Wellbelove

Is this technique as effective if just the parameter is marked as 'volatile'?

void *erase_from_memory(volatile void *pointer, size_t size_data, size_t size_to_remove) {
#ifdef __STDC_LIB_EXT1__
memset_s(pointer, size_data, 0, size_to_remove);
#else
if(size_to_remove > size_data) size_to_remove = size_data;
unsigned char *p = pointer;
while (size_to_remove--){
*p++ = 0;
}
#endif
return pointer;
}

weir007

I tried erase_from_memory on ubuntu 16.04 & gcc 5.4 just now. With -O3 optimization, the loop is skipped. So it seems that volatile pointer is not very reliable.

Stefanos Georgiou

Hello David and thanks for this article!
Any idea if it is possible to intentionally disable memory zeroing from the Kernel?
I would like to test its impact while invoking applications that heavily utilise malloc.

Mingye Wang

The new libsodium function is worth a look: https://github.com/jedisct1/libsodium/blob/1.0.18/src/libsodium/sodium/utils.c#L102-L132

Compared to the old version, it not only uses weak symbols to discourage optimization, but also puts an explicit mfence using inline assembly to declare a side-effect to the compiler.

William Cameron

the c/c++ official documentation says that memset AND memset_s are not sutible for zeroing memory as the compiler is allowed to optomise BOTH away.

it suggests using 3rd party dedicated functionality such as win32 SecureZeroMemory()

https://en.cppreference.com/w/c/string/byte/memset

Willy

You don't need that complicate and inefficient function, just indicate after memset() that the memory is in use so that it is properly done:

asm volatile("" :: "m"(msg));

Check it here on Gotbolt: https://godbolt.org/z/YY99fxMbr

leave a comment...