вторник, 28 июня 2011 г.

Как поймать то, чего нет?

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

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

У меня был на руках только путь к перемещенному файлу. Поэтому пришлось придумывать нечто новое. И это новое было придумано.

Решение основывалось на системном механизме, называемом кеш менеджер. Информации по нему достаточно, поэтому описывать целиком не вижу смысла. Основная идея - кеширование файловых потоков, файловые потоки это не только данные файла, но и метаданные ФС, такие как Extended Attributes/Alternate Data Streams (а вот Reparse Point врятли кешируются).

Для моего случая интересен факт того, что каждый раз при 1й записи ( то есть при создании исполняемого файла ), файловая система вызывала
один из 4х интерфейсов кеш менеджера, чтобы закешировать файловые потоки ( предварительно создав и инициализировав CacheMap и прочие внутренние структуры связанные с файлом ). Узнать закеширован ли тот или иной файл можно, посмотрев соот-ю структуру FILE_OBJECT, а точнее поле PrivateCacheMap, если оно не нулевое - файл закеширован.

У кешменеджера есть рабочие потоки выполняющие операции write-behind/read-ahead. Write-behind скидывает содержимое кеша на диск с интервалом 1-3 секунды, если это не сделано явно. То есть можно записать в файл, а данные все равно какоето время будут в кеше, т.е. в виртуальной памяти, а не на диске. Таким образом накапливая данные в кеше, и сбрасывая их спустя время одномоментно, уменьшается количество дисковых операций ввода-вывода(если быть более точным, то уменьшает число позиционирования головок жесткого диска). Собственно это и есть главная ф-ция кешменеджера. Read-ahead в свою очередь выполняет упреждающее чтение файлов, когда инициирована операция чтения.

Также, с кешем связан еще один важный механизм, называемый fast I/O.
Везде, где возможно, чтение и запись в закешированные файлы разруливается через высокоскоростной механизм fast I/O.
Fast I/O означает чтение и запись кешированных файлов без работы по генерации IRP.
Это дает существенное быстродействие в некоторых случаях(пример - memory-mapped files).

Именно fast i/o запросы к ФС при создании процесса для получения метаданных(уже не помню, но скорее всего там идет получение EA) позволили ловить запуск несуществующих процессов.

Конкретно для моей задачи нужно было ловить FastIoQueryOpen в минифильтре.

Эквивалент операции FastIoQueryOpen для минифильтра это IRP_MJ_NETWORK_QUERY_OPEN.
Поэтому регистрируем эту операцию, устанавливаем Pre и Post колбеки.

В Pre колбеке просто возвращаем FLT_PREOP_SUCCESS_WITH_CALLBACK, чтобы вызвался Post колбек, а уже в нем проверяем статус операции
( на момент Post колбека операция завершена ), и если тот равен STATUS_OBJECT_NAME_NOT_FOUND, то просто сравниваем имя файла для данной операции, со списком сохраненных ранее файлов. Если совпало - это запуск процесса.

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

8 комментариев:

  1. к чему такие извращения ?
    если cache manager для данного файла уже инициализирован - то для него был ранее вызван mj_create
    не проще было через его перехват для данной fs поймать все что нужно ?

    ОтветитьУдалить
  2. Извращения из-за извращенной задачи. Поймать нужно не все что нужно, а именно запуск процесса, причем файл процесса редиректился в другое место. Создаваться могли не только исполняемые файлы, а любые другие.

    ОтветитьУдалить
  3. Рискну предположить, что задача возникла в ходе тестирования некого приложения?

    ОтветитьУдалить
  4. Скорее в ходе написания некоего приложения

    ОтветитьУдалить
  5. если верить Windows NT File System Internals - кеш для файла инициализируется даже не при открытии (которое может случиться например для получения атрибутов) файла, а при первой операции чтения/записи
    так что ты тоже не первый read/write ловишь. Соотв-но если файл читается целиком весь за один раз - ...

    ОтветитьУдалить
  6. В блоге так и написано: при записи, т.е. при создании файла. Про кеш я написал просто для информации, т.к. ловлю я fast i/o, которые основаны на доступе к закешированным данным.
    Вобщем целиком это както так выглядело - из инета загружается файл, затем в моем же фильтре он редиректиться в другое место, затем тот кто его загружает пытается запустить скачанный файл( а на старом месте его уже нет), и вот этот момент я и ловлю через доступ к несуществующему файлу через fast i/o. Все запутано, но все просто на самом деле, просто я объяснять не умею =]

    ОтветитьУдалить
  7. ты нашел частное неправильное в общем случае решение, которое не будет работать например для сетевых дисков
    Чтобы проверить что файл предназначен для image mapping (что и происходит при запуске процесса) нужно проверить что SECTION_OBJECT_POINTERS.ImageSectionObject != NULL, а вовсе не PrivateCacheMap

    ОтветитьУдалить
  8. Я нашел единственно верное решение, никаких секций там просто нет, так как нет файла.

    ОтветитьУдалить