- PVSM.RU - https://www.pvsm.ru -

Заставляем любой процесс работать с транзакционной NTFS: мой первый шаг к созданию песочницы для Windows

TransactionMaster В ядре Windows есть модуль, отвечающий за поддержку группировки файловых операций в некоторую сущность, называемую транзакцией. Действия над этой сущностью изолированы и атомарны: её можно применить, сделав перманентной, или откатить. Очень удобно при установке программ, согласитесь? Мы всегда переходим от одного согласованного состояния к другому, и если что-то идёт не так, все изменения откатываются.

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

Давайте разберёмся, как же это работает, поэкспериментируем с моей программой, и поймём, при чём тут вообще песочницы.

Репозиторий

Для тех, кому не терпится попробовать: TransactionMaster на GitHub [1].

Теория

Поддержка транзакционной NTFS, или TxF, появилась в Windows Vista, и позволила существенно упростить код, отвечающий за восстановление при ошибках в процессе обновления ПО и самой ОС. Фактически, задачу по восстановлению перенесли на ядро операционной системы, которое стало применять полноценную ACID-семантику [2] к файловым операциям — только попроси.

Для поддержки этой технологии, были добавлены новые API функции, которые дублировали уже имеющуюся функциональность, добавляя один новый параметр — транзакцию. Сама транзакция стала одним из многих объектов ядра ОС, наряду с файлами, процессами и объектами синхронизации. В простейшем случае, последовательность действий при работе с транзакциями заключается в создании объекта транзакции вызовом CreateTransaction [3], работе с файлами (с использованием таких функций как CreateFileTransacted [4], MoveFileTransacted [5], DeleteFileTransacted [6] и им подобных), и применению/откату транзакции с помощью CommitTransaction [7]/RollbackTransaction [8].

Теперь давайте взглянем на архитектуру этих функций. Мы знаем, что документированный слой API, из таких библиотек как kernel32.dll, не передаёт управление в ядро ОС напрямую, а обращается к нижележащему слою абстракции в пользовательском режиме — ntdll.dll, который уже и производит системный вызов. И вот тут нас ожидает сюрприз: никакого дублирования функций для работы с файлами в контексте транзакций в ntdll, как и в ядре, просто нет.

Слои API

И тем не менее, прототипы этих фукнций из Native API не менялись с незапамятных времён, а значит о том, в контексте какой транзакции выполнять операцию они узнают откуда-то ещё. Но откуда? Ответ заключается в том, что у каждого потока есть специальное поле, в котором хранится дескриптор текущей транзакции. Область памяти, где оно находится, называется код последней ошибки [9] и идентификатор потока [10].

Таким образом, функции с суффиксом *Transacted устанавливают поле текущей транзакции, вызывают аналогичную функцию без суффикса, а затем восстанавливают предыдущее значение. Делают они это, используя пару функций RtlGetCurrentTransaction [11]/RtlSetCurrentTransaction [12] из ntdll. Код самих функций весьма прямолинеен, за исключением случая с WoW64, о чём будет ниже.

Что всё это значит для нас? Изменяя переменную в памяти процесса, мы можем контролировать, в контексте какой транзакции он работает с файловой системой. Не нужно ставить никаких ловушек и перехватывать вызовы функции, достаточно доставить дескриптор транзакции в целевой процесс и подправить несколько байт в его памяти для каждого из потоков. Звучит элементарно, давайте сделаем это!

Подводные камни

Cамые первые эксперименты показали, что идея работоспособна: Far Manager [13], которым я пользуюсь вместо проводника Windows, прекрасно переживает подмену транзакций на лету, и позволяет смотреть на мир в их контексте. Но также обнаружились и программы, которые постоянно создают новые потоки для файловых операций. И в первоначальном сценарии это прореха, поскольку отслеживать создание потоков в другом процессе не слишком-то удобно (не говоря уже о том, что "опоздания" здесь критичны). Примером приложения из второго класса является недавно портированный WinFile [14].

Отслеживающая DLL

К счастью, синхронное отслеживание создания потоков с последующей настройкой для них транзакций совершенно элементарно изнутри целевого процесса. Достаточно внедрить в него DLL, и загрузчик модулей будет вызывать её точку входа с параметром DLL_THREAD_ATTACH [15] каждый* раз при создании нового потока. Реализовав эту функциональность я починил совместимость ещё с доброй дюжиной программ.

* Технически, вызов срабатывает не всегда, и это поведение иногда можно пронаблюдать в интерфейсе моей программы. По большей части, исключениями являются потоки из рабочего пула самого загрузчика модулей. Всё дело в том, что оповещение DLL-библиотек происходит под блокировкой загрузчика, и это значит: загружать новые модули в этот момент нельзя. А потоки загрузчика, как вы понимаете, именно этим и занимаются, распараллеливая доступ к файловой системе. Для подобных случаев предусмотрено исключение: если указать THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH [16] в качестве флага при вызове NtCreateThreadEx [17], можно избежать присоединения нового потока к существующим DLL, и, соответственно, взаимных блокировок. Примерно это здесь и происходит.

Запускаем проводник

Осталась третья, последняя категория программ, которые до сих пор падают при попытке заставить их работать внутри транзакции. Одна из этих программ — проводник Windows. Я не могу точно диагностировать проблему, но приложение это сложное, и горячее переключение внутрь транзакции сказывается на нём не очень. Возможно, причина в том, что оно имеет много открытых файловых дескрипторов, часть из которых перестаёт быть действительными в новом контексте. А может это что-то ещё. В подобных ситуациях помогает перезапуск процесса, да так, чтобы он с самого начала работал в транзакции. Тогда никаких несогласованностей возникнуть не должно.

А потому, я добавил в программу возможность запуска новых процессов, для которых транзакция и слежение за новыми потоками настраивается ещё до достижения точки входа, пока процесс приостановлен. И знаете что, оно заработало! Правда, поскольку проводник активно использует объекты COM вне процесса, предпросмотр ломается при перемещении файлов. Но в остальном — всё стабильно.

Что там с WoW64?

Эта подсистема для запуска 32-битных программ на 64-битных системах является крайне удобным инструментом, но необходимость учёта её особенностей часто осложняет системное программирование. Выше я упоминал, что поведение Rtl[Get/Set]CurrentTransaction заметно отличается в случае подобных процессов. Причина этому кроется в том, что потоки в WoW64-процессах имеют целых два блока окружения. Они имеют разные размеры указателя, и их желательно поддерживать в согласованном состоянии, хотя, в случае транзакций, 64-битный TEB имеет приоритет. Когда мы устанавливаем транзакции удалённо, мы должны воспроизвести поведение этих функций. Это не сложно, но забывать об этом не стоит, а подробности можно посмотреть здесь [18]. И последнее, для WoW64 процессов нужна дополнительная 32-битная копия нашей отслеживающей DLL.

Нерешённые проблемы

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

Во вторых, особого внимания заслуживает случай с исполняемыми файлами, которых не существует снаружи транзакции. Помнится, был какой-то вирус, который обманывал наивные антивирусы подобным образом: распаковывался внутрь транзакции, запускал себя, а затем откатывал транзакцию. Процесс есть, а исполняемого файла нет. Антивирус мог решить, что сканировать нечего, и проигнорировать угрозу. Здесь тоже нужно поработать над креативными решениями, поскольку, по некоторой причине, NtCreateUserProcess [19] (и, соответственно, CreateProcess [20]) игнорирует текущую транзакцию. Конечно, всегда остаётся NtCreateProcessEx [21], но с ним ожидается много возни для устранения проблем с совместимостью. Ничего, что-нибудь придумаю.

Причём тут песочницы?

Взгляните на картинку. Здесь три разных программы показывают содержимое одной и той же папки из трёх разных транзакций. Классно, правда?

Взгляд изнутри транзакций

И всё же, моя программа — ни в коем случае не песочница, ей не хватает одной важной детали — границы безопасности. Конечно, это не мешает некоторым компаниям продавать сходные поделки под видом полноценных песочниц, позор им, что я могу сказать. И, несмотря на то, что это кажется совершенно невозможным, — как вообще мы можем запретить программе изменить переменную в своей же памяти, будь мы даже отладчиком? — у меня припасён один восхитительный трюк, который позволит завершить начатое и создать первую известную мне песочницу, которая не будет требовать драйвера, но будет виртуализировать файловую систему. А до тех пор — ждите обновлений, используйте Sandboxie [22] и экспериментируйте с технологией AppContainer [23]. Спасибо за внимание.

Репозиторий проекта на GitHub: TransactionMaster [1].
Эта же статья на английском [24].

Автор: diversenok

Источник [25]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/delphi/344783

Ссылки в тексте:

[1] TransactionMaster на GitHub: https://github.com/diversenok/TransactionMaster

[2] ACID-семантику: https://ru.wikipedia.org/wiki/ACID

[3] CreateTransaction: https://docs.microsoft.com/ru-ru/windows/win32/api/ktmw32/nf-ktmw32-createtransaction

[4] CreateFileTransacted: https://docs.microsoft.com/ru-ru/windows/win32/api/winbase/nf-winbase-createfiletransactedw

[5] MoveFileTransacted: https://docs.microsoft.com/ru-ru/windows/win32/api/winbase/nf-winbase-movefiletransactedw

[6] DeleteFileTransacted: https://docs.microsoft.com/ru-ru/windows/win32/api/winbase/nf-winbase-deletefiletransactedw

[7] CommitTransaction: https://docs.microsoft.com/ru-ru/windows/win32/api/ktmw32/nf-ktmw32-committransaction

[8] RollbackTransaction: https://docs.microsoft.com/ru-ru/windows/win32/api/ktmw32/nf-ktmw32-rollbacktransaction

[9] код последней ошибки: https://docs.microsoft.com/ru-ru/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror

[10] идентификатор потока: https://docs.microsoft.com/ru-ru/windows/win32/api/processthreadsapi/nf-processthreadsapi-getcurrentthreadid

[11] RtlGetCurrentTransaction: https://github.com/processhacker/processhacker/blob/027e920932f8ca8b971aa499b1788065d3cdb720/phnt/include/ntrtl.h#L4376-L4381

[12] RtlSetCurrentTransaction: https://github.com/processhacker/processhacker/blob/027e920932f8ca8b971aa499b1788065d3cdb720/phnt/include/ntrtl.h#L4386-L4391

[13] Far Manager: https://farmanager.com/

[14] WinFile: https://github.com/microsoft/winfile

[15] DLL_THREAD_ATTACH: https://docs.microsoft.com/ru-ru/windows/win32/dlls/dllmain

[16] THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH: https://github.com/processhacker/processhacker/blob/027e920932f8ca8b971aa499b1788065d3cdb720/phnt/include/ntpsapi.h#L1757

[17] NtCreateThreadEx: https://github.com/processhacker/processhacker/blob/027e920932f8ca8b971aa499b1788065d3cdb720/phnt/include/ntpsapi.h#L1765-L1780

[18] здесь: https://github.com/diversenok/NtUtilsLibrary/blob/2f7b1c82fcdcf49907c7e94ef6c36262eaf95016/NtUtils.Transactions.Remote.pas#L73-L141

[19] NtCreateUserProcess: https://github.com/processhacker/processhacker/blob/027e920932f8ca8b971aa499b1788065d3cdb720/phnt/include/ntpsapi.h#L1737-L1752

[20] CreateProcess: https://docs.microsoft.com/ru-ru/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw

[21] NtCreateProcessEx: https://github.com/processhacker/processhacker/blob/027e920932f8ca8b971aa499b1788065d3cdb720/phnt/include/ntpsapi.h#L1098-L1111

[22] Sandboxie: https://sandboxie.com

[23] AppContainer: https://docs.microsoft.com/ru-ru/windows/win32/secauthz/appcontainer-isolation

[24] Эта же статья на английском: https://habr.com/en/post/485788/

[25] Источник: https://habr.com/ru/post/485784/?utm_source=habrahabr&utm_medium=rss&utm_campaign=485784