Дело о задержках при запуске процессов
В последнее время у меня было много дел по работе, так что времени на блог совсем не оставалось. Правда, теперь я собираюсь исправиться и возобновить публикации на регулярной основе. Прежде чем приступить к описанию очередной технической проблемы, с которой я недавно столкнулся, с удовольствием сообщаю, что интеграция Sysinternals в состав корпорации Майкрософт продвигается по плану. Когда наш веб-узел в конце октября переедет на портал Microsoft TechNet, мы с Брайсом собираемся объявить о выпуске новой замечательной программы.
Когда я не в пути, я редко работаю со своим персональным компьютером - разве что иногда читаю на нем электронную почту в гостиной. Подобно большинству пользователей ОС Windows, я искренно недоумеваю по поводу таинственных задержек при выполнении стандартных задач, будь то запуск программы или открытие веб-страницы. Так, присоединив свой портативный компьютер к внутреннему домену Майкрософт, я с завидной регулярностью стал замечать задержки при запуске процессов. Вооружившись инструментарием Sysinternals, я приступил к расследованию, подозревая, что недавнее подключение к домену Майкрософт сыграло в произошедшем какую-то роль.
Мне удалось эмпирическим путем установить, что в течение 30 секунд после задержки, связанной с запуском одного процесса, все прочие процессы запускались безотлагательно. Я открыл программу Process Explorer, подождал 30 секунд и с помощью диалогового окна Run (Запуск программы) проводника вызвал блокнот. Во время очередной задержки в дереве процессов Process Explorer блокнота не оказалось. На этом основании я сделал вывод о том, что задержка связана не с запуском блокнота как таковым, а с потоком проводника, его запускающим.
Для установления причины проблемы полезно было бы взглянуть на стек вызывающего потока. Впрочем, мое нетерпение было слишком велико, чтобы просматривать больше десятка потоков проводника, так что я подключил к программе Process Explorer отладчик Windbg из набора инструментов отладки Майкрософт для Windows, запустил блокнот из диалогового окна Run (Запуск программы) проводника и сконцентрировал внимание на отладке. Чтобы открыть в отладчике Windbg список потоков, я выбрал в меню View (Вид) пункт Processes and Threads (Процессы и потоки). Выделив первый в списке поток, я вернулся в меню View (Вид) и открыл диалоговое окно Call Stack (Стек вызовов).
На вершине стека всегда находится последняя из вызванных функций. Кадр ZwWaitForSingleObject в этой позиции свидетельствует об ожидании потоком передачи сигнала какому-то объекту. Последующие кадры стека относятся к библиотеке удаленного вызова процедур (RPCRT4). Ссылка на функцию OpenLpcPort подсказала мне, что поток пытается инициировать удаленный вызов процедур с другим процессом локальной системы. Похоже, заключил я, ожидание связано с вызовом функции GetMachineAccountSid, подсвеченным на снимке экрана. Как и для пользователей, для принадлежащих к домену компьютеров создаются учетные записи. Имени функции GetMachineAccountSid достаточно, чтобы понять, что она возвращает идентификатор безопасности (SID) учетной записи домена компьютера.
Далее я установил точку останова на возврате вызова функции GetMachineAccountSid из другой функции, OpenLpcPort. Через некоторое время, по продолжительности совпадающее с задержками при запуске, активировалась командная строка отладчика. По соглашению в архитектуре x86 значения, возвращаемые функциями, передаются в регистр EAX. Зная это, я поинтересовался его значением.
Преобразовав его в десятичную форму при помощи команды «?», я приступил к поиску значения 1789 в файле глобальных определений ошибок WinError.h из пакета Platform SDK.
Просмотрев документацию на веб-узле MSDN и прочие ресурсы Интернета, я не нашел никакой информации о возможных причинах возникновения ошибки с таким кодом. В то же время, было очевидно, что словосочетание «сбой отношения доверия» (trusted relationship failure) подразумевает, что домен, к которому компьютер пытается подключиться, не состоит в доверительных отношениях с доменом, к которому этот компьютер принадлежит. В данных же конкретных условиях такая ошибка не имела никакого смысла, поскольку я был отключен от сети, и из всех доменов мой компьютер мог бы успешно подключиться только к своему собственному.
Следуя своей интуиции, я открыл командную строку и запустил служебную программу PsGetSid, желая узнать, какая ошибка будет выведена в ответ на попытку запросить идентификатор безопасности компьютера в домене (напомню, что имя учетной записи домена компьютера выглядит как имя компьютера с постфиксом «$»).
Неудивительно, что при попытке такого запроса произошла задержка, которая, как я полагаю, была связана с интервалом ожидания сети, и на консоли появилась ошибка все с тем же кодом. Далее посредством удаленного доступа я подключился к домену и выполнил ту же команду еще раз.
Подключившись к домену, я не только получил безошибочный ответ на свой запрос, но и перестал сталкиваться с задержками при загрузке процессов. Когда я вновь отключился от домена, запуск последующих процессов происходил без задержек, даже по истечении 30 секунд. Правда, после перезагрузки без последующего подключения к домену задержки возобновились.
Теперь я решил разобраться в устройстве функции GetMachineAccountSid. Как показала трассировка стека, в этой функции содержится вызов библиотеки DLL Netlogon, которая, в свою очередь, выполняет удаленный вызов процедуры к функции NetrLogonGetTrustRid. Мне было известно, что служба Netlogon исполняется в рамках подсистемы локального администратора безопасности (службы LSASS).
Я подключил к службе LSASS отладчик Windbg и установил точку останова на функции NetrLogonGetTrustRid. После запуска очередного процесса отладчик добрался до точки останова, я выяснил, что когда определенному полю в структуре данных присваивается значение NULL, служба Netlogon пытается подключиться к контроллеру домена. Даже если по какой бы то ни было причине такая попытка не удается, служба возвращает одну и ту же ошибку с кодом 1789. После того как я вновь подключился к домену, вызов был успешно выполнен и полю в структуре данных было присвоено значение, совпадающее с идентификатором безопасности учетной записи компьютера, причем значение это сохранялось даже после отключения от домена. Вот чем объясняется изменение поведения, следующее за подключением к домену.
Затем, вновь обратившись к функции GetMachineAccountSid, я выяснил, что она кэширует ошибку в течение 30 секунд, после чего запрашивает у службы NetLogon повторную попытку подключения к контроллеру домена. Этим объяснялись тридцатисекундные периоды быстрого запуска. Изучив последовательность вызова кода в отладчике, я обнаружил, что запросы на получение идентификатора безопасности компьютера, выполняемые функцией OpenLpcPort, осуществляются в рамках проверки соответствия запрашиваемого идентификатора идентификатору, переданному в качестве одного из параметров. Если соответствие подтверждается, функция OpenLpcPort заменяет переданный ей идентификатор безопасности идентификатором системной учетной записи, а затем вызывает функцию NtSecureConnectPort, которая сопоставляет идентификатор безопасности домена с локальным идентификатором. Функция NtSecureConnectPort принимает идентификатор безопасности в качестве параметра и проводит подключение к указанному порту команд локального вызова процедур (LPC) лишь в том случае, если этот порт создан учетной записью с совпадающим идентификатором безопасности.
Итак, я получил ответы на некоторые вопросы, но главный вопрос все еще оставался нераскрытым: зачем вообще удаленный вызов процедур нужен при запуске процессов? В первоначальной трассировке стека последним кадром был NegotiateTransferSyntax, но совершенно очевидно, что существовали и другие кадры, которые механизм преобразования символов оказался не в силах определить. Когда точка останова, установленная мной в функции OpenLpcPort, была достигнута, трассировка стека расширилась.
В одной из нижних его строк виден вызов функции ShellExecCmdLine. Она вызывается из класса CRunDlg, ответственного за реализацию диалогового окна Run (Запуск программы). В конечном итоге это, похоже, приводит к исполнению расширений обработчика выполнения оболочки, причем расширение, ответственное за удаленный вызов процедур, реализуется в библиотеке DLL MpShHook. Мне было трудно сразу понять, к чему относится эта библиотека, но поскольку под рукой была
Меня терзало подозрение о том, что этот обработчик относится к реализации защиты в режиме реального времени Windows Defender. Впоследствии разработчики Windows Defender эту мою мысль подтвердили. Отчет программы Autoruns также показал, что Windows Defender регистрирует обработчик выполнения оболочки.
Итак, загадка была разгадана! Вкратце, последовательность выглядит так:
- Диалоговое окно Run (Запуск программу) в составе проводника вызывает функцию ShellExecuteCmdLine.
- Функция ShellExecuteCmdLine обращается к обработчикам выполнения оболочки.
- Обработчик, служащий для защиты в режиме реального времени средствами Windows Defender, реализованный в библиотеке MpShHook.Dll, посредством удаленного вызова процедур связывается со службой Windows Defender, передавая в одном из аргументов идентификатор безопасности службы.
- Библиотека удаленного вызова процедур вызывает функцию GetMachineAccountSid, имея целью проверить соответствие известного идентификатора безопасности фактическому идентификатору безопасности домена, в котором состоит компьютер. В случае установления соответствия идентификатор безопасности сопоставляется с аналогичным идентификатором учетной записи локальной системы.
- Для получения идентификатора безопасности учетной записи компьютера функция GetMachineAccountSid выполняет удаленный вызов процедуры к службе Netlogon.
- Если идентификатор безопасности учетной записи компьютера до сих пор не получен, служба Netlogon пытается подключиться к контроллеру домена.
- Если по истечении интервала ожидания сети (по продолжительности соответствующего задержке) подключиться к контроллеру домена не удается, служба Netlogon возвращает ошибку отношения доверия.
- Удаленный вызов процедуры, инициированный Защитником Windows, выполняется с помощью несопоставленного идентификатора безопасности.
- После проведения службой Защитника Windows проверок в режиме реального времени процесс запускается.