﻿# Перезаписывать память \- зачем?

Мы решили опубликовать эту статью в базе знаний, чтобы продемонстрировать программистам, как легко приватные данные могут выйти за рамки программы, работающей с ними\. В анализаторе PVS\-Studio есть диагностика V597, позволяющая выявлять вызовы функции memset\(\), которые не очищают память\. Но опасность выглядит неубедительной и не правдоподобной\. Эта статья хорошо показывает, что опасность реальна и её нельзя игнорировать\.


> Статья написана сотрудником компании ABBYY, впервые опубликована: "\[Блог компании ABBYY\\\. Перезаписывать память – зачем?\]\(https://habr\.com/ru/company/abbyy/blog/127259/\)", публикуется здесь с разрешения правообладателя\\\.

В недрах Win32 API есть [функция SecureZeroMemory](https://msdn.microsoft.com/en-us/library/aa366877(v=vs.85).aspx) с очень лаконичным описанием, из которого следует, что эта функция перезаписывает область памяти нулями и устроена таким образом, что компилятор при оптимизации кода никогда не удаляет вызов этой функции\. Там же говорится, что следует с помощью этой функции перезаписывать память, ранее использованную для хранения паролей и криптографических ключей\.

Остается один вопрос \- зачем это? Можно найти пространные рассуждения о риске записи памяти программы в файл подкачки, файл hibernate или аварийный дамп, где его может найти злоумышленник\. Это похоже на паранойю \- далеко не всякий злоумышленник имеет возможность наложить руку на эти файлы\.

На самом деле, возможностей получить доступ к данным, которые программа забыла перезаписать, гораздо больше \- иногда не нужно даже иметь доступа к машине\. Дальше мы рассмотрим пример, и каждый сам решит, насколько оправдана паранойя\.

Все примеры будут в псевдокоде, подозрительно похожем на C\+\+\. Будет много букв и не очень чистого кода, потом станет понятно, что в более чистом коде ситуация ненамного лучше\.

Итак\. В далекой\-далекой функции мы получаем ключ шифрования, пароль или номер кредитной карты \(далее \- просто секрет\), используем его и не перезаписываем:

```cpp
{
  const int secretLength = 1024;
  WCHAR secret[secretLength] = {};
  obtainSecret( secret, secretLength );
  processWithSecret( what, secret, secretLength );
}
```

В другой, совершенно никак не связанной с предыдущей, функции, наш экземпляр программы запрашивает у другого экземпляра файл с некоторым именем\. Для этого используется RPC \- древняя как динозавры технология, присутствующая на многих платформах и широко используемая Windows для реализации межпроцессного и межмашинного взаимодействия\.

Обычно для использования RPC нужно написать описание интерфейса на языке IDL\. В нем будет описание метода примерно такого вида:

```cpp
//MAX_FILE_PATH == 1024
error_status_t rpcRetrieveFile(
    [in] const WCHAR fileName[MAX_FILE_PATH],
    [out] BYTE_PIPE filePipe );
```

здесь второй параметр имеет специальный тип, дающий возможность передавать потоки данных произвольной длины\. Первый параметр \- массив символов под имя файла\.

Это описание компилируется компилятором MIDL, получается заголовочный файл \(\.h\) с функцией

```cpp
error_status_t rpcRetrieveFile (
  handle_t IDL_handle, 
  const WCHAR fileName[1024], 
  BYTE_PIPE filePipe);
```

здесь MIDL добавил служебный параметр, а второй и третий параметры те же, что были в предыдущем описании\.

Вызываем эту функцию:

```cpp
void retrieveFile( handle_t binding )
{
  WCHAR remoteFileName[MAX_FILE_PATH];
  retrieveFileName( remoteFileName, MAX_FILE_PATH );
  CBytePipeImplementation pipe;
  rpcRetrieveFile( binding, remoteFileName, pipe );           
}
```

Все отлично \- retrieveFileName\(\) получает строку длиной не более MAX\_FILE\_PATH−1, завершенную нулевым символом \(нулевой символ не забыли\), вызываемая сторона получает строку и работает с ней \- получает полный путь к файлу, открывает его и передает данные из него\.

Все полны оптимизма, с этим кодом делается несколько выпусков продукта, но слона пока никто не заметил\. Слон вот\. С точки зрения C\+\+, параметр функции

```cpp
const WCHAR fileName[1024]
```

это не массив, а указатель на первый элемент массива\. Функция rpcRetrieveFile\(\) \- всего лишь прослойка, которая сгенерирована тем же MIDL\. Она упаковывает все свои параметры и вызывает всегда одну и ту же функцию WinAPI NdrClientCall2\(\), смысл которой "Windows, выполни, пожалуйста, RPC\-вызов вооот с этими параметрами", и передает параметры списком функции NdrClientCall2\(\)\. Одним из первых параметров идет строка форматирования, сгенерированная MIDL по описанию в IDL\. Очень похоже на старый добрый printf\(\)\.

NdrClientCall2\(\) внимательно смотрит на полученную строку форматирования и упаковывает параметры для передачи другой стороне \(это называется marshalling\)\. Рядом с каждым параметром указан его тип \- каждый параметр упаковывается в зависимости от типа\. В нашем случае для параметра fileName указан адрес первого элемента массива и в качестве типа \- "массив из 1024 элементов типа WCHAR"\.

Теперь в коде встречаем подряд два вызова:

```cpp
processWithSecret( whatever );
retrieveFile( binding );
```

Функция processWithSecret\(\) отъедает 2 килобайта под хранение секрета на стеке, а при завершении забывает о них\. Дальше вызывается функция retrieveFile\(\), она извлекает имя файла длиной 18 символов \(18 символов \+ завершающий нулевой \- всего 19, т\.е\. 38 байт\)\. Имя файла снова хранится на стеке и скорее всего, это будет точно та же область памяти, что была использована под секрет в первой функции\.

Дальше происходит удаленный вызов и функция упаковки добросовестно упаковывает весь массив \(не 38 байт, а 2048\) в пакет и этот пакет затем передается по сети\.

КРАЙНЕ НЕОЖИДАННО

Секрет передается по сети\. Программа даже не планировала когда\-либо передавать секрет по сети, но он передается\. Такой дефект может быть гораздо удобнее в "использовании", чем даже просмотр файла подкачки\. Кто теперь параноик?

Пример выше выглядит довольно сложным\. Вот похожий по смыслу код, который можно опробовать на codepad\.org

```cpp
const int bufferSize = 32;

void first()
{
  char buffer[bufferSize];
  memset( buffer, 'A', sizeof( buffer ) );
}

void second()
{
  char buffer[bufferSize];
  memset( buffer, 'B', bufferSize / 2 );
  printf( "%s", buffer );
}

int main()
{
  first();
  second();
}
```

В нем неопределенное поведение\. На момент написания поста результат работы \- строка из 16 символов 'B' и 16 символов 'A'\.

Сейчас самое время для размахивания вилами и факелами и гневных возгласов, что никто в своем уме не использует обычные массивы, что нужно использовать std::vector, std::string и класс УниверсальныйВсемогутер, которые "правильно" работают с памятью, и священных войн на не менее чем 9 тысяч комментариев\.

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

Кто здесь виноват? Как обычно, виноват разработчик \- он неверно понял, как функция rpcRetrieveFile\(\) работает с полученными параметрами\. В результате \- неопределенное поведение, которое в данном случае приводит к неконтролируемой передаче данных по сети\. Это исправляется либо изменением RPC\-интерфейса и правкой кода на обеих сторонах, либо использованием массива достаточно большого размера и его полной перезаписью перед копированием в него параметра\.

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