Начиная с Windows XP SP2 и, возможно, даже с Windows 2000 SP3, хотя тут я не уверен, в системе появился , который вполне может быть использован в ваших собственных приложениях для реализации чего-нибудь типа Web-сервера. К слову, данное API также доступно и для .Net-приложений, начиная с .Net 2.0, и, кроме того, IIS 6.0 написан с использованием этих интерфейсов. Но самое интересное в этом всём, что данный API имеет поддержку в режиме ядра, которую обеспечивает ему драйвер http.sys. Помимо прочего, этот драйвер имеет пару особенностей, которые могут быть интересны разработчикам TDI-фильтров. О них я и расскажу чуть ниже.
Введение про прямую отправку
Интерфейс транспортных устройств () имеет в своём составе запрос, который предписывает транспортному драйверу (например, tcpip.sys) отправить порцию данных по установленному ранее соединению. Этот запрос называется и он хорошо всем известен ибо описан в официальной документации. Однако, те TDI-драйвера, которые поставляются вместе с Windows, поддерживают чуть больше функциональности, чем об этом заявлено, в частности, tcpip.sys поддерживает прямую отправку данных путём вызова его Send-обработчика (функция TCPSendData) напрямую из кода драйвера-клиента. Такой вызов полностью обходит всю подсистему ввода/вывода ядра, а перехватить его в TDI-фильтре можно, подменив адрес обработчика на свой в функции завершения специального query-запроса к tcpip.sys. Добавлю ещё, что данный метод отправки данных используется в драйверах http.sys и netbt.sys, а вот обычные сокеты почему-то упорно не хотят использовать эти возможности, что немного странно.
Перехват прямых обработчиков
Как я уже сказал выше, существует некий I/O-запрос, который можно отправить к \Device\Tcp и получить на выходе адрес функции-обработчика tcpip!TCPSendData, которая имеет следующий вид:
При чём этот запрос отправляется не через internal device I/O, как можно было бы подумать, а через обычный device I/O, не забудьте учесть это. Определяется запрос следующим образом:
Второй I/O-запрос это для прямой отсылки датаграмм и нам сейчас не интересен. Как из определения, для обоих буферов, входного и выходного, используется один и тот же буфер, адрес которого можно взять из поля Type3InputBuffer в параметрах текущей стековой ячейки запроса. Его следует привести к PVOID*, т.к. это адрес переменной, в которую на выходе должен быть записан адрес обработчика. В функции завершения запроса вы можете сохранить оригинальный обработчик и записать в эту переменную адрес своей функции.
Особенности ядерного HTTP-сервера и прямой отправки
Первая особенность заключается в том, что в поле MinorFunction указанной стековой ячейки выставлено не TDI_DIRECT_SEND, как можно было бы ожидать, а обычное TDI_SEND. Т.е. если вы пытаетесь отличать прямые Send-запросы от обычных на основе этих значений, то это ошибка, как видите.
Вторая особенность заключается в том, что, в отличие от сокетного драйвера, ядерный HTTP-сервер, как правило, шлёт данные не в одном MDL, а в цепочке из несколько MDL-структур (см. поле Next в структуре MDL). Т.е. если вы копируете данные из того MDL, который указан в IRP, или же подменяете содержимое этого MDL на своё с целью инжекта данных в поток, то вам придётся делать это в цикле, чтобы пройтись по всем MDL в цепочке. При этом, если вы полностью подменяете MDL в запросе, т.е. создаёте свой MDL и записываете его адрес в запрос, то не забудьте поставить свою функцию завершения в pIoStack, чтобы в ней вернуть оригинальные данные на место, иначе получите падение в настоящей функции завершения транспортного клиента.
Довольно часто на форумах спрашивают, как в драйвере получить строку, содержащую путь к исполняемому файлу-образу процесса по его ID или по адресу его объекта (EPROCESS) или ещё как-то. Мне казалось, что данная задача уже обсосана со всех сторон везде, где только можно, но похоже, что это не так. На самом деле, задачу можно и даже нужно разделить на две подзадачи:
Получить адрес файлового объекта
Извлечь путь к файлу из файлового объекта
Первая задача напрямую не документирована, нам предлагается несколько обходных путей, со второй задачей проблем более-менее нет. Рассмотрим всё по порядку, а в конце я напишу о способе, которым пользуюсь сам. И, конечно же, здесь и далее подразумевается, что адрес объекта-процесса у вас уже есть или вы можете легко его достать (например, из с помощью вызова ).
Колбеки файловых фильтров
Все способы, предлагаемые Microsoft, сводятся либо к написанию файлового фильтра, либо к использованию определённых колбеков, но суть одна и та же - вам необходимо вести и поддерживать в своём драйвере карту, ключом которой является или ID, или адрес объекта-процесса, а значением - связанный с ним файловый объект, ну или сразу путь к файлу-образу.
Для реализации данного решения необходимо написать каркас файлового фильтра (не важно, legacy или на минифильтрах), который нужен для перехвата операции создания секции. Для legacy-фильтров далее необходимо зарегистрировать колбек-нотификатор вызовом , поле называется PostAcquireForSectionSynchronization, а для минифильтров ничего делать не нужно, только не забудьте при регистрации заполнить правильно ячейку для операции .
В пост-колбеке в параметрах операции мы имеем уже готовенький файловый объект, который есть не что иное, как исполняемый файл-образ. Единственное, что нам неизвестно, это является ли данный файл-образ именно исполняемой программой или же это .DLL; по расширению файла определять это как-то глупо, поэтому придётся открывать файл и смотреть PE-заголовок. Кроме того, неизвестно самое главное - является ли данный файл-образ именно тем файлом, на базе которого создана секция процесса, или же это просто какой-то исполняемый файл, загруженный процессом в ходе его работы. Обе эти проблемы можно решить несложной эвристикой, но я подробно останавливаться на этом не буду, т.к. очевидно, что данный метод не надёжен.
Колбек на исполняемые образы
Этот колбек устанавливается через, наверное, всем знакомый уже вызов . Для главного исполняемого файла-образа процесса этот колбек вызывается самый первый раз для нового процесса, затем он же вызывается для ntdll.dll, для kernel32.dll и т.д. Помимо прочего в параметрах приходит также и ID процесса, в который проецируется указанный исполняемый образ, так что составить карту, где ключ это ID процесса, а значение путь к файлу-образу труда не составит.
К сожалению, с этим колбеком есть ряд косяков. Первый заключается в том, что имя файла-образа всегда приходит в Native-формате. Но это не самое страшное, на тему преобразования путей вы можете почитать в этом блоге. Вторая проблема, что путь иногда приходит не весь, а обрезанный слева. Как правило, такая ситуация встречается для модулей режима ядра (т.е. для драйверов), но для пользовательских модулей гарантии тоже никто не даст, тем более, что в документации сказано, что в каких-то случаях путь не будет предоставлен вообще. Ну и третий косяк заключается в том, что доступ к файловому объекту дают только начиная с Vista, что не очень хорошо.
Использование NT-сервисов
Существует всем известный и даже документированный NT-сервис, называется . Его можно вызвать с классом ProcessImageFileName и на выходе будет путь к файлу-образу процесса в родном формате. Чтобы получить путь в DOS-формате, нужно будет использовать класс ProcessImageFileNameWin32, который доступен только начиная с Windows Vista.
Данный сервис не очень хорошо справляется с сетевыми именами. Плюс это банально оверхед: если у вас уже есть адрес объекта-процесса, вам придётся открывать его через , получать хендл и звать этот query-сервис. А если у вас уже есть ID процесса, то всё равно эффективнее будет получить указатель на объект-процесс и далее уже как написано ниже, чем открывать его через .
Использование системного API
Начиная с Windows Vista, ядро экспортирует весьма полезную в нашем деле API-функцию:
NTSTATUS PsReferenceProcessFilePointer ( IN PEPROCESS pProcess, OUT PFILE_OBJECT* ppFileObject);
Всё просто: передаёте ей указатель на процесс, она возвращает адрес файлового объекта, который представляет собой главный исполняемый файл-образ этого процесса. Возвращённый указатель необходимо освободить вызовом , т.к. внутри функции на него вешается дополнительная ссылка. Добавить тут особо больше нечего, способ всем хорош, кроме того факта, что на системах до Vista работать не будет по причине отсутствия этой самой функции.
Получение файлового объекта напрямую
Почти все вышеуказанные способы имеют свои недостатки, при чём недостатки настолько серьёзные, что всерьёз их воспринимать я лично не считаю нужным. Ну, по крайне мере для нашей задачи о получении пути к основному файлу-образу процесса. Итак, допустим, у вас есть ID процесса (в ядре он обычно типа HANDLE). С помощью вызова получите адрес объекта-процесса. А дальше самое интересное. В структуре процесса EPROCESS есть два интересных поля:
Значение поля SectionBaseAddress можно получить функцией PsGetProcessSectionBaseAddress, которая замечательно экспортируется из ядра, начиная с Windows XP. Чтобы получить значение SectionObject мы можем сделать вот что: сконструируем (можно на стеке) фейковую структуру EPROCESS, состоящую из полей типа ULONG_PTR и инициализируем её таким образом, чтобы каждое поле имело свой номер, который по факту будет смещением поля от начала структуры. Затем вызовем PsGetProcessSectionBaseAddress и возвращённое значение помножим на sizeof (ULONG_PTR), получим смещение поля SectionBaseAddress в байтах, отнимем sizeof (PVOID) и получим смещение поля SectionObject. Оно-то нам и нужно. Все необходимые нам далее структуры кратко указаны ниже:
Мы получили значения поля SectionObject, его настоящий тип это SECTION, выше эту структуру мы описали. Далее для Windows версий до Vista всё просто: добираемся до поля FilePointer, берём его и извлекаем его имя. Для Vista и выше всё чуть сложнее. Во-первых, нужно будет использовать соответствующую структуру для CONTROL_AREA, и во-вторых, файловый указатель из поля FilePointer нужно извлекать как fast ref, но это не сложно, информацию об этом, думаю, найдёте в сети самостоятельно.
Извлечение имени файлового объекта
Итак, получили указатель на файловый объект. Теперь всё зависит от того, какой файловой системе принадлежит этот файл - локальной или сетевой, для этого анализируем поле DeviceType у девайса FileObject- >DeviceObject. Далее нужно определиться, в каком формате нужно имя - в родном или в DOS-виде с буквой тома? Для случая DOS-формата для локальной файловой системы достаточно вызвать , а в случае сетевой файловой системы нужно будет перечислить все сеансовые буквы и проанализировать имена сетевых девайсов на совпадение с именем тома файлового объекта, которое, в свою очередь, можно получить через . Но тут не все так просто будет: имена сетевых редиректоров ни чем не регламентированы, поэтому здесь потребуется некая эвристика, чтобы корректно проанализировать имена сетевых девайсов. При чём, в случае сетевой файловой системы даже для того, чтобы получить UNC-путь, придётся повозиться с анализом девайсов. Удачи.
Косяков в технологии на системах до Vista вообще не мало и потому сегодня расскажем ещё об одном из них. Речь пойдёт о , извещающем клиента о том, что удалённая сторона закрывает установленное соединение (из документации): "A call to ClientEventDisconnect notifies the local-node client that its remote-node peer is closing their established endpoint-to-endpoint connection."
Суть проблемы
Дело в том, что по документации (да и по логике вообще) указанный выше нотификатор должен вызываться транспортным драйвером только для установленных соединений. Однако я столкнулся с ситуацией, когда данный колбек вызывается для соединения, которое было лишь привязано (associated) к локальному адресу, но ещё не было установлено (established), при чём во флагах операции установлен release-флаг, что означает получение пакета FIN для соединения. В итоге, всё это приводило, например, к тому, что в моём фильтре данные объекта-соединения (протокол, удалённый адрес и т.д.) в определённые моменты использовались недействительные, т.к. до момента установки соединения соответствующие поля в моей структуре объекта-соединения были тупо не инициализированы.
Исследования
Я проверял эту ситуацию неоднократно и не находил ошибки в своём коде. Это не значит, что её нет, разумеется, однако это значит, что вероятность этого крайне мала. Флаги объекта-соединения во время такого "некорректного" вызова однозначно указывали на то, что до этого момента не было ни попытки установить соединение (connect), ни исходящего разрыва соединения (outgoing disconnect), ни чего-либо подобного. Т.е. тупо создали локальный адрес, создали удалённую точку, связали их и тут же жахнули входящим дисконнектом.
Кстати, два интересных момента: во-первых, инициатором всех запросов был netbt.sys, а этот драйвер известен своим не совсем стандартным поведением (в отличие от afd.sys того же, например), во-вторых, на Windows 7 я ни разу так и не смог воспроизвести данную проблему, возможно, там этот косяк TCP/IP-стека таки исправили. В итоге, я решил никак не реагировать на подобные вызовы и тупо игнорировать их. Если вы знаете, что всё это значит и зачем это, просьба отписать мне на почту. Спасибо.
К сожалению, частые сбои в работе хостера этого блога таки сподвигли меня на создание оффлайновой копии блога в виде единого .chm-файла, который без проблем может быть открыт во всех системах, начиная с Windows XP и заканчивая Windows 7. Ссылка на этот файл расположена ниже и будет периодически обновляться по мере добавления новых сообщений.
Файл копии блога
На данный момент свежий .chm-файл с копией блога можно взять . Если вы заметили какие-либо проблемы во время открытия или чтения .chm-файла, напишите мне на почту и я постараюсь их исправить. Также пишите любые пожелания касаемо .chm-файла. Спасибо.
Почти всё, о чём пойдёт речь ниже, - недокументировано и это, в общем-то, правильно, а данная информация представлена здесь исключительно в образовательных целях, потому что никакого практического применения ей я не вижу. Всё, что написано далее, будет касаться исключительно протоколов IP, TCP и UDP, поставщика AFD, сокетых хендлов и кое-чего ещё по мелочи.
Общие сведения
Прежде всего необходимо разобраться с архитектурой сокетов хотя бы в общих чертах. Сразу же замечу, что эти нюансы ни коим образом не нужны обычным приложениям, использующим сеть. Данная информация прежде всего требуется для создания так называемых Windows Sockets Service Provider (WSP), то есть , а ещё точнее - некоего компонента, который будет отвечать за передачу пакетов на транспортном уровне (например, TCP, UDP, etc.) в режиме пользователя. На самом деле WSP бывают , но другие нас сейчас не интересуют. Помимо этого, данная информация полезна при написании Layered Service Provider (LSP), т.е. сокетных фильтров пользовательского уровня.
Транспортный поставщик (provider, далее "поставщик") - это прежде всего, некий DLL-модуль, реализующий один или более базовых протоколов. Каждый такой модуль регистрируется в специальном ключе реестра и может быть представлен структурой . Приложение может запросить у системы список зарегистрированных протоколов вызовом функции , однако обычно этого делать не нужно, потому как существует набор встроенных протоколов, - это TCP, UDP, IP и некоторые другие. При необходимости новый поставщик может быть установлен в систему программно вызовом .
Для создания сокета с использованием того или иного протокола приложение вызывает функцию (или , что в принципе одно и то же), при этом реально протокол идентифицируется не только значением, переданным в параметре protocol, но также его семейством (af) и типом (type). Вооружившись всеми тремя значениями, библиотека WinSock (далее WS) ищет в своём кэше подходящий протокол, если ничего не найдено, то кэш обновляется из реестра и процедура повторяется, если снова ничего не найдено, возвращается ошибка. При поиске поставщика учитывается , в котором они зарегистрированы; изменить его можно либо программно с помощью функции , либо вручную посредством утилиты sporder.exe.
Когда WS находит подходящего поставщика, она вызывает его внутреннюю функцию с именем , она как и другие SPI-функции не экспортируется, а передаётся в общем порядке в таблице указателей функций во время загрузки и инициализации поставщика при вызове . Функция WSPSocket создаёт сокет и возвращает некое значение, которое называется хендлом сокета и может быть интерпретировано как обычный файловый хендл, по крайне мере, на данный момент система гарантирует это. Тем не менее, это значение приложению следует воспринимать как "чёрный ящик" и использовать только в специально предназначенных для этого сокетных функциях, а также в функциях, которые оговорены отдельно (например, функции чтения/записи файлов).
Если всё вышесказанное подытожить, то получается такая картина: сама WS это некий менеджер, предоставляющий сетевым приложениям интерфейс для доступа к сервисам поставщиков, а вот как раз последние и реализуют всю основную логику для установления соединения, передачи данных и т.д. и для этого они могут использовать любые подручные средства, например, AFD, являясь одним из встроенных поставщиков, реализующий такие протоколы как TCP, UDP и IP, использует вспомогательный драйвер, который есть не что иное, как TDI-клиент.
Ancillary Function Driver (AFD)
AFD - это, как мы уже знаем, один из тех самых транспортных поставщиков, о которых речь шла в предыдущем разделе. Сам AFD архитектурно состоит из двух компонентов: модуля пользовательского режима msafd.dll и драйвера режима ядра afd.sys. При этом msafd.dll реально никогда не используется (там тупо заглушка-перенаправление), а код поставщика слинкован в mswsock.dll. Драйвер AFD является полнофункциональным TDI-клиентом, т.е. использует чуть ли не все возможности клиентского интерфейса TDI. К слову, полный исходный код AFD можно найти в сети как часть исходного кода Windows NT 4.0, а его компоненты пользовательского режима есть в исходном коде Windows 2000, который также можно выловить в сети.
Связь пользовательской DLL с драйвером AFD осуществляется на основе обычных управляющих запросов (IOCTL), отправляемых к устройству под именем "\Device\Afd". Обычно, для этого используется вызов NtDeviceIoControlFile и APC-колбеки для асинхронных запросов. Запросы все эти недокументированы, но их использование тем не менее даёт возможность контролировать состояние отдельных сокетов. Для примера давайте посмотрим, что конкретно происходит при создании самого обычного TCP-сокета:
Приложение вызывает функцию socket или WSASocket.
Менеджер ищет подходяшего поставщика и находит AFD.
Менеджер вызывает функцию WSPSocket найденного поставщика.
Менеджер возвращает значение, полученное от вызова поставщика.
Теперь давайте посмотрим, что делает AFD в своей WSPSocket:
Определяет имя транспорта и загружает дополнительные DLL-модули.
Создаёт и инициализирует структуру для обслуживания нового сокета.
Создаёт и инициализирует т.н. open packet и записывает его в буфер для EAs.
Создаёт файл с именем "\Device\Afd\Endpoint", что приводит к созданию сокетного объекта, но уже в ядре.
Запрашивает некоторую информацию по новому сокету из драйвера (размер окна передачи и так далее).
Создаёт контекст с некоторой информацией о сокете и передаёт её драйверу, при этом часть информации о сокете становится видимой за пределами текущего процесса.
Создаёт хендл для сокета, который и возвращается приложению.
На самом деле всё чуть сложнее, но суть уже понятна, я думаю. Теперь мы знаем, что обычный AFD'шный сокет состоит из следующих компонентов:
Некий реальный хендл, с которым связаны структуры в режиме ядра.
Некие структуры, выделенные в адресном пространстве процесса (в куче).
Некий дескриптор сокета, который мы знаем под алиасом SOCKET (по факту, это обычный хендл).
Пункт 6 нужен в т.ч. для корректной работы механизма , который позволяет воссоздать в одном процессе сокет, созданный в другом процессе. Пункт 7 осуществляется по следующим правилам:
Если сокет представляет из себя настоящий файловый хендл, получаемый от таких функций как , то поставщик должен вызвать функцию , чтобы уведомить WS о создании нового хендла. При этом WS возвращает новый хендл, который может отличаться по значению, однако будет по-прежнему пригоден для использования в стандартных файловых операциях вроде или .
В любом другом случае (т.е. сокет представляет из себя не файловый хендл, а что-то другое) поставщик должен вызвать функцию , она создаст файловый хендл, который и должен быть возвращён приложению. Для создания файлового хендла эта функция задействует драйвер под именем ws2ifsl.sys, единственная задача которого, это перенаправление файлово-сокетных запросов (например, ReadFile) в соответствующие им аналоги (например, ). Для непосредственно перенаправления используется механизм APC плюс выделенный в приложении поток, который почти всегда спит.
Такая постобработка сокетного хендла нужна для того, чтобы читать/писать в сокет можно было также и стандартными функциями вроде ReadFile/WriteFile. В поставщике AFD используется первый вариант, потому что там сокетный хендл это файловый хендл, открытый на устройстве "\Device\Afd", и перенаправлением запросов чтения/записи занимается сам драйвер afd.sys. Для не-файловых сокетов подобное перенаправление выполняется драйвером ws2ifsl.sys.
Поиск сокетных хендлов
Об AFD мы уже знаем немало, например, мы знаем, что значения сокетных дескрипторов типа SOCKET это не что иное как самый натуральный HANDLE, открытый на устройстве "\Device\Afd". Знание этого факта позволяет нам достаточно просто найти все сокеты всех процессов для стандартных протоколов вроде TCP, UDP и IP. Алгоритм поиска в режиме ядра и в режиме пользователя будут несколько отличаться, поэтому разберём оба и начнём с режима ядра.
Первое, что нужно сделать в режиме ядра, это получить указатель на объект-устройство "\Device\Afd", чтобы было с чем сравнивать устройства файловых объектов. Это можно легко сделать, вызвав .
Затем следует запросить полный список хендлов для всех процессов. Для этого нужно вызвать функцию с классом SystemExtendedHandleInformation. При необходимости здесь же можно отфильтровать те процессы, которые нас не интересуют.
Теперь у нас есть значения хендлов (HANDLE) и идентификаторы процессов (PID) и надо найти такие хендлы, которые представляют собой файловые объекты и которые открыты на устройстве драйвера AFD. Для этого следует перебрать все интересующие нас хендлы, для каждого из которых у нас будет структура примерно следующего вида:
Отсюда тип объекта можно узнать по индексу в поле ObjectTypeIndex, если не файл - пропускаем, иначе приводим поле Object к типу и сравниваем поле DeviceObject с полученным ранее объектом-устройством. Если совпало, значит это хендл сокета, который мы можем использовать для последующих операций с ним, иначе пропускаем. Значение хендла лежит в поле Handle и его тип HANDLE.
На самом деле, в реальном коде так делать нельзя, потому что значение в поле Object нам возвращается без дополнительной ссылки, т.е. формально обращаться мы к нему не можем, т.к. на момент обращения по данному указателю уже может быть мусор. Правильно делать так, как показано чуть ниже, только в п.3 там надо вызвать , передав ей в качестве типа объекта значение *IoFileObjectType, - так мы получим указатель на файловый объект, одновременно убедившись, что тип объекта действительно "File".
Когда мы нашли интересующие нас хендлы, их нужно скопировать в тот процесс, в котором будет осуществляться работа с ними. Для этого необходимо сначала получить хендл своего процесса, при этом учтите, что значение здесь не подойдёт, потому что это не хендл, а псевдохендл. Затем для каждого хендла в списке сделать следующее:
Получить указатель на объект целевого процесса вызовом .
Приаттачиться к целевому процессу вызовом .
Сделать копию хендла вызовом ZwDuplicateHandle (недокументированно).
Отсоеденить целевой процесс вызовом .
Освободить целевой процесс вызовом .
В режиме пользователя всё немного сложнее. Мы также получаем список хендлов через с тем же классом, также проверяем тип объекта, а вот принадлежность хендла к AFD придётся проверять рабоче-крестьянским методом. Для этого мы прежде всего скопируем хендл в свой процесс через . Теперь проверим тип объекта вызовом , если вернула FILE_TYPE_PIPE, значит это канал и мы этот хендл пропускаем. В противном случае вызываем с классом ObjectNameInformation и сравниваем полученное имя со строкой "\Device\Afd" либо с маской "\Device\Afd\*". На данный момент я видел только два варианта имён этих объектов: "\Device\Afd" и "\Device\Afd\Endpoint", но возможно есть и другие.
Сокетные операции
Операции это IOCTL-запросы, посылаемые драйверу поставщика AFD, то бишь к afd.sys. Их достаточно много, список основных запросов приведён ниже. Некоторые из них мы рассмотрим в следующих частях данного материала. Некоторую информацию о внутреннем устройстве тех или иных запросов можно подсмотреть в проекте ReactOS, хотя насколько точно она соответствует реальному положению вещей в Windows - судить не берусь, не знаю.
Некоторые из этих запросов имеют точное соответствие с сокетными операциями уровня приложений. Например, когда приложение вызывает сокетную функцию , происходит следующее: shutdown находит поставщика и вызывает его обработчик . Далее в случае AFD тот посылает драйверу запрос IOCTL_AFD_PARTIAL_DISCONNECT, в ответ на который тот корректно закрывает соответствующие TDI-объекты (удалённую точку и/или локальный адрес). В ходе этой процедуры для соответствующих объектов посылаются такие запросы как , , а также и .
Или возьмём, к примеру, функцию , она также находит поставщика и вызывает его обработчик , который, в случае AFD, пошлёт своему драйверу запрос IOCTL_AFD_POLL, чтобы узнать состояние сокета. И так далее. Ниже список наиболее интересных на мой взгляд запросов драйвера AFD, сгруппированных по смыслу.
Серверные запросы:
IOCTL_AFD_BIND - создаёт и инициализирует объект локальный адрес. IOCTL_AFD_START_LISTEN - устанавливает колбек на входящие соединения. IOCTL_AFD_WAIT_FOR_LISTEN - ожидает входящего соединения. IOCTL_AFD_ACCEPT - принимает входящее соединение. IOCTL_AFD_SUPER_ACCEPT - более производительный вариант приёма входящего соединения. IOCTL_AFD_DEFER_ACCEPT - откладывает приём соединения, если приложение не смогло принять решение немедленно.
Клиентские запросы:
IOCTL_AFD_CONNECT - инициирует исходящее соединение. IOCTL_AFD_PARTIAL_DISCONNECT - полное или частичное закрытие сокета.
Информационные запросы:
IOCTL_AFD_GET_CONTEXT_LENGTH - запрос размера контекста сокета. IOCTL_AFD_GET_CONTEXT - запрос контекста сокета. IOCTL_AFD_SET_CONTEXT- сохранение контекста сокета в ядре. IOCTL_AFD_GET_INFORMATION- запрос информации о сокете. IOCTL_AFD_SET_INFORMATION - установка параметров сокета. IOCTL_AFD_QUERY_HANDLES- запрос TDI-хендлов сокета. IOCTL_AFD_QUERY_RECEIVE_INFO- запрос кол-ва доступных для приёма байт. IOCTL_AFD_GET_UNACCEPTED_CONNECT_DATA - запрос данных, пришедших с удалённого хоста вместе с SYN-пакетом. IOCTL_AFD_GET_ADDRESS - запрос удалённого адреса в случае присоединённого сокета, и локального адреса в противном случае.
Запросы-нотификаторы:
IOCTL_AFD_ROUTING_INTERFACE_CHANGE - мониторинг изменений маршрута следования для заданного адреса. IOCTL_AFD_ROUTING_INTERFACE_QUERY- запрос маршрутаследованиядля заданного адреса. IOCTL_AFD_ADDRESS_LIST_CHANGE - мониторинг изменений в составе адресов локальных интерфейсов. IOCTL_AFD_ADDRESS_LIST_QUERY- запрос списка адресов локальных интерфейсов.
Отправка/приём данных:
IOCTL_AFD_SEND - отправка потока данных. IOCTL_AFD_SEND_DATAGRAM - отправка датаграммы. IOCTL_AFD_TRANSMIT_FILE- передача содержимого файла. IOCTL_AFD_RECEIVE - приём потока данных. IOCTL_AFD_RECEIVE_DATAGRAM - приём датаграммы. IOCTL_AFD_POLL- проверка состояния сокета.
Другие запросы:
IOCTL_AFD_EVENT_SELECT - инициирует проверку событий и сигнализирует соответствующий объект, если есть новые события. IOCTL_AFD_ENUM_NETWORK_EVENTS - возвращает список событий сокета, ожидающих обработки.
Заключение
Формат входных и выходных структур для управляющих запросов AFD меняется от версии к версии системы, так что написание собственных нативных сокетов на IOCTL-запросах представляется нетривиальной задачей, требующей анализа исходного кода операционной системы, а также реверсинга некоторых её частей (например, драйвера afd.sys). В следующих частях, если таковые будут, рассмотрим написание собственного простейшего клиента AFD. Надеюсь, что данная информация оказалась полезной, однако следует помнить, что практического применения ей найти едва ли возможно.
Недавно я столкнулся с ошибкой в коде своего фреймворка, которая приводила к падению системы на Windows 7 x86, поэтому я подумал, что будет не лишним рассказать о возможной проблеме. Вообще, ошибка сама по себе примитивнейшая, но её последствия оказалось не так просто диагностировать. Падение происходило при освобождении IRP в функции , если точнее - в функции ExpReleasePoolQuota. Для тех, кто ещё не в курсе, дам некоторые пояснения, потом рассмотрим непосредственно ошибку и возможности по её диагностированию.
Квотирование пула
Структуры IRP, как и большинство других системных структур, выделяются из пула (обычно, невыгружаемого). Все выделения из пула могут быть квотированы, за исключением тех, которые выполняются для системного процесса (процесс ядра System). Другими словами, если вы вызываете , то ничего страшного не случится, но если вы вызовете, например, , то у текущего процесса станет на N байт меньше доступной пуловой памяти. Когда лимит пула для этого процесса будет исчерпан, все последующие выделения из пула для него будут заканчиваться неудачей. При освобождении блока пула через квота процесса будет восстановлена на N байт, выделенных ранее.
Блок пула логически делится на заголовок блока, тело блока и (на 32-битных системах) ещё служебные 4 байта после тела блока, выделяемые только при квотировании. При выделении блока пула с квотированием, ядро ставит флаг квоты в заголовке блока и записывает указатель на объект-процесс в сам блок пула. Так вот в 64-битных ядрах этот указатель на процесс пишется в заголовок пула (поле ProcessBilled), а на 32-битных ядрах этот же указатель пишется аккурат после тела блока, в те самые 4 байта, упомянутые выше. И ещё, обратите внимание, что при освобождении блока пула, флаг квоты в заголовке сбрасывается только на x64-системах, а на 32-битных ядрах лишь обнуляется указатель процесса. Далее речь пойдёт только о x86-системах.
Квотирование пуловой памяти запросов
Теперь давайте посмотрим, как выделяются пакеты запросов (IRP). Варианта, в основном, только два - либо запрос выделяется из кэша запросов, т.е. из подходяшего -списка, либо происходит выделение нового блока из пула. В случае, когда какой-либо драйвер создаёт новый запрос через с параметром ChargeQuota, установленым в TRUE, и если запрос выделяется из пула, а не из кэша, то квота текущего процесса уменьшается на число байт, равное размеру заголовка блока пула + размер пакета запроса + sizeof (PVOID), это 4 байта, в которые записывается указатель на объект-процесс. Т.е. указатель на процесс фактически записывается за телом запроса, точнее за первой с конца стековой ячейкой (stack location).
При освобождении запроса через вычтенная ранее квота восстаналивается, а указатель на процесс, расположенный за телом блока, обнуляется. Далее блок пула либо освобождается окончательно, либо помещается в кэш для дальнейшего переиспользования. И всё это может происходить много-много раз, особенно на многопроцессорных системах, т.е. драйвер выделяет запрос из кэша, потом освобождает его и ядро возвращает его уже не в кэш, а сразу в пул, т.к. кэш к этому моменту переполнен, и наоборот.
Самая интересная ситуация, это когда драйвер выделяет запрос из пула (т.к. в кэше пусто) с квотированием, а затем, при освобождении, ядро положит его в кэш, но при этом квота будет возвращена принудительно вызовом ExReturnPoolQuota внутри функции IopFreeIrp. Вызов ExReturnPoolQuota обнулит процесс в конце тела запроса. Это обнуление как раз и нужно для того, чтобы блоки, которые выделяются из lookaside-списков (не важно, IRP это или что-то ещё), при освобождении не изменяли квоту процесса повторно.
Теперь представьте ситуацию: какой-то драйвер (не ваш) создал IRP с квотированием, затем освободил его; указатель на квотируемый процесс был обнулён, а сам IRP был положен в кэш. Далее ваш драйвер достаёт этот же IRP из кэша и каким-то случайным образом изменяет значение этого указателя с NULL на какой-нибудь не нулевой. В этом случае, когда ваш драйвер попытается освободить IRP через IoFreeIrp (или другим способом, не важно), то при освобождении блока пула указатель на процесс не пройдёт валидацию (т.к. флаг квоты ещё установлен, помните?) и система будет свалена с кодом типа D, либо с , если совсем не повезло и текущее перезаписанное значение адреса процесса невалидно в контексте а.п. текущего процесса.
Ошибка в функции завершения
Существует несколько способов обработки IRP-запросов в драйверах-фильтрах. Один из них, синхронный, заключается в том, чтобы установить функцию завершения (completion routine) для запроса и послать его нижележащему драйверу, при этом контекстом для функции завершения следует указать адрес объекта-события, а само событие сигнализировать в функции завершения. При чём сигнализировать нужно только тогда, когда флаг pIrp - > PendingReturned установлен, иначе это просто не имеет смысла, потому что в противном случае вызывающему результат возвращается сразу из и чего-то ещё ждать просто не за чем.
Теперь самое важное. Помимо всего прочего, функция завершения фильтра должна передать флаг отложенности следующему драйверу, для этого вызывается макрос (можете посмотреть, что он делает, в заголовках WDK). Проблема в том, что IoMarkIrpPending можно и нужно вызывать только в функциях завершения фильтров, иначе говоря, если у вас не фильтр, то этого делать нельзя, потому что тогда в функции завершения текущая стековая ячейка, возвращаемая через , будет указывать на нулевую ячейку, которой на самом деле не существует, т.к. стековые ячейки изначально выделяются только под фильтры и один целевой драйвер, который на дне стека.
Диагностика и исправление ошибки
Понимаете, в чём ошибка? Вызывая IoMarkIrpPending не в фильтре, вы изменяете те самые 4 байта указателя квотируемого процесса, потому что в не-фильтровой функции завершения адрес текущей стековой ячейки будет указывать за пределы структуры IRP. В итоге значение указателя процесса примет вид 0x1000000, потому что см. поле Control структуры .
Как правильно было бы поступить здесь? Синхронные I/O запросы создаются функцией , у неё есть параметр - адрес события, указываете его, отсылаете IRP целевому драйверу, и, если вернули STATUS_PENDING, - ждёте на этом событии. Я не стал так делать по нескольких причинам. Во-первых, потому что мои функции во фреймворке предназначены как для работы в фильтрах, так и для работы с любыми другими запросами, которые программисты будут создавать сами. Я не стал смешивать несколько методов сразу и решил везде использовать функцию завершения для сигнализации завершения запроса, но, к сожалению, совсем забыл про квотирование пула и различия в способах обработки запросов. Ну и во-вторых, что не менее важно, сигнализация события в pIrp - > UserEvent происходит уже после того, как все функции завершения будут вызваны, и получается, что если самый верхний драйвер, который создал запрос, вернёт STATUS_MORE_PROCESSING_REQUIRED и затем захочет переиспользовать его, то событие никогда не будет сигнализировано и где-то может случиться зависание. Поэтому сигнализация в функции завершения представляется мне наиболее надёжным методом.
Исправление вышеуказанной ошибки тривиально, достаточно в нашей универсальной функции завершения вставить проверку примерно плана (см. строчку 20). Несмотря на это, диагностика ошибки, её локализация оказалась не такой простой. Во-первых, Verifier такую ситуацию не ловит. Во-вторых, представьте, вот валится система с кодом BAD_POOL_CALLER типа D. Что делать? Вы видите в стеке IopFreeIrp и понимаете, что речь об освобождении IRP. Вызываете !irp и получаете отлуп, потому что память IRP уже частично невалидна. Делаете dt _IRP < irp_address > и видите, что запрос был завершён, т.к. номер текущей стековой ячейки (поле CurrentLocation) больше, чем общее кол-во стековых ячеек в запросе (поле StackCount). Далее смотрите целевой девайс, при чём смотреть стековые ячейки тут бесполезно, они уже стёрты к этому моменту. Берёте pIrp - > Tail.Overlay.OriginalFileObject, даёте команду !fileobj на него и смотрите девайс. Допустим, это \Device\NamedPipe (т.е. пайпы), а у вас, положим, сетевой фильтр, - следовательно, запрос не ваш, но, возможно, когда-то был вашим и вы его испортили и испорченный положили обратно в кэш (и такое может быть иногда). Или может быть вам повезло и запрос ваш, тогда ещё проще.
Отладочного кода вообще пришлось написать прилично. Я изначально сделал так: везде, где только можно, понавставлял проверок, в том числе и перед освобождением моих IRPs, создаваемых в моём коде. Проверка заключалась в переборе всех вообще потоков в системе, у каждого потока я брал список IRPs, у каждого IRP брал заголовок пула, смотрел флаг квоты, если флаг установлен, далее смотрел адрес квотируемого процесса в конце блока, если он был равен вдруг 0x1000000, то я тупо делал бряк и смотрел в отладчике, что это за IRP. Таким образом, я мог проверять в любой момент все IRP в системе, привязанные к потокам.
Мне повезло, я выделял IRP через IoBuildDeviceIoControlRequest, поэтому мой запрос был привязан к потоку и моя проверка смогла поймать его и брякнуться в той функции, где запрос создавался. Таким образом, далее я смог поставить бряк на запись (команда ba w4 < address >) и найти место, где указатель на процесс перезаписывается, но вы учтите, что запросы, создаваемые через IoAllocateIrp, к потокам не привязываются, и вышеуказанные проверки здесь никак не помогли бы. В этом случае, можно было бы проверять конкретные IRPs сразу перед их освобождением, т.е. перед вызовами IoFreeIrp в вашем коде.
Не новость, конечно, но я сегодня увидел это впервые и порадовался. Хочу запечатлеть это ибо отчасти отражает реалии.
Какие бывают фамилии
Менеджер по продажам - Кидалов. Менеджер по работе с клиентами - Втиралов. Менеджер проекта - Проебалкин. Эксперт-аналитик - Мозгоклюев. Программисты - Криворуков, Распиздяйцев, Бестолковченко и Рукожопов. Ведущий тестировщик - Слепцов.
С некоторых пор система запретила обмен своей электронной валюты на валюту некоторых других платёжных системы. В том числе и на валюту системы . Мотивировано это было тем, что система Яндекс.Деньги имеет слабую подсистему идентификации пользователей, в связи с чем невозможно отследить средства, утёкшие из WM через Яндекс.Деньги. Ну это не единственная претензия к Яндекс.Деньгам, но суть уже понятна, думаю. Примерно так, хотя, возможно, это всё не более, чем делёжка сфер влияния, как это всегда было и будет. Сам я довольно активно пользуюсь WebMoney и, в принципе, доволен, нареканий не имею, и зачем нужна "ещё одна платёжная система" в лице Яндекс.Денег - не понимаю.
Проблема и решение
Сегодня мне потребовалось оплатить работу одного фрилансера. К сожалению, WebMoney он принимать не хотел из-за заморочек с выводом средств на счёт в банке. Да и опыта вообще работы в этой системе у него не было. В общем, у нас оставался только один путь - Яндекс.Деньги. Я почти не пользуюсь этой системой, у меня там почти всегда ноль. Я стал искать человека, чтобы обменять WMZ на яндексы и в итоге я такого человека нашёл и обменял у него. Но попутно поиски в сети дали следующую информацию:
Идём в .
Добавляем на страницу, регистрируемся, заходим.
Переходим к пополнению счёта.
Выбираем WebMoney, пополняем.
На Яндексе переходим к счёта деньгами других платёжных систем.
По просьбам трудящихся расскажем сегодня о легальных и не очень способах предотвращения запуска неугодных процессов. При этом идентифицировать процесс будем по полному пути к его исполняемому файлу-образу (обычно, это .exe-файл). Задачу поставим так (одно из двух):
Заблокировать создание и/или инициализацию структур процесса.
Полностью заблокировать выполнение не-системного кода процесса уже после его успешного запуска.
При этом будем иметь в виду, что в любом случае нам необходимо заблокировать именно создание непосредственно самого процесса. Говорю это потому, что некоторые товарищи пытаются сначала перехватить , вероятно, в надежде обнаружить там имя исполняемого файла. Имя-то, конечно, там есть, проблема в другом: если вернуть ошибку из перехватчика NtCreateSection, то функция CreateProcess установит на выходе код ошибки, равный ERROR_BAD_EXE_FORMAT, а это, как вы понимаете, действительности не соответствует, и некоторые программы (например, Проводник) могут выдавать ложные и неуместные сообщения. Более правильные варианты решения задачи приведены ниже.
Блокировка в режиме пользователя
Да, и такой механизм существует, он вполне легален, хоть и не документирован, а его поддержка впервые появилась в Windows 2000. Для реализации этого способа потребуется написать DLL, содержащую, как минимум, один экспорт, и прописать эту DLL в определённом параметре системного реестра. Самое интересное, что до сих пор не многие знают об этом способе, а ведь он как-никак позволяет внедрить свою DLL в автозапуск, при чём в привилегированные системные процессы типа Winlogon-а. Например, ребята из ЛК, похоже, не знают, или знают, но отчего-то не используют это в своих продуктах. Autoruns тоже не показывает этот ключ, и складывается впечатление, что Microsoft упорно пытается скрыть эту информацию. Может быть я не прав, не знаю. Обновление: в новых версиях продуктов ЛК этот ключ уже контролируется. Долго же ребята соображали.
Соглашение о вызове stdcall. В первом параметре получим полный путь к исполняемому файлу-образу запускаемого приложения, а uReason может быть одно из следующего:
В Windows 2000 колбек-функция вызывается всегда два раза. Первый раз вызывается со значением APPCERT_IMAGE_OK_TO_RUN, что означает голосование, при чём один голос "против" перевешивает все остальные "за", т.е. вы можете вернуть любой ошибочный статус и процесс гарантировано создан не будет, ну или STATUS_SUCCESS если вам это безразлично. Второй раз колбек вызывается для уведомления о результатах голосования и тогда значение второго параметра будет либо APPCERT_CREATION_ALLOWED либо APPCERT_CREATION_DENIED. Начиная с Windows XP вроде бы логика чуть-чуть поменялась и колбек вызывается только один раз, но тут я не поручусь, проверять надо, оставим это вам на домашнее задание.
Ключ: HKLM\System\CurrentControlSet\Control\Session Manager\AppCertDlls Тип значения: REG_EXPAND_SZ
Теперь пару слов о том, как регистрировать вашу DLL и кто её вызывает. Вы прописываете путь к вашей DLL в указаном чуть выше ключе системного реестра. Имя параметра можете использовать любое, но желательно не трогать параметр по-умолчанию, а также зарезервированные системные имена типа AppSecDll, чуть более подробно об этом по ссылке .
Инициализация этого списка начинается после запуска процесса Winlogon-а, потому что Winlogon это первое Win32-приложение, которое запускается нативным менеджером сеансов (smss.exe). Непосредственно чтение параметров реестра, загрузка указанных там модулей и вызов колбек-функций выполняется внутри функции и только один раз в течении всей жизни процесса. Другими словам, после регистрации ваш модуль будет подгружен только в новые процессы, но не в уже существующие.
Как вы уже, наверное, догадались, любое приложение, которое хочет обойти эту блокировку, может сделать это без каких-либо проблем. Для этого ему достаточно пропатчить код функции CreateProcessInternalW или BasepIsProcessAllowed в памяти собственного процесса. Найти код можно, к примеру, используя несложный паттерн. Добавить здесь особо нечего, посмотрим теперь, что мы можем сделать в режиме ядра.
Легальный API для ISVs
Наконец-то ISVs достали Microsoft настолько, что компания решила пойти им навстречу, разработав специальные API-интерфейсы, которые независимые разработчики продуктов в области информационной безопасности могут использовать в своих драйверах. Одна из таких API-функций называется , с помощью которой вы сможете получить информацию о запускаемом процессе и, при необходимости, заблокировать его выполнение.
Суть проста: вы устанавливаете колбек-функцию , в которую вам передают первичную информацию о процессе (в частности, Native-путь к исполняемому файлу-образу процесса в поле .ImageFileName), а вы возвращаете соответствующий статус в одном из полей структуры (.CreationStatus). Если статус ошибочный, процесс создан не будет. Ничего сложного, сам нотификатор во многом аналогичен своей предыдущей версии.
К великому сожалению, Microsoft раскачивалась очень долго и эти интерфейсы стали доступы общественности лишь начиная с Windows Vista SP1 и Windows Server 2008, более подробно про всё это можно почитать в документе, ну и в MSDN, разумеется. На этом всё, теперь давайте посмотрим на недокументированные и/или не совсем очевидные возможности ядра, которые могут подойти для нашей задачи.
Фильтр файловой системы
Казалось бы, как файловый фильтр может помочь нам в блокировке процесса? Ну давайте вспомним, что API-функция CreateProcess помимо всего прочего открывает исполняемый файл-образ запускаемого приложения. Делает она это посредством функции . Операция открытия файла в файловом фильтре видна как запрос , там будет и имя файла-образа и всё остальное.
Самое важное во всём этом действе то, что в запрошенных правах доступа функция CreateProcess выставляет флаг FILE_EXECUTE, конкретно битовую маску смотрите в I/O stack location. По этому флагу мы можем определить, что файл открывается на исполнение. Однако, этого недостаточно, потому что на исполнение открываются не только исполняемые файлы приложений (.exe), но и модули библиотек (.dll), а также драйвера (.sys), ActiveX-компоненты (.ocx), кодеки (.ax) и прочее и прочее. Ну, здесь два варианта:
Либо вы тупо анализируете путь к файлу-образу.
Либо вы каким-то своим алгоритмом анализируете содержимое файла.
В первом случае у вас должен быть под рукой список путей к исполняемым файлам-образам приложений, которые вы хотите блокировать. Во втором случае вам потребуется прочитать PE-заголовок файла, проверить отсутствие DLL-флага в характеристиках файлового заголовка (file header), его значение 0x2000. Если флага нет, значит перед нами не DLL, а исполняемое приложение, далее читайте его содержимое и используйте уже ваш собственный алгоритм анализа.
Как открыть файл в файловом фильтре и как читать из него, тема вообще говоря отдельная, рассмотрим её в другой раз. Здесь лишь скажу, что как только вы поняли, что запрос следует заблокировать, достаточно завершить его с соответствующим кодом ошибки, вызвав , не забудьте перед этим установить код статуса в блоке I/O-статуса запроса. К недостаткам этого способа можно отнести вероятность ложного срабатывания, т.е. строго говоря, если исполняемый файл открывается с доступом на исполнение, то это ещё не значит, что создаётся процесс. Будьте внимательны.
Колбеки фильтровфайловых систем
Этот способ является более правильной вариацией на тему перехватов ядерного сервиса NtCreateSection. Суть в том, что здесь мы перехватываем не все секции подряд, а только те, которые создаются на базе файла, при этом у нас под рукой будет и уже проинициализированный файловый объект, из которого получить полное имя вообще не проблема, и признаки исполняемого файла.
Итак, начинаете вы с того, что, как и в предыдущем способе, пишете как положено каркас файлового фильтра с заглушками. Никакие запросы обрабатывать здесь не придётся. Далее вы регистрируете колбек, заполнив соответствующим образом структуру FS_FILTER_CALLBACKS, - здесь единственный колбек, который вам нужен, это PreAcquireForSectionSynchronization. Далее зовёте функцию и за сим всё, теперь ваш колбек будет вызван каждый раз, когда система будет создавать файловую секцию. Ну разумеется, что ваш колбек будет вызван только в том случае, если фильтр приаттачен к тому, на котором расположен файл, используемый для создания секции.
При получении управления в колбеке ваши действия примерно такие же, как и в предыдущем способе. Вы анализируете флаги доступа к секции в поле .PageProtection, если есть флаг PAGE_EXECUTE, значит создаётся исполняемая секции на основе, возможно, исполняемого файла. Далее или путь проверяйте к файлу, или его содержимое, в зависимости от логики вашего перехватчика. Недостаток здесь тот же - вероятность ложного срабатывания, потому как исполняемые секции создаются не только для приложений, но и для модулей библиотек, что, в принципе, естественно. Чтобы запретить создание секции, верните ошибочный статус, в противном случае верните успех.
Завершение процесса после инициализации
Все предыдущие способы можно назвать превентивными, т.к. они блокируют запуск процесса ещё во время его создания и/или инициализации. Это вполне рабочие способы, которые однако могут наделать шума (например, сообщения об ошибках) там, где было бы неплохо сделать всё по-тихому. В этом разделе попробуем расписать такой способ, при котором процесс тихо умирал бы как раз в тот момент, когда он уже полностью проинициализирован и готов к выполнению "полезного" кода.
Вкратце, суть в следующем. Жизнь процесса в режиме пользователя начинается в функции LdrInitializeThunk, расположенной в ntdll.dll. Эта функция является точкой входа для пользовательской APC, которую первичный поток вызывает через механизм исключений, переключая таким образом выполнение в режим пользователя. Идея заключается в том, чтобы вклинить собственый код во время выполнения LdrInitializeThunk, но до того, как она начнёт вызывать точки входа статически слинкованных модулей. Задача нашей APC - вызвать функцию завершения для собственного же потока (а значит и текущего процесса) примерно так:
ExitProcess (ERROR_SUCCESS);
Это самое простое и эффективное, что мы можем сделать в этот момент. Вызов здесь гарантирует, что никакой пользовательский код, кроме системного, не будет выполнен. Если же вы допускаете выполнение кода статически слинкованных модулей, тогда решение может быть ещё проще: при выполнении вашей APC пропатчите точку входа приложения так, чтобы оно выполнило либо тупо ret либо, что более правильно, всё тот же ExitProcess, как показано чуть выше. Да, ну разумеется, что всё это делаем не из ядра, а уже из shell-кода, внедрённого ранее драйвером.
Теперь посмотрим, как всё это выглядит из ядра. При запуске процесса последовательность загрузки модулей всегда одна и та же: сначала исполняемый файл-образ приложения (например, notepad.exe), затем проецируется системная DLL (это которая ntdll.dll), и затем уже все остальные, например, kernel32.dll, shell32.dll и прочие, а также статически слинкованные модули. Первые два модуля проецируются при выполнении сервиса , остальные догружаются в ходе инициализации в LdrInitializeThunk.
Зная всё вышеизложенное, мы можем зарегистрировать нотификатор о загрузке исполняемых файловых образов через . При каждом вызове колбека будем обновлять свой некий внутренний список, каждым элементом которого является вот такая простая структурка:
Если процесс ещё не добавлен в список (ищем по полю ProcessId), то добавляем новый элемент списка, в котором сохраняем ProcessId, а поле NumberOfModules ставим в единицу.
Если процесс уже есть в списке, то смотрим чему равно поле NumberOfModules. Если уже равно 2, то алгоритм см. чуть ниже. Если меньше 2, соответственно, увеличиваем этот счётчик на единицу и больше ничего не делаем.
Допустим, в колбеке мы определили, что два базовых модуля уже загружены и спроецированы для текущего процесса. Теперь нам требуется принять решение, будем ли мы блокировать этот процесс или нет. Если нет, то удаляем элемент из списка и возвращаем управление, в противном случае мы должны также удалить найденный элемент из списка, затем необходимо внедрить shell-код и зашедулить APC в процесс, ID которого у нас под рукой, то бишь текущий процесс (а если ещё точнее, то текущий поток).
Здесь же сразу хочу отметить, что вам также понадобится установить на завершение процессов. Это нужно для того, чтобы удалить соответствующий элемент из списка, иначе в определённых ситуациях получим утечку памяти. Как подготовить shell-код, рассказывать не будем ибо отдельная тема, скажем лишь пару слов про доставку APC из LoadImage-нотификатора. Код писать лень сейчас, дам только необходимые объявления:
VOID KeInitializeApc ( OUT PRKAPC Apc, IN PRKTHREAD Thread, IN KAPC_ENVIRONMENT Environment, IN PKKERNEL_ROUTINE KernelRoutine, IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL, IN PKNORMAL_ROUTINE NormalRoutine OPTIONAL, IN KPROCESSOR_MODE ApcMode OPTIONAL, IN PVOID NormalContext OPTIONAL);
BOOLEAN KeInsertQueueApc ( IN OUT PRKAPC Apc, IN PVOID SystemArgument1 OPTIONAL, IN PVOID SystemArgument2 OPTIONAL, IN KPRIORITY Increment);
В качестве второго параметра при инициализации APC следует указать адрес KTHREAD для текущего потока. Это значение можно получить через функцию . Последнее, что требуется сделать после постановки APC в очередь, это форсировать его доставку. Для этого необходимо установить флаг UserApcPending в структуре текущего потока, а сделать это можно, вызвав функцию следующим образом:
Таким образом, после возврата в режим пользователя первое же, что будет выполнено, это наш shell-код. Напомню, что в данном случае shell-код должен в обязательном порядке заканчиваться командой ret, потому что другого способа указать на завершение выполнения нашей APC просто нет. В любом случае, прототип нашей пользовательской APC-функции должен выглядеть так (соглашение о вызове stdcall):
typedef VOID (*PKNORMAL_ROUTINE) ( IN PVOID NormalContext, IN PVOID SystemArgument1, IN PVOID SystemArgument2);
Завершение процесса после инициализации 2
В предыдущем разделе мы говорили о принудительном завершении процесса, при этом подразумевалось, что тип процесса - не Native, а Win32 GUI или Win32 CUI. В случае же Native-процесса мы не можем ждать загрузки статических модулей вроде kernel32.dll, user32.dll по той простой причине, что такие процессы их не используют. В этом разделе попробуем немного усовершенствовать предыдущий способ, сделав его пригодным для процессов любого типа.
Итак, прежде всего зарегистрируем CreateThread-нотификатор с помощью функции . В момент вызова колбека нам понадобится указатель на объект создаваемого потока, для этого вызовем , указав ID потока, который нам передали в колбек-функцию. Далее нам следует зашедулить APC в этот поток, он уже готов к этому. Эта APC будет выполнена ещё до того, как управление перейдёт к загрузчику, более того, эта APC будет выполнена ещё до того, как будет исполнен какой-либо пользовательский код.
Остаётся совсем немного. Нам нужно завершить выполнение этого потока. Но мы не можем просто вызвать здесь NtTerminateThread, потому что ядро запрещает потокам убивать себя же в том случае, если этот поток является последним и единственным потоком для своего процесса. Мы можем легко обойти это ограничение, вызвав NtTerminateThread в контексте процесса ядра. Для этого я рекомендую использовать WorkItem, в который следует передать адрес объекта текущего потока. В вашей функции рабочего потока вы беспрепятственно откроете объект потока через , получите хендл и, уже используя этот хендл, вызовите функцию NtTerminateThread, адрес которой, напомню, можно взять из SDT.
Замечание касаемо Windows XP и Windows Server 2003: в этих и более ранних системах функция PsLookupThreadByThreadId вернёт ошибку, т.к. к этому моменту для потока ещё не инициализорована маска доступа. Единственный выход, который я здесь вижу, это вручную просканировать недокументированную и неэкспортируемую таблицу PspCidTable, - кстати, этот способ рабочий. Добавлю только, что на Windows Vista и выше такой проблемы нет.
В заключение
Ничего нового я здесь не сообщил. Что-то есть в документации, что-то есть в поиске, что-то обсуждалось на форумах и, если захотите, то в сети вы сможете найти ещё больше информации по теме. Если вспомню ещё что-то, обязательно обновлю этот пост. Пока всё.
Решил написать об этом пару слов, потому как заболел сам. Самым натуральным со всеми обычно сопутствующими признаками. Сначала жена, через пару дней я. Болею вот уже 5-ый день, улучшения заметны, но выздоровление продвигается довольно медленно на мой взгляд. Началось всё с температуры, болтающейся от 38 до 39, и жуткого кашля, нападающего приступами. Вчера к вечеру лёгкое разжижение стула. Всё по плану, симптомы у всех один-в-один.
Вызывали врача, как взрослого, так и детского. Из лекарств в итоге нужны были только (жаропонижающее на основе ) и (противовирусное). Второе лекарство недешёвое, на семью 3 человека может вполне встать в несколько тысяч на весь курс, потому как одна взрослая упаковка стоит чуть более 400 рублей. После каждого принятия Арбидола наблюдается лёгкий спад температуры (37-38) и сонливость. Сегодня один приём случайно пропустил, получил сразу же 38.5. У жены температура зашкаливала за 39, потому дополнительно пила парацетамол (в чистом виде), - помогает сразу. У меня до 39 не доходило, поэтому жаропонижающие не пил - нельзя. Терплю вот. Жена вроде бы на поправку пошла, температуры почти нет, разве что с желудком проблемы (тошнота) и кашель ещё накатывает иногда. Если будут какие осложения (хотя я сомневаюсь) - постараюсь отписать об этом.
Отдельно хочу отметить про ребёнка (чуть более 2х лет ему). Примерно за неделю или две до этого начали капать ему в качестве профилактического средства. После прихода врача заменили на прописанный Арбидол (детский). В итоге, к моему великому удивлению, несмотря на двух активно болеющих взрослых, сынулькин переносит сейчас болезнь крайне легко, носится аки дьяволёнок. Температура поднималась пару раз только и не выше 37 с копейками. Не кашляет. Короче, у кого малыш, - возьмите на заметку.
Напоследок хочу отметить пару моментов. Во-первых, в нашем городе отсутствуют медицинские маски (марлевые повязки). Их просто не закупили в аптеки. Об этом говорят как аптекарши, так и надомные врачи. Медсёстрам в больницах приказом сверху велено шить маски дома. Я, честно говоря, не в курсе, это распиздяйство только в этом сезоне или это так всегда было здесь, но в любом случае не смешно. Во-вторых, наши врачи говорят, что никакого сверхужасного "свиного" гриппа по улицам не ходит, всего лишь новый штамм, сезонный. Ну, впрочем, кто бы сомневался. И в-третьих, я настоятельно рекомендую ознакомиться вот с статейкой, возможно, кому-то откроет глаза.
Существует достаточно малое кол-во задач, где требуется в рантайме определять тип объекта. Навскидку могу придумать только ситуацию, когда мы перечисляем все существующие объекты в системе, при этом информация о типе объекта в каждом из элементов списка отсутствует. Хотя тут опять же вопрос: например, если мы перечисляем все существующие хендлы, то там в каждом элементе списка есть индекс типа, если мы перечисляем все объекты в какой-либо папке менеджера объектов, то там для каждого элемента возвращается имя типа. Конечно, может иметь место ситуация, при которой приложение передаёт драйверу адрес объекта, полученный ранее от того же драйвера, а драйвер должен понять, что с ним делать, и для этого пытается получить тип объекта. Но на самом деле подобная ситуация недопустима, т.к. во-первых, создаёт почву для атак на ядро операционной системы, а во-вторых, является следствием неправильной архитектуры изначально.
Итак, в этом сообщении я расскажу немного о типах объектов и о том, как получить информацию о типе объекта, имея на руках указатель на объект или хендл. Подразумевается, что получение типа объекта заключается в получении указателя на объект типа, всё остальное - дело техники.
Напомню на всякий случай: ядро Windows полагается на то, что драйвер знает тип объекта, адресом которого манипулирует, другими словами ситуация, при которой драйвер не знает тип объекта, адресом которого располагает, - это ахтунг, а разработчика следует вздёрнуть без каких-либо обсуждений. И помните, что использование недокументированных и, тем более, часто меняющихся структур, чревато проблемами совместимости, тестируйте тщательнее.
Структура заголовка объекта
Заголовок объекта распологается сразу перед его телом и содержит информацию, не являющуюся специфичной для какого-то конкретного типа, например, кол-во ссылок и хендлов, созданных на этом объекте, а также идентификатор типа объекта, дескриптор безопасности и кое-что ещё. Ниже представлены две версии структуры заголовка, одна для Windows XP, 2003 и Vista, вторая для Windows 7, потому как там она немного изменилась. Размер обоих вариантов одинаков и составляет 24 байта (0x18).
Как бы это вам попонятнее объяснить. Дело в том, что тип объекта это тоже тип объекта. Чтобы наглядно продемонстрировать вам это, представьте, что можно создать объект, например, типа File, а можно таким же образом создать и объект типа Type, и тип у этого объекта будет "тип объекта", при этом объект первого типа, как вы наверняка уже знаете, будет представлен структурой , а объект второго типа будет соответствовать структуре OBJECT_TYPE, которая совершенно недокументирована. Честно говоря, не имею понятия, зачем так сделано, но тем не менее это выглядит именно так. Ниже смотрите определения структур (приводятся не полностью).
Первый вариант для систем начиная с Windows XP и заканчивая Windows Vista без SP:
В ядре существует папка менеджера объектов (directory), обычно содержащая по одному экземпляру всех доступных типов объектов (всего их 30-40). Папка эта имеет имя \ObjectTypes, а полное имя каждого типа имеет вид, соответственно, \ObjectTypes\Xxx, где Xxx это, например, Process или File. Зная имя типа, вы можете не напрягаясь получить указатель на объект типа. Для этого вам следует вызвать ObOpenObjectByName и указать полное имя типа в атрибутах, например, \ObjectTypes\Thread или \ObjectTypes\Callback, передав затем полученный хендл в функцию .
Отдельно скажем пару слов о ObOpenObjectByName. Функция недокументирована, однако экспортируется ядром и её можно использовать в своих драйверах. Для этого достаточно объявить её в коде, прототип выглядит следующим образом (параметры, отмеченные как OPTIONAL, могут быть установлены в ноль):
NTSTATUS ObOpenObjectByName ( IN POBJECT_ATTRIBUTES ObjectAttributes, IN POBJECT_TYPE ObjectType OPTIONAL, IN KPROCESSOR_MODE AccessMode, IN OUT PACCESS_STATE AccessState OPTIONAL, IN ACCESS_MASK DesiredAccess OPTIONAL, IN OUT PVOID ParseContext OPTIONAL, OUT PHANDLE Handle);
Замечание касаемо Windows 7: начиная с этой системы данную функцию нельзя вызывать со вторым параметром, равным NULL. Другими словами, если в предыдущих системах можно было открыть объект по имени, не зная его тип, то теперь лавочку прикрыли и правильно сделали.
Таблица типов объектов
Как вы, наверное, уже заметили, глядя на приведённые выше структуры, тип объекта не всегда идентифицируется именем или указателем на объект-тип (object type). В ядре Windows 7 особенно заметна тенденция к использованию индексов, которые идентифицируют элемент в некой глобальной таблице типов. Таблица такая действительно существует, но на самом деле нам она совершенно без надобности, т.к. мы можем воссоздать её копию, которая будет содержать как минимум индексы и соответствующие им указатели на объекты-типы. Сделать это до неприличия просто:
Перечислить имена всех типов в папке \ObjectTypes. Воспользуйтесь функциями и ZwQueryDirectoryObject.
Для каждого типа получить указатель на его объект-тип. Как это сделать, рассказано чуть выше в предыдущем разделе.
Для каждого объекта-типа извлечь его индекс, значение которого находится в поле Index структуры OBJECT_TYPE.
Продолжать перечисление элементов до тех пор, пока ZwQueryDirectoryObject не вернёт STATUS_NO_MORE_ENTRIES или другую ошибку.
По завершении цикла у вас на руках будет имя, индекс и адрес объекта-типа для каждого из существующих в системе типов. Функция ZwQueryDirectoryObject недокументирована, потому привожу здесь её определение:
NTSTATUS ZwQueryDirectoryObject ( IN HANDLE DirectoryObjectHandle, OUT PVOID DirObjInformation, IN ULONG BufferLength, IN BOOLEAN GetNextIndex, IN BOOLEAN IgnoreInputIndex, IN OUT PULONG ObjectIndex, OUT PULONG DataWritten OPTIONAL);
Буфер на выходе будет содержать структуру переменной длины примерно следующего вида:
При чём ObjectTypeName в этом случае всегда будет равно "Type", а ObjectName будет содержать имя типа, например, "Device" или "Mutant". Напоследок добавлю только, что элемент таблицы можно оформить примерно следующим образом:
Замечание касаемо Windows 7: начиная с этой системы чтобы получить адрес объекта-типа по имени, например, \ObjectTypes\Event, вам сначала придётся получить адрес объекта собственно типа (это который имеет имя \ObjectTypes\Type). Для этого воспользуйтесь новой функцией ObGetObjectType, передав в неё указатель *IoFileObjectType:
POBJECT_TYPE ObGetObjectType ( IN PVOID Object);
Полученный на выходе указатель передавайте в ObOpenObjectByName при перечислении объектов-типов.
Как получитьобъекттипа по индексу типа?
Получить адрес объекта-типа по индексу возможно лишь единственным способом, - перечислением всех элементов в воссозданной таблице типов. Это недокументировано, но других способов нет.
Как получитьобъекттипапо именитипа?
Имея таблицу, созданную как показано в предыдущем разделе, найти указатель на объект-тип по его имени - задача тривиальная, ведь для каждого типа у нас есть его имя, соответственно, достаточно пройтись по таблице и сравнить имена. Если использование таблицы не устраивает в силу недокументированности способа, делаем тупо: ObOpenObjectByName на полное имя типа (т.е. \ObjectTypes\Xxx, где Xxx - имеющееся имя), после чего ObReferenceObjectByHandle и готово. Не забываем после использования типа.
Как получитьобъекттипапо хендлуобъекта?
Самый простой способ узнать тип объекта по хендлу заключается в использовании документированной функции с классом ObjectTypeInformation. На выходе получите структуру переменной длины:
Ну а имея уже имя типа можно поступить как показано в предыдущем разделе.
Как получитьобъекттипапо указателю на объект?
Допустим, у вас есть указатель на объект и вы не знаете его тип. Вы можете пойти недокументированным путём и, возможно, огрести проблемы в будущем, а можете сделать документированно и больше не думать об этом. Во втором случае чутка потеряете в производительности, но не думаю, что это существенно, т.к. вряд ли вы когда-нибудь будете каждую секунду извлекать типы всех объектов в системе.
Итак, в случае недокументированного способа вы можете сэкономить на производительности и взять тип прямо из заголовка объекта. Тут только нужно учесть, что тип может идентифицироваться либо указателем на сам объект-тип, либо индексом. В первом случае достаточно сделать как-то так:
Во втором случае чуть сложнее, см. выше соответствующий раздел.
Легальный документированный способ заключается в открытии объекта с целью получения хендла, а затем поступаем как показано выше в соответствующем разделе. Для открытия объекта по указателю можно использовать документированную функцию . При этом помните, что не следует запрашивать доступ больше, чем это реально необходимо, думаю, в данном случае GENERIC_READ будет более чем достаточно (хотя я бы попробовал для начала READ_CONTROL).
Для успешной и корректной генерации дампа памяти во время падения системы должны быть выполнены как минимум все нижеследующие условия:
В системных настройках должен быть указан соответствующий параметр.
Файл подкачки должен находиться на системном томе (там, где папка Windows).
Файл подкачки должен иметь достаточный размер (для полного дампа его размер должен быть больше или равен размеру ОЗУ).
Образы драйверов, участвующих в создании дампа (т.е. включённых в dump stack), не должны быть повреждены в памяти.
Дополнительная информация
Помните, что дамп не создаётся сразу:
На первом этапе (на синем экране) содержимое дампа записывается в файл подкачки.
При следующей загрузке системы дамп извлекается из файла подкачки и копируется в файл, указанный в реестре.
Помимо этого я рекомендую устанавливать тип дампа в "Дамп памяти ядра" (англ. "Kernel memory dump"), потому как минидампа обычно слишком мало, а полный дамп требуется не так уж и часто (по крайне мере это так в случае правильной продуманной архитектуры), - в основном, когда требуется доступ к а.п. пользовательских процессов.
Это первое сообщение в блоге, посвящённое TDI, интерфейсу транспортных устройств. Если кратко, ну совсем кратко, то TDI это такая штука, которая позволяет драйверам режима ядра использовать сеть (насколько мне известно, это и было целью создания этого интерфейса). Под использованием подразумевается как отправка/приём собственных, так и фильтрование чужих пакетов, например, их изменение или блокировка/удаление. Сильно подробно сейчас останавливаться не буду, скажу только, что технология эта очень старая, ещё в NT4 была реализована и надо сказать, что с тех пор изменилась не сильно. Всё сказанное ниже справедливо для всех Windows начиная от Windows NT 4 до Windows 7, за это время TDI не претерпел существенных изменений.
Зачем это нужно?
В данном сообщении ставиться целью познакомить вас с тем, каким образом можно портировать или реализовать с нуля сетевой сервер в режиме ядра. Но на самом деле начать нужно не с этого, а с ответа на простой вопрос: зачем реализовывать сервер в режиме ядра, когда это можно сделать в пользовательском режиме почти не напрягаясь даже? Причины на то могут быть разные, но основных я вижу две:
Получение прироста производительности.
Обход фаерволов при реализации malware.
В общем, и то и другое более-менее понятно. По поводу реализации или портированию Web-серверов в режим ядра посоветую почитать High-Performance Memory-Based Web Servers (в частности, раздел ) и скачать вот документик. Теперь давайте посмотрим, зачем нам может пригодиться фильтрация сетевых запросов на уровне TDI:
Обход фаерволов при реализации malware.
Фильтрация нежелательных сетевых запросов при реализации фаервола.
Изменение сетевого поведения стороннего приложения (например, Web-сервера).
Реализация плагина к собственному сетевому драйверу (например, ядерный Web-сервер).
С первым и вторым пунктами, думаю, всё ясно. Третий пункт немного похож на
Сейчас хотелось бы поговорить немного о так называемой теговой файловой системе. К сожалению, не имею достаточного кол-ва времени для детального изучения вопроса, потому здесь хочу лишь коротко тезисно изложить своё видение проблемы. Для краткости далее теговую файловую систему будем называть TFS (Tagged File System). Если не указано иное, далее в тексте будет подразумеваться, что TFS реализована на базе файлового фильтра (см. ниже соответствующий раздел).
Суть проблемы
Сегодня, как мне кажется, у пользователей накопилось достаточное кол-во информации (в виде, конечно же, файлов), которую иногда уже неудобно систематизировать с помощью современных иерархических файловых систем. Целью систематизации при этом является не столько ускорение поиска информации (файлов), сколько его полнота. Рассмотрим самый примитивнейший пример. Допустим, мы имеем на диске множество музыкальных композиций. Допустим, что согласно типичной иерархии они расположены следующим образом (по уровням вложенности):
Жанр
Исполнитель
Альбом
Композиция
При чём часто на первый пункт просто забивают и раскидывают mp3-шки сразу по исполнителям. Теперь представьте, что пользователь хочет найти, положим, все записи за 1999 год. Теперь представьте, что пользователь хочет не просто найти их, но и добавить в список воспроизведения в тот же WinAmp, например. Давайте посмотрим, как эта ситуация решается сейчас:
Открываем Проводник в папке с музыкой.
Ищем все музыкальные файлы (в Vista это очень просто).
Сортируем по столбцу "Год" (добавляем столбец при необходимости).
Выделяем все рядом расположенные строки, у которых "Год" = 1999.
Перетаскиваем мышкой эти строки в список воспроизведения WinAmp'а.
В принципе, ничего сложного, но этот сценарий мог бы быть и другим:
Нажимаем в WinAmp'е кнопку "Add folder" (добавление папки).
Выбираем папку вида C:\Music\Tags\Year\1999, жмём OK и готово.
Как видим, такой сценарий несколько проще. Здесь C:\Music - это просто обычная созданная пользователем папка с именем Music на диске C:. Year - это имя тега, заранее заданного пользователем, и обслуживаемого самой TFS. 1999 - одно из значений тега. Кто-то шибко умный мог бы сказать здесь: "а почему бы не создать папку C:\Music\Tags\Year\1999 вручную и не напихать туда жёстких ссылок на все MP3-файлы за 1999 год?". Дело в том, что жёсткие ссылки имеют некоторые ограничения:
На данный момент ссылки поддерживаются только в файловой системе NTFS.
Ссылки могут быть созданы только в пределах того же тома, что и целевой файл.
Их максимальное кол-во составляет 1023 штуки на один файл (т.е. считай что на файл можно будет повесить не более 1023 тегов).
Возможно, данный пример не слишком убедителен. Хорошо, вот ещё один. Предположим, что у вас есть коллекция видеозаписей самой разной тематики: дети, свадьба, путешествия и т.п. Теперь представьте, что вам нужно найти все видеозаписи, на которых запечатлены ваши дети во время путешествий. Как вы будете искать эти записи? В случае, например, музыкальных файлов или фотографий - да, Windows позволяет назначить ключевые слова прямо в свойствах файла, но для видеофайлов такая возможность отсутствует (точнее, такая возможность появилась только начиная с Windows 7). Ну-с, что будем делать? Вообще, Windows 7 позволяет назначать теги только трём типам документов: звукозапись, изображение и видеозапись, т.е. остальные типы файлов (например, архивы, программы или ещё что-то) пролетают и вы не сможете найти их, используя стандартные механизмы поиска Windows.
Так что же это такое?
Вы совершенно справедливо можете заметить, что вышеозначенная проблема уже давно решена в приложениях класса "каталогизатор" (здесь и далее подразумевается каталогизатор, не содержащий в комплекте компонентов режима ядра и прочих файловых фильтров). Хорошо, давайте попробуем вместе найти преимущества TFS перед каталогизаторами. Для этого давайте посмотрим на типичные задачи, которые этими каталогизаторами решаются, а также на сценарии их использования.
Прежде всего, пользователь так или иначе наполняет базу. В базе содержатся, грубо говоря, файлы, описание каждого файла (возможно, ещё их псевдонимы, т.е. понятные пользователю имена), дата/время добавления в базу и, конечно же, имя и значение тега для каждого файла. Более того, файлы в каталогах могут объединяться в группы, которым также можно назначать теги. Итак, что может предложить TFS при наполнении базы:
Помещение файлов в каталог без участия сторонней программы, т.е. полностью в автоматическом режиме. Да, такое возможно и для этого пользователю достаточно настроить TFS соответствующим образом. Например, в некой панели управления пользователь говорит, что все файлы, положенные в папку "C:\Tags\Дети, путешествия", должны автоматически помещаться во внутрь TFS, а файлу назначаться соответствующие теги (при этом обратите внимание, что папку "Дети, путешествия" пользователь мог и не создавать заранее, а TFS может "создать" её на лету из доступных тегов). Далее файл перетаскивается положим в Проводнике в эту папку, при этом файл физически копируется/перемещается во внутренности TFS, а в виртуальной папке "C:\Tags\Дети, путешествия" появляется ещё один элемент с именем нового файла. Т.е. что получается: пользователь больше не парится по поводу физического расположения файла, а новый файл автоматически приобретает теги и его теперь легко найти по тегам плюс стандартные атрибуты файлов, - они ведь никуда не исчезают ибо теги это всего лишь дополнение.
Автоматическая расфасовка файлов по физическим хранилищам в зависимости от целевых тегов. Эту фичу можно рассматривать как развитие предыдущей. Мы можем реализовать иерархию тегов и это даст нам новые возможности. Ну смотрите, предположим, пользователь в Проводнике перемещает файл "Отчёт1.doc" в папку "C:\Tags\Backup\Отчёт". В этот момент у нас на руках уже есть следующая информация: имя файла - это "Отчёт1.doc", категория файла - "Backup" (она же есть первый уровень теговой иерархии) и, собственно, сам тег, который следует назначить файлу, - "Отчёт" (это второй уровень теговой иерархии). Используя два последних атрибута нового файла, TFS может без участия пользователя, прямо во время операции записи, переместить содержимое файла, например, на сервер онлайн-бэкапа, или тупо засунуть его на шаренную папку, или в ZIP-архив, или ещё куда, - это не важно сейчас. При последующем чтении файла из "C:\Tags\Backup\Отчёт\Отчёт1.doc" содержимое будет считываться самой TFS оттуда, куда оно было перемещено ранее.
Т.е. вы понимаете, что происходит-то? Всё, что есть у пользователя, это некая волшебная папка "C:\Tags", ну и может быть ещё какой-нибудь UI для настройки TFS (на том же C# писанный). И это всё. Пользователь работает с этой папкой как с самыми обычными папками, к которым он так привык, а уже TFS самостоятельно распределяет файлы куда надо и ведёт базу тегов. Мне кажется, это здорово. Ради справедливости заметим, что с помощью уведомлялок каталогизаторы могут предоставить часть функциональных возможностей, аналогичных описанным выше, однако это будет иметь некоторые ограничения (иногда существенные).
Вторая не менее важная задача, - это поиск файлов. При использовании каталогизатора вы должны будете запустить его, ввести теги, получить результат в виде списка и перетащить найденные элементы в окно приложения, в котором будете с этими файлами работать. Здесь же TFS предлагает следующее:
Упрощённый поиск файлов по тегам. Выше я уже говорил, что для поиска теперь достаточно выбрать папку, соответствующую требуемому тегу или теговой группе (т.е. набору тегов). Сами теги или теговые группы визуально видны пользователю как отдельные папки, что позволяет выбрать набор файлов, всего лишь указав в приложении одну папку.
Унифицированный способ выбора файлов. И это очень важно, потому что по большому счёту обычный каталогизатор не способен передать файлы (точнее, их имена) из своей базы в приложение, он может лишь найти их и показать пользователю. Используя TFS, любое, абсолютно любое приложение может предоставить пользователю тот интерфейс поиска и выбора файлов, к которому он уже привык.
Полная совместимость со всеми существующими файлово-ориентированными приложениями. Этот момент не менее важен, потому что не вынуждает разработчиков дополнительно поддерживать какую-то там никому не известную файловую систему.
Третий момент, который хотелось бы упомянуть, это физический перенос файловой базы с одной системы на другую. По другому эту операцию можно назвать созданием снимка (snapshot) содержимого TFS, потому что при этом фактически все содержимое всех файлов, где бы оно ни находилось, копируется в указанное пользователем место, - например, на внешний HDD в виде одного файла-контейнера. Для создания такого снимка пользователю достаточно будет сделать одно из двух (в зависимости от того, в каком виде нужно получить результат):
Для создания единого файла-контейнера пользователю достаточно будет скопировать файл "C:\Tags\snapshot.tfs" куда ему нужно. Размер этого файла равен общему размеру всех файлов в TFS, однако этот файл полностью виртуальный. При его открытии TFS блокирует себя на запись, а разблокировка происходит только при закрытии последнего хендла на этот файл. При чтении этого файла TFS последовательно возвращает данные хранящихся внутри файлов, что равносильно чтению тома в RAW-режиме. Восстановление также просто как и резервное копирование, достаточно записать имеющийся файл "snapshot.tfs" в папку "C:\Tags" на целевой TFS.
Для создания копии структуры файловой системы пользователю опять же не придёться сильно напрягаться. Ему всего лишь потребуется скопировать содержимое виртуальной папки "C:\Tags\Mirror", в которой находятся ссылки на все файлы, которые были помещены в TFS ранее, без дубликатов. Помимо собственно файлов в папке "C:\Tags\Mirror" расположен виртуальный файл "meta.tfs", содержащий метаинформацию (описание тегов, атрибуты файлов, etc.). Восстановление такой резервной копии проходит в два этапа: сначала целиком и успешно в целевую TFS копируется файл с метаинформацией, в этот момент файловая система автоматически воссоздаёт иерархию тегов. Далее при копировании содержимого файлов она уже знает как и куда следует расположить их содержимое. Но если честно, вот здесь я не уверен, что удасться обойтись исключительно средствами драйвера.
Скорее всего, я что-то упустил, если вспомню - обязательно допишу потом, а сейчас давайте подытожим немного, чем же так хороша теговая файловая система в том виде, как я описал выше:
Теговая файловая система обеспечивает более полный и более функциональный поиск. При этом не требуется вносить какие-либо изменения в уже существующие приложения, т.е. мы имеем полную совместимость и удобство для пользователя.
Теговая файловая система позволяет абсолютно прозрачно автоматизировать некоторые файловые операции, например, создание резервных копий, сжатие, шифрование и прочее, при этом физически содержимое файла может находиться практически где угодно.
Реализация
На мой взгляд реализовать TFS можно двумя способами:
В первом случае мы получим более высокую производительность, но нам потребуется дополнительно один выделенный том, который нужно будет отформатировать под TFS. Во втором случае дополнительно нам ничего не понадобится и мы сможем реализовать гораздо больше возможностей, однако производительность может пострадать. Тем не менее, т.к. файловый фильтр имеет существенно больше возможностей и т.к. его реализация значительно проще, я предпочёл бы использовать здесь именно его. Так или иначе, вы уже поняли, что TFS привносит в файловую систему несколько новых понятий. Вот некоторые из них:
Виртуальная папка - это папка, которая не существует физически на диске, а создаётся файловой системой на лету. Это делается элементарно путём обработки запроса IRP_MJ_DIRECTORY_CONTROL с кодом IRP_MN_QUERY_DIRECTORY. Такие папки являются либо well known (т.е. предопределённые) для TFS, либо создаются пользователем в панели управления. В качестве примера встроенной виртуальной папки можно привести одну из корневых папок с именем "\Tags", которая предназначена прежде всего для быстрого доступа к тегам.
Виртуальный файл - это тоже самое, что и виртуальная папка, но внешне представляет собой именно файл, т.е. не имеет атрибута Directory. Виртуальные файлы это, как правило, метафайлы. Метафайлы, в свою очередь, нужны чтобы во-первых хранить метаинформацию, а во-вторых позволяют осуществлять те или иные управляющие действия с TFS, не прибегая при этом к специализированным управляющим запросам (IOCTL имеется в виду). Например, если открыть файл "\Tags\snapshot.tfs", то любые изменения файловой системы будут заблокированы, а разблокировка произойдёт только по закрытию этого файла; сие равносильно запросу FSCTL_LOCK_VOLUME.
Тег - это новый атрибут файла. Визуально для пользователя тег выглядит как обычная папка, расположенная во встроенной папке "\Tags". Когда приложение запрашивает содержимое этой папки (например, через FindFirstFile / FindNextFile), файловая система возвращает имена всех файлов, имеющих указанный тег (имя папки и есть тег). Собственно, именно за счёт этого и достигается совместимость с уже существующими приложениями.
Заключение
Честно говоря, не покидает ощущение, что у меня так и не получилось убедить вас в необходимости реализации теговой файловой системы в современном мире. Что ж, возможно, это оттого, что она действительно и не нужна вовсе ;) А возможно, что я не смог продемонстрировать всех её достоинств. Тем не менее, есть люди которым это необходимо уже сегодня, - подумайте об этом. Так или иначе надеюсь, что суть понятна. Позже я дополню это сообщение новыми мыслями. Спасибо за внимание.
Данная заметка касается только image-based virtual disks, т.е. дисков, содержимое которых находится в специальном файле-образе (или в файле-контейнере, если угодно), а сам файл лежит на томе с локальной файловой системой (FAT, NTFS, ...).
Здесь и далее предполагается, что вы уже знакомы с понятием виртуального диска в Windows и уже имеете некоторый опыт в разработке драйверов этого класса. Если вы ещё только начинаете разработку такого драйвера, настоятельно рекомендую изучить вот эти примеры и попробовать сделать хоть что-то самостоятельно. Более подробно разработку виртуальных дисков будем проходить в одном из следующих сообщений.
Суть проблемы
Всё очень просто. Представьте, что у вас есть поток, который обрабатывает запросы к вашему устройству (device object типа FILE_DEVICE_DISK). Теперь представьте, что где-то в недрах ядра крутятся ещё несколько потоков, реализующих часть логики менеджера кэша (точка входа такого потока называется CcWorkerThread). Теперь предположим, что ранее вы создали файл образа вашего диска и теперь открываете его так:
Вы не указываете флаг FILE_NO_INTERMEDIATE_BUFFERING и это разрешает драйверу файловой системы (FSD) использовать кэш (современные файловые системы типа FAT и NTFS используют встроенный кэш ядра). Использование кэша означает, что часть операций чтения/записи будут отдаваться на откуп менеджеру кэша. Например, файловая система может сказать наверх, что данные успешно записаны, но по факту они будут сброшены на диск несколько позже, в одном из вышеупомянутых потоков менеджера кэша.
Теперь предположим, что вы создали device object вашего диска, назначили букву, отформатировали в одну из локальных файловых систем (например, NTFS). Теперь предположим, что какое-либо приложение (это может быть банально Проводник) открыло файл уже на вашем диске следующим образом:
Теперь, если приложение вдруг решит записать что-либо в этот файл, произойдёт примерно следующее (упрощёно):
Запрос уйдёт к FSD, который отдаст его менеджеру кэша, а тот поставит его в свою внутреннюю очередь для отложенной обработки. Схема стандартная, ничего интересного тут нет.
Рано или поздно рабочий поток кэша начнёт сбрасывать данные из памяти на диск. Для этого по сути будет задействован тот же механизм, что и при некэшируемой записи, т.е. будет создан запрос ввода/вывода, который отправится к FSD, а тот, уже минуя кэш, вызовет драйвер вашего диска.
Обработчик записи IRP_MJ_WRITE вашего драйвера поставит запрос в очередь вашему рабочему потоку. Ваш же поток посредством ZwWriteFile снова вызовет FSD и вот здесь произойдёт дедлок.
Функция NtfsWaitSync здесь ждёт событие, которое сигнализируется в функции завершения, что в свою очередь означает завершение запроса, отправленного к storage device (т.е. к девайсу вашего диска). Теперь посмотрим на стек для пункта 3, это стек вашего потока обработки запросов к виртуальному диску (текущий запрос - тот самый, которого ждёт NTFS в предыдущем стеке):
Здесь функция ядра CcCanIWrite проверяет, возможна ли сейчас запись данных в кэш (т.е. проще говоря, есть ли там ещё место). Если да, то функция немедленно возвращает управление со значением TRUE, в противном случае функция инициирует очистку кэша (если это необходимо) и будет ждать завершения обработки всех запросов на запись, уже инициированных ранее менеджером кэша.
Ну и как вы уже догадались, функция CcCanIWrite не вернёт управление до тех пор, пока не будут завершены запросы на запись, а этого не случится, пока не будет завершён этот текущий запрос. Ну вот и дедлок, собственно.
Схема 1: решение проблемы
Откажитесь от системного кэширования. Этого легко добиться, выставив флаг FILE_NO_INTERMEDIATE_BUFFERING в функции ZwOpenFile. Вы всё равно не сможете нормально задействовать кэш ибо архитектура менеджера кэша Windows NT не предусматривает его использование устройствами хранения (вообще-то подразумевается, что storage stack имеет свой кэш плюс ещё железки сами по себе имеют собственное ОЗУ). Скорость работы диска снизится, это да, но зато вы гарантированно получаете избавление от дедлока.
Схема 2: если кэш всё таки нужен
Ну так и реализуйте его самостоятельно. Для этого всего-то необходимо:
Модифицировать реализацию вашей очереди таким образом, чтобы запросы в ней либо обрабатывались и завершались (если кэширование выключено), либо только обрабатывались (если кэширование включено), при чём во втором случае в очередь следует ставить не IRP, конечно же, а уже извлечённую из него информацию о запросе - буфер с данными, смещение и размер данных в байтах.
Модифицировать обработчик IRP_MJ_WRITE таким образом, чтобы в зависимости от настроек кэширования текущего девайса, он либо завершал запрос с кодом STATUS_SUCCESS (если включено) либо просто возвращал STATUS_PENDING (если не включено).
Здесь только один момент: следует подумать над тем, что делать в случае, когда при отложенной записи (т.е. уже после фактического и успешного завершения запроса) произошла ошибка. Например, системый кэш умеет оповещать пользователя о таких ошибках, показывая соответствующее сообщение в системном трее (типа "Ошибка отложенной записи"), а что будете делать вы? Стоит подумать.
В заключение
Что примечательно, что при выборе второй схемы вы можете использовать и свой кэш и системный. Для этого просто следуйте второму варианту и не выставляйте флаг FILE_NO_INTERMEDIATE_BUFFERING в ZwOpenFile, - это схема 3. Фактически получается, что в своём драйвере вы можете реализовать несколько уровней кэширования:
Без кэширования (схема 1).
Обычное кэширование (схема 2).
Двойное кэширование (схема 3).
Не уверен, что в этом есть большой смысл, но тем не менее.
В этом сообщении я буду много философствовать и высказывать свою точку зрения на те или иные аспекты жизни, работы и так далее. Особой конкретики не ждите, сразу предупреждаю, здесь собраны, в основном, рассуждения и немного фактов. Это сообщение будет периодически дополняться, изменяться, но скорее всего это будет происходить не часто. Любители чего-то глобального вряд ли найдут в этом сообщении что-либо интересное, это ведь всего лишь маленькое мнение маленького человечка...
Блог и статьи
Зачем вы называете сообщения этого блога статьями? Эти маленькие заметки не то что на статью, даже на докладец не тянут, а вы статья, статья... Статья - это как правило глубокое исследование чего-либо, мои же сообщения носят как правило поверхностный характер, хотя возможно, что и не всегда.
Юношество и начало карьеры
Между прочим, написать это сообщение меня сподвиг в том числе и удивлённый вопрос одного человека. Он спрашивал, как так получилось, что я зажрался до такой степени, что даже не могу прислать ему один файл с исходным кодом на C в обмен на $1000. Я ничего тогда вразумительного не ответил, но попробую сделать это сейчас. Немного исторической информации:
Родился в более-менее обеспеченной семье, но всегда хотел иметь много денег. И даже не потому хотел, чтобы что-то конкретное купить, а потому что хотелось быть относительно независимым человеком. Те крохи, что я получал на карманные расходы, хватало разве что на пару шоколадок и чай в столовой (для этого они, собственно, и выдавались).
Высшего образования не имею, лишь среднее-специальное и то не совсем обычное. При этом удалось поболтаться в двух институтах и в одном техникуме. Так получилось, потому что первые деньги за IT-услуги я заработал ещё в школе, а так как родители на карманные расходы особо не давали, то я что называется "дорвался". Мне настолько понравился процесс зарабатывания денег, что о другом и думать не желал. Работал помощником админа, админом, преподавателем, оператором, мастером на выезде, ну и конечно же программистом и разработчиком.
В крупных компаниях типа Acronis или Luxoft работать не приходилось и это, наверное, плохо. Плохо тем, что в больших компаниях, особенно в тех, что уже длительное время существуют на рынке, давно поставлены процессы разработки и у них есть чему поучится в этом плане. Когда в одной конторе мне поставили задачу "написать программу да такую шобы всё было как надо и не хуже чем у других" мне пришлось, набирая людей, крепко задуматься о методах оптимизации управления этим проектом. Были перепробованы куча софта начиная от ToDoList и заканчивая MS Project и какой-то навороченной Web-cистемой. Были прочитаны чуть ли не все разделы в Википедии (и не только) на тему управления проектами, персоналом и прочего. В итоге, не найдя золотой середины, выработал и по сей день продолжаю совершенствовать нечто своё, ближайшим аналогом которого является модель Agile.
Итак, как же происходил карьерный рост? Начну, с вашего позволения, издалека. Лет примерно в 16 с корешами-гопниками устроился на овощебазу грузчиком. Примерно по 100-200 рублей в день - неплохо для начала, и на пиво как раз хватало. Через некоторое время пошёл к знакомым опять же корешам-гопникам на завод по изготовлению металических ворот, заборов, решёток и прочего. Работал там резчиком (блин, чуть не сжёг себя болгаркой), затем немного сварщиком ну и на подхвате, конечно. Платили, честно, не помню уже сколько, но явно не столько, сколько это стоило на самом деле. Через пару месяцев ушёл оттуда. Периодически промышлял мойкой машин с одной из местных команд, деньги быстрые и работа не пыльная, в общем, если не считать милицию и хозяев квартир тех домов, в подвалах которых мы брали воду. Последние гоняли нас из-за какого-то мудака, который однажды забыл закрыть кран в подвале и устроил там мини-потоп. Милиция же из ближайшего отделения, не мудрствуя лукаво, с завидной регулярностию приезжала раз (а то и два) на дню для сбора дани, каковая на тот момент составляла примерно от 100 до 2000 рублей с команды, в зависимости от времени суток (и, соответственно, кол-ва вымытых машин). Всё это перемежалось с расклеиванием листовок на столбах, раздачей флажков у метро, и даже Гербалайфом, которым я успел достать всех знакомых...
Это было совершенно идиотское время. Абсолютно бесцельное шарахание по улицам, поиски пустых бутылок, попрошайничество по 1-2-5 рублей у прохожих, положенный на учёбу болт, а также пьянки-гулянки то с одной то с другой компанией - вот неполный перечень того, что происходило в течении нескольких лет. Но среди вот этого всего ужастика был один положительный момент - я занимался программированием. Ещё в 8 классе, видя стремление к изучению компьютеров, родители записали меня в специализированное учебное заведение, где я отучился 5 лет и которое закончил с отличием. Программа этого заведения была не сказать чтобы уж сильно глубокой, но в преподавателях трудились сертифицированные Microsoft'ом специалисты, и не теоретики, а самые настоящие "практикующие" разработчики. Здесь я получил базу и более-менее изучил C++. Уже после я продолжил самостоятельное обучение дома, купив несколько книг и диск с Visual Studio 6.0 на ближайшей барахолке (о, как же я был счастлив, став обладателем этого диска, а также ещё одного под названием Super Hacker 98).
Ещё чуть позже уже в техникуме судьба сводит меня с начинающим хакером (в современном смысле этого слова), успевшим к тому моменту написать RAT (троян с функционалом backdoor, проще говоря), а также взломать сайты нескольких малоизвестных компаний. Под его влиянием я начинаю более глубоко изучать системное программирование и драйвера в частности. Примерно в это же время пишу свою первую malware и продаю её за $500. Какое же убожество на самом деле это было! Но заказчика (был найдён, если не ошибаюсь, на xakepy.ru) тем не менее оно полностью устроило, а это означало мой первый коммерческий успех. На самом деле этот род деятельности уже тогда был совсем не тем, чем мне хотелось заниматься, но мой выбор тогда сильно ограничивался, - на нормальную белую работу интересных предложений не было, а работа за гроши ради непонятно чего не есть правильно.
После вышеописанных событий ничего особенного не происходило. Тем не менее, проводя теперь всё свободное время вместе с новыми знакомыми в компьютерных классах техникума, я постепенно забыл прошлое, забил на всех "корешей" и постепенно изучение программирования на C/C++ и ассемблере стало моим единственным времяпровождением (не считая, конечно, походов в кафешки и обсуждений с юными хакерами больших планов на будущее :) ). В то время у меня было два основных источника дохода: стипендия - как отличнику мне полагалось аж ~700 руб., и пособие по безработице - на тот момент оно составляло у меня 30% от минималки, то бишь примерно 1000 руб. Жить на такие деньги, конечно, невозможно и я по-прежнему сидел на шее у родителей...
Однажды вечером, когда мы втроём как обычно сидели в инете, параллельно помогая местному админу создавать образы систем для удалённой загрузки через PXE, в аудиторию зашли четверо характерных представителя гламурной молодёжи - два "гламурных падонка" и две ихние чиксы. Один из парней сказал, что его направил к нам админ, и достал скомканный листок бумаги, на котором были напечатаны 6 задачек примерно такого плана:
По заданной строке текста напечатайте строку в которой все гласные буквы удваиваются, а двойные согласные заменяются одиночными.
Пусть дан текст в виде символьной строки оканчивающийся точкой. Определите кол-во слов которые являются перевёртышами (пример: шалаш, читаются одинаково что с начала, что с конца).
Пусть в файле хранятся сведения об автомобилях и их владельцах (марка, фамилия и номер ГАИ). Создать файл содержащий фамилии владельцев данной марки.
Всё это нужно было сделать на TASM'е под DOS. Я сказал, что сделаем по 150 руб. за лабу, ребята согласились. И хотя у них на лбу была написана финансовая состоятельность, я не стал сильно завышать цену, у меня были немного другие планы. Я распределил работы между мной и третьим нашим товарищем (линуксоидом, кстати) пополам. К сожалению, эта сволочь подвела меня, сделав всего 2 задачи из 3, в остальном проблем не наблюдалось и через пару дней мы получили наши долгожданные копейки. А затем произошло то, на что я и рассчитывал. Выяснилось, что в том институте несколько групп почти в полном составе не владеют предметом (программированием) ибо платники. Ко мне выстроилась приличная очередь из желающих получить лабу и/или курсовик на Pascal'е с ассемблерными вставками. Я немного поднял цены - до 200 руб. за лабу и до 800 руб. за курсовик, правда один расплатился новым жёстким диском, что в общем, меня вполне устраивало т.к. в то время я как раз собирал новый компьютер. Это была ещё и интересная практика в языке ассемблера, не говоря уже о заработке в несколько десятков тысяч рублей (за время около 1 месяца).
В институтах, где мне доводилось болтаться, промышлял тем же, только цены там были чуть другие - примерно от 1000 до 5000 руб. за курсовик на Pascal'е, но там и задачки другие, и люди мало того, что не знали ни Pascal'я ни алгоритмов, так ещё и предметную область курсовика не понимали в упор. Зато чуть ли не в каждой группе находился человек, который всем обещал сделать "бесплатно" и "по дружбе", но так ничего никому и не делал.
Всё это было здорово и весело до тех пор, пока однажды деньги не кончились. Наступило лето, ни лабы ни курсовики никому уже не нужны были, а долбанная учёба надоела так, что готов был уже хоть на какую работу устраиваться, пусть даже за копейки. И работу такую я нашёл, правда, по знакомству. Официально должность называлась "выездной мастер" (ну или типа того, я никогда не видел эти документы) и её суть заключалась в том, чтобы выезжать к клиенту (на дом или в офис) и устранять возникшие технические неисправности с компьютерным оборудованием и периферией. Оплата почасовая, половину заработанного забираешь себе, половину отдаёшь конторе. Но на деле можно было мухлевать, при чём как с клиентом, так и с конторой. Неориентирующемуся в вопросе клиенту можно было наговорить ужасов часов на 5, хотя работы на 15 минут, а это как никак уже получалось 5 * $10 = $50 вместо стандартных $10 за < = 1 час. А в отчёте (квитанции) для конторы можно было написать, что работал реально 1 час, хотя сидел у клиента 2 часа и денег, соответственно, отдавать меньше получится. Некоторые добрые клиенты (особенно, женского пола) сами предлагали мол вы пишите там что хотите, нам типа не жалко, в те времена это особо никто не контролировал...
Через некоторое время в ЦЗН мне предложили вакансию оператора ПК в одной аудиторской конторке примерно за 4000 руб/мес. Её рабочий офис находился в съёмной квартире и это было чистой воды разводилово. В главном офисе мне дали прямо с порога тест, нужно было за минуту набрать определённый текст, - таким образом выяснялась скорость моей печати, из этого высчитывалась месячная оплата. Дело было зимой, и пальцы чуть не хрустели при наборе. В итоге мне в жёсткой форме объяснили, что им плевать на мои проблемы и моя з/п будет даже не 4000, а 3000 с чем-то там. Правда, работа не 8 часов в день, а что-то около 5-6 часов в вечернюю смену. Я согласился. Работали там в основном студентки или иногородние, у которых было без вариантов. На самом деле не всё было так плохо, например, с нами работал один китаец, его скорость достигала 400 зн/мин., при этом набранный текст почти не содержал ошибок. Как ему это удавалось - не знаю, но он получал существенно больше, чем остальные. Работа вообще заключалась в перепечатке с небольшими изменениями документов, как правило, в Excel'е. Скукотища страшная, но зато был полноценный халявный интернет.
После этого были ещё пара непрофильных конторок, где работал эникейщиком, и написана ещё пара программулек злобного действия, которые были проданы почти так же успешно, как и первая. Всё это время я писал, писал и ещё раз писал код. Ночью, утром, в любое свободное время. Очень и очень много кода было написано мною в тот период. И это был очень хреновый код. Впоследствии, он был уничтожен мною чуть менее чем полностью, а сейчас я пишу совершенно в другом стиле. Кроме этого, были прочитаны тонны Web-страниц на различных профильных форумах, были куплены или взяты в библиотеках книги по алгоритмам и структурам данных, по методологиям программирования, по техникам отладки, по реверсингу и прочему хаку и т.д. и т.п. Более 60% всего прочитанного мне не пригодилось ни в одном проекте, и это немного печально. Но, тем не менее, профессиональное развитие так или иначе шло своим ходом и однажды я понял, что хочу уехать из города. Он просто надоел мне, меня в нём ничто не держало, ни девушка ни хоть сколько-нибудь интересная работа, да и друзей-то настоящих у меня никогда не было.
Москва
Вот так, совсем один, я приехал в Москву. Конечно, я не был полным идиотом и приехал не просто так, а на работу, заранее договорившись с работодателем. На самом деле это произошло совершенно случайно, однажды по одному из размещённых в сети резюме мне позвонил человек и после непродолжительной беседы выдал тестовое задание. Задание было выполнено идеально и мне было сделано предложение. Это была первая моя софтовая компания, небольшая, человек 30-50 всего.
Почти сразу я узнал, что в мои обязанности будет входить написание различного рода adware и сопутствующего говна. Сначала я воспринимал это как игру, как-то не было чувства реальности происходящего. И хотя з/п росла чуть ли не ежемесячно (последний раз это было что-то около $3500) и выплачивалась более чем стабильно, эта работа вскоре стала рутиной, не приносящей никакого удовольствия. Однако свои обязанности старался выполнять добросовестно, насколько тогда это позволяла моя квалификация. На самом деле экспириенс это был преинтереснейший, были написаны удивительные вещи, например, так называемая студия тестов, позволяющая сэмулировать и визуализировать выполнение отдельных частей системы на целевой машине. Был написан клиентский модуль, куча плагинов к нему, и всем этим делом можно было централизовано управлять, при чём не какой-то там админкой на PHP, а самой настоящей системой под управлением Linux, на C++ писанной.
Через пару лет я был уволен из этой компании, хотя в этом решение руководства полностью совпадало с моими планами. Незадолго до этого я получил доступ к информации по внутренней статистике нашей системы и, честно говоря, был даже удивлён, узнав кол-во пациентов, - я думал их на порядки меньше. Кроме этого, некоторая финансовая отчётность также была способна вскружить голову, а уж когда я узнал о наличии в арсенале своего человека в правительстве, то стало и вовсе интересно, а желание узнавать дальше пропало напрочь. Насколько мне известно, к текущему моменту компания прекратила своё существование и это, наверное, к лучшему. Позже я узнал, что компаний подобного рода в одной только Москве штук 10 (может и больше, не в курсе), и вы знаете, у них у всех почти одинаковый офис - маленький, арендованный где-нибудь в большом бизнес-центре, персонала реально может быть всего человека три, но зато везде есть секретарша, на кой ляд она там не знаю, но наверно "так положено", - офис, хуле. И даже продукты такие компании делают одни и те же. Кстати, небольшое наблюдение - 30% персонала являются обычно гражданами Украины, что, в общем, закономерно, потому как в нормальные конторы иногородних/иностранцев без регистрации и прочего не берут, а украинцам так вообще беда, со слов одного знакомого админа, сразу, как видят хохляцкий паспорт, - "до свидания" даже без объяснения причин (возможно, сейчас что-то поменялось, я не в курсе).
Москва - очень дорогой город. Я почувствовал это сразу, когда только приехал. И хотя некоторые товары здесь дешевле, чем в регионах, услуги же дороже на порядки (например, такси, нотариусы, парикмахерские...). Всего за один год цены на аренду 1-комн. квартиры поднялись с 16000 до 22000 руб. (в среднем). А ещё через год некоторым умудрялись сдавать и по 30 и по 35 т.р. Плюс стоит добавить сюда процветающий лохотрон, например, даже моя жена однажды купилась; беременные женщины вообще почему-то становятся доверчивее. Как-то днём в квартиру позвонила женщина, представилась сотрудником СЭС (даже документы предъявила, где их только печатают...) и объявила, что-де завтра будут чистить трубы и подъезд от тараканов и обрабатывать их каким-то раствором, а чтобы полчища насекомых не оккупировали нашу квартиру, мы просто обязаны приобрести у неё какую-то мазь за 100 рублей и обмазать ею все решётки на стенах. Так эти суки окучивали все подъезды нашего дома и с каждой квартиры поимели по 100 руб. Разумеется, никто из СЭС на следующий день не приехал.
Постановка задачи
Уже через полтора года работы в Москве стали появляться мысли о том, что неплохо бы вообще заработать денег. Нет, не просто денег, а много денег, желательно миллион, а лучше миллиард, конечно, хотя бы рублей для начала. Да, я работал в стабильной (на тот момент) компании, относительно неплохо вроде бы имел, но в то время я уже жил не один, а с беременной женой. При чём жили мы, разумеется, в съёмной квартире, хозяева - вот суки - имели тенденцию завышать цену или периодически ебать моск на тему "не пора ли вам съехать". Последняя квартира обходилась нам в 23000 руб. в месяц. В конце концов, я понимал, что семья со временем может стать больше, да и вообще не комильфо это, вроде в цивилизованном мире живём и что я, не мужик, что ли?.. Вот с такими мыслями и затосковал я. Ибо без высшего образования да с таким опытом работы (неофициальным, конечно) ни хуя мне в этой жизни не светит. В этом я уже успел убедиться, походив по собеседованиям в крупные московские компании, из которых лишь в одной получил предложение, но на деньги чуть меньшие, чем уже имел, - соответственно, смысла не было. Правда, после увольнения из первой компании я всё таки осилил ещё две московские организации (это было непросто) и ещё около года тупо сидел в офисе. Но уже к этому времени в голове очень чётко сформировалось понимание, что я делаю что-то не то.
Вообще, мне кажется, что ищущий да обрящет, т.е. если вы действительно хотите денег, то вы найдёте способ их заработать, и это не просто слова. Но если вы сами не хотите этого, то их у вас никогда и не будет. Кстати, на эту тему могу порекомендовать прослушать вот эту запись. Если отбросить там всю хуету на тему сетевого маркетинга, то в общем и целом этот человек говорит дело. На самом деле вот такие записи сами приходят на почтовый ящик в качестве спама. Сначала я тупо удалял их. Затем стал читать и слушать кое-что, и затем уже стал слушать каждую такую запись, выискивая те мысли, которые могли бы мне хоть как-то пригодиться. Я заинтересовался такими "тренингами" и даже специально нашёл несколько в сети. И в них действительно попадаются правильные и, несомненно, достойные внимания мысли. Например: вы замечали, что большинство миллионеров/миллиардеров это не выходцы из нищеты, а люди, выросшие в семье со средним или высоким достатком, как правило? Почитайте биографию этих людей, это действительно интересно. А знаете, почему таким людям проще добиться материального успеха? Нет, не потому что у них дескать "все дороги открыты, связи, стартовый капитал и всё такое". Нет. А потому что это долбанная психология. Вы хотите себя жалеть? Вы будете жалеть себя до конца жизни. Вы хотите пить пиво с корешками у ларька? Вы так и будете его пить пока не сопьётесь к ебени матери. Вы хотите обвинить своего начальника в том, что он жирует, а вам платит не столько, сколько вы стоите? А ему похуй и всем остальным тоже похуй на ваши проблемы. И вы так и дальше будете продолжать обвинять начальство, но денег от этого у вас больше не станет. Понятно, к чему я? Если вы будете продолжать общаться большую часть времени с людьми низкого уровня - вы там и останетесь со всеми вытекающими последствиями. Если вы хотите чего-то большего, - смотрите на тех, кто уже добился этого. Смотрите, как они этого добились и пытайтесь проецировать это на себя (с необходимым, разумеется, корректировками). Конечно, это не даёт никакой гарантии, что вы вообще когда-либо станете миллиардером, но это уже шаг на пути, который вы задумали.
Мне ещё крупно повезло: жена оказалась очень грамотным человеком, бизнесменом по натуре, с высшим образованием в области управления, успевшая поработать в сфере торговли и на руководящих должностях в том числе. Реально её экономическая грамотность и стремление двигаться куда-то действительно сильно помогли мне в достижении и собственных целей. Это вообще очень важно, - кто-то должен быть рядом, с кем можно просто пообщаться, ведь давно известно, что если проговаривать мысли вслух, они легче усваиваются. Если вы не женаты/замужем, я посоветовал бы вот в те моменты, когда надо что-то обдумать, принять какое-то решение, - начинать обсуждать этот вопрос с самим собой и лучше вслух. Как бы глупо или страшно это ни выглядело, ни в коем случае не нужно этого боятся, проверено. Ну злоупотреблять, конечно, тоже не стоит.
Итак, мне нужно было много денег. Тем не менее, любую задачу лучше всего разбивать на этапы и поставить какую-то цель. В качестве первого этапа я решил заработать 1 млн рублей. Во времени решил себя особо не ограничивать (такие вещи imho нельзя планировать ибо много от чего зависит), но примерно предполагал, что накопить указанную сумму получиться в течении 5 лет (да, довольно пессимистично). С целью же в моём случае проблемы не было - нам нужна была квартира, но не в гадюшнике под названием "Москва", а в областном центре (около 200 км от Москвы). Вообще не вижу смысла покупать квартиру в Москве, - экология никакая, чуркистан процветает, на машине ездить уже невозможно... и за эту радость вы выложите в лучшем случае миллионов 6 руб. (в худшем - плюс бесконечность). Оно вам надо? Мне точно нет, поэтому я нашёл городок (г. Владимир), где новую двухкомнатную квартиру можно взять почти "за даром" - всего 2-2,5 млн руб.
Набираем высоту
На самом деле нельзя сказать, чтобы я прилагал какие-то особенные усилия для достижения этих целей, но те действия, которые я совершал, так или иначе увеличивали сумму моего счёта одновременно принося творческое удовлетворение и душевное равновесие. Что же я делал? Прежде всего, я работал в той или иной компании полный рабочий день, получал за это зарплату и, в общем, весьма неплохую даже для Москвы. Конечно, мы откладывали и копили сколько могли, но было несколько моментов:
Маленький ребёнок требует очень много вложений.
Цены на аренду квартир постоянно росли, не шибко сильно, но зато уверено и непрерывно.
Не скрою, сдерживать себя было непросто, ведь до приезда в Москву я почти не имел свободных денег.
Москва - очень дорогой город сам по себе, походы в магазин здесь могли вылиться в двухмесячную з/п в провинции.
В виду этих обстоятельств обычная офисная работа никак не могла привести меня к цели, и вместе с семьёй я уехал из Москвы к родителям жены, предварительно договорившись с руководством о предоставлении мне статуса удалённого сотрудника. Почти сразу же получилось так, что компания по неизвестной мне причине передумала и мне было предложено вернуться в московский офис. Я вынужден был отказаться (из-за московской экологии серьёзно заболел мой ребёнок) и в итоге остался без работы. Всё это было печально и около двух месяцев я не мог нормально работать, вероятно, это было что-то вроде депрессии. И вот здесь мне повезло второй раз...
Однажды, это было ещё в Москве, ко мне в icq постучался человек. Он нашёл мой номер на одном из форумов и у него был какой-то технический вопрос, который он никак не мог решить самостоятельно. Я уже не помню точно, что это был за вопрос, но что-то связанное с драйверами режима ядра вообще и с файловыми фильтрами в частности. Т.к. именно в этой области я уже имел определённые умения, то ответил на вопрос, а человек исчез из поля зрения (впрочем, я никогда не интересовался своими собеседниками больше, чем нужно). И вот, в тот самый момент, когда я сидел практически без дела, выполняя мелкие фрилансерские зуказивки, этот человек снова вышел на связь и спросил, не желаю ли я поработать над одним проектом из области системных разработок, где я смогу применить все свои знания и опыт. Озвученная задача имела прямое отношение к тем самым файловым фильтрам и была настолько сложной и трудоёмкой, что, честно говоря, стало даже как-то ссыкотно. Сначала думал отказаться, но поставленные сроки, в общем, не были такими уж пугающими и я назвал сумму, от которой самому чуть не поплохело (в это время на моём счету был 0 и сумма даже в $5000 уже казалась невозможной). Но человек довольно легко согласился, а позже я понял, что реально продешевил.
Так началось наше сотрудничество, позже переросшее в партнёрство. Я занимался технической системной частью проекта, получал за это денежку, и хотя приблизительно я представлял суть решения, в котором используется мой код, я никогда не интересовался откуда эти деньги и как там вообще дела с продажами проекта, - всё таки каждый должен заниматься своим делом и тем, что у него получается лучше всего.
С начала моей работы над проектом прошло около полугода и однажды жена сообщила, что пора идти за квартирой. В тот момент я узнал, что у нас на счету уже более чем 1 миллион и 200 тысяч рублей. Как-то незаметно это прошло для меня. Работаешь, работаешь... и вдруг бах, - можно идти за квартирой, пусть в рассрочку, но это уже вторично, в общем.
Укрепляем позиции
После вышеописанных событий я задумался, ведь я уже кой-чего умею, кой-чего могу и кой-какого опыта как-никак, а всё же поднабрался. Всё это время я смотрел на молодёжь, на коллег, на всех кто так или иначе пишет код, высказывает мысли и т.п. и постепенно понимал, что уровень большинства разработчиков очень низок и что это, чёрт возьми, хорошо. Впоследствии, некоторые заказчики, с которым у меня не получалось договориться по цене (она была слишком высокой по их мнению), приходили ко мне второй и третий раз. Потому что слишком долго искали подходящего человека, или потому что человек их обманывал и т.д. И вот в такой вот ситуации я подумал, ведь если есть один человек, которому пригодились мои знания и умения, то, возможно, есть и другие. Я решил попытаться найти их и через некоторое время мне это удалось. В среднем, я находил примерно одного заказчика раз в 3 месяца. Для этого я использовал такие русскоязычные средства, как форумы и сайты удалённой работы (фриланс). На мой взгляд самые известные и эффективные среди таких сайтов, это форумы RSDN и WASM, а также биржи удалённой работы free-lance.ru и weblancer.net. Ошибка думать, что на фрилансерских сайтах можно найти только одну мелочёвку да Web, это не так.
Конечно, не со всеми всё получалсь и не со всеми всё шло гладко. Кроме того, почти всегда работа выполнялась без каких-либо бумажек и был, например, случай, когда после долгих препинаний о том, как должна выглядеть поддержка написанного кода, заказчик просто пропал. Впрочем, там всё ещё сложнее было и здесь я лишь хочу посоветовать всем, уж если не заключаете письменный договор, то хотя бы чётко оговаривайте все нюансы. И только тогда вы сможете расслабиться, когда ваше чутьё будет в состоянии подсказать вам с кем стоит иметь дело, а с кем лучше не надо. Да, мне случалось отказываться от якобы "выгодных" заказов. Например, представьте, что вам пишет человек и говорит, что хотел бы с вами работать. Задайте ему вопрос - почему именно со мной? У адекватного заказчика уже давно заготовлен ответ на этот вопрос, неадекватный же начнёт нести околесицу и нормального логичного ответа вы не дождётесь. Невменяемых лучше посылать сразу, даже если они сулят десятки или даже сотни тысяч и даже если они гарантируют 100%-ную предоплату.
Таким образом в течении последних 2-3 лет я выполнил около десятка заказов, одни были средние ($1-10k), другие более крупные (от $15k). Очень много было "системных" заказов с Украины, не знаю, почему так. Один раз позвонили из Калифорнии, хотели виртуализацию файловой системы. Из Европы и США вообще почему-то обычно сразу звонят на мобильный, наши же пишут в аську, как правило, или на почту. Среди заказов было в основном то, что укладывается в понятие "фильтр", при чём не обязательно в режиме ядра, - файловый фильтр, фильтр дозвона, WDM-фильтры реальных устройств, особенно, устройств хранения (storage), различные проактивки и антируткиты, сетевые фильтры, виртуальные диски, файловые системы, - всего понемногу, но разнообразие опыта это хорошо, imho.
Итоги
На данный момент имею двух партнёров в двух различных проектах, при чём сейчас уже это не более чем вялотекущая поддержка написанного и отлаженного кода. Имею двух постоянных заказчиков, придумывающих периодически что-нибудь новенькое, - это интересно ;) О своей текущей деятельности распространяться желанием не горю, но уверен, воображение нарисует вам именно ваше будущее, к которому, несомненно, можно и нужно стремиться. Надеюсь, теперь будет понятна моя позиция относительно малобюджетных заказов, как мог объяснил.
Сейчас есть серьёзные мысли в ближайшем будущем плотно заняться продажами продуктов собственного производства (не без помощи, конечно) и наконец-то организовать собственный онлайн-бизнес. Первая попытка - jFirewall Personal Pro - благополучно провалилась, но принесла другие, ещё более интересные, результаты. И обо всём этом я обязательно напишу, когда придёт время.
В заключение
Ваши челюсти ещё не вывихнуты из-за непрерывной зевоты, напавшей во время чтения этого сообщения? Это хорошо. Надеюсь также, что мне удалось написать здесь хоть что-то полезное. В одном из следующих сообщений будет продолжение, ждите "Часть 2", где я попробую изложить что-нибудь более увлекательное, а сейчас усталость вынуждает меня прекратить эту писанину...
Как известно, начиная с Windows Server 2003 DDK поддерживается сборка драйверов под 64-битные платформы. Также известно, что инструкция __asm более не доступна для использования в 64-битном коде. Чтобы использовать ассемблер при сборке теперь необходимо в обязательном порядке создавать .asm-файл и писать ассемблерный код там. Новый файл необходимо добавить в проект сборки, это делается путём добавления соответствующей записи в макрос AMD64_SOURCES, например, так:
AMD64_SOURCES = AMD64\code64.asm
Что здесь важно, так это то, что файл code64.asm физически обязан находится в подпапке AMD64, иначе утилита build просто не найдёт его. Странно, но это так, расположение файла в данном случае имеет значение.
Формат ассемблерного файла
Кратко формат ассемблерного файла для тех, кто совсем не в курсе:
public RoutineName _TEXT SEGMENT RoutineName proc xor eax, eax ret RoutineName endp _TEXT ENDS END
Использование ассемблерной функции в C-коде
Соглашение о вызове всегда __fastcall на 64-битных системах. Соответственно, на C эту же функцию следует объявить следующим образом:
Однажды я столкнулся со странной на первый взгляд проблемой во время написания полноценного TDI-фильтра. Проблема проявлялась только при работе драйвера netbt.sys на 139/tcp порту, и именно поэтому мне не удалось обнаружить её сразу.
Симптомы проблемы
Я тогда поставил перед собой задачу поддерживать собственный список активных TCP-соединений, где мне нужно было хранить адреса, порты и некоторую другую информацию о каждом соединении. Т.к. единственный документированный способ реализовать такое, это фильтрация соответствующих TDI-запросов, то я стал мониторить вот эти: TDI_CONNECT, TDI_DISCONNECT, а также нотификаторы ClientEventConnect и ClientEventDisconnect, т.е. при успешном коннекте я добавлял информацию о соединении в список, а при успешном закрытии соединения, соответственно, удалял её. Запустил броузер и стал смотреть логи - всё было хорошо, коннекты появлялись и исчезали. Сверялся на всякий случай с netstat'ом, - соответствие было полное. Но только до тех пор, пока я не расшарил папку на виртуалке и не попытался зайти на неё с хостовой системы. В этот момент я увидел входящее соединение на 139 порт, netstat показывал тоже самое. Через несколько секунд (примерно 10-15) сниффер показал, что с виртуалки пришёл пакет RST, после чего netstat на виртуальной системе показывал отсутствие активных соединений, а вот мой фильтр утверждал, что соединение всё ещё находится в состоянии Established.
Исследования и решение
Дальнейшее исследование выявило, что в момент прихода RST-пакета фильтр не ловит ни TDI_DISCONNECT, ни TDI_DISASSOCIATE_ADDRESS, ни даже нотификатор ClientEventDisconnect. И выяснилось, что инициатором закрытия соединения выступал всё таки netbt.sys, но оказывается, что для этого он использовал не TDI_DISCONNECT, как я ожидал, а совершенно банально и тупо ZwClose, что выливалось в получение моим фильтром запроса IRP_MJ_CLEANUP. Я тут же встроил удаление соединения из списка в обработчик этого запроса и всё волшебным образом заработало в полном соответствии с таблицей соединений tcpip.sys (которой и пользуется netstat, между прочим).
Не знаю, честно говоря, зачем это может понадобится, но если кому-то всё же интересно, то здесь я расскажу как это сделать правильно.
Не самые удачные способы
Первое, что обычно приходит на ум, это поиск окна запущенного компонента по его заголовку или по классу. Оба варианта, к сожалению, не подходят. Во-первых, заголовок может быть на любом языке, поэтому получится не универсально. Во-вторых, по классу окна вы тоже никогда ничего не узнаете, потому что диалоговые окна в Windows создаются как правило на основе одного и того же класса "#32770 (Dialog)", в этом можете сами убедиться, если посмотрите в Spy++ (входит в состав Visual Studio).
Итак, эти два способа отпадают напрочь. Есть ещё один чуть более стабильный способ. Он заключается в том, чтобы зашить в программу некий "хэш" окна и затем перебирая все top-level окна найти нужное по этому самому хэшу, подсчитав его для каждого из окон. Что есть "хэш"? Например, это может быть совокупность идентификаторов элементов управления (контролов) нужного окна. Т.е. мы можем зашить в программу значения этих идентификаторов и проверять их для каждого найденного окна. Ну если кто не знает, то перебрать все элементы управления в окне можно вызовом функции EnumChildWindows. Способ этот тоже не шибко-то универсален, поэтому давайте рассмотрим ещё один момент и перейдём уже к описанию правильного алгоритма.
Иногда предлагают способ решения проблемы через перечисление модулей всех пользовательских процессов на предмет наличия там .CPL-файла. Спешу огорчить, это не работает по одной простой причине: компонент не открыт, а его модуль при этом запросто может быть загружен в Проводник (explorer.exe). Например, такая ситуация имеет место с компонентом "Дата и время" (timedate.cpl) в Windows Vista. В общем, и этот способ не советую.
Правильный способ
Этот способ прост как валенок если знать одну особенность. Почти все компоненты Панели управления (за некоторыми исключениями) являются по сути обычными DLL (файлы с расширением .CPL) с жёстко определёнными точками входа. Отсюда следует как минимум, что для запуска компонента требуется процесс, в который эта DLL и будет загружена. В случае с .CPL-элементами запускается процесс rundll32.exe, который вызывает недокументированную функцию Control_RunDLL из shell32.dll, и вот она уже и загружает указанный .CPL и вызывает нужную точку входа. Это первый признак, - имя процесса. Но его недостаточно, т.к. во-первых, rundll32 запускается не только для компонетов Панели управления, и во-вторых, компонент может быть запущен косвенно через control.exe (хотя control внутри себя вызовет rundll32, тем не менее тоже стоит иметь в виду). Поэтому необходимо также проверить и командную строку процесса на наличие в ней имени .CPL-файла, по которому уже можно сказать оно это или нет. Например, для компонента "Дата и время" командная строка будет примерно такая:
Ну тут вроде всё понятно, осталось расписать как всё таки добраться до командной строки чужого процесса. Тут проблема в том, что документированного способа вообще-то нет, поэтому придёться использовать полудокументированные возможности:
Открыть процесс через OpenProcess с правами минимум PROCESS_QUERY_INFORMATION и PROCESS_VM_READ.
Запросить адрес PEB вызовом полудокументированной NtQueryInformationProcess с классом ProcessBasicInformation.
Прочитать командную строку по адресу Peb.ProcessParameters.CommandLine опять же через ReadProcessMemory (не саму строку, а её буфер, который в поле Buffer структуры UNICODE_STRING).
Далее идёт анализ полученной строки и логика приложения. Всё просто, в общем, и для стандартных компонентов Панели управления типа "Дата и время", "Клавиатура", "Мышь", "Язык и региональные стандарты" это будет работать. Но некоторые из компонентов не являются .CPL-файлами, а представляют собой обычное оконное приложение с кастомным интерфейсом (например, "Учётные записи пользователей"). В этом случае нужно конкретно для каждого такого компонента смотреть его имя процесса и параметры командной строки (если они есть) и соответственно проверять их в приложении.
В случае, если нужно получить именно окно запущенного компонента, тогда несмотря на то, что мы проверяем параметры процесса, я бы посоветовал начинать перечисление не с процессов, а с окон. Получив хендл окна, мы можем запросить у него ID процесса и далее действовать уже как в алгоритме, показанном выше. Получить ID процесса по хендлу окна можно с помощью функции GetWindowThreadProcessId.
Панель управления в Windows Vista
Вышеуказанный способ не будет работать для большинства из компонентов в Windows Vista, особенно для тех, которые появились впервые или были существенно переработаны по сравнению с прошлыми версиями. Это связано с тем, что компоненты здесь теперь представляют собой что-то типа виртуальных папок, таких же как, например, папка "Мой компьютер" в Windows XP. К сожалению, не смогу дать вам больше информации по этой теме, так как заниматься подобным не приходилось, но если бы возникла необходимость, то в первую очередь я получил бы список окон оболочки. Это можно сделать, создав экземпляр интерфейса IShellWindows. Это что-то типа массива, каждый элемент которого есть суть окно, хостером которого является как правило либо Проводник (explorer.exe) либо Internet Explorer (iexplore.exe) - нас интересуют первые. Далее с помощью метода QueryInterface я попробовал бы поприводить полученные указатели к тем или иным Shell-интерфейсам и посмотреть какую информацию из них можно извлечь.
Цель этих манипуляций это получить что-то типа CLSID для текущего содержимого окна, по которому можно идентифицировать это содержимое. Предполагаю, что часть из этих идентификаторов документирована (например, для папок "Мой компьютер" или "Панель управления"), а часть скорее всего нет, так что тут тоже геморрой ещё тот может быть. Но возможно что я ошибаюсь и есть какие-то хорошо документированные и специально для этого предназначенные механизмы, появившиеся впервые именно в Windows Vista. У меня нет ни времени ни желания копаться в этом, потому предлагаю вам либо проделать это всё самостоятельно, либо спросить на форуме, - там точно есть люди, которые в теме и смогут помочь. Ещё ссылка по теме здесь.
Итак, пару слов скажем сегодня об именах не просто файлов, а об именах исполняемых модулей. Посмотрим, откуда они берутся в дикой природе и что вообще с ними делать. Да, перед тем, как двигаться дальше, советую ознакомится вот с этим материалом. Ну всё, поехали.
Речь, конечно же, пойдёт в основном о тех модулях, которые загружаются в системный процесс aka процесс ядра, он же называется System и имеет идентификатор 4. Сразу оговорюсь, что не все модули, о которых пойдёт речь ниже, отображаются непосредственно на виртуальное адресное пространство (а.п.) именно системного процесса, т.е. другими словами несмотря на то, что код всех этих модулей предназначен для выполнения в режиме ядра, их образы не обязательно проецируются в процесс System. В качестве примера можно привести системный модуль графической подсистемы Win32k.sys, функции которого выполняются в основном в режиме ядра, хотя сам он проецируется исключительно в пользовательские процессы (т.е. попытка чтения памяти в этом модуле в контексте процесса System приведёт к исключению и, как следствие, - к падению системы). Ну, в общем, это логично, ведь ядру графика без надобности, значит и модуль этот там не нужен.
Бывает, что нужно получить список таких модулей, при чём путь к каждому модулю требуется в формате Win32 (он же DOS-формат); это может быть нужно, например, чтобы вывести этот список на экран и показать пользователю. Основная проблема здесь заключается в том, что в ядре начинающиеся с буквы диска DOS-пути не применяются, это сущности исключительно подсистемы Win32. Ну и возникает закономерная проблема - как преобразовать путь вида
\Windows\System32\ntoskrnl.exe
в его DOS-представление вида
C:\Windows\System32\ntoskrnl.exe
Впрочем, почему же сразу в DOS-представление? Для начала можно попытаться привести хотя бы к полному Native-формату - уже большое дело будет. А о преобразовании Native-имён в DOS-формат см. предыдущие сообщения.
Способы получения списка модулей
Прежде всего, давайте выясним, как нам получить список модулей, загруженных в ядро, и в каком виде мы получим результат. Способа есть два, при чём оба недокументированы, хотя первый из них недокументирован меньше, чем второй (для нас это ничего не меняет, в общем).
Вызов системного сервиса
Этот способ заключается в вызове функции ZwQuerySystemInformation с классом SystemModuleInformation. На выходе будет массив структур, содержащих такую информацию как базовый адрес модуля, размер модуля в памяти, а также полный путь к исполняемому файлу-образу, из которого данный модуль был загружен. Чем хорош этот способ так это тем, что формат выходных структур не меняется от Windows XP до Windows 7, в этом плане способ является достаточно надёжным. Однако ж, как всем известно, траву Microsoft'овские програмёры курят весьма забористую, потому и здесь не обошлось без ляпов. А именно: путь к файлу-образу модуля, возвращаемый этой функцией, находится там, во-первых, в ANSI-формате (однобайтовая кодировка), и, во-вторых, длина этой строки ограничена 256 символами. Собственно, ляп этот настолько серьёзный, что делает этот способ совершенно непригодным к использованию. Переходим к способу номер 2, здесь добавить больше нечего.
Перечисление элементов системного списка
Все системные модули, так или иначе загружаемые в ядро, попадают в итоге в некий список; в исходном коде ядра переменная, содержащая указатель на начало этого списка, называется PsLoadedModuleList (не экспортируется). Собственно, указатель на этот список нам и не нужен, мы можем взять из него произвольный элемент и раскрутить остальные по цепочке, т.к. они все связаны посредством механизма двусвязного списка LIST_ENTRY. Каждый из элементов списка содержит базовый адрес модуля, размер образа в памяти, адрес точки входа, путь к файлу-образу модуля и, отдельно, его имя. Последние два элемента лежат там в исходном Unicode-формате, что нам и нужно. У этого способа есть только один маленький недостаток - он не позволяет перечислить загруженные в ядро модули пользовательского режима, например, ntdll.dll. Для таких модулей предусмотрен отдельный список. Впрочем, это не страшно, потому что такие модули перечисляются так же как и обычные модули, загруженные в пользовательские процессы (через списки в PEB).
Чтение реестра
Этот пункт не относится к получению списка именно загруженных модулей, но с помощью этого способа можно получить список всех зарегистрированных в текущей конфигурации модулей ядра. Итак, способ заключается в перечислении всех подключей ключа "\Registry\Machine\System\CurrentControlSet\Services", это удобно сделать с помощью функции RtlQueryRegistryValues с флагом RTL_REGISTRY_SERVICES. В каждом подключе следует проверить значение Type, которое должно быть равно 1, 2, 4 или 8. Если так, значит перед нами драйвер и значение ImagePath содержит путь к файлу исполняемого модуля в одном из перечисленных ниже форматов. Кстати, учтите, что значения ImagePath может не быть вообще. Для драйверов это означает, что его образ лежит в папке "\SystemRoot\System32\Drivers", а его имя равно имени ключа плюс ".sys".
Реально сервис NtQuerySystemInformation для получения списка модулей использует второй способ, что, в общем, естественно. Есть только один существенный момент: доступ к списку модулей в ядре обложен синхронизацией; в исходном коде ядра указатель на объект синхронизации называется PsLoadedModuleResource (не экспортируется). По-хорошему если, мы должны захватывать этот лок перед проходом по списку и освобождать его по окончании работы с ним, но вся проблема в том, что этого указателя у нас нет в непосредственном доступе (бо не экспортируется). В принципе, мы можем положить на синхронизацию, если пишем что-то некоммерческое ибо по моим наблюдениям при обычной работе системы, когда драйвера загружаются/выгружаются не слишком часто, - ничего страшного не происходит. Поиск указателя на PsLoadedModuleResource требует некоторой смекалки и наличие дизассемблера длин, но я бы не сказал, что это сложная задача. Ну тут решать вам, конечно.
Пути к файлам образов модулей могут быть возвращены/извлечены в следующих форматах:
Имя файла.
Путь относительно папки Windows.
Путь относительно системного диска.
Полный ненормализованный путь (с использованием "\SystemRoot").
Полный ненормализованный путь (с использованием символьной ссылки тома).
Имена, начинающиеся с префикса "dump_".
Чуть позже расскажу, что делать в каждом из случаев. Алгоритм получения списка модулей
Со способами в общих чертах вроде разобрались, из двух предложенных нам подходит разве что второй, ибо первый вообще не вариант (to ms: хорошо покурили, да?). Сейчас попробуем разобрать его более детально. Прежде всего давайте изучим некоторые структуры. Вот структура DRIVER_OBJECT, описывающая объект загруженного драйвера:
Ну вроде всё понятно, да? Поля DriverStart и DriverSize содержат, соответственно, базовый адрес и размер модуля в памяти в байтах. Зачем эти значения дублируются ещё и здесь, когда их легко достать из блока загрузчика, - не очень понятно, но тем не менее сие есть факт. Адрес блока загрузчика лежит в поле DriverSection и имеет следующую структуру:
Связь этих структур осуществляется посредством стандартного механизма двусвязного списка через поле InLoadOrderLinks. Т.е. получается, что, зная указатель на элемент списка PLIST_ENTRY, можно тупо привести его к PKLDR_DATA_TABLE_ENTRY и получить, соответственно, указатель на блок загрузчика. Ну а как получить следующий элемент списка, думаю, объяснять не нужно - это базовая техника и, если вы читаете это сообщение, то она должна быть вам уже знакома (как и многое другое). Да, здесь ещё обратите внимание, что поскольку перечисление блоков мы начинаем не с головы списка (PsLoadedModuleList), а с одного из последних элементов, то один из элементов, следующих за блоком нашего драйвера, может оказаться пустышкой, т.е. по факту головой списка (такой элемент всегда один). Обнаружить такой элемент на практике можно по значениям в BaseDllName, FullDllName и других полях. Например, если значения BaseDllName.Length или FullDllName.Length определённого элемента равны 0, то этот элемент и есть голова списка - его можно просто исключить из перечисления и перейти к следующему.
Упоминание этого блока загрузчика для режима пользователя можно найти здесь, однако процесс загрузки и структура этого блока в режиме ядра незначительно отличаются. В DllBase и SizeOfImage лежит то же, что и в DriverStart и DriverSize в объекте драйвера. В BaseDllName всегда находится только имя файла без какой-либо информации о пути, а вот то, что нас интересует, лежит аккурат в FullDllName и при чём в Unicode. Проблема в том, что не всегда тут лежит полный путь, - бывает что там тоже самое, что и в BaseDllName (в основном, для бутовых драйверов) или относительный путь. Кстати, если вдруг кому важно, - регистр символов может быть любой. Всего возможно 5 вариантов формата пути, рассмотрим их все.
Имя файла (без информации о пути)
Да, вот так вот просто, одно-одинёшенько имя файла, например, "acpi.sys" или "mountmgr.sys". Распознать этот случай можно, прочесав строку на наличие символа бэкслеша ( \ ), если не нашли - оно. Делать так:
Резолвим ссылку "\SystemRoot", при этом помним, что эта символьная ссылка может содержать вложенные ссылки (ссылки на ссылки).
Приклеиваем справа строку "\System32\Drivers\".
Приклеиваем справа имя.
Полный путь готов, ничего хитрого.
Путь относительно папки Windows
Распознаётся банально по отсутствию обратного слеша в начале строки. Для большей точности можно сравнивать начало пути со строкой "System32\", но по факту других путей, в которых отсутствовал бы обратный слеш в начале строки, я не встречал, потому, думаю, эта проверка будет излишней. Всё просто:
Резолвим ссылку "\SystemRoot".
Приклеиваем справа бэкслеш и путь модуля.
Кстати, именно этот формат не встречается в списке загруженных модулей (по крайне мере я не видел), зато частенько попадается в реестре в значении ImagePath для бутовых драйверов.
Путь относительно системного диска
Это, пожалуй, самый сложный случай, потому что нет никакой возможности определить, что стоит в начале пути - ссылка, устройство или относительный путь (опять же, относительно чего? - тоже вопрос). Короче говоря, перед нами просто какая-то строка, начинающаяся с обратного слеша и всё, и чего делать - непонятно. К счастью, на практике встречается только одна разновидность относительных путей, начинающихся с обратного слеша - путь относительно системного диска. В любом случае существует две стратегии для преобразования таких путей. Первая и наиболее надёжная такая:
Найти имя тома вида "\Device\HarddiskVolume1", его можно взять из "\SystemRoot".
Если существует, значит всё правильно, преобразование завершено.
Если не существует, можно попробовать стратегию номер 2.
Вторая стратегия менее надёжна, т.к. не учитывает всех гипотетический ситуаций (которых лично я на практике не встречал):
Проверяем начало строки на наличие "\SystemRoot" и "\??".
Если есть такое, то это вообще другой случай - используем, соответственно, другую стратегию преобразования.
Если нет такого, тогда считаем имя относительным тома и тупо склеиваем имя тома, например, "\Device\HarddiskVolume1", и относительный путь модуля.
Полный ненормализованный путь
Как следует из названия, путь у нас уже полный, но ещё не нормализованный. Распознаётся такая ситуация путём сравнения начала строки с "\SystemRoot\" и "\??\X:" (при чём X это буква от A до Z). Если совпало значит наш случай, иначе возможно, что это предыдущий случай или что-то неожидаемое.
Что делать в случае "\SystemRoot":
Резолвим "\SystemRoot".
Приклеиваем справа остаток пути модуля.
Что делать в случае "\??\X:":
Резолвим "\??\X:".
Приклеиваем справа остаток пути модуля.
Имена, начинающиеся с префикса "dump_"
Распознать такой случай просто, у имени файла должен быть префикс "dump_" и файл не должен существовать физически на диске. Примеры:
Как не сложно догадаться, эти образы являются копиями соответствующих модулей, в данном случае это atapi.sys и wmilib.sys. Более интересный вопрос - зачем система создаёт эти копии? Один из сотрудников Microsoft сообщает нам, что эти копии создаются на случай падения системы и записи дампа, при чём создаются копии только тех драйверов, которые непосредственно принимают участие в записи дампов (т.е. являются частью так называемого dump stack). Цель такого подхода снизить вероятность сбоя при использовании повреждённых в памяти и/или на диске драйверов, необходимых для записи дампа и гибернации. Более того, дампы пишутся полностью в обход файлового стека (file system stack) и частично минуя верхние этапы стека устройств хранения (storage stack). Всё это делает запись дампа довольно надёжной операцией, но накладывает некоторые ограничения. Например, дамп пишется в pagefile, при чём только в тот, который расположен на системном томе. Если его там не окажется, то дамп просто не будет записан.
Замечания к преобразованию
Как вы, наверно, уже заметили, все способы преобразования неполного или ненормализованного имени сводятся к тому, чтобы понять что стоит слева (или что надо поставить слева), т.е. как правило либо ссылка заменяется на её целевое имя, либо просто добавляется то, что подразумевается по умолчанию.
Для более точного результата после статического анализа и преобразования следует проверить, действительно ли получившаяся строка содержит путь к реально существующему файлу. Если нет, то либо вы что-то сделали не так, либо возможно имеет смысл попробовать другую стратегию для преобразования. Это не всегда нужно (например, в последнем пункте это точно не нужно).
Получение указателя PsLoadedModuleList
Если по каким-то причинам требуется перечислить все драйвера не с середины, а с начала списка (т.е. начиная с головного элемента), то необходимо каким-то образом достать указатель PsLoadedModuleList (кстати, первый элемент списка это всегда модуль самого ядра). Есть несколько способов, но imho самый грамотный из них заключается в том, чтобы взять указатель на следующий за нашим элемент списка, - это и будет его голова. Вот такой код будет работать на всех системах от Windows XP до Windows 7:
У этого кода два небольших ограничения. Связано это с тем, что между непосредственно загрузкой и вызовом точки входа драйвера могут быть загружены один или более других драйверов. Другими словами, при выполнении этого кода наш драйвер должен быть последним загруженным. Итого, ограничения следующие:
Не работает, если режим запуска драйвера = boot.
Работает корректно только при вызове из DriverEntry.
Официальная информация о жёстких ссылках находится здесь, а также в описании функции CreateHardLink.
Если говорить о жёстких ссылках применительно к именам файлов, то возникает вполне определённая проблема - никакими средствами (кроме прямых запросов к файловой системе) невозможно определить является ли данное имя жёсткой ссылкой или нет, потому как с точки зрения файловой системы жёсткая ссылка - это всего лишь ещё одно имя файла, такое же как и его первоначальное имя, данное ему при создании. Чем эта ситуация плоха для нас? А тем же, чем и ситуация с точками повторной обработки - возможностью обмана. Что делать? Прежде всего, грамотно поставить вопрос, потому что правильно сформулированный вопрос, как мы знаем, есть половина ответа. Итак вопрос: как определить, является ли данное имя файла его первоначальным именем? Вот в таком виде задача становится уже более-менее решаемой.
Прежде чем пытаться разрезолвить жёсткую ссылку, для начала следует проверить, а есть ли вообще у файла ссылки? Потому что если их нет, то перед нами явно не жёсткая ссылка. Узнать сие можно вызвав ZwQueryInformationFile c классом FileStandardInformation и проверив поле NumberOfLinks структуры FILE_STANDARD_INFORMATION. Если оно больше 1, значит ссылки таки есть, иначе их нет.
Допустим, мы знаем, что у файла есть ссылки. Теперь самое интересное, жёсткая ссылка - это не ещё одна запись в родительской папке, это не файл. Физически, это ещё один атрибут файла типа FileName (т.е. ещё одно имя). Но, если копнуть глубже, то выяснится, что это всё же не совсем обычное имя. Атрибуты типа FileName в NTFS представлены вот такой вот примерно структурой. Поле NameFlags определяет тип имени, которое содержится в этом атрибуте. Если оно равно 1, то имя длинное (оригинальное то бишь), если 2, то имя короткое (формата 8.3). При этом если это поле равно 1 или 2, то таких атрибутов у файла, как правило, два - одно для короткого, другое для длинного имени. Кроме того, это же поле может быть комбинацией 1 и 2 и иметь значение 3. Это обычно означает, что в этом же атрибуте содержится и короткое и длинное имя (например, если оригинальное имя, указанное при создании файла, уже укладывается в формат 8.3).
А теперь внимание! Если поле NameFlags равно 0, это означает не что иное, как жёсткую ссылку. А теперь внимание ещё раз! Это значение не пригодится нам ни разу, нам ведь нужно только длинное имя - вот его и ищем. Короче говоря, имея на руках полное имя файла, узнать соответствующее ему первичное длинное имя можно следующим образом:
Убедится, что полученная запись представляет собой обычный файл. Для этого следует проверить поле Type заголовка и поле Flags в теле записи.
Перечислить атрибуты типа FileName в полученной файловой записи.
Проверить поле NameFlags в структуре атрибута. Если бит 0 не установлен в 1, то просто переходим к следующему атрибуту.
Мы нашли длинное имя (бит 0 установлен в 1 в поле NameFlags). Теперь мы должны взять File ID родительской папки из поля DirectoryId и получить по нему полное имя файла. Для этого используем стандартные средства: ZwOpenFile для получения хендла, ObQueryNameString для извлечения имени устройства тома, ZwQueryInformationFile с классом FileNameInformation для запроса имени файла относительно тома и другие.
Остаётся склеить полученное имя папки с именем самого файла. Последнее находится в поле FileName структуры атрибута, а его длина задаётся полем FileNameLength. Здесь обратите внимание, что длина имени файла в атрибуте указывается в символах, а не в байтах как мы привыкли.
Имеем нормализованное первичное имя файла.
Если после перечисления имён мы так и не нашли атрибута с флагом LongName, то делаем вывод, что первичное имя было удалено и тут уже поступайте как хотите - либо берите первое попавшееся имя из доступных атрибутов типа FileName, либо возвращайте ошибку вроде STATUS_OBJECT_NAME_NOT_FOUND. И то и другое по сути будет верно.
Описанный алгоритм проверен и работает на всех системах начиная от Windows XP и заканчивая Windows 7. В принципе, есть и ещё один способ. Он также заключается в перечислении атрибутов, но здесь предлагается сравнивать не поле NameFlags, а поле LastAccessTime (CreateTime не подходит, т.к. оно одинаковое для всех атрибутов типа FileName, почему так - мне не известно, вероятно, при создании жёсткой ссылки это поле просто копируется из заголовка файла). Обычно для первичного (оригинального) имени файла это поле имеет наименьшее значение по сравнению с остальными атрибутами этого же типа. Этот способ проверялся на тех же системах и он действительно работает.
Из двух способов наиболее надёжным мне представляется первый (основанный на поле NameFlags), т.к. я не проверял влияние различных вызовов ZwSetInformationFile на временные поля атрибутов - думаю, они будут менятся. Однако, у первого способа есть неприятный нюанс - если оригинальное имя файла было удалено вызовом ZwSetInformationFile, то первичное имя файла банально теряется, оставшиеся ссылочные имена так и остаются ссылочными и не помечаются никакими флагами (NameFlags у них по-прежнему будет равен 0). Что делать в этой ситуации я не знаю, да и нужно ли, ведь если оригинального файла нет, тогда спрашивается за что боремся? В качестве одного из решений могу предложить написать файловый фильтр для мониторинга IRP_MJ_SET_INFORMATION с классом FileLinkInformation и, в зависимости от результата, перестраивать свой execution environment в соответствии с новыми данными. Другие способы получения подобной информации мне, к сожалению, неизвестны.
Кстати, попробуйте в Windows Vista натравить данный алгоритм, например, на файл \??\C:\Windows\notepad.exe или \??\C:\Windows\explorer.exe. Будете удивлены, узнав, что это тоже жёсткие ссылки, а настоящие файлы находятся где-то в WinSxS-папках.
Ну и на закуску - вот код, который частично демонстрирует описанный алгоритм. В отладочную консоль выводится примерно следующее. Полный код приводить не буду - оставим это вам на домашнее задание, по описанию алгоритма воспроизвести его достаточно просто. Кстати, совсем забыл ещё такой момент - порядок следования атрибутов типа FileName не фиксирован, они не сортируются по имени файла и атрибут первичного имени совершенно необязательно предшествует другим атрибутам этого же типа.
Напоследок отметим несколько свойств жёстких ссылок:
Максимальное количество ссылок для одного файла - 1023.
Ссылка и её цель всегда находятся на одном томе.
При удалении ссылки через ZwSetInformationFile удаляется только сама ссылка, но не целевой файл.
NTFS, вообще, очень весёлая файловая система. С одной стороны она имеет мощные механизмы для реализации тех или иных прикладных задач, с другой стороны она... мелко пакостит. Очередная пакость заключается в том, что имеющееся на руках имя файла может указывать не на обычный файл, а на точку повторной обработки (reparse point) - далее RP. Чем это плохо для нас с точки зрения имён? Тем, что любое приложение, обладающее достаточными правами, может обмануть нас, создав RP в том или ином виде и обращаясь к нужному файлу через неё; таким образом даже после всех вышеупомянутых обработок имени мы не сможем быть уверены, что это настоящее имя файла, а не очередной псевдоним.
В ядре и в поставляемых с ним драйверах RP используются, в частности, для организации символьных ссылок NTFS (не путайте с символьными ссылками ядра, это не одно и тоже), а также для создания точек монтирования томов (volume mount points). Вообще, важно отметить, что RP это более общий, скажем так, механизм. Т.е. он предназначен не только для создания псевдонимов файлов (хотя это именно то, что интересует нас в первую очередь). В общем случае RP - это всего лишь свойство файла, некая ячейка произвольной длины формата REPARSE_GUID_DATA_BUFFER. Поле ReparseTag этой структуры означает тип и назначение хранящихся в ней данных, при чём несколько тегов зарезервированы за Microsoft; проверяется это макросом IsReparseTagMicrosoft. Если тег принадлежит Microsoft, то формат его данных соответствует структуре REPARSE_DATA_BUFFER. Её поля SubstituteNameOffset, SubstituteNameLength и PathBuffer позволяют получить целевое имя ссылки или точки монтирования тома.
Итак, прежде всего нам следует проверить, указывает ли наше имя на RP или на обычный файл. В данном случае всё достаточно просто, потому что разработчики Windows предусмотрели все необходимые API. Нам достаточно вызвать функцию ZwQueryFullAttributesFile и посмотреть на поле FileAttributes структуры FILE_NETWORK_OPEN_INFORMATION. Если оно содержит флаг FILE_ATTRIBUTE_REPARSE_POINT, значит наше имя представляет собой RP. Как мы уже знаем, RP это абстрактная сущность и сама по себе не несёт никакой смысловой нагрузки, поэтому этой проверки недостаточно, см. далее.
Получить имя целевого объекта RP возможно следующим способом. Прежде всего, мы должны открыть файл посредством ZwOpenFile. Затем на полученный хендл следует вызвать ZwFsControlFile с кодом FSCTL_GET_REPARSE_POINT, при этом выходной буфер должен быть размером MAXIMUM_REPARSE_DATA_BUFFER_SIZE. В случае успешного завершения запроса, мы должны привести буфер к типу REPARSE_DATA_BUFFER и убедиться, что поле ReparseTag равно одному из следующих значений:
IO_REPARSE_TAG_MOUNT_POINT
Этот тег указывает на то, что файл является точкой монтирования тома. Как правило, это папка.
IO_REPARSE_TAG_SYMLINK
Этот тег сообщает нам, что файл является символьной ссылкой. Это может быть как файл так и папка.
Если это так, тогда просто берём целевое имя из поля PathBuffer, учитывая смещение имени в этом буфере SubstituteNameOffset и его длину SubstituteNameLength. В противном случае ничего не делаем, т.к. в общем случае мы не знаем формат тега RP. Ну и как всегда, не забываем закрыть хендл и освободить память под буфер.
Что есть путь к файлу? С точки зрения файловой системы это адрес объекта. При чём с точки зрения драйвера файловой системы (FSD) файл может представлять собой не только файл в привычном понимании (как некая сущность, содержащая данные и имеющая имя). Это может быть и папка (directory), и файловый поток (alternate data stream, ADS), и даже логический диск aka том (volume). Далее в тексте под файлом будет подразумеваться любой из перечисленных объектов, если не оговорено иное.
В данном сообщении идентификаторы (object IDs) в качестве имён файлов не рассматриваются. Кроме того, сетевые имена также обсуждать не будем, т.к. это вообще сама по себе большая тема и об этом следует говорить отдельно.
Базовые понятия
Прежде чем развивать тему, необходимо дать некие базовые определения, - примитивы, скажем так. Поехали:
Папка в пространстве имён менеджера объектов
Такие папки для упрощения будем называть виртуальными. Это логично, т.к. они не существуют физически на диске, а какая-либо информация о них находится исключительно в памяти в специальных структурах ядра, точнее в структурах его компонента - менеджера объектов (Object Manager). Более того, эти папки не имеют прямого отношения к файловой системе и нам они интересны только с той точки зрения, что в них содержатся имена устройств хранения (например, имена томов), - к этим именам мы ещё вернёмся. Для управления такими папками существуют специальные функции ядра - ZwOpenDirectoryObject, ZwCreateDirectoryObject и другие. Кстати, виртуальные папки могут быть вложенными по аналогии с деревом папок в файловой системе. Для просмотра информации о виртуальных папках и их содержимого я рекомендую утилиту WinObjEx, скачать которую можно здесь. Далее в тексте, где не указано явно, что папка виртуальная, будет подразумеваться её физическая природа и принадлежность файловой системе.
Объект в пространстве имён менеджера объектов
Такие объекты располагаются исключительно в виртуальных папках. Объект может быть практически любого типа из всех существующих в NT, но нас интересуют объекты только тех типов, которые имеют непосредственное отношение к файловым системам. К таким объектам относятся виртуальные или физические устройства (device), символьные ссылки (symbolic link), драйвера (driver) и файлы (file). Как и в файловой системе такие объекты имеют имя, при чём объект всегда идентифицируется по полному имени, состоящему из пути (path) и собственно имени объекта (name).
Объект-устройство
Применительно к файловым системам объект-устройство - это объект ядра, представляющий либо управляющее устройство драйвера файловой системы aka CDO (Control Device Object), либо объект тома aka VDO (Volume Device Object), либо какое-то другое служебное устройство. Интерес для нас сейчас представляют только VDO, потому что имя VDO является частью полного канонического нормализованного имени файла (об этом чуть позже). Как правило, имя устройства начинается с "\Device", но это совершенно не обязательно, поскольку драйвер может создать устройство с практически с любым именем в любой виртуальной папке. В официальной документации про объекты этого типа можно почитать здесь.
Символьная ссылка
Символьная ссылка - это объект, указывающий на другой объект, при чём целевой объект должен иметь имя (в NT некоторые объекты могут быть безымянными в случае, когда доступ по имени к ним не требуется). Ссылки удобны тем, что позволяют создать для одного и того же объекта дополнительные имена (псевдонимы).
В случае файловых систем для файла также можно создать ссылку, единственный момент, что ссылки в любом случае будут обрабатываться не FSD, а менеджером ввода/вывода (I/O manager), а к драйверу файловой системы имя придёт уже в нормализованном виде. Известный пример такой ссылки - это "\SystemRoot", которая указывает на системную папку Windows, при чём эта ссылка содержит вложенные ссылки, которые разбираются ядром рекурсивно. Другой пример - ссылка "\DosDevices", указывающая на виртуальную папку "\??" для обеспечения обратной совместимости со старыми драйверами.
Объект-драйвер
Драйвер с точки зрения менеджера объектов - это всего лишь структура DRIVER_OBJECT. Частично её поля документированы и их описание можно посмотреть в официальной документации здесь. Хотя на самом деле самое интересное находится как раз в недокументированных полях этой структуры, а именно - DriverStart, DriverSize и DriverSection. Я расскажу об этих полях в следующих сообщениях. Объекты этого типа, как правило, имеют имя, которое присваивается им при создании во время загрузки соответствующего файла-образа (image) драйвера. Имя объекта-драйвера начинается обычно с префикса "\Driver" либо "\FileSystem", в зависимости от родительской виртуальной папки.
Типы имён файловс точки зрения подсистем
С точки зрения подсистем можно выделить два типа имён файлов (по количеству подсистем). Речь идёт, конечно же, о полных именах, включающих в себя и информацию об устройстве и путь до файла.
Имена подсистемы Win32
К этим именам относятся привычные нам DOS-имена, начинающиеся с буквы логического диска и двоеточия (например, "C:"), а также переходный формат, представляющий собой DOS-имя, предваряемое символами "\\?\" или "\\.\", при чём вариант с точкой не везде приемлем, в частности, его нельзя указывать при загрузке библиотек.
Вариант с двумя слешами может иметь место, когда Win32-приложение хочет использовать именно Native-имена (и все их преимущества), но при этом по каким-либо причинам не может или не хочет использовать функции Native-слоя (которые, к слову, вообще-то недокументированы и не рекомендованы к использованию в Win32-приложениях). Символы "\\?" и "\\." в конечном итоге будут преобразованы в "\??" и в таком виде путь отправится в ядро, а точнее в один из его сервисов (system service), потому что перед тем как выполнить операцию сервисы, как правило, преобразовывают имена в нормализованный вид.
Эти же имена используются и в ядре. Прежде всего, скажем, что такие имена всегда начинаются с обратной косой черты ( \ ). Как и в Win32-именах, имя начинается с информации о логическом диске (томе), но при этом она указывается либо в формате имени устройства, например, "\Device\HarddiskVolume1", либо в виде символьной ссылки, например, "\??\C:". К слову сказать, индексация имён томов на жёстких дисках вида "\Device\HarddiskVolume1" всегда начинается с 1, а не с 0, в отличие от прочих устройств хранения, таких как CD/DVD и Floppy.
С точки зрения составляющих компонентов пути и их комбинаций могут быть следующие варианты:
Базовое (base) имя
Также известно как заголовочное (title) имя. Представляет собой имя файла без расширения. Не содержит информации ни о пути, ни об устройстве хранения, ни даже о расширении. В нашем примере базовое имя будет выглядеть так:
ntkrnlpa
Имя
Тоже самое, что и базовое имя, но содержит информацию о расширении. При этом расширением считается точка и следующие за ней символы. Точек в имени файла может быть несколько, в этом случае берётся последняя (самая правая) из них. Строго говоря, расширение не является обязательным компонентом имени файла и сами FSD никогда их не различают, тем не менее бывают редкие случаи, когда расширение имеет значение в самом ядре. Сравниваем с эталоном:
ntkrnlpa.exe
Имя относительно папки
Один из вариантов относительного имени. Содержит частичную информацию о родительских папках, а также базовое имя и расширение. Не содержит информацию об устройстве хранения. Кстати, учитывайте вообще, что, строго говоря, для неполных путей косая черта в начале не является обязательной, т.е. если вы склеиваете куски имени в одно полное имя, вы должны самостоятельно расставить слеши там, где это требуется, но не более одного экземпляра. Опять сравниваем с эталоном:
\System32\ntkrnlpa.exe
Имя относительно тома
Второй вариант относительного имени. Содержит всю информацию о всех родительских папках, базовое имя файла и расширение. Не содержит только информацию об устройстве хранения. Сравниваем с эталоном:
\Windows\System32\ntkrnlpa.exe
Полное имя
Полное имя файла, содержащее информацию обо всех компонентах пути. Здесь я имею в виду ненормализованное имя. Ненормализованное имя отличается от нормализованного тем, что может содержать символьные ссылки. В следующем примере имя содержит ссылку "\Device\Harddisk0\Partition1", которая указывает на устройство тома "\Device\HarddiskVolume1":
А вот другой пример полного ненормализованного имени. Здесь "\SystemRoot" - это ссылка на "\Device\Harddisk0\Partition1\Windows":
\SystemRoot\System32\ntkrnlpa.exe
Нормализованное имя
Нормализованное имя содержит все компоненты пути и не содержит ссылок. Если мы сравним с эталоном, то окажется, что эталон представляет собой уже нормализованное имя, т.к. "\Device\HarddiskVolume1" - это не ссылка, а имя устройства тома:
Здесь различают длинные имена файлов и короткие. Не путайте эти имена с полными или относительными, другими словами, например, полное имя запросто может быть коротким. Официальная информация о коротких и длинных именах находится здесь. За основу в качестве эталона возьмём вот такой пример:
Это, как мы уже знаем, полное нормализованное имя главного исполняемого файла приложения Internet Explorer. С точки зрения длины это имя можно записать в следующих вариантах:
Короткое имя
Короткое имя можно представить как ещё один атрибут объекта файловой системы, такой же, как, например, жёсткие ссылки - по сути это одно и то же. Кстати говоря, имя файла это тоже атрибут, с той только разницей что имя файла - это обязательный атрибут, а короткое имя - нет, т.е. короткого имени у файла может и не быть. Теперь перейдём к нашему примеру. Если, предположим, папка "Program Files" имеет короткое имя, то эталон можно переписать вот так:
В таком виде имя можно передать драйверу файловой системы посредством ZwCreateFile и ошибок не будет, это имя указывает на тот же файл, что и эталонное. Замечу, что символа тильды ( ~ ) может и не быть в коротком имени - каждая файловая система сама решает по какому принципу короткое имя будет сгенерировано.
Физически короткие имена файлов хранятся в виде дополнительных записей в родительских папках. Получить короткое имя файла для длинного можно с помощью функции ZwQueryDirectoryFile с классом FileBothDirectoryInformation, при этом в качестве хендла, конечно же, следует передать хендл не самого файла, а родительской папки. Запретить файловым системам автоматическое создание коротких имён можно посредством системного реестра, процедура описана здесь, однако вручную присвоить файлу короткое имя можно, вызвав ZwSetInformationFile с классом FileShortNameInformation (в этом случае следует передать хендл самого файла).
Длинное имя
Пожалуй, добавить здесь больше нечего. Длинное имя это просто оригинальное имя файла.
Вспомогательные функции
Перед тем, как приступать к преобразованию, я бы посоветовал написать несколько функций-примитивов для выполнения частых рутинных задач (речь, разумеется, о режиме ядра). Ниже я расскажу о некоторых нюансах, с которыми вы столкнётесь при их реализации. Вот краткие описания и прототипы этих функций, думаю, всё понятно (Xxx - это префикс вашего драйвера, я рекомендую использовать префиксы для выделения групп функций):
GetFileName
Возвращает полное нормализованное имя файла, заданного первым аргументом. Эту функцию лучше написать самому, особенно если её использование предполагается в файловом фильтре. ObQueryNameString для этого лучше не использовать, она хороша для объектов другого типа. Имя диска для этой функции следует получать через GetVolumeName. Параметр bInPreCreate должен быть установлен в TRUE, если мы вызываем функцию в файловом фильтре при обработке IRP_MJ_CREATE, при чём до того, как мы вызвали IoCompleteRequest или IoCallDriver, в противном случае сюда следует передать FALSE.
NTSTATUS XxxGetFileName ( IN PFILE_OBJECT pFileObject, IN BOOLEAN bInPreCreate, OUT PUNICODE_STRING pusFileName);
GetFileNameWin32
Возвращает полное имя файла, заданного первым аргументом. В отличие от предыдущей функции здесь имя возвращается в формате Win32 и начинается с буквы диска и двоеточия. Имя диска для этой функции следует получать через GetVolumeNameWin32. Параметр bInPreCreate здесь имеет то же самое значение, что и в предыдущей функции.
NTSTATUS XxxGetFileNameWin32 ( IN PFILE_OBJECT pFileObject, IN BOOLEAN bInPreCreate, OUT PUNICODE_STRING pusFileName);
NTSTATUS XxxGetSymbolicLinkTargetName ( IN PUNICODE_STRING pusSymbolicLinkName, OUT PUNICODE_STRING pusSymbolicLinkTargetkName);
GetObjectName
Возвращает имя объекта ядра. Эту функцию удобно написать на основе ObQueryNameString.
NTSTATUS XxxGetObjectName ( IN PVOID pObject, OUT PUNICODE_STRING pusObjectName);
GetVolumeName
Возвращает имя объекта-устройства. Эту функцию можно написать на основе уже написанной нами GetObjectName.
NTSTATUS XxxGetVolumeName ( IN PDEVICE_OBJECT pVolumeDeviceObject, OUT PUNICODE_STRING pusVolumeDeviceName);
GetVolumeNameWin32
Возвращает Win32-имя объекта-устройства. Эту функцию следует писать на основе функции ядра IoVolumeDeviceToDosName. В итоге на выходе получим аккурат букву диска с двоеточием.
NTSTATUS XxxGetVolumeNameWin32 ( IN PDEVICE_OBJECT pVolumeDeviceObject, OUT PUNICODE_STRING pusVolumeDeviceName);
PathCombine
Соединяет две части имени в одно полное имя. Здесь нужно написать полный аналог функции пользовательского режима PathCombine. При этом функция должна корректно обрабатывать такие случаи, когда, например, первая часть имени заканчивается на слеш, а вторая начиная с него, т.е. в результате функция должна один из них убрать и при этом умудрится не добавить нового. Вообще, всегда старайтесь предусмотреть все варианты, ну или хотя бы все, имеющие смысл.
NTSTATUS XxxPathCombine ( OUT PUNICODE_STRING pusFullName, IN PUNICODE_STRING pusRelativePath, IN PUNICODE_STRING pusFileName);
IsSymbolicLink
Возвращает TRUE, если имя объекта указывает на символьную ссылку, и FALSE в противном случае. Проверяется всего лишь вызовом ZwOpenSymbolicLinkObject.
BOOLEAN XxxIsSymbolicLink ( IN PUNICODE_STRING pusSymbolicLinkName);
GetLowestDeviceObject
Возвращает самое нижнее (настоящее) устройство в стеке. Если указанное устройство является единственным в стеке, то возвращается указатель на него же.
PDEVICE_OBJECT XxxGetLowestDeviceObject ( IN PDEVICE_OBJECT pDeviceObject);
Объекты томов
Следует прояснить один момент для тех, кто ещё не в курсе. Как правило, для тома создаётся два VDO (volume device object) - реальный и ещё один, назовём его виртуальным. "Реальный" потому что указывает на физический раздел жёсткого диска; это устройство создаётся ядром. Виртуальное же создаётся драйвером файловой системы в случае успешного монтирования тома.
Для нас важно запомнить главное: все операции с файлами, папками и другими объектами файловой системы осуществляются на виртуальном VDO, но при составлении полного имени файла имя устройства мы должны брать из реального VDO, т.к. виртуальные, как правило, имён не имеют. Извлечение имён файловых объектов
Посмотрим, как можно получить полное имя файла по указателю на FILE_OBJECT. Эта информация пригодится вам при реализации функций GetFileName и GetFileNameWin32.
Бывает, что на руках есть указатель на FILE_OBJECT какого-либо объекта файловой системы, будь то файл, папка или том. И это очень хорошо, доложу я вам! Потому что иногда требуется получить имя файла, соответствующего какому-либо не-файловому объекту, и тогда вся проблема заключается не в получении имени, а в получении указателя на FILE_OBJECT, а это иногда бывает задачка потруднее раз этак в несколько. Классический пример такой проблемы - получение нормализованного пути к исполняемому файлу процесса, при том что о процессе нам известен разве что его ID. Проблема эта решаемая, но получить указатель на FILE_OBJECT по ID процесса невозможно без знания некоторых не совсем, скажем так, документированных особенностей ядра. Мы рассмотрим эту задачу в одном из следующих сообщений, когда будем обсуждать объекты процессов.
Итак, если у нас есть указатель на FILE_OBJECT, указывающий на объект файловой системы, то автоматически мы имеем следующие элементы:
Указатель на объект-устройство, представляющее том, на котором расположен файл. Этот указатель просто получить, всего лишь взяв его из поля DeviceObject структуры FILE_OBJECT. Это то самое реальное VDO, о которым было сказано в предыдущем разделе.
Имя файла относительно тома или имя файла относительно другого файла или папки. Это то имя, которое мы можем найти в поле FileName структуры FILE_OBJECT. Имя здесь находится в виде либо относительно тома (т.е. полный путь) либо относительно родительской папки или файла (т.е. частичный путь). Кстати, по-хорошему если это поле имеет смысл только для объектов-файлов, относящихся к файловым системам и полученных в результате какой-либо операции файлового ввода/вывода (file I/O).
В случае, если поле RelatedFileObject в структуре FILE_OBJECT не равно NULL и наш код выполняется в обработчике IRP_MJ_CREATE до того, как мы потеряли контроль над соответствующим IRP, то начальная часть пути должна быть извлечена из поля FileName по указателю RelatedFileObject. Это имя указывается всегда относительно тома.
Что нам нужно для реконструкции полного имени - мы уже знаем. Попробуем теперь расписать алгоритм:
Прежде всего, получим имя устройства тома. В зависимости от того, какое имя нам нужно - Native или Win32, вызовем соответственно либо GetVolumeName либо GetVolumeNameWin32. Тут стоит заметить, что буква диска может быть просто не назначена данному тому и получить её в принципе невозможно, т.о. будет доступно только Native-имя.
Затем посчитаем сколько нам нужно байт для хранения полного имени. Это будет сумма следующих значений:
Длина строки с именем устройства тома, полученной на шаге 1.
Длина строки в поле FileName структуры FILE_OBJECT.
Длина имени объекта в поле RelatedFileObject структуры FILE_OBJECT.
10 байт на недостающие слеши и прочие служебные символы. На самом деле, этот пункт в общем-то и не нужен даже, но не помешает, это уж точно. Я бы посоветовал убрать его только если ваш драйвер вынужден экономить память, хотя с другой стороны экономия памяти и жонглирование строками - как-то мало сочетаются.
Выделить область памяти необходимого размера. Я выделяю её обычно из невыгружаемого пула (non-paged pool), хотя это не совсем правильно, вам я советую использовать только выгружаемый пул (paged pool), за исключением тех случаев, когда доступ к строкам необходим на повышенном IRQL, например, в DPC (хотя это, вообще говоря, весьма нестандартная ситуация).
Скопировать в начало буфера имя устройства тома. Это делается банально с помощью RtlCopyUnicodeString.
Добавить в конец имя, относительно которого файл создаётся или открывается (имя объекта RelatedFileObject). Если, конечно, оно есть. Это делается опять же банально с помощью функции RtlAppendUnicodeStringToString.
Добавить в конец имя файла из поля FileName по нашему указателю на оригинальный FILE_OBJECT. Опять же функция RtlAppendUnicodeStringToString.
Напоследок напоминаю, что во-первых, имени файла может не быть ни в оригинальном объекте, ни в объекте RelatedFileObject, т.е. FileName.Length == 0, при этом в поле Flags обычно устанавливается флаг FO_DIRECT_DEVICE_OPEN, что означает открытие не какого-то файла, а самого устройства тома. И во-вторых, перед какими-либо манипуляциями с объектом желательно навесить на него ссылку посредством ObReferenceObject, а также проверить его тип. Тип проверяется опять же флагами, среди которых не должно быть таких как, например, FO_NAMED_PIPE или FO_MAILSLOT.
Ну вот и всё. Теперь вы умеете извлекать полные имена из файловых объектов. Приведённый здесь способ хорош тем, что позволяет полностью избежать проблем с рекурсией в файловом фильтре. Достигается это за счёт почти полного отсутствия подзапросов на открытие чего-либо. Данный алгоритм можно применять и к хендлам. Для этого хендл сначала нужно привести к указателю на сам объект с помощью функции ObReferenceObjectByHandle, и, конечно же, не забыть сделать ObDereferenceObject по завершении работы с ним.
Преобразование коротких имён
В предыдущем разделе мы научились получать нормализованное имя для файловых объектов, но кроме этого есть и другая проблема. Имя в поле FileName структуры FILE_OBJECT находится в том формате, в котором оно было передано в ядро и, если оно было передано в коротком виде, то в этом виде мы его и получим, никто кроме нас не будет преобразовывать его в длинный вид (если только мы не пишем минифильтр - там эту работу уже проделали за нас).
В этом разделе попробуем добавить функциональности в код GetFileName, а именно - каждый компонент имени будем преобразовывать в длинный формат вместо короткого. Напомню: короткое имя может быть как у папок так и у файлов, но его также может и не быть вовсе, всё это нужно учесть.
Есть ещё один момент. Запросы на открытие родительских папок мы будем отправлять виртуальному VDO файловой системы. Указатель на это устройство мы можем получить, вызвав функцию IoGetRelatedDeviceObject, но возвращённое устройство является верхним на стеке. Возникает проблема - если мы пишем файловый фильтр, то это устройство может быть нашим же FiDO (filter device object), таким образом получим самую натуральную рекурсию, т.к. запрос на открытие папки придёт опять же в наш фильтр. Чтобы избежать этого, полученный указатель на VDO следует передать нашей функции GetLowestDeviceObject с тем, чтобы получить указатель на самое нижнее устройство в стеке, коим и будет искомое VDO. В принципе, если у вас не файловый фильтр и если скорость для вас критична, то этого можно не делать.
Сейчас давайте посмотрим, как извлечь короткое имя для одного конкретного файла:
Разделим этот путь на две составляющие. При чём путь к папке обязательно должен заканчиваться обратной косой чертой ( \ ), чтобы указать FSD, что папка открывается как папка, а не как файл.
Путь к папке: \Device\HarddiskVolume1\Progra~1\Intern~1\
Имя файла: iexplore.exe
Далее делаем так:
Вызываем IoCreateFileSpecifyDeviceObjectHint, передав в качестве имени путь к папке, а в качестве DeviceObject виртуальный VDO, полученный от IoGetRelatedDeviceObject и обработанный GetLowestDeviceObject. В параметре CreateOptions также указываем FILE_DIRECTORY_FILE.
Вызываем ZwQueryDirectoryFile. В качестве FileHandle передаём полученный на предыдущем шаге хендл, остальные параметры такие: FileInformation = наш буфер, выделенный заранее. Length = длина этого буфера. FileInformationClass = FileBothDirectoryInformation. ReturnSingleEntry = TRUE. FileName = имя файла. RestartScan = TRUE.
Если ошибка, ничего не делаем. Возможно, что мы ошиблись и путь к файлу таковым не является. Это важный момент при преобразовании всех компонентов пути, а не только имени файла. Об этом чуть позже.
Приводим буфер к типу FILE_BOTH_DIR_INFORMATION и проверяем поле ShortNameLength. Если оно равно 0, значит короткого имени для файла нет и текущее имя является длинным. В нашем случае это вполне возможный вариант, ничего делать не нужно.
Если поле ShortNameLength > 0, то мы без дополнительных проверок и сравнений берём и заменяем наше имя на то, которое мы получили в буфере в поле FileName.
Не забываем освободить буфер и закрыть хендл по ZwClose.
Ну вроде бы всё понятно, да? Теперь пример преобразования всех компонентов:
\Device\ : HarddiskVolume1 = ???
\Device\HarddiskVolume1\ : Progra~1 = Program Files
\Device\HarddiskVolume1\Program Files\ : Intern~1 = Internet Explorer
Первый компонент пути мы пропускаем, т.к. он в принципе не может быть именем файла. Следующий компонент "HarddiskVolume1" теоретически может быть именем файла, но вызов IoCreateFileSpecifyDeviceObjectHint завершается с ошибкой, поэтому здесь оставляем всё как есть.
Открываем корневую папку диска и в ней находим элемент для "Program Files" с коротким именем "Progra~1". Заменяем короткое имя на только что полученное длинное.
Открываем папку "Program Files" и в ней находим элемент для "Internet Explorer" с коротким именем "Intern~1". Заменяем короткое имя на только что полученное длинное.
Открываем папку "Internet Explorer" и в ней находим элемент для "iexplore.exe", при чём короткое имя отсутствует. Ничего дополнительно делать не требуется.
Преобразование завершено, мы получили длинные имена вместо коротких.
Нормализация имён
Думаю, вы уже поняли, что нормализация - это процесс преобразования полного имени таким образом, чтобы оно не содержало символьных ссылок. Именно полного, потому что относительное имя без информации о контексте преобразовать в нормализованный вид невозможно. Если в имени ссылок нет - значит, оно уже нормализованное. На всякий случай уточню - имеются в виду только ссылки, являющиеся сущностями менеджера объектов под названием символьные ссылки (symbolic link); сюда не относятся жёсткие ссылки (hard link), ссылки сторонних файловых систем и какие-либо другие ссылки.
Предположим у нас есть вот такое имя:
\SystemRoot\System32\Drivers\acpi.sys
На глазок мы сразу же определяем, что имя ненормализованное, т.к. "\SystemRoot" - это символьная ссылка. Но это мы с вами видим, а что делать функции преобразования? Каким образом она должна определить, что перед нами - ссылка? Ничего сложного тут нет, потому что во-первых, у нас есть функция IsSymbolicLink, через неё мы пропускаем все части полного имени, и во-вторых, у нас есть системная функция FsRtlDissectName, которая поможет нам перечислить все промежуточные компоненты имени. Если IsSymbolicLink возвращает TRUE, то мы знаем, что это ссылка, - тут же извлекаем целевое имя по GetSymbolicLinkTargetName и заменяем ссылку на это имя в результирующем буфере. При этом целевое имя опять может быть ссылкой, поэтому результат здесь нужно проверять рекурсивно пока не достигнем объекта, отличного от ссылки.
Отделили первый компонент имени и выяснили, что это символьная ссылка.
Извлекли целевое имя ссылки и положили в буфер, разбор имени начинаем сначала.
\Device - это не ссылка, поэтому просто оставляем как есть.
\Device\Harddisk0 - это тоже не ссылка, оставляем как есть.
\Device\Harddisk0\Partition1 - это ссылка.
Извлекли цель, заменили ею ссылку и опять разбор имени начинаем сначала.
\Device - не ссылка, оставляем как есть.
\Device\HarddiskVolume1 - не ссылка, оставляем как есть.
\Device\HarddiskVolume1\Windows - не ссылка, оставляем как есть.
\Device\HarddiskVolume1\Windows\System32 - не ссылка, оставляем как есть.
\Device\HarddiskVolume1\Windows\System32\Drivers - не ссылка, оставляем как есть.
\Device\HarddiskVolume1\Windows\System32\Drivers\acpi.sys - не ссылка, оставляем как есть.
Ну вот и всё, на шаге 12 имеем полное нормализованное имя. Довольно громоздкий алгоритм получается. А теперь вздохните с облегчением, потому что всего этого можно было и не делать. Вместо этого можно было возложить всю работу на I/O manager'а:
Открыть файл, передав исходный путь как есть в функцию ZwOpenFile.
После успешного открытия файла, получить указатель на объект-файл посредством функции ObReferenceObjectByHandle.
Получить полный путь, вызвав нашу функцию GetFileName.
Уменьшить количество ссылок обратно по ObDereferenceObject.
Закрыть хендл файла по ZwClose.
Возникает вопрос - какой способ лучше? Ну скажем так, если у вас файловый фильтр - то лучше такие вещи делать вручную, чтобы создавать как можно меньше IRP для FSD. Если другой фильтр или вообще не фильтр - тогда можно вполне безопасно использовать вариант с открытием файла. Если откровенно, второй вариант можно использовать и в файловом фильтре, но тут слишком много нюансов, связанных с реентерабельностью (рекурсией, если по-простому), - если не уверены, лучше не связываться с такими вещами; ну тут вам вашу ситуацию виднее, конечно, но в любом случае помните, что запросы на открытие файлов - это дополнительная нагрузка на файловую систему и, если эти запросы требуются слишком часто, то, несомненно, стоит подумать об оптимизации.
Заключение
Мы рассмотрели в этот раз имена объектов и научились преобразовывать их из одного формата в другой. Хотелось бы добавить, что, как показывает практика, неумелое обращение со строками в драйверах (особенно в фильтрах и особенно в файловых) зачастую приводит к снижению производительности всей системы в целом. Поэтому если вы интенсивно используете строки или часто шлёте подзапросы нижелажащим драйверам, то, несомненно, стоит подумать об оптимизации производительности вашего кода.
В следующих сообщениях мы продолжим развивать тему имён файлов, а также рассмотрим имена исполняемых модулей, откуда они беруться и что с ними делать. На данный момент у меня всё. Комменты, как всегда, открыты для наполнения их чем-нибудь полезным.
Вообще, файловые фильтры - это один из моих любимых типов драйверов, и именно поэтому сегодня хотелось бы сказать пару слов о способах фильтрации файловой системы. Речь пойдёт о Windows XP и выше, ибо Windows 2000 уже давно вне сферы моих интересов. Кроме того, всё нижесказанное будет касаться, в основном, локальных файловых систем типа FAT, NTFS или CDFS.
Признаюсь, я не большой любитель теории и обычно я стараюсь сделать на практике хоть что-то логически законченное и рабочее, и только после этого посмотреть вокруг и подумать, где может быть проблема. Примерно так же было и с файловыми фильтрами, когда я только приступал к их изучению. Не факт, что это правильный путь, но с другой стороны если у вас, простите, есть мозги, то какой бы путь вы не выбрали, - вы несомненно добьётесь результата. Кстати, возможно вот такая точка зрения вам будет больше по душе, - выбор, как всегда, за вами.
Прежде всего стоит напомнить, что драйвера как правило сами по себе не являются самодостаточными, обычно они являются частью какого-либо приложения, призванного решить ту или иную задачу (бывают и исключения - например, руткиты (rootkits), но это отдельная история). Поэтому, я думаю, было бы правильно сказать о том, для чего фильтрация тех или иных обращений к файловой системе может быть полезна. Вот неполный список:
Шифрование содержимого отдельных объектов "на лету".
Кэширование содержимого объектов.
Блокировка доступа к отдельным объектам.
Резервное копирование изменившихся объектов.
Скрытие объектов.
Подмена содержимого объектов.
Мониторинг событий файловой системы.
Под "объектом" в этом списке понимается файл, файловый поток, папка, том или их сочетания. Как видим, файловые фильтры могут решать весьма сложные задачи и могут быть весьма востребованными в определённых областях. Теперь посмотрим на каких уровнях и какими способам можно фильтровать запросы к файловой системе. Если кратко, то фильтрацию можно осуществлять на всех уровнях, доступных в Windows XP и выше, а именно:
Подсистема Win32
Native слой
Уровень ядра
На каждом уровне существует как минимум один способ перехватить запросы к файловой системе. Какой способ будете использовать именно вы - это следует решать на месте, исходя из