﻿# Безопасная очистка приватных данных

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

![0388_Safe_Clearing_of_Private_Data_ru/image1.png](https://import.viva64.com/docx/blog/0388_Safe_Clearing_of_Private_Data_ru/image1.png)

## memset\(\)

Возможно вы уже читали [статью](https://pvs-studio.ru/ru/blog/posts/cpp/k0041/) с описанием уязвимости программ, использующих [_memset\(\)_](https://msdn.microsoft.com/en-us/library/aa246471(v=vs.60).aspx) для затирания памяти\. Но она не в полном объеме раскрывает все возможные случаи неправильного использования _memset\(\)_\. Проблемы возникнут не только с очисткой буферов, созданных на стеке, но и с буферами, выделенными в динамической памяти\.

## Стек

Вначале рассмотрим случай из вышеуказанной статьи с использованием переменной, созданной на стеке\.

Напишем код, который работает с паролем:

```cpp
#include <string>
#include <functional>
#include <iostream>

//Приватные данные
struct PrivateData
{
  size_t m_hash;
  char m_pswd[100];
};

//Функция что-то делает с паролем
void doSmth(PrivateData& data)
{
  std::string s(data.m_pswd);
  std::hash<std::string> hash_fn;

  data.m_hash = hash_fn(s);
}

//Функция для ввода и обработки пароля
int funcPswd()
{
  PrivateData data;
  std::cin >> data.m_pswd;

  doSmth(data);
  memset(&data, 0, sizeof(PrivateData));
  return 1;
}

int main()
{
  funcPswd();
  return 0;
}
```

Пример достаточно условен, он полностью синтетический\.

Если мы соберем отладочную версию и выполним такой код под отладчиком \(я использовал Visual Studio 2015\), то увидим, что все в порядке\. Пароль и вычисленный хэш стираются после использования\. 

Посмотрим на ассемблерный код под отладчиком Visual Studio: 

```cpp
.... 
    doSmth(data);
000000013F3072BF  lea         rcx,[data]  
000000013F3072C3  call        doSmth (013F30153Ch)  
  memset(&data, 0, sizeof(PrivateData));
000000013F3072C8  mov         r8d,70h  
000000013F3072CE  xor         edx,edx  
000000013F3072D0  lea         rcx,[data]  
000000013F3072D4  call        memset (013F301352h)  
  return 1;
000000013F3072D9  mov         eax,1  
....
```



Наблюдаем вызов нашей функции _memset\(\)_, которая очистит приватные данные после использования\. 

Казалось бы, на этом можно закончить, но нет, попробуем собрать релиз\-версию с оптимизацией кода\. Посмотрим в отладчике, что у нас получилось:

```cpp
.... 
000000013F7A1035  call
        std::operator>><char,std::char_traits<char> > (013F7A18B0h)  
000000013F7A103A  lea         rcx,[rsp+20h]  
000000013F7A103F  call        doSmth (013F7A1170h)  
    return 0;
000000013F7A1044  xor         eax,eax   
....
```

Как видно, все инструкции, соответствующие вызову функции _memset\(\)_, удалены\. Компилятор посчитал, что нет смысла вызывать функцию очищающую данные, так как они больше не используются\. Это не ошибка, а законные действия компилятора\. С точки зрения языка вызов _memset\(\)_ не нужен, так как далее буфер не используется\. А раз так, удаление вызова _memset\(\)_ не окажет влияние на поведение программы\. Соответственно наши приватные данные не удалены из памяти, что очень плохо\. 

## Куча

А вот теперь давайте погрузимся глубже\. Проверим, а что будет с данными которые будут размещены в динамической памяти с помощью функции _malloc_ или оператора _new_\.

Модифицируем наш код для работы с _malloc_:



```cpp
#include <string>
#include <functional>
#include <iostream>

struct PrivateData
{
  size_t m_hash;
  char m_pswd[100];
};

void doSmth(PrivateData& data)
{
  std::string s(data.m_pswd);
  std::hash<std::string> hash_fn;

  data.m_hash = hash_fn(s);
}

int funcPswd()
{
  PrivateData* data = (PrivateData*)malloc(sizeof(PrivateData));
  std::cin >> data->m_pswd;
  doSmth(*data);
  memset(data, 0, sizeof(PrivateData));
  free(data);
  return 1;
}

int main()
{
  funcPswd();
  return 0;
}
```

Будем проверять Release\-версию, так как в Debug все вызовы находятся на своих местах\. После компиляции в Visual Studio 2015 посмотрим ассемблерный код:

```cpp
.... 
000000013FBB1021  mov         rcx,
        qword ptr [__imp_std::cin (013FBB30D8h)]  
000000013FBB1028  mov         rbx,rax  
000000013FBB102B  lea         rdx,[rax+8]  
000000013FBB102F  call
        std::operator>><char,std::char_traits<char> > (013FBB18B0h)  
000000013FBB1034  mov         rcx,rbx  
000000013FBB1037  call        doSmth (013FBB1170h)  
000000013FBB103C  xor         edx,edx  
000000013FBB103E  mov         rcx,rbx  
000000013FBB1041  lea         r8d,[rdx+70h]  
000000013FBB1045  call        memset (013FBB2A2Eh)  
000000013FBB104A  mov         rcx,rbx  
000000013FBB104D  call        qword ptr [__imp_free (013FBB3170h)]  
    return 0;
000000013FBB1053  xor         eax,eax  
....
```

Как видим, в этом случае с Visual Studio все в порядке, наша очистка данных работает\. Но давайте посмотрим, что будут делать другие компиляторы\. Попробуем использовать **gcc** версии 5\.2\.1 и **clang** версии 3\.7\.0\.



Для **gcc** и **clang** я немного модифицировал исходный код, была добавлена распечатка содержимого, находящегося в выделенной памяти, до очистки и после очистки памяти\. Я распечатал содержимое по указателю уже после освобождения памяти\. В реальных программах такого делать нельзя, так как совершенно неизвестно, как поведет себя программа в таком случае\. Но для эксперимента я позволил себе такую вольность\.

```cpp
....
#include "string.h"
....
size_t len = strlen(data->m_pswd);
for (int i = 0; i < len; ++i)
  printf("%c", data->m_pswd[i]);
printf("| %zu \n", data->m_hash);
memset(data, 0, sizeof(PrivateData));
free(data);
for (int i = 0; i < len; ++i)
  printf("%c", data->m_pswd[i]);
printf("| %zu \n", data->m_hash);
....
```

Итак, фрагмент ассемблерного кода, созданный компилятором **gcc**:

```cpp
movq (%r12), %rsi
movl $.LC2, %edi
xorl %eax, %eax
call printf
movq %r12, %rdi
call free
```

Сразу после распечатки содержимого \(_printf_\) мы видим вызов функции _free\(\)_, а вызов функции _memset\(\)_ удален\. Если исполнить код и ввести произвольный пароль \(например "MyTopSecret"\), то мы получим следующий вывод на экран:

MyTopSecret\| 7882334103340833743 

MyTopSecret\| 0

Хэш изменился\. Видимо это побочный эффект работы менеджера памяти\. Наш же секретный пароль "MyTopSecret", остался в неприкосновенном виде в памяти\. 

Теперь проверим для **clang**:

```cpp
movq (%r14), %rsi
movl $.L.str.1, %edi
xorl %eax, %eax
callq printf
movq %r14, %rdi
callq free
```

Наблюдаем аналогичную картину, вызов _memset\(\)_ удален\. Вывод на экран выглядит таким же образом:

MyTopSecret\| 7882334103340833743 

MyTopSecret\| 0

В данном случае, и **gcc**, и **clang** решили оптимизировать код\. Так как память после вызова функции _memset\(\)_ освобождается, то компиляторы считают этот вызов ненужным и удаляют его\.

Как оказалось, компиляторы при оптимизации удаляют вызов _memset\(\)_ при использовании и стековой и динамической памяти приложения\.

Ну и напоследок проверим как поведут себя компиляторы при выделении памяти с помощью _new_\.

Еще раз модифицируем код: 

```cpp
#include <string>
#include <functional>
#include <iostream>
#include "string.h"

struct PrivateData
{
  size_t m_hash;
  char m_pswd[100];
};

void doSmth(PrivateData& data)
{
  std::string s(data.m_pswd);
  std::hash<std::string> hash_fn;

  data.m_hash = hash_fn(s);
}

int funcPswd()
{
  PrivateData* data = new PrivateData();
  std::cin >> data->m_pswd;
  doSmth(*data);
  memset(data, 0, sizeof(PrivateData));
  delete data;
  return 1;
}

int main()
{
  funcPswd();
  return 0;
}
```

Visual Studio добросовестно чистит память:

```cpp
000000013FEB1044  call        doSmth (013FEB1180h)  
000000013FEB1049  xor         edx,edx  
000000013FEB104B  mov         rcx,rbx  
000000013FEB104E  lea         r8d,[rdx+70h]  
000000013FEB1052  call        memset (013FEB2A3Eh)  
000000013FEB1057  mov         edx,70h  
000000013FEB105C  mov         rcx,rbx  
000000013FEB105F  call        operator delete (013FEB1BA8h)  
    return 0;
000000013FEB1064  xor         eax,eax
```

Компилятор **gcc** в этом случае также решил оставить код для очистки памяти:

```cpp
call printf
movq %r13, %rdi
movq %rbp, %rcx
xorl %eax, %eax
andq $-8, %rdi
movq $0, 0(%rbp)
movq $0, 104(%rbp)
subq %rdi, %rcx
addl $112, %ecx
shrl $3, %ecx
rep stosq
movq %rbp, %rdi
call _ZdlPv
```

Соответственно изменился и вывод на экран, наши данные удалены:

MyTopSecret\| 7882334103340833743 

\| 0

А вот **clang** решил опять оптимизировать наш код и вырезал "ненужную" функцию:

```cpp
movq (%r14), %rsi
movl $.L.str.1, %edi
xorl %eax, %eax
callq printf
movq %r14, %rdi
callq _ZdlPv
```

Распечатаем содержимое памяти:

```cpp
MyTopSecret| 7882334103340833743 
MyTopSecret| 0
```

Пароль остался жить в памяти и ждать, когда его украдут\.

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

Как же правильно очистить приватные данные? 

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

В Visual Studio, например, можно использовать [_RtlSecureZeroMemory_](https://msdn.microsoft.com/en-us/library/windows/hardware/ff562768%28v=vs.85%29.aspx)\. Начиная с C11 существует функция [_memset\_s_](https://en.cppreference.com/w/c/string/byte/memset)\. В случае необходимости вы можете создать свою собственную безопасную функцию\. В интернете достаточно много примеров, как её сделать\. Вот некоторые из вариантов\.

Вариант [N1](https://wiki.sei.cmu.edu/confluence/display/c/msc06-c.+beware+of+compiler+optimizations)\.

```cpp
errno_t memset_s(void *v, rsize_t smax, int c, rsize_t n) {
  if (v == NULL) return EINVAL;
  if (smax > RSIZE_MAX) return EINVAL;
  if (n > smax) return EINVAL;
  volatile unsigned char *p = v;
  while (smax-- && n--) {
    *p++ = c;
  }
  return 0;
}
```

Вариант [N2](https://stackoverflow.com/questions/13299420/mac-os-x-equivalent-of-securezeromemory-rtlsecurezeromemory/13299459)\.

```cpp
void secure_zero(void *s, size_t n)
{
    volatile char *p = s;
    while (n--) *p++ = 0;
}
```

Некоторые идут дальше и делают функцию, которые заполняют массив псевдослучайными значениями и при этом работают различное время, чтобы затруднить атаки, связанные с замером времени\. Их реализацию также можно найти в интернете\.

## Заключение

Статический анализатор PVS\-Studio умеет находить такие ошибки\. Он сигнализирует о проблемной ситуации с помощью диагностики [V597](https://pvs-studio.ru/ru/docs/warnings/v597/)\. Эта статья как раз и написана, как расширенное описание того, почему эта диагностика важна\. К сожалению, многие программисты считают, что анализатор "придирается" к их коду и на самом деле никаких проблемы нет\. Ведь программист видит вызов функции _memset\(\)_ в отладчике, забыв что это отладочная версия\.