Может ли обнуление памяти путем ксерокопирования данных с самим собой быть оптимизировано в C? - PullRequest
0 голосов
/ 05 мая 2018

Иногда в целях безопасности нам нужно обнулить память, чтобы предотвратить непреднамеренный доступ к конфиденциальным данным, например, чтобы безопасно удалить ключ после шифрования некоторых данных. Большинство людей предлагают сделать это, чтобы записать случайные данные в массив, содержащий конфиденциальную информацию, потому что это не может быть оптимизировано компилятором. Понятно, что наивное использование таких функций, как memset, может быть оптимизировано оптимизирующим компилятором благодаря правилу «как будто», если это последняя операция, выполняемая с данными до того, как они выйдут из области видимости. Однако получение и запись случайных данных идет медленно, и я, возможно, нашел решение. Я хочу получить экспертное заключение, прежде чем развертывать его в рабочем коде.

Хропирование чего-либо с самим собой, по самой природе оператора, всегда приводит к нулевому значению, и это очень быстро. Обход блока памяти и его ксерокопирование сами по себе, кажется, очень эффективным решением проблемы обнуления, но я боюсь, что его можно оптимизировать с помощью достаточно хорошего оптимизирующего компилятора. Он кроссплатформенный и переносимый, и не требует использования стандартной библиотеки, за исключением использования типа данных size_t. Я включил справочную реализацию того, что я имею в виду ниже. В нем есть функция с именем nuke, которая принимает указатель data_to_zero и итеративно size байтов xor с самим собой.

void nuke (void *data_to_zero, size_t size)
{
    size_t i;

    for (i = 0; i < size; i++) {
        ((unsigned char*)data_to_zero)[i] ^= ((unsigned char*)data_to_zero)[i];
    }
}

Эта реализация довольно медленная, но значительно быстрее, чем получение достаточно случайных данных и запись их в data. После оптимизации он работает быстрее, чем memset реализации, к которым у меня есть доступ в любом случае, что удивительно.

Я еще не изучил ассемблер, но вывод сборки после оптимизации с использованием GCC и Clang на уровне O2 и O3 на 64-битном процессоре x86 содержит инструкцию xorl где-то в коде, иногда дважды. Это указывает мне на то, что ксерокопирование памяти действительно происходит, но я хотел бы, чтобы кто-то, кто знает, о чем они говорят, подтвердил.

Это жизнеспособное решение?

Ответы [ 2 ]

0 голосов
/ 06 мая 2018

Вероятно, хороший, не зависящий от языка способ сделать это - позволить чему-либо зависеть от значений в памяти. Например, вы могли бы сначала установить в ноль всю память, , а затем перебрать все байты, преобразовав их во временную переменную. Если ответ отличен от нуля, выведите исключение. Это исключение, конечно, никогда не должно произойти, но компилятор вряд ли это выяснит (особенно если XOR не выполняется в другой конструкции цикла, отсюда и требование выполнения операций в последовательности).

Это дает дополнительное преимущество - выдает ошибку (с вероятностью 255 из 256, если сравниваются байты), если массив изменен или если (ненулевой) ключевой материал остался в массиве. Это может быть хорошей идеей во встроенных средах, где производительность ЦП может быть изменена.

Операция относительно эффективна, так как состоит из одного цикла с предварительно определенным числом циклов и одной , если -строить, кроме, конечно, доступа к памяти.

Это можно сочетать с безопасными способами установки содержимого массива на ноль, как в ответе Малина .

0 голосов
/ 05 мая 2018

Правильный способ сделать это - вызвать функцию memset_s(). Он использует спецификатор volatile для информирования компилятора о том, что вызов функции memset_s () не следует оптимизировать.

К сожалению, это решение может быть не настолько эффективным, насколько это возможно, из-за природы энергозависимого типа, защищающего от всех видов оптимизаций, оно может помешать компилятору использовать оптимальные инструкции по сборке и может привести к менее эффективному коду. Другая проблема с memset_s() заключается в том, что он представлен в C11.

Если вы не можете использовать memset_s(), то вы должны рассмотреть один из следующих методов:

  1. Другим решением может быть «прикоснуться» к памяти путем доступа к памяти после memset (), например, *(volatile char*)pwd= *(volatile char*)pwd. Проблема этого решения в том, что оно может работать не для всех реализаций.
  2. написать свою собственную версию memset_s() (ПРИМЕР 1). Проблема в том, что он все еще не гарантированно работает - стандарт C утверждает, что доступ к изменчивым объектам является частью неизменяемого наблюдаемого поведения - но он ничего не говорит о доступах через выражения lvalue с изменяемыми типами
  3. Насколько мне известно, лучший способ - использовать указатель на переменную функцию (ПРИМЕР 2)

В заключение: независимо от того, что вы выберете, настоятельно рекомендуется также всегда проверять полученный сборочный код на предмет того, что память действительно очищена и ни один из вызовов памяти не оптимизирован.

ПРИМЕР 1.

static void secure_memzero(void * p, size_t len)
{ 
    volatile uint8_t * _p = p;

    while (len--) *_p++ = 0;
}

ПРИМЕР 2.

static void * (* const volatile memset_ptr)(void *, int, size_t) = memset;

static void secure_memzero(void * p, size_t len)
{

    (memset_ptr)(p, 0, len);
}

void
dosomethingsensitive(void)
{
    uint8_t key[32];

    ...

    /* Zero sensitive information. */
    secure_memzero(key, sizeof(key));
}
...