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

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


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


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


    Репозиторий


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


    Теория


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


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


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


    Слои API

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


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


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


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


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


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


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


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


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


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


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


    Что там с WoW64?


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


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


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


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


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


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


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

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


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

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 12

      0
      ms почему-то решили выпилить поддержку транзакций из ntfs:
      docs.microsoft.com/en-us/windows/win32/fileio/about-transactional-ntfs
        0

        Да, я тоже это заметил. Возможно, имеется ввиду именно Win32 API, поскольку в документации для драйверов об этом ничего нет. Учитывая, что я пользуюсь именно Native API, а также то, насколько Microsoft бережно относится к его обратной совместимости, надеюсь, они не уберут транзакции в ближайшем будущем.

          0

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


          Тем не менее, думаю можно быть уверенными в том, что ни "компрессию" ни "транзакции" никогда не выпилят из NTFS. M$ либо оставит всё как есть до смерти Windows, либо перейдет на новую файловую систему без этих deprecated features.

            0
            Думаю, что так и будет, Майкрософт крайне редко удаляет старые технологии. Тот же Native Api тоже не рекомендуется к использованию, но… что массово используют, то будут поддерживать.
            Или же реально весь этот функционал перейдет в ядро.
          0
          Возможно, что тут дело как с потоками ntfs — мало кто юзает по назначению, а больше для других целей? Я не знаком с этой темой, просто предполагаю. Про эту технику, до статьи автора, я узнал из аверских блогов — 2 года назад на какой-то хакерской конфе показывали способ обойти антивирус с помощью транзакций (кому интересно, гуглите Process Doppelgänging ).
          0
          Я правильно понимаю, что функционал TxF объявлен deprecated? (См. docs.microsoft.com/en-us/windows/win32/fileio/deprecation-of-txf). Есть ли смысл развивать идею, основанную на таком API?
            +1

            Про deprecated ответил выше. В конце-концов это всего лишь эксперимент, вся идея для программы и статьи возникла из неожиданной простоты реализации. Посмотрим.

            +1
            Да уж, 9 лет назад я даже написал СУБД, использующую для реализации ACID TxF, как давно это было…

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

            Так что думаю, для песочницы не особо сгодится, только для маленьких операций. И да, deprecated уже очень давно и с неизвестным сроком окончания.
              0
              как эксперимент прикольно
                +1
                Интересно как отреагирует ReadDirectoryChangesW:
                • Будет ли отслеживать изменения в директории произведенные внутри одной транзакции
                • Будет ли отслеживать изменения в директории произведенные из всех транзакций

                image
                  0

                  Отличный вопрос. Как и ожидалось, всегда видны изменения снаружи транзакций + внутри той, где был открыт дескриптор директории. Изоляция работает предсказуемо.


                  Из документации

                  Transacted Operations
                  If there is a transaction bound to the directory handle, then the notifications follow the appropriate transaction isolation rules.


                  Меня это ещё вот на какую мысль навело. Тот же Process Monitor имеет минифильтр в режиме ядра, а значит видит все операции. Но он не показыает, в контексте какой транзакции они были проведены. Получается, что транзакции — идеальный инструмент для запутывания логов Process Monitor'a =)

                    0
                    Подразумеваю что после CommitTransaction должны быть видны изменения для всех, кто вне транзакции.
                    По поводу Process Monitor: возможно его минифильтр просто не доработан.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое