«Разрубить Гордиев узел» или преодоление проблем шифрования информации в ОС Windows

Современная операционная система это сложный иерархичный процесс обработки и управления информацией. Актуальные версии ОС Windows в этом вопросе не являются исключением. Для того, чтобы интегрировать средство защиты в среду ОС Windows, зачастую хватает встраивания на прикладном уровне. Однако, если речь заходит о шифровании информации в среде ОС Windows, все становится намного сложнее.
Основной «головной болью» разработчика средств шифрования в этом процессе является обеспечение «прозрачности шифрования», т.е. необходимо гармонично встроиться в структуру процессов операционной системы и при этом обеспечить невовлечение пользователей в процесс шифрования и уж тем более его обслуживание. Требования к современным средствам защиты все больше и больше исключают пользователя из процесса защиты информации. Таким образом, для этого самого пользователя создаются комфортные условия, не требующие принятия «непонятных» решений по защите информации.
В данной статье будут раскрыты идеи эффективной интеграции средства шифрования информации на диске с процессами файловой системы ОС Windows.
Перед разработчиками ставилась цель создать механизм шифрования информации на диске, отвечающий требованиям максимальной прозрачности для пользователей. Требования должны будут выполняться за счет эффективного взаимодействия этого механизма шифрования с процессами операционной системы Windows, которые отвечают за управление файловой системой. Эффективность механизма шифрования должна также подтверждаться высокой производительностью процессов шифрования и рациональным использованием ресурсов операционной системы.
Изначально была поставлена задача предоставлять одновременный доступ к зашифрованному и расшифрованному содержимому, а также шифровать имена файлов. Это и порождает основные сложности, т.к. такое требование идет в разрез со сложившейся архитектурой Windows. Чтобы понять суть проблемы, для начала нам следует разобрать некоторые основные моменты данной операционной системы.
В Windows все файловые системы полагаются на такие подсистемы, как менеджер памяти и кэш менеджер, а менеджер памяти, в свою очередь, полагается на файловые системы. Казалось бы, замкнутый круг, но все станет ясно дальше. Ниже, на рисунке 1, изображены перечисленные компоненты, а также менеджер ввода/вывода, который принимает запросы от подсистем (например Win32) и от других драйверов системы. Также на рисунке используются термины «фильтр» и «стек файловой системы», о чем подробнее будет рассказано ниже.

Рисунок 1

e65fcd3159294b788805afed6db5db4c.png

Разберем такой механизм, как отображение файла на память. Суть его заключается в том, что при доступе к виртуальной памяти в реальности читается часть файла. Реализуется это при помощи аппаратных механизмов процессоров и непосредственно самого менеджера памяти. Любые диапазоны виртуальной памяти процесса описываются дескрипторами. Если для какого-то диапазона нет дескриптора, значит, на этом участке виртуальной памяти ничего нет, и доступ к такому диапазону неизбежно ведет к краху процесса. В случае, если для какого-либо диапазона закреплена физическая память, доступ к этим виртуальным адресам ведет к обычному доступу к физической памяти, такую память еще называют анонимной. В случае, если файл отображен на виртуальную память, то при доступе по этим адресам считывается часть файла с диска в физическую память, после чего доступ к ней будет выполняться обычным образом. Т.е. эти два случая очень похожи, разница лишь в том, что для последнего в соответствующую физическую память предварительно будет считана часть файла. Обо всех таких типах доступов дескриптор и содержит информацию. Эти дескрипторы являются структурами менеджера памяти, которыми он управляет для того, чтобы выполнять требуемые задачи. Как не трудно догадаться, для того, чтобы считать часть файла в страницу физической памяти, менеджер памяти должен послать запрос файловой системе. На рисунке 2 изображен пример виртуальной памяти процесса с двумя диапазонами, А и В, доступ к которым еще ни разу не выполнялся.

Рисунок 2

cb6e82ab6ff34e45b7672859943e40e1.png

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

Рисунок 3

76cebbb155da40199c31800ffe986331.png

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

Рисунок 4

8dc126a89f024d178108c850edc059d9.png

Как показано на рисунке выше, процесс выполняет чтение файла в буфер В. Чтобы выполнить чтение, процесс обращается к менеджеру ввода/вывода, который формирует и посылает запрос файловой системе. Файловая система, получив запрос, не считывает файл с диска, а вызывает кэш менеджер. Далее кэш менеджер оценивает, отображен ли файл на его виртуальную память, и если нет, то он вызывает менеджер памяти для того чтобы отобразить файл/часть файла. В данном примере файл уже отображен, а доступ к нему ни разу не выполнялся. Далее кэш менеджер копирует в буфер В процесса файл, отображенный на диапазон виртуальной памяти А. Поскольку доступ к диапазону А выполняется первый раз, управление получит менеджер памяти, затем он оценит диапазон, и, поскольку это отображенный на память файл, считает его часть в физическую память, после чего отобразит ее на диапазон виртуальной памяти А. После этого, как уже было описано ранее, доступ к диапазону А будет выполняться, минуя менеджер памяти.
Ничто не мешает одновременно кэшировать файл и отображать его на память сколько угодно раз. Даже если файл будет закэширован и отображен на память десятков процессов, физическая память, которая используется для этого файла, будет одна и та же. В этом и заключается суть экономии физической памяти. На рисунке 5 приведен пример, где один процесс читает файл обычным образом, а другой процесс отображает этот же файл на свою виртуальную память.

Рисунок 5

b2f203d4860c4114863b92208c253fd7.png

Как видно из рисунка выше, физическая память отображается на виртуальную память процесса В и виртуальную память кэш менеджера. Когда процесс А будет выполнять чтение файла в буфер D, он обратится к менеджеру ввода/вывода, который сформирует и пошлет запрос файловой системе. Файловая система, в свою очередь, обратится к кэш менеджеру, который просто скопирует файл, отображенный на диапазон виртуальной памяти С кэш менеджера, в буфер D процесса А. Поскольку в момент обращения к кэш менеджеру файл уже был не только отображен, но и ранее выполнялся доступ к диапазону С, на которую отображен файл, то операция будет выполнена без участия менеджера памяти. Процесс В при чтении/записи диапазона Е по сути получит доступ к тем же самым физическим страницам памяти, к котором при копировании файла получал доступ кэш менеджер.
Файловые системы принимают запросы от пользовательского ПО или других драйверов. Перед доступом файл должен быть открыт. В случае успешного выполнения запросов открытия/создания файлов файловая система сформирует структуры памяти, которые используются кэш менеджером и менеджером памяти. Также следует отметить, что эти структуры уникальны для файла. Т.е. если конкретный файл диска был открыт на тот момент, когда пришел такой же запрос на этот же файл, файловая система будет использовать ранее сформированные структуры памяти. По сути, они являются программным представлением файла диска в памяти. На рисунке 6 приведен пример соответствия открытых экземпляров файлов и их структур.

Рисунок 6

b92b0761594f4ecebc1394a503225edb.png

На рисунке процесс А открыл файл С и файл D, а процесс B открыл файл С два раза. Таким образом, имеется три открытых экземпляра файла С, когда структура, сформированная файловой системой, всего одна. Файл D был открыт один раз, следовательно, имеется один открытый экземпляр, которому соответствует структура, сформированная файловой системой.
Любые запросы, направленные к файловой системе, не сразу обрабатываются ею. Запросы сначала проходят по цепочке драйверов, которые желают отслеживать такие запросы. Такие драйвера называют фильтрами. Они имеют возможность просматривать запросы до того, как они достигнут файловой системы, а также после того, как файловая система обработает их. Например, фильтр шифрования файлов может отслеживать запросы чтения/записи для того, чтобы расшифровать/зашифровать данные. Таким образом, не дорабатывая сами файловые системы, мы можем зашифровать данные файла. Фильтры могут привязывать свои уникальные данные к структурам файлов, которые формирует файловая система. Вместе драйвера фильтров и драйвер файловой системы формируют стек файловой системы. Количество фильтров может быть разным, также могут быть разными и сами фильтры. Теоретически их может и не быть совсем, но практически так не бывает. На рисунке 7 изображен стек файловой системы, в состав которого входят три фильтра.

Рисунок 7

2b5a56b31ee44b56854d1332dd44c5bc.png

До того, как запрос достигнет файловой системы, он проходит последовательно через фильтры 1, 2 и 3. Когда запрос будет обработан файловой системой, то фильтрами он виден в обратном порядке, т.е. запрос проходит последовательно через фильтры 3, 2 и 1. Также, на примере выше фильтр 1 и фильтр 3 привязали свои структуры к структуре файла, которую сформировала файловая система после выполнения запроса открывания/создания файла.
Подавляющее большинство задач решается посредством фильтрации, но наш случай уникален. Как было ранее отмечено, уникален он тем, что нужно предоставлять одновременный доступ к зашифрованному и расшифрованному содержимому, а также шифровать имена файлов. Тем не менее, мы попытаемся разработать фильтр, который позволит решить такую задачу.
На рисунке 8 изображена ситуация, когда файл ранее был открыт расшифрованным.

Рисунок 8

7e75507cf8e647fca72a258ce3ff08cf.png

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

Рисунок 9

0bb39d231eca413d99c55a30336cc19a.png

Теперь представим себе, что пришел запрос на чтение расшифрованного содержимого. Файловая система воспользуется услугами кэш менеджера и передаст ему структуру файла, к которой и кэш менеджер, и менеджер памяти привяжут свои уникальные данные для дальнейшего управления отображением и кэшированием файла. Эта ситуация изображена на рисунке 10.

Рисунок 10

ba13017519e143ccb63bdb40bb85c9fb.png

Далее приходит запрос на чтение зашифрованного содержимого, и файловая система снова передает структуру файла кэш менеджеру, а поскольку кэш менеджер и менеджер памяти ранее к этой структуре привязали уникальные данные для этого файла, они просто воспользуются ими для выполнения запроса. Т.е. теперь в этих структурах будет указано, куда был отображен файл, и кэш менеджер просто скопирует данные файла из виртуальной памяти в буфер запросчика. Мы помним, что файл при первом доступе к нему был закэширован расшифрованным, таким образом при зашифрованном доступе в буфере запросчика окажутся расшифрованные данные.
Мы только что разобрали две фундаментальные проблемы. На практике их было больше. Например, в рамках поставленной задачи к каждому зашифрованному файлу необходимо добавлять заголовочную информацию, что тоже не решается посредством фильтрации. Чтобы обойти все эти проблемы разом, было найдено решение — виртуальная файловая система.
Виртуальная файловая система, по своей сути, мало чем отличается от обычной. Радикальное отличие состоит в том, что обычная файловая система работает с диском, а виртуальная работает с файлами других файловых систем, т.е. является обычным потребителем сервисов файловых систем. На рисунке 11 изображена концепция виртуальной файловой системы.

Рисунок 11

a44fa5bb2d234ff0910af78dd5dfbdf8.png

В отличие от обычных файловых систем, виртуальная файловая система не регистрирует себя в системе (об этом не было сказано выше, но для того чтобы операционная система могла пользоваться сервисами конкретной файловой системы, она должна зарегистрироваться), а раз так, то запросы к ней направляться не будут. Чтобы решить эту проблему, мы используем фильтр для обычной файловой системы, но на этот раз его функции будут очень простыми и ограниченными, одна из них (и главная) — это отслеживание доступа к зашифрованным файлам. Как только фильтр увидит такой запрос, он просто перенаправит его к виртуальной файловой системе. Если же будет выполняться доступ к обычному файлу, запрос будет передан оригинальной файловой системе.
Теперь давайте еще раз разберем расшифрованный и зашифрованный доступы, но уже в контексте виртуальной файловой системы. На рисунке 12 приведен пример, когда выполнялся доступ к расшифрованному и зашифрованному содержимому конкретного файла.

Рисунок 12

98949b8c08c4453585f8a8634491090a.png

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

Рисунок 13

c183f03aecc1417f95e19506f93a6a82.png

Теперь если будет выполняться расшифрованное чтение, виртуальная файловая система, пользуясь сервисами кэш менеджера, как показано на рисунке 14, передаст ему структуры памяти, которые она вернула при открывании файла расшифрованным.

Рисунок 14

554eb662873c418f8c64f8299e0ae8a1.png

Кэш менеджер отобразит файл на виртуальную память и скопирует данные в буфер запросчика. Соответственно, в процессе копирования менеджер памяти пошлет запрос на чтение виртуальной файловой системе, которая в свою очередь пошлет запрос на чтение родной файловой системе, а поскольку кэш менеджеру были переданы структуры памяти для расшифрованного файла, виртуальная файловая система расшифрует данные и сообщит о завершении менеджеру памяти, так закэшируется расшифрованное содержимое. В случае если будет выполняться зашифрованное чтение, виртуальная файловая система передаст, как изображено на рисунке 15, кэш менеджеру структуры памяти, которые она сформировала при открывании файла зашифрованным.

Рисунок 15

cbdcf95ffd2241df9ba029f65cc753e3.png

Кэш менеджер снова отобразит файл на виртуальную память (поскольку в этих структурах файл еще не отображался) и скопирует данные в буфер запросчика. И опять в процессе копирования менеджер памяти пошлет запрос на чтение виртуальной файловой системе, которая снова пошлет запрос на чтение родной файловой системе, а поскольку кэш менеджеру были переданы структуры памяти для зашифрованного файла, то, не расшифровывая данных, виртуальная файловая система сообщит менеджеру памяти о завершении. Так файл закэшируется зашифрованным.
Как мы видим, виртуальная файловая система решает фундаментальные проблемы, не позволяющие иметь одновременный доступ к расшифрованному и зашифрованному содержимому файла, из-за чего приходится отказываться от классических механизмов операционной системы. В процессе фильтрации мы можем только добавлять данные к структурам памяти файлов, которые возвращает файловая система, и, поскольку мы не формируем эти структуры, мы не можем вмешиваться в них и управлять ими. А посредством виртуальной файловой системы мы их полностью формируем, и, следовательно, имеем полный контроль над ними, что необходимо в рамках решения данной задачи. Например, нам нужно обеспечивать согласованность расшифрованного и зашифрованного содержимых файлов. Представьте себе ситуацию, когда были записаны данные в расшифрованный файл и данные находятся еще в кэше, а не на диске. И в этот момент приходит запрос на чтение зашифрованного содержимого файла. В ответ на это виртуальная файловая система выгрузит расшифрованное содержимое на диск и сбросит зашифрованный кэш, что заставит кэш менеджер заново послать запрос на чтение виртуальной файловой системе для зашифрованного чтения. В рамках фильтрации подобная задача не решается в принципе.
При разработке виртуальной файловой системы приходилось сталкиваться с необычными проблемами. Отчасти это вызвано тем, что мы работаем с файлами файловых систем, когда обычные файловые системы работают с диском. Так, например, была найдена ошибка в файловой системе NTFS. Проявлялась она в том, что доступ к файлу X:\$mft\<любое имя> приводил к повисанию всего доступа к диску X. В результате исследования было установлено, что NTFS не освобождала механизмы синхронизации для файла $mft, который является перечислителем всех файлов диска. И соответственно, чтобы найти какой-либо файл на диске, сначала нужно прочитать $mft файл, доступ к которому повис. Другой пример, который нельзя назвать необычным, это найденная ошибка в ядре Windows 8, в результате которой менеджер памяти полагает, что структуры памяти файла всегда последней версии. Из-за этого он пытается использовать некоторые части этой структуры, которых в действительности может не быть. И это приводило к BSOD.
Реализация виртуальной файловой системы значительно сложнее реализации фильтра, но наличие такого механизма дает большую гибкость при манипуляциях с файлами. В том числе такую, о которой мы только что говорили. Хотя, на первый взгляд, может показаться, что задача тривиальна.
В результате применения данного подхода к шифрованию были успешно реализованы функции предоставления одновременного доступа программных процессов к зашифрованному и расшифрованному содержимому, а также реализовано шифрование имен файлов, что позволяет обеспечить высокую степень прозрачности при реализации криптографической защиты информации.
Надо отметить, что данный подход по обеспечению «прозрачности» шифрования файлов в ОС Windows успешно реализован в корпоративном продукте Secret Disk Enterprise («Аладдин Р.Д.»), который применяется многими организациями в России. Это в свою очередь доказывает жизнеспособность и перспективность применения данной идеи в процессе создания программ шифрования файлов на диске.
В качестве заключения стоит отметить, что технологическая сложность файловой системы ОС Windows и отсутствие стандартных механизмов решения задач, подобных описываемой в данной статье, будут всегда являться препятствием создания удобных и простых программ, обеспечивающих защиту информации. В таком случае единственным верным решением является самостоятельная реализация оригинального механизма, позволяющего обойти данные ограничения без потери функциональности ПО.

© Habrahabr.ru