Дело о задержках при выводе диалоговых окон открытия файлов в ОС Windows Vista

Пару недель назад я выступал с несколькими докладами на конференции TechEd/ITForum в Барселоне (между прочим, два из этих докладов, «Подробно об избавлении от вредоносных программ» и «Изменения в ядре Windows Vista», были признаны лучшими докладами недели, а мое интервью на конференции можно посмотреть здесь). Конференция прошла с большим успехом, а ОС Windows Vista, которую я решил испробовать на публике впервые, проявила себя с лучшей стороны. Правда, просматривая демонстрационные материалы перед одним из докладов, я заметил, что стандартное для всех Windows-приложений диалоговое окно открытия файлов выводится с задержкой от 5 до 15 секунд.

В тот момент у меня не было времени разобраться в причинах такого поведения, так что подобные задержки повторились и во время моего доклада «Изменения в ядре Windows Vista», оказавшись весьма некстати. Все это удивительно напоминало ситуацию, о которой я некоторое время назад рассказывал в записи «Дело о задержках при запуске процессов». Напомню, в том случае во время запуска процессов Защитник Windows посредством удаленного вызова процедур (RPC) пытался связываться с контроллером домена, что при отсутствии подключения к домену приводило к зависанию. Я вовсю извинялся за поведение операционной системы и пытался отвлечь аудиторию объяснением очередных демонстрационных материалов.

Возможность разобраться в проблеме у меня появилась лишь по возвращении домой. Я решил действовать в целом так же, как и при исследовании задержек, вызванных Защитником Windows. Я запустил блокнот из отладчика Windbg, входящего в комплект инструментов отладки для Windows, вызвал диалоговое окно открытия файлов при помощи сочетания клавиш CTRL+O, и при возникновении задержки приступил к изучению стека основного потока блокнота.

clip_image001

Напомню, что стек – это список вложенных функций, вызванных потоком, в обратном хронологическом порядке. Соответственно, читается стек снизу вверх. Так вот, взглянув на стек, я увидел, что блокнот загрузил библиотеку Browseui.Dll и вызвал ее функцию CAddressBand::SetNavigationState. Эта функция вызвала другую функцию, CBreadcrumbBar::SetNavigationState, которая, в свою очередь, вызвала функцию CBreadcrumbBar::SetIDList, и так далее.

Взглянув на имена функций, я сразу понял, что же произошло. При первом вызове диалогового окна открытия файлов в приложении оно переходит в папку документов пользователя. В ОС Windows Vista моя папка документов находится по адресу C:\Users\Markruss\Documents. Оболочка пытается сделать текст в новой адресной строке диалогового окна понятнее, заменив путь к папке словосочетанием "Mark Russinovich\Documents", и вызывает для этого функцию GetUserNameEx, которая должна выяснить отображаемое имя моей учетной записи, сохраненное в объекте пользователя в службе Active Directory. Это мое предположение подтверждается тем, что первым параметром, который функция SHGetUserDisplayName передает функции GetUserNameEx, является элемент перечисления EXTENDED_NAME_FORMAT 3: NameDisplay.

Я установил точку останова на возврате вызова этой функции и обратился к ней, когда задержка завершилась. Функция GetUserNameEx возвратила код ошибки ERROR_NO_SUCH_DOMAIN. Изучение функции SHGetUserDisplayName показало, что в конечном итоге взамен нее вызывается GetUserName. Вместо поиска отображаемого имени пользователя эта функция получает идентификатор безопасности (SID) пользователя от маркера процесса (так называется структура данных ядра, которая определяет владельца процесса), а затем посредством вызова функции LookupAccountName преобразует идентификатор в имя учетной записи, то есть, в моем случае, “markruss“. В результате всех этих операций диалоговое окно открытия файлов приобрело такой вид:

clip_image002

Когда же я пришел на работу и подключился к корпоративной сети, то увидел следующее:

clip_image003

По сути, я раскрыл дело, но мне было интересно, где именно происходила задержка, и поэтому нужно было разобраться, что творится по ту сторону вызова функции Secure32!CallSPM, который находится на вершине стека. Мне было известно, что процесс локального администратора безопасности (Local Security Authority, LSASS) занимается проверкой подлинности, в том числе взаимодействием с контроллерами домена и преобразованием имен учетных записей. Имея это в виду, я подключил отладчик Windbg к процессу Lsass.exe (между прочим, прежде чем выходить из отладчика командой “qd”, его нужно обязательно отключить от процесса LSASS; в противном случае закроется и сама служба LSASS, а через полминуты произойдет перезагрузка системы). Мне удалось установить, что библиотека Secur32.Dll одновременно исполняет роль клиента и сервера, и убедиться в том, что она загружается в процесс LSASS. Теперь требовалось выявить серверную функцию, взаимодействующую с функцией Secur32!SecpGetUserName. Эту задачу я решил «в лоб» - последовательно выгружая функции, реализуемые библиотекой Secur32.Dll, я отбирал те из них, в именах которых содержалось слово "name".

clip_image004

После установки на некоторые из таких функций точек останова я в очередной раз инициировал задержку. Исследовав поток исполнения функции SecpGetUserName, я получил следующий стек.

clip_image005

Согласно документации, функция DsGetDcName возвращает имя контроллера домена в указанном домене. Очевидно, функции SecpTranslateName необходимо сначала найти контроллер домена, а затем уже подать ему запрос на получение отображаемого имени учетной записи. Дальнейшее исследование показало, что процесс LSASS кэширует результат поиска в течение 45 секунд. Это объясняет, почему сразу после первоначальной задержки вызов диалогового окна открытия файлов в другом приложении проходит без помех. После этого я натолкнулся на тупик, когда функция Netapi32!DsrGetDcNameEx2 выполнила RPC-запрос.

Памятуя, что интерфейс Netapi32 исполняет роли клиента и сервера одновременно, я сбросил его символы и установил точки останова на функциях, в именах которых содержится строка "dc". Продолжая отслеживать исполнение процесса LSASS, я к немалому удивлению наткнулся на ту же самую функцию, Netapi32!DsrGetDcNameEx2. Дальнейшее отслеживание ее вызова привело в конечном итоге к тому, что поток вызвал функцию ядра (Ntdll!KiFastSystemCallRet).

clip_image006

Я был близок к окончанию расследования. Нужно было лишь ответить на последний вопрос: к какому драйверу устройства обращалась служба Netlogon для отправки датаграммы обозревателя? Для этого следовало обратить внимание на первый параметр, передаваемый функции NlBrowserDeviceIoControl, который по моим прикидкам должен был оказаться дескриптором объекта-файла. Далее я открыл отладчик Windbg в режиме отладки локального ядра (кстати сказать, в ОС Windows Vista для этого нужно загрузиться в режиме отладки), который позволяет исследовать активные структуры данных ядра, и сбросил данные дескриптора. Узнав, какой объект устройства был открыт, я выяснил, что искомый драйвер - Bowser.sys (драйвер получателя датаграмм NT LAN Manager).

clip_image007

Я был уверен, что расследование успешно завершено, однако предпринятая через некоторое время попытка воспроизвести задержку закончилась неудачей. Я повторил уже знакомую процедуру и узнал, что функция LsapGetUserNameForLogonSession кэширует отображаемое имя на 30 минут. Более того, вместе с именем кэшируются учетные данные, так что в течение 30 минут после подключения или отключения от корпоративной сети задержек ожидать не стоит.  Я подтвердил свои предположения, подождав 30 минут и вновь столкнувшись с задержкой.

Итак, мое расследование подошло к концу. Как выяснилось, при переходе в папку документов диалоговое окно открытия файлов в ОС Windows Vista пытается вывести в адресной строке отображаемое имя пользователя и по ходу действия ищет контроллер домена, отправляя через драйвер устройства Bowser.sys датаграмму LAN Manager. Я понял, что избежать задержек при вызове диалогового окна открытия файлов в описанной ситуации невозможно, и что в любой системе, участвующей, но в данный момент не подключенной к домену, такие задержки обязательно будут происходить - по крайней мере, до выхода пакета обновления 1 (SP1) для ОС Windows Vista.