Изоляция процессов выполнение программ в выделенной области памяти
Мы продолжаем рассматривать операционные системы со всеми их проблемами и техническими подробностями. В этой статье поговорим о динамических переменных и менеджере памяти. В прошлых статьях мы уже успели узнать, что каждая запущенная программа формируется в четкое описание, называемое процессом . В состав этого описания входят участки памяти для хранения исполняемых машинных инструкций, глобальных переменных, а также участок памяти, выделенный под стек . В логическом адресном пространстве они соседствуют друг с другом, однако в физической памяти это не обязательно так. Каждый из сегментов памяти разбит на страницы, которые могут быть разбросаны по физической памяти в соответствии с пожеланиями операционной системы. Такой состав сегментов памяти не учитывает одну ситуацию. Не всегда можно заранее знать какой объем данных необходимо разместить в памяти для обработки.
Зачем нужны динамические переменные?
Очень большая доля данных поступает на обработку уже в тот момент, когда приложение обработки уже запущено и работает. Объемы данных, которые поступят в будущем неизвестны, поэтому память для размещения этих объемов выделяется в процессе работы. Иными словами выделяется динамически. Например, при обработке цифровых снимков их размер может быть заранее неизвестен.
Есть снимки большого размера, есть малые. Это становится совершенно не важно с появлением еще одного очень значимого механизма операционной системы, называемого управление памятью . Это самое управление занимается тем, что выделяет память по запросу приложения. Если бы процесс был один, то думать было бы нечего. Просто можно было отдать все что есть, но в многозадачной операционной системе так поступать нельзя. Такой ценный ресурс как память может понадобиться любому процессу в абсолютно любой момент времени. Такой важный вопрос как распределение памяти между различными процессами находится в ведении операционной системы.
Как получить дополнительное место для хранения данных?
Итак, приложение, нуждающееся в дополнительном месте для размещения новых данных отправляет запрос операционной системе. Библиотека языка Си имеет для этого функцию malloc() .
Параметром является необходимое количество байт и возвращает она адрес начала выделенного участка. Участок памяти выделяется из специального места. Называется оно куча . Звучит не очень, но так уж получилось. Абсолютно ничего не мешает выделить место и для второго снимка, лишь бы хватило места.
При этом в куче из незанятого пространства выделяется область, предназначенная для хранения еще одной порции данных. Адрес начала вновь выделенного участка попадает в объявленный указатель. Имея указатели на выделенные участки, можно как помещать туда информацию, так и считывать ее оттуда. Логично предположить, что эти участки выделены внутри адресного пространства процесса, иначе бы так просто обращаться к ним было бы невозможно.
Где расположена «куча»?
Место под кучу выделяется в адресном пространстве процесса. Незанятое пространство между глобальными данными и стеком используется для размещения там динамически выделяемых участков памяти по запросу самого процесса.
Красным цветом на схеме выделена та самая куча. В данном примере показано, как для данных выделяется место в куче, а указатели на выделенные участки памяти являются данными стекового фрейма функции.
Это и хорошо и плохо. Смотря какая квалификация у программиста. После завершения функции, как мы уже знаем , стековые переменные могут быть перезаписаны переменными другой функции и это лишь вопрос времени и вероятности. Но давайте о проблемах чуть позже. Правильным действием тут является отправка запроса на освобождение выделенной ранее памяти. Освобождением занимается функция free() с параметром указатель на ранее выделенную область. Сделаем все правильно чтоб немного рассмотреть работу менеджера памяти.
Технические подробности
Манипуляции с регистрами
Первым делом при запуске процесса необходимо выделить хотя бы немного места под хранения динамических переменных. Делается это путем занесения в служебные регистры необходимых данных.
Как мы ранее выяснили, часть регистров заняты выполнением своих особенных задач. Адресные регистры ( PAR0 - PAR15 ) учебного процессора занимаются размещением в физической памяти страниц из логического адресного пространства процесса. Для определения полного физического адреса ячейки памяти необходимо младшие 12 бит логического адреса сложить с содержимым адресного регистра , сдвинутого на 8 бит влево.
Старшие 4 бита логического адреса определяют номер страницы памяти и соответственно номер адресного регистра с которым необходимо проводить сложение. Задачей менеджера является во-первых, выделить новые страницы под кучу. А это информация кроме как где она будет физически находиться, еще и атрибуты этих страниц памяти. В атрибутах есть информация о размере страниц памяти и правах на чтение и запись. Эти атрибуты позволяют выявить аварийную ситуацию, при некорректной работе программы. Аварийная ситуация это сигнал, позволяющий операционной системе удалить процесс нарушитель из очереди задач и сообщить об этом пользователю.
Обращение к системному менеджеру памяти
Теперь рассмотрим как запрос прикладного приложения пользователя добирается до менеджера памяти. Системный менеджер памяти представляет собой функции ядра операционной системы. Работа с памятью в условиях многозадачности это весьма интеллектуальная задача и ее нельзя доверять программисту с непонятным уровнем квалификации. Поэтому все что остается прикладному программисту это сделать простой запрос. Указать сколько байт потребовалось и куда положить адрес выделенного участка памяти. Функция malloc() является частью стандартной библиотеки, поэтому выполняется в пространстве пользователя. В свою очередь, эта функция обращается к прикладному интерфейсу операционной системы, где представлена другая функция, обладающая целым набором параметров ( mmap ).
Этим набором можно указать кроме необходимого количества байт пространства еще и атрибуты участка памяти, касающиеся чтения и записи и при желании другую необходимую информацию. Эта API функция, в свою очередь, вызывает программное прерывание, сопровождающееся переключением контекста пользователя в контекст ядра операционной системы. В контексте ядра вызывается функция менеджера памяти, которая в куче процесса занимается поиском свободного пространства необходимого размера, необходимыми отметками о том, что память занята и возвратом адреса начала выделенного участка.
Как мы ранее выяснили , переключение контекста и довольно длинная цепочка вызовов приводят к существенным задержкам, так что лучше снижать число обращений за памятью.
Менеджер памяти прикладного приложения
Теперь о том, как снижается число обращений к операционной системе с целью выделения памяти из кучи. Здесь все просто, один раз оптовой партией берем большой кусок памяти и долго потом не обращаемся к операционной системе.
Просто и одновременно сложно. В этом большом фрагменте теперь нам самим прийдется искать свободные участки необходимого размера и вести учет занятых и незанятых участков памяти. Такие функции выполняют роль менеджера памяти, но уже не в пространстве операционной системы , а в пространстве пользовательского приложения. Это снизит число обращений к операционной системе. Они нужны будут только для того, чтобы еще увеличить размер выделенной области если та что была уже закончилась. Необходимо отметить, что поиск фрагментов нужного размера, возвращение этих фрагментов обратно и весь связанный с этим учет все равно довольно трудоемкая работа, требующая большое количество процессорного времени.
Проблемы менеджеров памяти
Утечка памяти
Настало время поднять вопрос о самых распространенных проблемах менеджеров памяти. Менеджеры работают сами по себе, программисты сами по себе. Согласованности зачастую нет никакой. Если программист выделил себе место под данные, а потом забыл вернуть, то область памяти так и останется висеть в списке занятых, хоть и не осталось в программе ни одного места, нуждающегося в существовании этого участка памяти. Эта проблема называется утечкой памяти.
Программа забирает память у операционной системы и не возвращает. Если это происходит циклически и часто, то в итоге программа забирает всю свободную память, что приводит к нарушению нормальной работы операционной системы и чаще всего к необходимости ее перезагрузки. В этом примере функция берет в пользование 10 байт, после окончания функции стековая переменная с адресом выделенного участка исчезает. Подвешенный участок памяти остается. Менеджер не тратит время процессора на поиски таких проблем, память утекает. Десятилетиями лучшие умы человечества пытаются решить эту проблему и найдены по крайней мере два направления.
Решения проблем утечки
Первое это как ранее уже озвучено, ведение учета занятых областей памяти и поиск ссылок на такие области. Если ссылки еще есть, то значит кому-то это нужно и занятое пространство не освобождается. Но если никто уже не ссылается, то занятое пространство возвращается в кучу обратно как свободный участок. Такой механизм называется сборкой мусора . Она происходит циклически по расписанию или по запросу приложения. Из популярных языков, имеющих в своем вооружении сборщик мусора можно выделить Java , CSharp , JavaScript , Go .
Сборщик мусора выявляет, что никто больше не владеет выделенным участком памяти и он будет возвращен в кучу как незанятый. Сборка мусора это довольно затратный процесс, снижающий быстродействие программы.
Одним из новых языков программирования, сменившим традиционный подход к управлению памяти является Rust .
В нем основу работы с динамически выделяемой памятью составляет такое понятие как область видимости. Сам факт выхода стековой переменной из области видимости при окончании функции должен быть сигналом к немедленному возврату выделенной памяти.
Необходимо отметить, что существуют гораздо более сложные случаи, требующие некоторого напряжения ума программиста. Напряжения только потому, что компилятор языка Rust не позволяет программисту собрать приложение пока существуют хоть сколько нибудь вероятно опасные строки исходного кода. Опасные это в смысле могущие привести к некорректному результату. Еще такая безопасность касается многопоточной работы, поэтому всяких тонкостей, которые видит компилятор в исходном коде может быть довольно большое количество. В общем, язык этот не так прост, но считается очень производительным и безопасным одновременно. Давайте перейдем к следующей проблеме.
Фрагментация памяти
А другая проблема менеджеров это фрагментация памяти . Такое явление, при котором появляются неиспользуемые фрагменты памяти в общем блоке. При активной работе менеджера их набирается изрядное количество. При освобождении какого-то участка памяти он помечается как незанятый. Но его специфический размер зачастую не подходит для занятия другими блоками данных.
Так происходит непрерывный процесс накопления неиспользуемых фрагментов. Это конечно же приводит к запросу у операционной системы еще одного большого блока данных. Казалось бы, можно взять и переупаковать занятые участки таким образом, чтобы убрать промежутки, но это только на словах звучит просто. Во первых, перемещения областей памяти это затратно по времени, да еще и указатели на области памяти не должны быть изменены. Никто не даст гарантию того, что большое число копий указателя еще не было скопировано в разные части программы. Перемещение фрагментов должно приводить и к смене содержимого указателей. А это уже совсем другая история и вероятно игра не стоит свеч. Вот в этот самый момент становится понятной фраза о том, что
любая программа стремиться занять всю доступную память.
Пишите качественные программы, думайте о том, что происходит внутри и тем самым мы приостановим обрушение лавины посредственности в наш и так не самый совершенный мир.
Изоляция процессов - это набор различных аппаратных и программных технологий, предназначенных для защиты каждого процесса от других процессов в операционной системе . Это достигается путем предотвращения записи процесса A в процесс B.
Изоляция процесса может быть реализована с помощью виртуального адресного пространства , где адресное пространство процесса A отличается от адресного пространства процесса B, что предотвращает запись A в B.
Обеспечить безопасность проще, запретив доступ к памяти между процессами, в отличие от менее безопасных архитектур, таких как DOS, в которых любой процесс может записывать данные в любую память любого другого процесса.
СОДЕРЖАНИЕ
Ограниченная межпроцессная коммуникация
В системе с изоляцией процессов ограниченное (контролируемое) взаимодействие между процессами все еще может быть разрешено по каналам межпроцессного взаимодействия (IPC), таким как общая память , локальные сокеты или Интернет-сокеты . В этой схеме вся память процесса изолирована от других процессов, за исключением тех случаев, когда процесс разрешает ввод от взаимодействующих процессов.
В некоторых случаях системные политики могут запрещать IPC. Например, в системах обязательного контроля доступа субъектам с разными уровнями чувствительности может быть запрещено общаться друг с другом. Последствия для безопасности в этих обстоятельствах обширны и охватывают приложения в систематике шифрования сетевых ключей, а также алгоритмы распределенного кэширования. Аналогичным образом затрагиваются протоколы, определяемые интерфейсом, такие как базовая архитектура доступа к облаку и совместное использование сети.
Операционные системы
Известные операционные системы, поддерживающие изоляцию процессов:
Веб-браузеры
Internet Explorer 4 использовал изоляцию процессов, чтобы позволить отдельным оконным экземплярам браузера создавать собственные процессы; однако в разгар войны браузеров от этого отказались в последующих версиях, чтобы конкурировать с Netscape Navigator (который стремился сконцентрироваться на одном процессе для всего пакета Internet). К этой идее «процесс на экземпляр» вернутся только через десять лет, когда просмотр с вкладками стал более обычным явлением.
В « Многопроцессорной архитектуре » Google Chrome и « Слабо связанном IE (LCIE) » в Internet Explorer 8 вкладки, содержащие веб-страницы, содержатся в их собственных полу-отдельных процессах уровня ОС, которые изолированы от основного процесса браузер, чтобы предотвратить сбой одной вкладки / страницы и сбой всего браузера. Этот метод (известный как multiprocess или process-per-tab ) предназначен как для управления памятью, так и для обработки, позволяя сбой на вкладках-нарушителях отдельно от браузера и других вкладок, а также для управления безопасностью.
Браузеры с изоляцией процессов
Языки программирования
Erlang (язык программирования) предоставляет аналогичную концепцию в пользовательском пространстве, реализуя строго разделенные легковесные процессы.
Количество инцидентов информационной безопасности растет. Согласно аналитике [1] 1 место по численности нарушений безопасности занимает события, связанные с вредоносным ПО (около 39%).
Заражение часто связано с нарушением правил эксплуатации. Использование антивирусов не приносит должного результата [2]. Таким образом, становится ясно, что с помощью только одного антивируса невозможно полностью защититься от вредоносного ПО, а пытаться добиться от персонала непреложного соблюдения правил компьютерной безопасности невозможно. Следовательно, необходимо использовать дополнительный компонент защиты. Одно из перспективных направлений – технология изоляции процессов «песочница» [3]. Она позволяет создавать отдельную среду выполнения процессов и ограничить область заражения компьютера. Целью данной статьи является анализ изоляции процессов в Windows 10 с помощью песочницы. Windows 10 была выбрана из-за ее популярности [4].
Определение изоляции процессов в терминах Windows 10
Для того чтобы анализировать изоляцию процессов, ее необходимо определить в терминах конкретной операционной системы. К сожалению, мы не можем пойти по простому пути и дать следующее определение: Процессы называются изолированными, если ни один из них не влияет на другой.
Такое определение возможно только в абстрактном мире математики и не выдерживает никакой проверки на практике. Два процесса могут влиять друг на друга косвенным образом и, таким образом, не быть изолированными [5]. Следовательно, его невозможно использовать при разработке решений изоляции процессов, а это значит, что необходимо дать другое определение.
Далее все понятия будут рассмотрены в рамках ОС Windows 10.
Определим понятие изоляции следующим способом. Выделим критерии, которым должны удовлетворять процессы и их окружение, чтобы их можно было назвать изолированными. В процессе выбора критериев изоляции процессов необходимо руководствоваться необходимостью соблюдать равновесие между избыточностью и недостаточностью правил для правильной, и безопасной работы программ.
Вопросы изоляции процессов, связанные с внутренними структурами
У каждого процесса есть принадлежащая ему структура EProcess [6]. Изменить ее можно только используя соответствующие API, получив дескриптор процесса. Следовательно, необходимо запретить либо получать дескриптор чужого процесса, либо вызывать функции, изменяющие поля структуры других процессов.
В структуре EProcess достаточно много полей, и некоторые из них процесс-собственник может менять. Однако изменение этих полей может привести к повышению привилегий процесса. Поэтому необходимо запретить вызов функций, изменяющих поля, которые прямо или косвенно относятся к безопасности.
Еще одной структурой, описывающей процесс, является РЕВ (блок окружения процесса) [7]. Он доступен из контекста процесса-собственника. Нельзя запретить его изменение, поскольку это нарушит работу приложения, однако необходимо запретить получение чужого дескриптора PEB. Т.к процесс может изменить свой контекст на чужой, прочитав его с помощью дескриптора.
Вопросы изоляции процессов, связанные с созданием новых процессов
Необходимо определить, что делать с порожденными процессами. Рассмотрим пример. Допустим, мы пытаемся изолировать два процесса (А и Б), и первый порождает дочерний процесс (В). Должен ли процесс-ребенок быть изолированным от родителя и процесса Б? Ответ на второй вопрос становится ясным, если разобраться как именно происходит создание процесса.
Во время создания объекта процесса в операционной системе, процесс-ребенок может наследовать много полей [8]. Также процесс получает дескриптор родительского процесса. Таким образом, невозможно соблюсти изоляции процессов между А и Б, не изолируя В (ребенка) от Б.
Нужно ли изолировать процесс-родитель от процессов-детей? Многие приложения используют один главный процесс, который следит за правильной работой процессов-детей. Например, браузер [9] имеет один порождающий процесс, который создает дополнительные процессы, отвечающие за нормальную работу вкладок. Т.к изоляция родителей и детей не позволит работать многим приложениям, то их взаимодействие должно оставаться разрешенным.
Вопросы изоляции процессов, связанные с завершением процессов
Далее, необходимо обсудить завершения процесса. Существуют типы процессов, которые могут принудительно завершить, используя системные вызовы, другие процессы с соответствующими правами действий. Следует, также, запретить эту возможность [9].
Также при некорректном завершении процесса структура EProcess все еще находится в адресном пространстве, и у другого процесса существует возможность получить доступ к некоторой информации из ее полей (например, GetExitCodeProcess) используя соответствующее API[10]. Следовательно, такие функции также необходимо запретить.
Вопросы изоляции процессов, связанные с памятью процессов
Один процесс может писать в память, принадлежащую другому процессу [11].
Поэтому следует запретить все вызовы, связанные с памятью сторонних процессов (если только это не его ребенок) из следующих библиотек:
- API Virtual
- API Heap
- File Map
Это все библиотеки, которые могут быть использованы для работы с памятью несистемными процессами. Тем самым, мы исключим возможность реализовать многие документированные техники встраивания кода в процесс, что однозначно нарушает изоляцию. Также, мы решим проблемы с разделяемой памятью, попросту запретив ее.
Необходимо запретить одному процессу создавать дамп памяти другого и все связанные с ним функции [12].
Большинство современных приложений поддерживает механизмы DEP (Data Execution Prevention, механизм, который не позволяет выполнить команду, находящуюся на странице памяти, не помеченной на выполнение.) [13], ASLR (Address space layout randomization, система рандомизации адресов стека и кучи) [14] и EMET (Enhanced Mitigation Experience Toolkit, механизм централизованного управления средствами снижения риска безопасности) [15]. Следует отключить возможность запуска приложений без этих механизмов и их отключение.
Вопросы изоляции процессов, связанные с механизмами межпроцессного взаимодействия и объектами синхронизации
Необходимо рассмотреть возможность блокировки каждой системы межпроцессорного взаимодействия [16].
Clipboard. Clipboard – это пользовательский буфер обмена. Он работает, когда пользователь копирует или вставляет информацию. Кажется, что нет смысла блокировать этот механизм, поскольку он всего лишь облегчает работу с КС. Здесь необходимо отслеживать активно ли окно, в которое идет информация пользователя, иначе любой другой процесс может ее прочитать, и, теоретически, она может оказаться от изолируемого процесса.
COM, Data Copy DDE, Mailslots, Pipes, Windows Sockets, RPC. Многие процессы используют перечисленные механизмы для взаимодействия компонентов приложения, и полностью их отключить нельзя. Однако можно запретить вызов функции поиска COM-интерфейсов, дескрипторов и указателей по отношению к изолированному процессу. Также, доступ к ним можно получить через группы объектов, например, к COM-объекту можно обратиться через CO-классы. Поэтому аналогичные функции поиска следует также запретить.
File Mapping. Данный механизм уже был рассмотрен в главе, посвященной памяти. Для реализации изоляции он должен быть отключен
Также существует 4 способа синхронизировать процессы средствами Windows, для которых синхронизация не является главной функцией.
Change notification. С помощью механизма уведомлений программа способна узнавать, наступило ли какое-либо событие в системе, однако используя это средство, невозможно напрямую синхронизироваться двум конкретным процессам (узнать о том, какой процесс спровоцировал уведомление можно только косвенно), поэтому блокировать его необязательно.
Console input. Объект, который создается при открытии консоли. Он сигнализирует о том, что в консоли есть непрочитанные входные данные. Поскольку наша задача покрыть максимальное количество программ, которые будут изолированы, необходимо отключить этот механизм.
Job. Он позволяет работать одновременно с группой процессов. Для изолируемых процессов следует отключить эту возможность, иначе один процесс может добавить другой в собственное задание и получить некоторый контроль, управляя заданием. Например, вызовом TerminateJobObject завершить изолируемый процесс.
Memory resource notification. Механизм позволяет узнавать о событиях физической памяти. Так же как и механизм Change notification, его не обязательно блокировать.
Вопросы изоляции процессов, связанные с изоляцией потоков
Говоря об изоляции процессов, необходимо также обсудить вопросы, связанные с изоляцией потоков. Стоит ли изолировать потоки, созданные одним процессом. Кажется, что этого делать не нужно, но не стоит забывать, что Windows позволяет создавать удаленные потоки [17], способные работать в контексте другого процесса. Поэтому стоит запретить вызов функций, создающих удаленные потоки. А изоляцию между остальными потоками, работающими в контексте одного процесса, делать не стоит, иначе нарушается идея многопоточного выполнения.
Однозначно нужно изолировать потоки изолируемых процессов. У потока есть структуры ETread, KThread и TED, по своему предназначению они аналогичны структурам процесса EProcess Kprocess PED. Естественно, что существуют аналогичные функции для работы с полями структур потока. Кажется логичным сохранить политику по отношению к этим структурам, которую мы применяли к структурам процесса, и запретить соответствующие функции.
Далее следует запретить получение дескриптора потока, чтобы избежать возможность управления другим процессом (например, TerminateThread).
Вопросы изоляции процессов, связанные с файлами
Нужно решить, что делать с файлами, с которыми процесс взаимодействует. Что будет, если оба процесса должны работать с одним файлом? Итак, если процессы хотят прочитать файл, то проблемы здесь не возникает. При чтении два процесса никак не изменят файл.
Но что делать, если один процесс открыл файл на запись, удаление или переименование? Разумнее всего дать возможность пользователю определить, какую версию файла он хочет сохранить, а до этого момента все процессы будут работать с локальной копией файла.
С созданием файла можно поступить так же. Только после завершения работы процесса, локальная копия файла переносится в общую файловую систему.
Приведенные выше выкладки позволяют сформулировать следующие критерии изоляции: процессы А и Б изолированы, если
- А не может получить дескриптор процесса Б
- А не имеет прямого доступа к внутренним структурам Б
- А не может вызывать функции позволяющие прочитать или модифицировать внутренние структуры Б
- A не может возможности напрямую изменять перечисленные поля собственной структуры EProcess
- А изолирован от детей Б
- А не может принудительно завершить процесс Б
- А не имеет доступа к любой памяти Б
- А не может сделать дамп памяти процесса Б
- У процесса включены технологии защиты памяти (DEP, ASLR, EMET)
- А не может скопировать данные из буфера обмена, когда его окно не активно
- А не использует совместно с Б механизмы COM, Data Copy, DDE, Windows Sockets, Mailslots, Pipes, RPC, Job
- А не использует совместно с Б механизмы синхронизации
- Если Б - консоль, А не использует Console input
- А работает с локальной копией файла
- Все изменения в файле сохраняются только после завершения работы А Для процесса Б аналогично.
Анализ работы «Sandboxie»
| |
Рис. 1 – SID процесса
То есть приложение запускается с флагом SECURITY_ANONYMOUS_LOGON_RID. У такого пользователя есть только привилегия SeChangeNotifyPrivilege, которая позволяет только перемещаться по каталогам. Еще одна странная особенность – это SID пользователя, который виден из-под песочницы. Ее общий вид такой: S-1-5-21-домен-1001. Sa = 21 означает, что SID был выдан контроллером домена или изолированным компьютером. То есть приложение считает, что оно работает на обычном компьютере.
Это подтверждается именами пользователей на изображениях 2.
| |
Рис. 2 – Образы процесса
Остается загадкой, почему приложения имеют разную версию.
В этом и заключается особенность песочницы. Она обманывает процесс, предоставляя все ресурсы через свой монитор. Воспользовавшись API-Monitor, можно увидеть, что основной контроль по обработке вызовов делает SbieCtrl.exe. Как это происходит?
Просмотрев все библиотеки Sandboxie, можно найти вызываемые функции в SbieDll.dll. Судя по всему, это основная библиотека, отвечающая за реализацию основных методов, поскольку все импортируемые в нее функции принадлежат либо KERNEL32 или ntdll, а остальные библиотеки подключают ее. Как минимум монитор вызывает ее функции.
Для того чтобы отследить поведение процесса, песочница перехватывает вызов функций dll уровня пользователя и далее обрабатывает согласно своим внутренним правилам. Эти правила нигде не описаны и приходится тестировать самостоятельно. Также неясно, как sandboxie перехватывает вызовы. Из библиотеки SboxHostDll.i64 экспортируются функции InjectDllMain и DllEntryPoint. Как видно из ассемблерного листинга, в InjectDllMain вызывается функция SbieDll_Hook 3.
|
Рис. 3 – Листинг InjectDllMain
В DllEntryPoint происходит настройка параметров, и вызов функции security_init_cookie. Как видно из листинга, она определяет точку входа DLL 4.
|
Рис. 4 – Листинг security init cookie
SbieDll_Hook перехватывает определенные в коде функции. Вот типичный пример ее использования:
qword_7D2AC5F8 = S b i e D l l _ H o o k ( ” ExitWindowsEx ” , qword_7D2AC5F8 , sub_7D2474E0 ) ;
[style=CStyle] Сейчас она перехватывает вызов ExitWindowsEx и, вероятно, ставит на нее обработчик sub_7D2474E0. В функциях вида sub_* описаны правила, которые определяют, может процесс делать то или иное действие. Сама же SbieDll_Hook вызывает внутри себя SbieDll_Hook_0. В ней самое интересное происходит в момент вызова EnterCriticalSection. В критической секции вызывается функция VirtualAlloc.
Тестирование Sandboxie
Необходимо протестировать Sandboxie на соответствие критериям изоляции, приведенным выше, и понять способна ли она обеспечивать изоляцию процессов. В ходе тестирования приложения запускаются внутри SandBoxie (Version 5.30 64-bit) со стандартными настройками в ОС Windows 10 (Версия 1073 Сборка 15063.674). Для тестирования некоторых правил изоляции необходимо получить права SE_DEBUG_NAME, и Sandboxie позволяет получить их. Это уже нарушает изоляцию процессов, но вполне возможно, что правила Sandboxie написаны достаточно грамотно, и мы сможем ослабить наши требования.
Для проверки гипотезы, что Sandboxie обеспечивает изоляцию процессов (в соответствии с полученным ранее определение) необходимо выполнить следующее:
- получить дескриптор процесса с правом PROCESS_TERMINATE.
- воспользоваться функцией CreateRemoteThread и подключиться к процессу
- создать задание и подключить процесс к ней.
- создать объект отладки и подключить к нему процесс.
- воспользоваться VirtualAllocEx и записать что-либо в процесс.
- воспользоваться VirtualQueryEx с помощью которой изменить страницы памяти процесса и заблокировать их.
- воспользоваться функцией DuplicateHandle перебираем все дескрипторы блокируем их.
- Обратиться к потоку процесса и завершить или приостановить его
- Открыть поток и изменить его контекст
Все эти действия песочница блокирует.
Sandboxie перехватывает системные вызовы. В Windows существует возможность защититься от перехвата вызовов функций Dll. Для этого необходимо:
- Загрузить оригинальный код функции вызовом функции LoadLibrary
- Получить указатель на текущую функцию
- Заменить текущий код функции на загруженный
Однако песочница перехватывает вызов LoadLibrary. Существует модификация этого метода. Вместо загрузки целой библиотеки, читается только нужные байты и уже они заменяются на текущий код функции. Здесь песочница перехватывает попытку писать в адресное пространство исполняемого кода. И вызывает функцию ProtectVirtualMemory.
| |
Рис. 5 – Чтение памяти процесса
Также получилось записать произвольные строки в неиспользуемую память процесса. На изображении 6 представлен пример записи строки в память RuntimeBroker. Остается неисследованным тип этой памяти.
|
Рис. 6 – Запись в память RuntimeBroker.exe
Заключение
В данной статье определены критерии изоляции в терминах Windows.
Тестирование показало, что Sandboxie не может обеспечить изоляцию, так как не удовлетворяет требованиям изоляции памяти.
Литература
Авторы: Козак Р. А.; Мозолина Н. В.
Дата публикации: 18.01.2019
Библиографическая ссылка: Козак Р. А., Мозолина Н. В. Анализ изоляции процессов в ОС WINDOWS с помощью «ПЕСОЧНИЦЫ» // Комплексная защита информации: материалы ХХIV научно-практической конференции. Витебск. 21–23 мая 2019 г.: УО ВГТУ. Витебск, 2019. С. 307–316.
Всем процессам в операционной системе Windows предоставляется важнейший ресурс – виртуальная память ( virtual memory ). Все данные, с которыми процессы непосредственно работают, хранятся именно в виртуальной памяти.
Название "виртуальная" произошло из-за того что процессу неизвестно реальное (физическое) расположение памяти – она может находиться как в оперативной памяти ( ОЗУ ), так и на диске. Операционная система предоставляет процессу виртуальное адресное пространство (ВАП, virtual address space ) определенного размера и процесс может работать с ячейками памяти по любым виртуальным адресам этого пространства, не "задумываясь" о том, где реально хранятся данные.
Размер виртуальной памяти теоретически ограничивается разрядностью операционной системы. На практике в конкретной реализации операционной системы устанавливаются ограничения ниже теоретического предела. Например, для 32-разрядных систем ( x86 ), которые используют для адресации 32 разрядные регистры и переменные, теоретический максимум составляет 4 ГБ (2 32 байт = 4 294 967 296 байт = 4 ГБ). Однако для процессов доступна только половина этой памяти – 2 ГБ, другая половина отдается системным компонентам. В 64 разрядных системах (x64) теоретический предел равен 16 экзабайт (2 64 байт = 16 777 216 ТБ = 16 ЭБ). При этом процессам выделяется 8 ТБ, ещё столько же отдается системе, остальное адресное пространство в нынешних версиях Windows не используется.
Введение виртуальной памяти, во-первых, позволяет прикладным программистам не заниматься сложными вопросами реального размещения данных в памяти, во-вторых, дает возможность операционной системе запускать несколько процессов одновременно, поскольку вместо дорогого ограниченного ресурса – оперативной памяти, используется дешевая и большая по емкости внешняя память .
Реализация виртуальной памяти в Windows
Схема реализации виртуальной памяти в 32-разрядной операционной системе Windows представлена на рис.11.1. Как уже отмечалось, процессу предоставляется виртуальное адресное пространство размером 4 ГБ, из которых 2 ГБ, расположенных по младшим адресам (0000 0000 – 7FFF FFFF), процесс может использовать по своему усмотрению (пользовательское ВАП), а оставшиеся два гигабайта (8000 0000 – FFFF FFFF) выделяются под системные структуры данных и компоненты (системное ВАП) 1 Специальный ключ /3GB в файле boot.ini увеличивает пользовательское ВАП до 3 ГБ, соответственно, уменьшая системное ВАП до 1 ГБ. Начиная с Windows Vista вместо файла boot.ini используется утилита BCDEDIT. Чтобы увеличить пользовательское ВАП, нужно выполнить следующую команду: bcdedit /Set IncreaseUserVa 3072. При этом, чтобы приложение могло использовать увеличенное ВАП, оно должно компилироваться с ключом /LARGEADDRESSAWARE. . Отметим, что каждый процесс имеет свое собственное пользовательское ВАП, а системное ВАП для всех процессов одно и то же.
Рис. 11.1. Реализация виртуальной памяти в 32-разрядных Windows
Виртуальная память делится на блоки одинакового размера – виртуальные страницы. В Windows страницы бывают большие ( x86 – 4 МБ, x64 – 2 МБ) и малые (4 КБ). Физическая память ( ОЗУ ) также делится на страницы точно такого же размера, как и виртуальная память . Общее количество малых виртуальных страниц процесса в 32 разрядных системах равно 1 048 576 (4 ГБ / 4 КБ = 1 048 576).
Обычно процессы задействуют не весь объем виртуальной памяти, а только небольшую его часть. Соответственно, не имеет смысла (и, часто, возможности) выделять страницу в физической памяти для каждой виртуальной страницы всех процессов. Вместо этого в ОЗУ (говорят, "резидентно") находится ограниченное количество страниц, которые непосредственно необходимы процессу. Такое подмножество виртуальных страниц процесса, расположенных в физической памяти, называется рабочим набором процесса (working set ).
Те виртуальные страницы, которые пока не требуются процессу, операционная система может выгрузить на диск , в специальный файл , называемый файлом подкачки (page file).
Каким образом процесс узнает, где в данный момент находится требуемая страница? Для этого служат специальные структуры данных – таблицы страниц ( page table ).
Структура виртуального адресного пространства
Рассмотрим, из каких элементов состоит виртуальное адресное пространство процесса в 32 разрядных Windows (рис.11.2).
В пользовательском ВАП располагаются исполняемый образ процесса, динамически подключаемые библиотеки ( DLL , dynamic-link library ), куча процесса и стеки потоков.
При запуске программы создается процесс (см. лекцию 6 "Процессы и потоки"), при этом в память загружаются код и данные программы (исполняемый образ, executable image ), а также необходимые программе динамически подключаемые библиотеки ( DLL ). Формируется куча ( heap ) – область, в которой процесс может выделять память динамическим структурам данных (т. е. структурам, размер которых заранее неизвестен, а определяется в ходе выполнения программы). По умолчанию размер кучи составляет 1 МБ, но при компиляции приложения или в ходе выполнения процесса может быть изменен. Кроме того, каждому потоку предоставляется стек (stack) для хранения локальных переменных и параметров функций, также по умолчанию размером 1 МБ.
Рис. 11.2. Структура виртуального адресного пространства
В системном ВАП расположены:
- образы ядра (ntoskrnl.exe), исполнительной системы, HAL (hal.dll), драйверов устройств, требуемых при загрузке системы;
- таблицы страниц процесса;
- системный кэш;
- пул подкачиваемой памяти (paged pool) – системная куча подкачиваемой памяти;
- пул подкачиваемой памяти (nonpaged pool) – системная куча неподкачиваемой памяти;
- другие элементы (см. [5]).
Переменные, в которых хранятся границы разделов в системном ВАП, приведены в [5, стр. 442]. Вычисляются эти переменные в функции MmInitSystem ( файл base\ntos\mm\mminit.c, строка 373), отвечающей за инициализацию подсистемы памяти. В файле base\ntos\mm\i386\mi386.h приведена структура ВАП и определены константы , связанные с управлением памятью (например, стартовый адрес системного кэша MM_SYSTEM_CACHE_START , строка 199).
Выделение памяти процессам
1. WinAPI функция VirtualAlloc позволяет резервировать и передавать виртуальную память процессу. При резервировании запрошенный диапазон виртуального адресного пространства закрепляется за процессом (при условии наличия достаточного количества свободных страниц в пользовательском ВАП), соответствующие виртуальные страницы становятся зарезервированными ( reserved ), но доступа к этой памяти у процесса нет – при попытке чтения или записи возникнет исключение . Чтобы получить доступ , процесс должен передать память зарезервированным страницам, которые в этом случае становятся переданными ( commit ).
Отметим, что резервируются участки виртуальной памяти по адресам, кратным значению константы гранулярности выделения памяти MM_ALLOCATION_GRANULARITY ( файл base\ntos\inc\mm.h, строка 54). Это значение равно 64 КБ. Кроме того, размер резервируемой области должен быть кратен размеру страницы (4 КБ).
WinAPI функция VirtualAlloc для выделения памяти использует функцию ядра NtAllocateVirtualMemory ( файл base\ntos\mm\allocvm.c, строка 173).
2. Для более гибкого распределения памяти существует куча процесса, которая управляется диспетчером кучи ( heap manager ). Кучу используют WinAPI функция HeapAlloc , а также оператор языка C malloc и оператор C++ new . Диспетчер кучи предоставляет возможность процессу выделять память с гранулярностью 8 байтов (в 32-разрядных системах), а для обслуживания этих запросов использует те же функции ядра, что и VirtualAlloc.
Дескрипторы виртуальных адресов
Для хранения информации о зарезервированных страницах памяти используются дескрипторы виртуальных адресов ( Virtual Address Descriptors, VAD ). Каждый дескриптор содержит данные об одной зарезервированной области памяти и описывается структурой MMVAD ( файл base\ntos\mm\mi.h, строка 3976).
Границы области определяются двумя полями – StartingVpn (начальный VPN ) и EndingVpn (конечный VPN ). VPN ( Virtual Page Number) – это номер виртуальной страницы; страницы просто нумеруются, начиная с нулевой. Если размер страницы 4 КБ (212 байт ), то VPN получается из виртуального адреса начала страницы отбрасыванием младших 12 бит (или 3 шестнадцатеричных цифр). Например, если виртуальная страница начинается с адреса 0x340000, то VPN такой страницы равен 0x340.
Дескрипторы виртуальных адресов для каждого процесса организованы в сбалансированное двоичное АВЛ дерево 3 АВЛ дерево – структура данных для организации эффективного поиска; двоичное дерево, сбалансированное по высоте. Названо в честь разработчиков – советских ученых Г. М. Адельсон Вельского и Е. М. Ландиса. ( AVL tree ). Для этого в структуре MMVAD имеются поля указатели на левого и правого потомков: LeftChild и RightChild .
Для хранения информации о состоянии области памяти, за которую отвечает дескриптор , в структуре MMVAD содержится поле флагов VadFlags.
Читайте также: