Безопасность доступа к памяти

Безопасность доступа к памяти (англ. memory safety) — концепция в разработке программного обеспечения, направленная на предотвращение программных ошибок, приводящих к уязвимостям, связанным с обращением к оперативной памяти компьютера, таким как переполнение буфера и висячие указатели.

Языки программирования с низким уровнем абстракции, такие как Си и Си++, поддерживающие непосредственный доступ к памяти компьютера (произвольную арифметику указателей, выделение и освобождение памяти) и приведение типов, но не имеющие автоматической проверки границ массивов, не считаются безопасными с точки зрения доступа к памяти[1][2]. Тем не менее, Си и C++ предоставляют инструменты (такие как умные указатели), которые помогают повысить безопасность доступа к памяти. Также с этой целью применяются техники управления памятью[3]. Однако, полностью избежать ошибок доступа к памяти, особенно в сложных системах, часто не удаётся[4].

Уязвимости, связанные с доступом к памяти

Один из наиболее распространённых классов уязвимостей программного обеспечения — проблемы безопасности памяти[5][6]. Эта проблема известна с 1980-х годов[2]. Под безопасностью памяти понимается предотвращение попыток использовать или изменять данные, если это не было явно определено программистом при создании программы[6].

Множество критически важных для производительности программ написаны на языках низкого уровня (Си и Си++), которые склонны к подобным уязвимостям. Отсутствие защищённости этих языков позволяет злоумышленникам получить полный контроль над программой, менять поток управления и получать несанкционированный доступ к конфиденциальной информации[2]. В настоящее время предложено множество решений защиты памяти, которые должны быть эффективны одновременно по критериям безопасности и производительности[2].

Впервые публичные обсуждения ошибок памяти состоялись в 1972 году[7]. В дальнейшем они оставались проблемой для многих программных продуктов, выступая причиной для применения эксплойтов. Например, червь Морриса использовал несколько уязвимостей, некоторые из которых были связаны с ошибками работы с памятью[7].

Типы ошибок памяти

Различают несколько типов ошибок памяти (уязвимостей), которые могут возникать в некоторых языках программирования[2][8]:

  • Нарушение границ массивов — выход обращения за пределы размера массива.
  • Переполнение буфера — запись за пределами выделенного буфера[9].
  • Чтение за границами буфера — чтение данных за границей выделенного буфера[10].
  • Ошибки при работе с динамической памятью — неправильное управление памятью и указателями во время выполнения[11].
    • Висячий указатель — указатель, не ссылающийся на допустимый объект. Особый подтип — использование после освобождения[12].
    • Обращение по нулевому указателю — вызывает исключение и аварийную остановку программы[13].
    • Двойное освобождение памяти — повторный вызов освобождения для одной области памяти[14].
    • Смешивание менеджеров памяти — использование различных средств управления памятью для одной области, например mix malloc/free и new/delete[15].
    • Потеря указателя — утеря адреса выделенного блока памяти, ведущая к утечке памяти[16].
  • Неинициализированные переменные — переменные, объявленные, но не инициализированные значением[17].
  • Ошибки нехватки памяти
    • Переполнение стека — превышение допустимого размера стека вызовов[18].
    • Переполнение кучи — попытка выделить больше памяти, чем доступно[19].

Обнаружение ошибок

Ошибки работы с памятью могут быть выявлены как на этапе компиляции, так и во время исполнения программы.

Для выявления ошибок до компиляции используются статические анализаторы кода, которые позволяют обнаружить:

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

Во время отладки могут использоваться специальные менеджеры памяти, создающие «мёртвые» области вокруг объектов для обнаружения ошибок[20]. Альтернативой являются специализированные виртуальные машины (например, Valgrind), а также системы инструментирования кода, такие как различные санитайзеры.

Способы обеспечения безопасности

Большинство языков высокого уровня устраняют эти проблемы удалением арифметики указателей, ограничением приведения типов и обязательным введением сборки мусора[21]. В отличие от низкоуровневых языков, высокоуровневые часто выполняют дополнительные проверки, например, контроля границ при доступе к массивам и объектам[22].

В современном Си++ безопасность обеспечивают умные указатели, реализующие идиому RAII, обеспечивающую автоматическое освобождение ресурсов при уничтожении объекта[23].

При использовании библиотечных функций важно проверять возвращаемые значения[24]. Например, в Си при ошибке выделения памяти возвращается нулевой указатель, а в Си++ генерируется исключение. Корректная обработка этих ситуаций предотвращает аварийное завершение программы.

Аппаратные расширения, такие как Intel MPX, ускоряют проверки границ указателей[25].

На уровне операционной системы безопасность памяти обеспечивается менеджером виртуальной памяти, разделяющим адресные пространства, и средствами синхронизации при многопоточности[26]. Аппаратный уровень обычно реализует дополнительные механизмы, например кольца защиты[27].

Наиболее надёжным, но и наименее дешёвым способом обеспечения безопасности доступа к памяти является формальная верификация, например, с помощью логики разделения[28].

См. также

Примечания

Литература

Ссылки