Доклады, выступления, видео и электронные публикации

Особенности реализации механизма очистки освобождаемых областей оперативной памяти в GNU/Linux

Прокопов В.С., Каннер А.М.
Россия, Москва, ЗАО «ОКБ САПР»

Одним из требований к средствам защиты информации от несанкционированного доступа (СЗИ НСД) является наличие механизма очистки или обезличивания освобождаемых областей оперативной памяти при ее перераспределении. Необходимость в наличии такого механизма защиты регламентировано требованиями государственных регуляторов в области защиты информации:

  • РД ФСТЭК «Средства вычислительной техники. Защита от несанкционированного доступа к информации. Показатели защищенности от НСД к информации» (для классов СВТ 4, 3, 2 и 1);
  • РД ФСТЭК «Автоматизированные системы. Защита от несанкционированного доступа к информации. Классификация автоматизированных систем и требования по защите информации» (для классов АС 3А, 2А, 1Г, 1В, 1Б и 1А)

Вообще говоря, в ОС семейства Linux за счет изолированности памяти каждого процесса (каждый процесс выполняется в собственном адресном пространстве), требование по очистке оперативной памяти носит скорее дополнительный характер. Связано это с тем, что в соответствии с остальными требованиями, предъявляемыми к СЗИ НСД, «нелегитимно» получить доступ к памяти какого-либо процесса может только пользователь с достаточно высокими привилегиями (т.е. это внутренний нарушитель и только с административными полномочиями). В рамках же любого класса защищенности по РД возможность такого доступа фактически пропадает с вводом корректным политик безопасности. Так или иначе очищать оперативную память необходимо хотя бы для уверенности. Разберемся по порядку, чем может быть опасен вариант отсутствия механизмов очистки памяти.

Очистка оперативной памяти: что и зачем очищать?

В оперативную память в процессе работы любого СВТ и любой ОС загружается любая обрабатываемая информация, в том числе и критично важные данные, такие как:

  • ключи шифрования или подписи;
  • пароли, вводимые пользователем;
  • конфиденциальная информация;
  • прочее.

В ОС Linux в определенных ситуациях существует возможность получить доступ на чтение к оперативной памяти (/dev/mem, /dev/kmem), и, следовательно, получить доступ к критичной информации. Чтобы минимизировать риск утечки информации, при освобождении участков памяти, необходимо их затирать (например, записывать последовательность из 0-й или каких-то произвольных значений).

Очевидно, что сам процесс затирания участков оперативной памяти СЗИ НСД должен быть реализован в виде модуля ядра ОС, который будет производить все свои действия в моменты перехвата определенных системных вызовов, связанных с выделением и освобождением областей памяти. В ходе реализации такого модуля порождается сразу несколько задач, которые необходимо решить:

  1. необходимо определить что и в какой момент нужно очищать, не вызвав при этом нарушений работы приложений или самой ОС;
  2. необходимо определить адреса начала и конца подлежащих очистке данных;
  3. необходимо использовать методы очистки, которые не будут оказывать сильного влияния на общую производительность системы.

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

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

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

Итак, что же вообще нужно очищать? Для начала стоит рассмотреть структуру памяти, которая используется для работы процессов (примерная схема изображена на рис. 1).

В ядре ОС Linux все области виртуальной памяти, которые выделяет ядро ОС, характеризуются структурой struct vm_area_struct (эту структуру можно найти с помощью функции find_vma, зная виртуальный адрес в памяти). В этой структуре содержатся адреса начала и конца области памяти, права доступа к памяти, а также некоторые интересующие нас флаги, в частности флаг VM_SHARED, который обозначает разделяемую область памяти. Все части процесса расположены в этих областях памяти (в областях памяти, описываемых структурой vm_area_struct), также мы можем получить список всех структур vm_area_struct, принадлежащих конкретному работающему процессу.

С другой стороны, существует более детальное деление процесса на секции и сегменты, такие как:

  • куча;
  • стек;
  • секция инициализированных данных;
  • секция кода;
  • секция неинициализированных данных;
  • секции инициализированных/неинициализированных данных для каждой загруженной в память разделяемой библиотеки.

Каждая секция/сегмент расположен в одной или нескольких областях памяти (vm_area_struct), таким образом, можно считать, что секции и сегменты отображаются на области памяти. С позиции очистки памяти нам интересны секции и сегменты, непосредственно хранящие данные — это сегмент кучи, отображаемые в оперативную память файлы, секции данных приложения и библиотек, стек приложения, стеки порождаемых приложением потоков. Далее рассмотрим методы, применяемые для очистки тех или иных областей памяти.

Рис. 1. Схематичное представление областей памяти, используемых процессом

Методы очистки памяти в куче (heap)

Со стороны интерфейса программирования куча управляется вызовами malloc (выделение памяти), realloc (изменение размера выделенной памяти), free (освобождение памяти). Однако, сами системные вызовы, которые вызываются при работе этих функций могут функционировать по разному (например, в зависимости от размера выделяемой/выделенной памяти).

При относительно маленьких размерах памяти (до 1-10Мб), память выделяется в сегменте кучи приложения, начальный и конечный (текущий) адреса которой можно узнать из структуры, описывающей процесс — task_struct. В этом случае все управляется системным вызовом brk (т.е. вызовы malloc и free вызывают brk с соответствующим значением), который используется для того, чтобы изменить конечный (текущий) адрес кучи. При этом подчеркнем факт того, что при вызове free, brk может и не вызываться для уменьшения размера кучи — например, free не станет делать лишний системный вызов при освобождении нескольких байт (освобождение будет отложено, а память будет зарезервирована на случай дополнительного выделения). Конкретная стратегия работы функций по работе с памятью зависит от их реализации.

Иначе обстоит дело с выделением памяти больших размеров (например, 100Мб). В данном случае функция malloc использует системный вызов mmap2 с флагом MAP_ANONYMOUS. Соответственно, в оперативной памяти выделяется еще одна область памяти и соответствующая ей структура vm_area_struct добавляется в список областей памяти, принадлежащих процессу. Соответствие библиотечных и системных вызовов здесь будет иное — malloc будет вызывать mmap2, realloc — mremap, а free — munmap.

Таким образом, чтобы очищать память в куче необходимо перехватывать системные вызовы brk, mremap и munmap.

При перехвате системного вызова brk нужно посмотреть на передаваемый ему адрес, и сравнить его с текущим адресом конца сегмента кучи (его можно узнать из структуры task_struct для текущего выполняемого процесса процесса — current). В случае, если передаваемый адрес меньше текущего и больше адреса начала кучи — необходимо очистить данные между передаваемым значением адреса и до текущего адреса конца кучи.

При перехвате mremap и munmap (в случае кучи) необходимо найти структуру типа vm_area_struct (с помощью функции find_vma) и проверить, что область не является разделяемой областью памяти, область не принадлежит отображению из файла, есть права на запись в эту область памяти (в противном случае присутствует высокий риск обнулить служебную область памяти выделенную системой). Если указанные проверки пройдены успешно — можно очистить необходимые участки памяти. В случае munmap необходимые адреса передаются в параметрах системного вызова, в случае mremap их необходимо сначала вычислить на основании четырех адресов, которые передаются в параметрах (старые начальный и конечный адреса и новые начальный и конечный адреса изменяемого участка памяти).

В процессе тестирования обнаружилось, что ядро ОС Linux версии 2.6.32 при вызове munmap самостоятельно обнуляет ненужную более область памяти. Однако, подтвердить наличие этой особенности во всех версиях ядра ОС Linux (включая более ранние, хотя бы ветки 2.6) на данный момент не получилось. Да и не стоит полагаться в этом вопросе на ядро ОС — его функционал может измениться непредсказуемым образом с любой новой версией, да и вообще нет гарантии того, что очищается вся память (это не основная функция ядра). Для этих целей лучше использовать собственное узконаправленное СЗИ. Кроме этого, отметим, что аналогичные системные вызовы применяются в большом количестве других Unix-подобных ОС, так что методы, изложенные в данной статье, с небольшими поправками применимы и для них.

Методы очистки памяти в секциях инициализированных/неинициализированных данных приложения и разделяемых библиотек

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

Методы очистки стека приложения

Со стеком приложения все относительно просто. Адрес начала стека и его размер в страницах можно узнать из структуры task_struct для текущего процесса. Единственное, что нужно помнить — стек растет вверх, а не вниз (соответственно адрес его начала больше адреса его конца).

Приведем пример кода для очистки стека приложения (необходимо выполнить при перехвате системного вызова exit_group):

unsigned int size = current->mm->stack_size * PAGE_SIZE;

char *start = (char *)(current->mm->stack_start — size);

memset(start, 0, size);

Методы очистки стека потока

Со стеком потока дела обстоят иначе. На уровне ядра ОС потоки (thread) и процессы (task_struct), вообще говоря, не различаются. По сути поток представляет собой «облегченный» процесс (поток может разделять с каким-нибудь процессом некоторые ресурсы — память, используемые файловые объекты и т.д.). Часто поток может иметь свой стек и прочие области памяти (описанные в его структуре task_struct). При создании многопоточных приложений вызываются те же самые системные вызовы (системный вызов clone(2)), что и при создании процессов, только с установленными флагами по «наследованию» ресурсов (CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND). При этом каждый поток будет иметь собственную («облегченную») структуру task_struct и свой уникальный идентификатор PID (process id). Но при этом все потоки и процесс, который порождает множество потоков, будут иметь один и тот же идентификатор группы потоков TID (thread id). Изначально у процесса, порождающего потоки, PID=TID.

Поэтому методы для очистки стека потока не будут отличаться от очистки стека приложения. Так как потоки выполняются на уровне ядра ОС как обычные процессы (используются те же механизмы управления процессами в ОС), то и очистка памяти их стека будет осуществляться в методе, описанном выше (при вызове системного вызова exit).

Методы очистки области памяти отображенных файлов

Вызовы mmap, mremap и munmap используются не только для выделения памяти в куче. Главное их предназначение — это отображение файлов в оперативную память. Файл читается с диска в память, процесс работает с этой памятью, а потом изменения записываются на диск. Для очистки памяти в этом случае в вызове mremap нужно делать все то же самое, что и для случая очистки памяти в куче, с одним отличием — необходимо проверять, существует ли страница для данного адреса в памяти.

Так как операция чтения с диска занимает относительно долгое время, ядро ОС не осуществляет загрузки сразу всего файла в память — данные считываются по мере необходимости (например при запросе данных, которые еще не были загружены). Таким образом, необходимо очищать память областями по 4096 байт (размер страницы), а перед очисткой проверять, существует ли эта страница в памяти. Проверить это можно, например, посмотрев на код возврата макроса put_user, который записывает данные в адрес пространства пользователя — передаем функции put_user адрес в странице, и смотрим на код возврата. Если макрос вернул 0 — очищаем страницу, иначе — игнорируем ее.

Для системного вызова munmap поступить аналогично нельзя, ведь если вызов close для файла вызовется после munmap, то в файл запишутся нули (или произвольные данные, смотря как очищать), что приведет к нарушению работы ОС/приложения. То есть в данном случае необходимо еще перехватывать и вызов close. На данный момент решение этой проблемы находится в проработке, возможные варианты решения сводятся к определению (запоминанию) всех страниц, соответствующих файлу, в памяти с последующим вызовом close и очисткой этих областей памяти. Указанный способ может сильно сказаться на производительности всей системы.

Оставшиеся области памяти

Если к моменту завершения программы (системный вызов exit_group) не освобождены какие-либо области памяти отображенных файлов, или просто анонимные области памяти (т.е. не принадлежащие файлу, выделенные с помощью mmap с флагом MAP_ANONYMOUS), их необходимо очистить в случае, если они не принадлежат файловым отображениям, разрешают запись и не являются разделяемой памятью.

Выводы

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

Стоит отметить, что указанные подходы к очистке памяти необходимо использовать в СЗИ НСД совместно с дополнительными политиками разграничения доступа и механизмом контроля потоков информации, что и реализуется в продукте компании «ОКБ САПР» ПАК СЗИ НСД «Аккорд-Х». Только в случае использования такого средства защиты становится возможным гарантировать сохранность критически важной информации, хранящейся и обрабатываемой в ОС семейства Linux.

Авторы: Прокопов В. С.; Каннер А. М.

Дата публикации: 01.01.2013

Библиографическая ссылка: Прокопов В. С., Каннер А. М. Особенности реализации механизма очистки освобождаемых областей оперативной памяти в GNU/Linux // Комплексная защита информации. Электроника инфо. Материалы XVIII Международной конференции 21–24 мая 2013 года, Брест (Республика Беларусь). 2013. С. 120–123.

Scientia potestas est
Кнопка связи