Дело о задержках при запуске процессов

В последнее время у меня было много дел по работе, так что времени на блог совсем не оставалось. Правда, теперь я собираюсь исправиться и возобновить публикации на регулярной основе. Прежде чем приступить к описанию очередной технической проблемы, с которой я недавно столкнулся, с удовольствием сообщаю, что интеграция 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 (Стек вызовов).

clip_image001[4]

 

На вершине стека всегда находится последняя из вызванных функций. Кадр ZwWaitForSingleObject в этой позиции свидетельствует об ожидании потоком передачи сигнала какому-то объекту. Последующие кадры стека относятся к библиотеке удаленного вызова процедур (RPCRT4). Ссылка на функцию OpenLpcPort подсказала мне, что поток пытается инициировать удаленный вызов процедур с другим процессом локальной системы. Похоже, заключил я, ожидание связано с вызовом функции GetMachineAccountSid, подсвеченным на снимке экрана. Как и для пользователей, для принадлежащих к домену компьютеров создаются учетные записи. Имени функции GetMachineAccountSid достаточно, чтобы понять, что она возвращает идентификатор безопасности (SID) учетной записи домена компьютера.

Далее я установил точку останова на возврате вызова функции GetMachineAccountSid из другой функции, OpenLpcPort. Через некоторое время, по продолжительности совпадающее с задержками при запуске, активировалась командная строка отладчика. По соглашению в архитектуре x86 значения, возвращаемые функциями, передаются в регистр EAX. Зная это, я поинтересовался его значением.

clip_image001[6]

 

Преобразовав его в десятичную форму при помощи команды «?», я приступил к поиску значения 1789 в файле глобальных определений ошибок WinError.h из пакета Platform SDK.

 

clip_image001[8]

 

Просмотрев документацию на веб-узле MSDN и прочие ресурсы Интернета, я не нашел никакой информации о возможных причинах возникновения ошибки с таким кодом. В то же время, было очевидно, что словосочетание «сбой отношения доверия» (trusted relationship failure) подразумевает, что домен, к которому компьютер пытается подключиться, не состоит в доверительных отношениях с доменом, к которому этот компьютер принадлежит. В данных же конкретных условиях такая ошибка не имела никакого смысла, поскольку я был отключен от сети, и из всех доменов мой компьютер мог бы успешно подключиться только к своему собственному.

Следуя своей интуиции, я открыл командную строку и запустил служебную программу PsGetSid, желая узнать, какая ошибка будет выведена в ответ на попытку запросить идентификатор безопасности компьютера в домене (напомню, что имя учетной записи домена компьютера выглядит как имя компьютера с постфиксом «$»).

clip_image001[10]

Неудивительно, что при попытке такого запроса произошла задержка, которая, как я полагаю, была связана с интервалом ожидания сети, и на консоли появилась ошибка все с тем же кодом. Далее посредством удаленного доступа я подключился к домену и выполнил ту же команду еще раз.

clip_image001

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

Теперь я решил разобраться в устройстве функции GetMachineAccountSid. Как показала трассировка стека, в этой функции содержится вызов библиотеки DLL Netlogon, которая, в свою очередь, выполняет удаленный вызов процедуры к функции NetrLogonGetTrustRid. Мне было известно, что служба Netlogon исполняется в рамках подсистемы локального администратора безопасности (службы LSASS).

clip_image001[12]

Я подключил к службе LSASS отладчик Windbg и установил точку останова на функции NetrLogonGetTrustRid. После запуска очередного процесса отладчик добрался до точки останова, я выяснил, что когда определенному полю в структуре данных присваивается значение NULL, служба Netlogon пытается подключиться к контроллеру домена. Даже если по какой бы то ни было причине такая попытка не удается, служба возвращает одну и ту же ошибку с кодом 1789. После того как я вновь подключился к домену, вызов был успешно выполнен и полю в структуре данных было присвоено значение, совпадающее с идентификатором безопасности учетной записи компьютера, причем значение это сохранялось даже после отключения от домена. Вот чем объясняется изменение поведения, следующее за подключением к домену.

Затем, вновь обратившись к функции GetMachineAccountSid, я выяснил, что она кэширует ошибку в течение 30 секунд, после чего запрашивает у службы NetLogon повторную попытку подключения к контроллеру домена. Этим объяснялись тридцатисекундные периоды быстрого запуска. Изучив последовательность вызова кода в отладчике, я обнаружил, что запросы на получение идентификатора безопасности компьютера, выполняемые функцией OpenLpcPort, осуществляются в рамках проверки соответствия запрашиваемого идентификатора идентификатору, переданному в качестве одного из параметров. Если соответствие подтверждается, функция OpenLpcPort заменяет переданный ей идентификатор безопасности идентификатором системной учетной записи, а затем вызывает функцию NtSecureConnectPort, которая сопоставляет идентификатор безопасности домена с локальным идентификатором. Функция NtSecureConnectPort принимает идентификатор безопасности в качестве параметра и проводит подключение к указанному порту команд локального вызова процедур (LPC) лишь в том случае, если этот порт создан учетной записью с совпадающим идентификатором безопасности.

Итак, я получил ответы на некоторые вопросы, но главный вопрос все еще оставался нераскрытым: зачем вообще удаленный вызов процедур нужен при запуске процессов? В первоначальной трассировке стека последним кадром был NegotiateTransferSyntax, но совершенно очевидно, что существовали и другие кадры, которые механизм преобразования символов оказался не в силах определить. Когда точка останова, установленная мной в функции OpenLpcPort, была достигнута, трассировка стека расширилась.

clip_image001[14]

 

В одной из нижних его строк виден вызов функции ShellExecCmdLine. Она вызывается из класса CRunDlg, ответственного за реализацию диалогового окна Run (Запуск программы). В конечном итоге это, похоже, приводит к исполнению расширений обработчика выполнения оболочки, причем расширение, ответственное за удаленный вызов процедур, реализуется в библиотеке DLL MpShHook. Мне было трудно сразу понять, к чему относится эта библиотека, но поскольку под рукой была

clip_image001[16]

Меня терзало подозрение о том, что этот обработчик относится к реализации защиты в режиме реального времени Windows Defender. Впоследствии разработчики Windows Defender эту мою мысль подтвердили. Отчет программы Autoruns также показал, что Windows Defender регистрирует обработчик выполнения оболочки.

clip_image001[18]

Итак, загадка была разгадана! Вкратце, последовательность выглядит так:

  1. Диалоговое окно Run (Запуск программу) в составе проводника вызывает функцию ShellExecuteCmdLine.
  2. Функция ShellExecuteCmdLine обращается к обработчикам выполнения оболочки.
  3. Обработчик, служащий для защиты в режиме реального времени средствами Windows Defender, реализованный в библиотеке MpShHook.Dll, посредством удаленного вызова процедур связывается со службой Windows Defender, передавая в одном из аргументов идентификатор безопасности службы.
  4. Библиотека удаленного вызова процедур вызывает функцию GetMachineAccountSid, имея целью проверить соответствие известного идентификатора безопасности фактическому идентификатору безопасности домена, в котором состоит компьютер. В случае установления соответствия идентификатор безопасности сопоставляется с аналогичным идентификатором учетной записи локальной системы.
  5. Для получения идентификатора безопасности учетной записи компьютера функция GetMachineAccountSid выполняет удаленный вызов процедуры к службе Netlogon.
  6. Если идентификатор безопасности учетной записи компьютера до сих пор не получен, служба Netlogon пытается подключиться к контроллеру домена.
  7. Если по истечении интервала ожидания сети (по продолжительности соответствующего задержке) подключиться к контроллеру домена не удается, служба Netlogon возвращает ошибку отношения доверия.
  8. Удаленный вызов процедуры, инициированный Защитником Windows, выполняется с помощью несопоставленного идентификатора безопасности.
  9. После проведения службой Защитника Windows проверок в режиме реального времени процесс запускается.