Компиляторы C ++ могут оптимизировать записи в память:
{
//all this code can be eliminated
char buffer[size];
std::fill_n( buffer, size, 0);
}
При работе с конфиденциальными данными типичный подход использует volatile*
указатели, чтобы гарантировать, что записи памяти испускаются компилятором. Вот как SecureZeroMemory()
реализована функция в библиотеке времени выполнения Visual C ++ (WinNT.h):
FORCEINLINE PVOID RtlSecureZeroMemory(
__in_bcount(cnt) PVOID ptr, __in SIZE_T cnt )
{
volatile char *vptr = (volatile char *)ptr;
#if defined(_M_AMD64)
__stosb((PBYTE )((DWORD64)vptr), 0, cnt);
#else
while (cnt) {
*vptr = 0;
vptr++;
cnt--;
}
#endif
return ptr;
}
Функция бросает переданный указатель на volatile*
указатель, а затем пишет через последний. Однако, если я использую его в локальной переменной:
char buffer[size];
SecureZeroMemory( buffer, size );
сама переменная не volatile
, Таким образом, согласно стандартному определению C ++ наблюдаемое поведение записывается в buffer
не считается наблюдаемым поведением и похоже, что его можно оптимизировать.
Ниже приведено много комментариев о файлах страниц, кэшах и т. Д., Которые все действительны, но давайте просто проигнорируем их в этом вопросе. Вопрос только в том, оптимизирован ли код для записи в память или нет.
Можно ли гарантировать, что код, выполняющий запись в память, не оптимизирован в C ++? Это решение в SecureZeroMemory()
соответствует стандарту C ++?
Нет портативного решения. Если он хочет, компилятор мог бы сделать копии данных, пока вы использовали их в нескольких местах в памяти, и любая нулевая функция могла бы обнулять только ту, которую она использовала в то время. Любое решение будет непереносимым.
С библиотечными функциями, такими как SecureZeroMemory
авторы библиотек, как правило, прилагают все усилия, чтобы компилятор не вставлял такие функции.
Это означает, что во фрагменте
char buffer[size];
SecureZeroMemory( buffer, size );
компилятор не знает что SecureZeroMemory
делает с buffer
оптимизатор не может доказать, что удаление фрагмента не влияет на наблюдаемое поведение программы.
Другими словами, разработчики библиотеки уже сделали все возможное, чтобы такой код не был оптимизирован.
volatile
Ключевое слово может быть применено к указателю (или ссылке, в C ++), не требуя приведения, означая, что доступ через этот указатель не должен быть оптимизирован. Объявление переменной не имеет значения.
Поведение аналогично const
:
char buffer[16];
char const *p = buffer;
buffer[0] = 'a'; // okay
p[0] = 'b'; // error
Это const
указатель на буфер существует, никак не изменяет поведение переменной, только поведение модифицированного указателя. Если переменная объявлена const
тогда запрещено генерироватьconst
указатели на это:
char const buffer[16];
char *p = buffer; // error
Так же,
char buffer[16];
char volatile *p = buffer;
buffer[0] = 'a'; // may be optimized out
p[0] = 'b'; // will be emitted
а также
char volatile buffer[16];
char *p = buffer; // error
Компилятор может свободно удалять доступ черезvolatile
lvalues а также вызовы функций, где он может доказать, что нет доступа к volatile
Значения случаются.
RtlSecureZeroMemory
Функция безопасна в использовании, потому что компилятор может видеть определение (включая volatile
доступ внутри цикла или, в зависимости от платформы, оператор ассемблера, который непрозрачен для компилятора и, следовательно, предполагается, что он не оптимизируемый), или он должен предполагать, что функция будет выполнять volatile
доступ.
Если вы хотите избежать зависимости от <WINNT.H> заголовочный файл, тогда аналогичная функция будет отлично работать с любым соответствующим компилятором.
Всегда существует состояние гонки между тем, когда в памяти хранится конфиденциальная информация, и временем, когда вы стираете ее. В это время ваше приложение может аварийно завершить работу и выгрузить ядро, или злонамеренный пользователь может получить дамп памяти адресного пространства процесса с конфиденциальной информацией в виде простого текста.
Может быть, вы не должны хранить конфиденциальную информацию в памяти в виде простого текста. Таким образом вы достигнете лучшей безопасности и полностью обойдете эту проблему.
Ни C, ни C ++ Standard не предъявляют никаких требований к тому, как реализации хранят вещи в физической памяти. Однако реализации могут свободно определять такие вещи, а качественные реализации, которые подходят для приложений, требующих определенных поведений физической памяти, будут указывать, что они будут последовательно вести себя подходящим образом.
Многие реализации обрабатывают как минимум два различных диалекта. При обработке своего диалекта «оптимизации отключены» они часто подробно описывают, сколько действий будет взаимодействовать с физической памятью. К сожалению, включение оптимизации обычно переключается на семантически более слабый диалект, который почти ничего не гарантирует о том, как какие-либо действия будут взаимодействовать с физической памятью. В то время как должна быть возможность обрабатывать много простых и простых оптимизаций, все еще обрабатывая вещи способом, который согласуется с диалектом «отключенных оптимизаций» в определенных легко идентифицируемых случаях, где это могло бы иметь значение, авторы компилятора не заинтересованы в обеспечение режимов, ориентированных на безопасные низко висящие фрукты.
Единственный надежный способ гарантировать, что физическая память обрабатывается определенным образом, — это использовать диалект, который обещает обращаться с физической памятью таким образом. Если вы сделаете это, получить необходимое лечение, как правило, будет легко. Если этого не сделать, ничто не гарантирует, что «творческая» реализация не сделает ничего неожиданного.