Безопасность, безопасность! А вы её тестируете?

В коде программ нет мест, где нельзя допустить ошибку. Ошибка может быть в самом простом месте. Если алгоритмы, механизмы обмена данными и интерфейсы люди привыкли тестировать, то с безопасностью всё обстоит гораздо хуже. Часто она реализуется по остаточному принципу. Программист думает, вот сейчас пару строк напишу, и всё будет хорошо. И даже тестировать не надо. Код слишком прост, чтобы допустить в нем ошибку! А вот и нет. Раз занимаетесь безопасностью и пишите какой-то код для этого, то тестируйте его не менее тщательно!

Где важна безопасность? Во многих приложениях. Но не будем рассуждать абстрактно. Давайте возьмём, например, исходные коды приложения Tor. Это свободное программное обеспечение для реализации второго поколения так называемой "луковой маршрутизации". Это система, позволяющая устанавливать анонимное сетевое соединение, защищённое от прослушивания. Подробнее что это такое и для чего используется, можно узнать, открыв статью в Википедии.

Казалось бы, в таком приложении надо быть максимально внимательным к безопасности данных. И не просто внимательным! Скажем так, такое приложение стоит разрабатывать, только находясь в состоянии паранойи и мании преследования.

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

Одним из механизмов страховки является обнуление буферов, которые больше не используются. В таких буферах могут содержаться пароли, ip-адреса и другие пользовательские данные. Если эти данные не уничтожать, то они в виде мусора потом могут быть отправлены в интернет. И это не сказки, а реалии жизни. О том, как такое может произойти, можно почитать в статье "Перезаписывать память - зачем?".

Разработчики TOR знают об этой опасности. И пытаются затирать содержимое буферов, используя функцию memset(). Это Epic Fail. Компилятор вправе убрать из кода вызов функции memset(), если заполняемый ею буфер более нигде не используется.

Рассмотрим фрагмент кода, взятый из TOR:

int
crypto_pk_private_sign_digest(....)
{
  char digest[DIGEST_LEN];
  ....
  memset(digest, 0, sizeof(digest));
  return r;
}

Смотрите. На стеке создается буфер 'digest'. Далее он используется. Не важно, как. Главное что в конце мы хотим его обнулить. Для этого написан вызов функции memset(). Однако, после этого буфер 'digest' более в функции никак не используется. При оптимизации компилятор заметит это и удалит вызов функции. Это не изменит логику работы программы. Но это сделает её опасной с точки зрения приватности данных.

Те, кого интересуют подробности, могут заглянуть сюда. Здесь приведён ассемблерный листинг, где показано как исчезает вызов функции memset(). В качестве компилятора используется Visual C++ 2010 и ключ "/O2".

Чтобы гарантированно затереть память следует использовать такие функции, как RtlSecureZeroMemory(). Они специально созданы для таких случаев и не могут быть удалены компилятором.

Вы скажете, что из мухи я делаю слона. И что никакие важные данные никуда не попадут. Возможно. Но кто даст гарантию? Раз разработчики сделали обнуление массива, то значит, они чего-то опасались. И делали они это не в одном, и не в двух местах. Много где. Жаль только, что усилия часто потрачены впустую. Чтобы не быть голословным, приведу список мест с ошибками.

Список файлов и строк, где анализатор PVS-Studio выдал предупреждение "V597 The compiler could delete the 'memset' function call, which is used to flush '...' buffer. The RtlSecureZeroMemory() function should be used to erase the private data":

  • crypto.c 1015
  • crypto.c 1605
  • crypto.c 2233
  • crypto.c 2323
  • tortls.c 2453
  • connection_or.c 1798
  • connection_or.c 2128
  • onion.c 295
  • onion.c 384
  • onion.c 429
  • rendclient.c 320
  • rendclient.c 321
  • rendclient.c 699
  • rendclient.c 942
  • rendclient.c 1284
  • rendclient.c 1285
  • rendservice.c 705
  • rendservice.c 900
  • rendservice.c 903
  • rendservice.c 904
  • rendservice.c 905
  • rendservice.c 906
  • rendservice.c 1409
  • rendservice.c 1410
  • rendservice.c 1411
  • rendservice.c 1412
  • rendservice.c 1413
  • rendservice.c 1414
  • rendservice.c 1415
  • rendservice.c 2078
  • rendservice.c 2079
  • rendservice.c 2080
  • rendservice.c 2516
  • rendservice.c 2517
  • rendservice.c 2518
  • rendservice.c 2668
  • rendservice.c 2669
  • rendservice.c 2670
  • tor-gencert.c 108

Я сознательно привел такой длинный список. Я хочу, чтобы вы осознали всю глубину проблемы отсутствия проверок кода, который должен позаботиться о безопасности. Кажется, ну как можно ошибиться, используя memset()!? Оказывается, ещё как можно.

И это проблема не только TOR. Это вообще проблема многих приложений и библиотек. Далеко ходить не надо. Какие библиотеки использует TOR? Например, он использует OpenSSL. Это криптографический пакет с открытым исходным кодом для работы с SSL/TLS. Давайте посмотрим, как очищают память разработчики OpenSSL.

Разработчики OpenSSL знают, что нельзя использовать memset() для обнуления буферов памяти. Поэтому, они написали свою функцию. Вот она:

unsigned char cleanse_ctr = 0;
void OPENSSL_cleanse(void *ptr, size_t len)
{
  unsigned char *p = ptr;
  size_t loop = len, ctr = cleanse_ctr;
  while(loop--)
  {
    *(p++) = (unsigned char)ctr;
    ctr += (17 + ((size_t)p & 0xF));
  }
  p=memchr(ptr, (unsigned char)ctr, len);
  if(p)
    ctr += (63 + (size_t)p);
  cleanse_ctr = (unsigned char)ctr;
}

Отличный параноидальный код. К нему претензий нет. Он затрет память. Причем не просто нулями, а случайными числами.

Вот только из-за ошибок, эта функция не спасает, и приватные данные останутся на своём месте. Посмотрим вот на этот код:

void usage(void)
{
  static unsigned char *buf=NULL,*obuf=NULL;
  ....
  OPENSSL_cleanse(buf,sizeof(buf));
  OPENSSL_cleanse(obuf,sizeof(obuf));
  ....  
}

Столько усилий было потрачено для написания функции OPENSSL_cleanse(). И всё мимо.

Присмотритесь. Не замечаете ничего плохого?

Выражения sizeof(buf) и sizeof(obuf) вычисляют размер указателя, а вовсе не размер буфера. В результате, в 32-битной программе в буфере будут затерты только первые 4 байта. Все остальные приватные данные стерты не будут.

Можно найти в OpenSSL и другие идентичные ошибки (см. V597):

  • ec_mult.c 173
  • ec_mult.c 176

Выводы:

  1. Если безопасность данных является важной составляющей в вашем программном обеспечении, то вы должны реализовывать соответствующие тесты. Например, создавая юнит-тесты для функции, вы должны дополнительно проверить, что в стеке не осталась важных данных. Для этого можно вызвать функцию, вначале которой создать массив вида "char buf[10000]" и поискать в нём строки, которые могли остаться в стеке.
  2. Тестируйте не только DEBUG версию, но и RELEASE. Рассмотренные в статье ошибка c memset() в DEBUG версии не проявят себя.
  3. Используйте статические анализаторы кода. Они могут сказать очень много интересного про ошибки и небезопасные места в коде.
  4. Критические с точки зрения приложения лучше делать открытыми. Сейчас я случайно забрел в открытый проект TOR и нашёл ошибки. Эту информацию можно использовать, чтобы сделать продукт лучше. А могу ли я проверить закрытый код? Нет. А значит, подобные ошибки могут оставаться незамеченными разработчиками годами.
  5. Сколько бы у программиста не было опыта и стажа, он не застрахован он простых и глупых ошибок. Помните, что утверждение "профессиональные программисты не допускают простых ошибок и опечаток" это миф. Это не так. Лучше быть самокритичным. Уже одно осознание, что можно допустить ошибку, помогает избежать многих из них. Зная это, вы не поленитесь написать дополнительный тест, запустить анализатор кода и просто перечитать написанный код.
Для получения подробной информации о возможностях оптимизации компилятора обратитесь к нашему Уведомлению об оптимизации.