Продолжается подписка на наши издания! Вы не забыли подписаться?

Vista UAC: окончательное решение

Автор: Thomas Hruska
Опубликовано: 23.04.2009

Когда я начинал этот проект, я не думал, что он у меня займет целых две недели. Наконец, я снова здесь, со статьей об этой штуке. Статья состоит из трех основных частей. Первая из них – это обзор, для тех, кому не важны тонкости UAC, и кто просто хочет получить хороший софт. Вторая касается действительно неприятных вещей... для тех, кто любит избыточную информацию. Последняя часть рассказывает о UAC Permanent Links – загрузке нескольких elevated-DLL, исполнения множественных elevated-функций и запуске множественных elevated-процессов – и все это после вывода единственного диалога UAC из не-elevated процесса. Теперь, без шума и пыли, перехожу к собственно статье.

В Windows Vista пользователь, независимо от имеющихся у него прав, работает с ограниченными привилегиями. Это необходимо для противодействия вредоносным (или просто некачественно написанным) программам. Но в работе часто встречаются моменты, когда пользователю (или администратору, временно севшему за компьютер пользователя и выполняющему операции из-под логина пользователя) необходимо выполнить те или иные действия, требующие расширенных привилегий (обычно доступных только под логином администратора). В подобных случаях Windows Vista выдает диалог, оповещающий пользователя о происходящем действии и предлагающий ему подтвердить или отвергнуть операцию. Если пользователь подтверждает операцию, то ОС проверяет, есть ли у пользователя необходимые привилегии, и, если они есть, выполняет эти операции. В ином случае пользователю предлагается выбрать одну из привилегированных учетных записей и ввести ее пароль. Подсистема Windows, отвечающая за это, называется UAC, а процесс временного расширения привилегий пользователя – elevating. Если в этой статье вы встретите слово elevated – то трактуйте его как «работающее с расширенными привилегиями». – прим.ред.

Общая картина

Да. Виста. О ней уже многое сказано. Что-то хорошее, что-то плохое, но на самом деле это не имеет значения. Виста неплохо выглядит, у нее есть новые API, а еще у нее есть UAC.

Да. UAC. Для любителей расшифровок, это значит User Account Control. Проклятие множества существующих приложений, а также причина, заставившая меня написать пакет Elevate. Сначала я делал его для своих собственных пользователей, но оказалось, что он не меньше нужен и другим разработчикам.

Всем хотелось бы думать, что их приложения автоматически заработают под Виста. Но вслед за простым присоединением Виста к концу линейки Win95/98/Me/NT/2000/XP/2003 следует грубое пробуждение, которое вынуждает принимать какие-то меры. Если вы читаете это, можно предположить, что вы уже столкнулись с одним из нескольких барьеров, поставленных UAC на пути успешного развертывания приложений.

Основная часть этой статьи посвящена работе UAC, и тому, как создать функцию CreateProcessElevated(), входящую в состав моего пакета Elevate. Вторая половина пакета Elevate предназначена для решения главной неприятности UAC для приложений с обычными привилегиями: постоянной необходимости проходить через диалог UAC для выполнения операций, требующих больших привилегий. Если это ваш случай, но вы не хотите позволять всему приложению исполняться с повышенными привилегиями, эта статья тоже для вас.

Поиск в Google по UAC обычно выдает какой-нибудь блог, описывающий, как использовать ShellExecuteEx() с недокументированной командой «runas», чтобы заставить процесс запускаться с расширенными правами. Того же самого при запуске исполняемого файла можно добиться с помощью изменения файла манифеста. На CodeProject есть также статья о том, как обойти ShellExecuteEx() с помощью NT-сервиса, но там есть разные проблемы, кроме очевидных, связанных с безопасностью.

Но если вы похожи на меня, ShellExecuteEx() – это не ваш выбор, и обходить UAC я тоже не хочу – у Microsoft были причины сделать UAC, и я собираюсь играть честно. У меня есть большая библиотека, и я сильно завишу от CreateProcess(). К несчастью, Microsoft не создала API CreateProcessElevated() для таких, как я. К счастью, я нашел способ создания набора API CreateProcess...Elevated() с почти полной функциональностью.

Прежде чем перейти к CreateProcessElevated(), посмотрим на код, использующий ShellExecuteEx():

  SHELLEXECUTEINFOA TempInfo = {0};

  TempInfo.cbSize = sizeof(SHELLEXECUTEINFOA);
  TempInfo.fMask = 0;
  TempInfo.hwnd = NULL;
  TempInfo.lpVerb = "runas";
  TempInfo.lpFile = "C:\\TEMP\\UAC\\Test.exe";
  TempInfo.lpParameters = "";
  TempInfo.lpDirectory = "C:\\TEMP\\UAC\\";
  TempInfo.nShow = SW_NORMAL;

  ::ShellExecuteExA(&TempInfo);

Давайте заставим Виста расширить привилегии этого процесса, вызова функции. Как уже говорилось, волшебное слово здесь – "runas". Насколько я понимаю, команда "runas" совершенно не документирована в MSDN. В конце концов, в процессе исследований я не нашел никаких ссылок на нее.

Должен прямо сейчас сказать, что там, где это возможно, лучше избегать использования команды "runas" и использовать модификацию файла манифеста. Это заставляет меня рассказать об изменениях манифеста.

Если вы никогда раньше не использовали манифест, вы, скорее всего, живете в консольном мире. Или в пещере. Или и там, и там. Я использую манифест только чтобы сказать загрузчику Windows, что нужно загрузить последние Common Controls. Манифесты нужны для загрузки Common Controls 6 и более поздних (поддержка тем) через так называемые Side-by-Side Assemblies (SxS, вы могли заметить каталог "assembly" в C:\WINDOWS и удивлялись, что бы это было). Это решение для DLL Hell от Microsoft, и в целом оно работает.

В .NET входит новый язык, C#, (выясняется, что в Active++ Jspresso был фатальный недостаток, от которого он и помер). .NET включает виртуальную машину, которую будут использовать все языки (видимо, из-за фатальных недостатков в процессорах Интел). .NET включает единую систему защиты (есть все-таки фатальный недостаток в хранении паролей не на серверах Microsoft). Реально проще перечислить вещи, которых .NET не включает. .NET наверняка революционно изменит Windows-программирование... примерно на год.

Манифест – это старый добрый XML-файл. Посмотрим на обычный файл манифеста, поддерживающего Common Controls 6:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" 
          manifestVersion="1.0"> 
<dependency> 
    <dependentAssembly> 
        <assemblyIdentity 
            type="win32" 
            name="Microsoft.Windows.Common-Controls" 
            version="6.0.0.0" 
            processorArchitecture="X86" 
            publicKeyToken="6595b64144ccf1df" 
            language="*" 
        /> 
    </dependentAssembly> 
</dependency> 
</assembly>

Internet говорит, что для получения привилегий администратора под Виста нужно изменить этот файл так:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" 
          manifestVersion="1.0"> 
<dependency> 
    <dependentAssembly> 
        <assemblyIdentity 
            type="win32" 
            name="Microsoft.Windows.Common-Controls" 
            version="6.0.0.0" 
            processorArchitecture="X86" 
            publicKeyToken="6595b64144ccf1df" 
            language="*" 
        /> 
    </dependentAssembly> 
</dependency> 
<v3:trustInfo xmlns:v3="urn:schemas-microsoft-com:asm.v3">
  <v3:security>
    <v3:requestedPrivileges>
      <v3:requestedExecutionLevel level="requireAdministrator" />
    </v3:requestedPrivileges>
  </v3:security>
</v3:trustInfo>
</assembly>

<ХРРРРРРРРЯСЬ!>

Неверный ответ! Похоже, неверно сформированный файл v3-манифеста приводит к краху Windows XP SP2. И не к какому-то дохлому краху, Синий экран смерти (BSOD), в комплекте с перезагрузкой, все как положено. Оставляя в стороне CreateProcess(), это вероятно происходит из-за краха XML-парсера в режиме ядра (единственный известный мне способ загнать XP в BSOD – это сбой ядра в нулевом кольце). Похоже, что v2-манифесты существенно более стабильны, и при этом все еще работают. Так что для максимальной стабильности корректный формат манифеста будет выглядеть так:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" 
          manifestVersion="1.0"> 
<dependency> 
    <dependentAssembly> 
        <assemblyIdentity 
            type="win32" 
            name="Microsoft.Windows.Common-Controls" 
            version="6.0.0.0" 
            processorArchitecture="X86" 
            publicKeyToken="6595b64144ccf1df" 
            language="*" 
        /> 
    </dependentAssembly> 
</dependency> 
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
        <requestedPrivileges>
            <requestedExecutionLevel 
                level="requireAdministrator" 
                uiAccess="false"/>
        </requestedPrivileges>
    </security>
</trustInfo>
</assembly>

Опция "level" для requestedExecutionLevel может быть равна 'asInvoker', 'requireAdministrator' или 'highestAvailable'. Об "uiAccess" я расскажу ниже.

Страдающие COM/DCOM-зависимостью или сильно вложившиеся в эту технологию, возможно, существенно меня обогнали, но я помещу здесь для вас «redundant moniker out-of-process elevation»-код.

HRESULT CreateElevatedComObject(HWND hwnd, 
                                REFCLSID rclsid, 
                                REFIID riid, 
                                __out void ** ppv)
{
    BIND_OPTS3 bo;
    WCHAR  wszCLSID[50];
    WCHAR  wszMonikerName[300];

    StringFromGUID2(rclsid, wszCLSID, cntof(wszCLSID)); 
    HRESULT hr = StringCchPrintf(wszMonikerName, 
                                 cntof(wszMonikerName)), 
                                 L"Elevation:Administrator!new:%s", 
                                 wszCLSID);
    if (FAILED(hr))
        return hr;
    memset(&bo, 0, sizeof(bo));
    bo.cbStruct = sizeof(bo);
    bo.hwnd = hwnd;
    bo.dwClassContext = CLSCTX_LOCAL_SERVER;
    return CoGetObject(wszMonikerName, &bo, riid, ppv);
}

// cntof() is defined as:
#define cntof(a) (sizeof(a)/sizeof(a[0]))

Я честно не представляю, что этот код делает. Но, похоже, это какая-то магия. Я отследил строку "Elevation:Administrator!new:" в ShellExecuteEx(), так что могу предположить, что этот код работает. Я стараюсь избегать COM везде и любыми способами. COM – это вообще чума.

User Interface Integration

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

К счастью, Microsoft упростила размещение иконки со щитом на кнопках:

GetDlgItem(IDC_SOMEBUTTONID)->SendMessage(BCM_SETSHIELD, 0, TRUE);

или используйте новый макрос:

Button_SetElevationRequiredState(ButtonHWnd, fRequired);

К несчастью, в любое другое место эту иконку поместить непросто. Можно вызвать новую Виста-функцию SHGetStockIconInfo() чтобы извлечь ее как HICON:

HICON ShieldIcon;
SHSTOCKICONINFO sii = {0};
sii.cbSize = sizeof(sii);
SHGetStockIconInfo(SIID_SHIELD, SHGFI_ICON | SHGFI_SMALLICON, &sii);
ShieldIcon = sii.hIcon;

Но если вы не создаете Vista-only приложение, придется выполнить LoadLibrary()/GetProcAddress() для SHGetStockIconInfo().

Вместо этого для получения HICON можно использовать LoadIcon() с IDI_SHIELD, но это, скорее всего, загрузит «низкокачественную» иконку со щитом. Новый API LoadIconMetric() в Виста работает лучше, поскольку загружает «высококачественные» иконки, которые, скорее всего, будут использоваться для мониторов с высоким разрешением.

Странности UAC

До сих пор я говорил о наиболее очевидных частях UAC, но UAC – это больше, чем просто вывод диалога пользователю. Это образ жизни. Или что-то типа того. И, как в большинстве вещей в жизни, UAC может быть совершенно странным.

У многих из нас есть работающие приложения. У некоторых из нас есть даже плохо написанные приложения, пишущие в каталог "Program Files". А некоторые приложения совсем плохо себя ведут, и пишут и в каталог Windows, и в ключ реестра HKLM.

UAC рассматривает плохо ведущие себя (non-elevated) приложения как незаконнорожденных детей. Для каждого плохо себя ведущего приложения есть нечто под названием " виртуальное хранилище (virtual store)". Оно состоит из файлов и ключей реестра, записанных в места, считающиеся неподходящими для записи non-elevated процессами. При попытке записи ОС копирует исходный файл в виртуальное хранилище пользователя и перенаправляет все вызовы к этой копии. Таким образом, приложение изменяет файлы/ключи реестра в виртуальном хранилище, а не там, где оно пыталось это сделать. Но только для данного пользователя. Остальные увидят исходный файл или копию в их виртуальных хранилищах.

Но подождите! Все становится страньше и страньше! Допустим, приложение надумало удалить файл, который оно изменяло в Program Files. Что ж, ОС перенаправит этот запрос к виртуальному хранилищу. То есть, хотя файл и был удален, если приложение вернется к этому вопросу, оно обнаружит, что файл по-прежнему существует. После удаления из виртуального хранилища ОС позволяет приложению увидеть исходный файл. Но если приложение снова попробует удалить его файл, это приведет к ошибке. Смысл в этом есть. Но это странно.

UAC также вызывает ряд других мелких проблем: с Interactive Services, пытающимися взаимодействовать с пользователем через собственный GUI, с административными разделяемыми каталогами и многим другим.

Что, конечно, приводит меня к разделенным токенам. Разделенные токены просто странные. При логоне в Виста и позже (очевидно) вы получаете разделенный токен. Архитектура логона (которая в Виста опять изменилась) берет ваш исходно могучий пользовательский токен и создает другой токен, из которого удаляет все администраторские привилегии. Этот другой токен используется для запуска всех приложений примерно так, как то, что в XP называлось Limited User Account (LUA). Когда UAC предлагает расширить привилегии, и вы соглашаетесь, для создания процесса вместо второго используется первый токен. В сущности, UAC спрашивает «Вы действительно хотите использовать ваш настоящий администраторский токен для запуска этого приложения?»

Еще одна странная вещь – сам диалог UAC. Если оставить его надолго без внимания, он сам себя автоматически отменит. Обнаружено в процессе исследований.

Последняя странность UAC состоит в том, что если запустить процесс с расширенными привилегиями, из-под него становится очень трудно запустить процесс с обычными привилегиями. Это является интересным нюансом для авторов инсталляторов. Мы, разработчики, любим разрешать пользователям запускать наши приложения в конце процесса установки, чтобы пользователи могли попробовать и охотнее покупали их. Об этом можно прочитать в статье по адресу http://www.tweak-uac.com/programming/vista-elevator2/.

CreateProcess() пасует перед ERROR_ELEVATION_REQUIRED

Это основы UAC, собранные вместе. Для многих ShellExecuteEx(), измененные манифесты, переключение в HKCU и запрет записи в ненужные места будут «приемлемым» решением. Но кое-кому хочется знать больше. А некоторым понадобятся все возможности CreateProcess() (и даже ShellExecute()/ShellExecuteEx() с собственными командами).

Теперь перейдем к пакету Elevate и CreateProcessElevated().

CreateProcess() жалко пасует перед Vista-процессами, требующими elevation через файл манифеста. Нужно заметить, что файл манифеста не говорит в действительности, что «вы должны использовать административный токен, чтобы запустить данный процесс». Вместо этого он говорит «вы не можете использовать токен с правами, меньше указанных, для запуска этого процесса. Поэтому сообщение об ошибке ERROR_ELEVATION_REQUIRED (740), возвращаемое CreateProcess(), несколько обманчиво.

Безотносительно этого, если вы читаете это, я могу предположить только, что вы отчаянно нуждаетесь в функции наподобие CreateProcessElevated(). Как раз для этого и нужен пакет Elevate. Пакет Elevate (Elevate_BinariesAndDocs.zip) состоит из двух компонентов и их документации: Elevation API DLL (Elevate.dll) и Elevation Transaction Coordinator (Elevate.exe). Оба они должны находиться в одном каталоге. Elevation API DLL экспортирует следующие функции:

Link_Create()
Link_CreateAsUser()
Link_CreateWithLogon()
Link_CreateWithToken()
Link_Destroy()
Link_CreateProcessA()
Link_CreateProcessW()
Link_ShellExecuteExA()
Link_ShellExecuteExW()
Link_ShellExecuteA()
Link_ShellExecuteW()
Link_LoadLibraryA()
Link_LoadLibraryW()
Link_SendData()
Link_GetData()
Link_SendFinalize()

CreateProcessElevatedA()
CreateProcessElevatedW()
CreateProcessAsUserElevatedA()
CreateProcessAsUserElevatedW()
CreateProcessWithLogonElevatedW()
CreateProcessWithTokenElevatedW()

SH_RegCreateKeyExElevatedA()
SH_RegCreateKeyExElevatedW()
SH_RegOpenKeyExElevatedA()
SH_RegOpenKeyExElevatedW()
SH_RegCloseKeyElevated()

ShellExecuteElevatedA()
ShellExecuteElevatedW()
ShellExecuteExElevatedA()
ShellExecuteExElevatedW()

IsUserAnAdmin()

Можно заметить, что названия API очень похожи на их не elevated-аналоги (например, CreateProcess() и CreateProcessElevated()). Этот пакет включает и ANSI, и Unicode-версии (A и W). Можно заметить, что IsUserAnAdmin() – это уже функция, экспортируемая из Shell32.dll, но в MSDN сказано, что ее могут убрать (http://msdn2.microsoft.com/en-us/library/ms647418.aspx). IsUserAnAdmin() сейчас полагается на Shell32.dll, но она может исчезнуть в следующей версии.

Идем дальше. Возможно, у вас есть код, подобный следующему:

Result = CreateProcess(...ParameterList...);
if (!Result)  return FALSE;
...Do something with process like WaitForSingleObject()...
::CloseHandle()'s;

который прекрасно работает, пока вы не пытаетесь запустить процесс, требующий более высоких привилегий. Чтобы использовать пакет Elevate, просто поместите его в систему и с помощью LoadLibrary()/GetProcAddress() вызовите функцию CreateProcessElevatedA():

// существующая строка кода.
Result = CreateProcess(...ParameterList...); 
if (!Result && GetLastError() == ERROR_ELEVATION_REQUIRED)
{
  HMODULE LibHandle = LoadLibrary("Elevate.dll");
  if (LibHandle != NULL)
  {
    DLL_CreateProcessElevated =
      (typecast)GetProcAddress("CreateProcessElevatedA");
    if (DLL_CreateProcessElevated)
    {
      ...Пользовательский хендл изменяется здесь*

      Result = DLL_CreateProcessElevated(...ParameterList...);
      ... Подключения пользовательского хендла здесь *...
    }
    FreeLibrary(LibHandle);
  }
}
if (!Result)  return FALSE;
//...

Опции "...ParameterList..." почти одинаковы. Изменять параметр нужно только при использовании флага STARTF_USESTDHANDLES в передаваемой внутрь структуре STARTUPINFO. Если вы перенаправляете стандартные обработчики (stdin, stdout, stderr), все может стать малость диковатым. Читайте документацию, но, в двух словах, вам потребуется получше познакомитья с именованными каналами и полностью прочитать остальную часть этой статьи.

Помните, что вы должны использовать LoadLibrary()/GetProcAddress(). DLL умышленно не может загружаться на всех версиях Windows, предшествующих Vista.

ShellExecute...Elevated()?!

Некоторые, возможно, удивятся, зачем нужен набор API ShellExecuteElevated(). На то есть две причины. Первая – это проблема особых случаев, возникающая при использовании команды "runas". Параметр lpVerb/lpOperation позволяет использовать только одну команду за раз. Так что если нужно заставить процесс (который обычно исполняется с нормальными привилегиями) исполняться с расширенными привилегиями и, например, использовать команду "print", обычный ShellExecute()/ShellExecuteEx() этого не позволит.

Вторая причина заключается в том, что может понадобиться запустить ShellExecute()/ShellExecuteEx() из уже elevated-окружения. Никакого другого способа сделать это также не существует.

Я признаю, что надобность в API ShellExecuteElevated() невелика, но я включил их для полноты.

Демонстрационное приложение

За неимением лучшего, демонстрационное приложение – пример использование CreateProcessElevated() во всей его красе. Чтобы использовать это приложение, скачайте его и разверните в какой-нибудь каталог, куда знаете как добраться из командной строки. Затем запустите non-elevated командную строку, и перейдите в каталог, куда были распакованы файлы. Введите "TestParent" и нажмите Enter. Вот что за этим последует:




Если оставить в стороне мое очевидное знание американского варианта олбанского, происходящее весьма впечатляет. Обычно elevated консольная программа из не-elevated консольной программы запускается в отдельном окне. В данном случае TestChild.exe (elevated) и TestParent.exe (non-elevated) работают в одном окне. Кроме того, stderr TestChild.exe перенаправляется в TestParent.exe. Это получилось благодаря очень тщательному просеиванию тонн документации и некоторым экспериментам.

И, хотя программа и не показывает этого, TestChild.exe использует те же переменные окружения, что и TestParent.exe.

Житейские мелочи

Прежде чем рассказывать о том, каким образом Elevate.dll и Elevate.exe делают возможным выполнение этого демо-приложения, мне придется рассказать о некоторых более гнусных деталях работы UAC. Потерпите, внутри оно белое и пушистое.

Когда я начинал этот проект, я хотел избежать использования ShellExecuteEx(), а для этого мне потребовалось выяснить, что же делает функция "tick". Сперва я подумал: «Что ж, им нужно вызывать CreateProcess() или что-то подобное. Видимо, это какой-то трюк для вызова» Моя первая остановка была файл traceapi.dll из Detours. Я подцепил ее к тестовому процессу с ShellExecuteEx(), и ничего не нашел. Я подумал, что traceapi.dll может быть битой, так что я потерял впустую день на выяснение, что, возможно, ShellExecuteEx () не использует CreateProcess () вообще.

Следующим шагом были раскопки самого вызова с помощью Visual Studio Disassembler. Я быстро понял, что мне нужны символы для Vista Shell32.dll и других DLL Vista. Я знаю, что Microsoft убирает с сервера символов часть из них, о которой людям лучше не знать. Я надеялся, что UAC elevation не попадает в эту часть. Как оказалось, они не убрали эту информацию, или просто забыли об этом. В любом случае, вы должны их поблагодарить за это.

Разбирательство с ShellExecuteEx() изрядно сбивает с толку. На самом деле, я до сих пор не уверен в том, что делают некоторые части кода. Если воспользоваться отладчиком, стек вызова будет выглядеть примерно так:

     shell32.dll!CShellExecute::ExecuteNormal()  + 0x7a
     shell32.dll!ShellExecuteNormal()  + 0x33
     shell32.dll!_ShellExecuteExW@4()  + 0x42
     shell32.dll!_ShellExecuteExA@4()  + 0x4a
     ShellExecuteTest.exe!main()  Line 18 + 0xc
     ShellExecuteTest.exe!mainCRTStartup()  Line 259 + 0x19
     kernel32.dll!@BaseThreadInitThunk@12()  + 0x12
     ntdll.dll!__RtlUserThreadStart@8()  + 0x27

Выглядит многообещающе, не правда ли? Да, в нормальных условиях примерно здесь должна выполняться команда. В данном же случае ShellExecuteEx() просто разогревается, чтобы запустить новый поток. Серьезно. Чтобы запустить новый процесс, запускается новый поток. Меня от этого тошнит. Продвинемся дальше, внутрь нового потока, до следующего места в стеке вызова:

     shell32.dll!CExecuteApplication::Execute()  + 0x22
     shell32.dll!CExecuteAssociation::_DoCommand()  + 0x5b
     shell32.dll!CExecuteAssociation::_TryApplication()  + 0x32
     shell32.dll!CExecuteAssociation::Execute()  + 0x30
     shell32.dll!CShellExecute::_ExecuteAssoc()  + 0x82
     shell32.dll!CShellExecute::_DoExecute()  + 0x4c
     shell32.dll!CShellExecute::s_ExecuteThreadProc()  + 0x25
     shlwapi.dll!WrapperThreadProc()  + 0x98
     kernel32.dll!@BaseThreadInitThunk@12()  + 0x12
     ntdll.dll!__RtlUserThreadStart@8()  + 0x27

Что ж, наконец-то? Да, но погружение в эту бездну заняло около пяти часов нажатий F10 и F11. Нужно пройти сквозь огромную путаницу COM-объектов. Да, именно так. Теперь COM участвует в запуске нового процесса. А за собой он тащит кучу разных DLL. Но знаете что? Мы еще не закончили.

В методе Execute() производится вызов CExecuteApplication::_VerifyExecTrust(). При этом используется COM и тратится масса времени CPU (как и на совершенно вульгарное и ненужное дублирование). Я сдался, так и не выяснив, что он делает. По общему впечатлению, скорее всего он как-то связан с диалогами, появляющимися при запуске EXE, скачанного через Интернет (IZoneIdentifier). Возможно, он проверяет наличие NTFS-потока 'Zone.Identifier', так что он может запускать этот модный (и раздражающий) диалог, который вы видите перед запуском скачанных файлов.

Я бросил _VerifyExecTrust() и занялся оставшейся частью. Стек вызовов выглядел весело:

     shell32.dll!AicpMsgWaitForCompletion()  + 0x36
     shell32.dll!AicpAsyncFinishCall()  + 0x2c
     shell32.dll!AicLaunchAdminProcess()  + 0x2ee
     shell32.dll!_SHCreateProcess()  + 0x59d0
     shell32.dll!CExecuteApplication::_CreateProcess()  + 0xac
     shell32.dll!CExecuteApplication::_TryCreateProcess()  + 0x2e
     shell32.dll!CExecuteApplication::_DoApplication()  + 0x3c
     shell32.dll!CExecuteApplication::Execute()  + 0x33
     shell32.dll!CExecuteAssociation::_DoCommand()  + 0x5b
     shell32.dll!CExecuteAssociation::_TryApplication()  + 0x32
     shell32.dll!CExecuteAssociation::Execute()  + 0x30
     shell32.dll!CShellExecute::_ExecuteAssoc()  + 0x82
     shell32.dll!CShellExecute::_DoExecute()  + 0x4c
     shell32.dll!CShellExecute::s_ExecuteThreadProc()  + 0x25
     shlwapi.dll!WrapperThreadProc()  + 0x98
     kernel32.dll!@BaseThreadInitThunk@12()  + 0x12
     ntdll.dll!__RtlUserThreadStart@8()  + 0x27

Я тащусь с имен этих функций. _DoExecute(), _Execute(), _TryApplication(), _DoCommand()... ОК, сейчас мы собираемся создать процесс. Нет, подождите, сейчас мы собираемся создать процесс... Ах нет, минуточку, это сейчас мы собираемся создать процесс... Маркетологи пробрались в исходники. Бедные разработчики. Мне жаль тех ребят, кому ежедневно придется работать с этим.

Во всяком случае, интересующая функция – AicLaunchAdminProcess(). Если мы дошли до AicpMsgWaitForCompletion(), значит, все зашло слишком далеко. Я почти два дня ломал голову, прежде чем выяснить, как вход в MsgWaitForMultipleObjects() может заставить UAС показать диалог без выделения и затем продолжит как ни в чем не бывало по нажатию Accept/Cancel.

Оказывается, не только запускается новый поток и инициализируется с полдюжины СОМ-объектов. Во всем этом действии еще и RPC замешано. RPC (Remote Procedure Call для озабоченных сокращениями) – это довольно старая технология, используемая в основном Microsoft для NT-сервисов. Он комбинируется с MIDL и позволяет, гм, удаленно вызывать процедуры. Целевая функция может находиться на другом или на том же самом компьютере. Это что-то типа одной из неэксплуатируемых дыр в безопасности, поскольку это довольно мало известная технология, в которой мало кто разбирается.

И здесь все становится интересным. RPC-вызов обращается к новенькому NT-сервису Vista, AppInfo (UUID "{201ef99a-7fa0-444c-9399-19ba84f12a1a}"). В AppInfo происходит вся магия. Но прежде чем перейти к магии, нужно обсудить Sessions и Integrity Levels.

Sessions и Integrity Levels в Vista

Часть изменений в Vista и работа UAC – это введение в то, что известно как сессии (Sessions). Иногда их еще называют "Secure Desktops". В Vista есть две сессии, но технически может быть гораздо больше. Их официальные названия - Session 0 и Session 1. В Session 0 работают NT-сервисы. В Session 1 находится используемый по умолчанию рабочий стол "WinSta0\Default" с Explorer и прочими приложениями. Просто для ясности, Window Stations – это не сессии.

Важно отметить, что кое-где в документации по Vista UAC неявно говорится, что каждая сессия предполагается полностью отдельной от других. Это не так. Скорее всего, авторы имеют в виду оконные сообщения или раннюю бету. В официальной документации, Impact of Session 0 Isolation on Services and Drivers in Windows Vista (http://www.microsoft.com/whdc/system/vista/services.mspx), предлагается использовать для коммуникаций между процессами, идущими в разных сессиях, RPC и именованные каналы. RPC, именованные каналы и сокеты являются одобренными механизмами межпроцессного взаимодействия (Interprocess Communication, IPC) в Vista.

Еще одно нововведение в Vista – Integrity Levels (IL). Есть четыре уровня Integrity Level: System (NT-сервисы), High (Elevated-процессы), Medium (большинство пользовательских процессов), Low (процессы типа Protected-mode IE). У каждого созданного объекта есть свой IL, ассоциированный с ним, и этот IL проверяется раньше, чем DACL. IL процесса/потока сравнивается с IL объекта до предоставления доступа к нему. Следует отметить, что объекты, созданные системными и elevated-процессами, по умолчанию имеют IL Medium (для внимательного читателя последнее предложение является ключом к пониманию, почему работает пакет Elevate).

AppInfo и consent.exe

AppInfo, очевидно, является ключом к UAC elevation. ShellExecuteEx() перенаправляет все запросы на elevation NT-сервису AppInfo с помощью RPC-вызова. AppInfo после этого вызывает "consent.exe" в контексте SYSTEM. Как и следует из имени, этот исполняемый файл выводит диалог подтверждения.

Но consent.exe – это вам не какое-нибудь заурядное приложение. То, что вы видите, когда диалог активен – это не WinSta0\Default из сессии 1. Это рабочий стол из сессии 0. По этой причине его назвали "secure desktop". consent.exe берет скриншот, переключается в рабочий стол сессии 0, плюхает на экран этот скриншот и выводит диалог. Все это только выглядит, как экран сессии 1. Вы не можете щелкнуть ни по чему, кроме диалога, потому что вам в прямом смысле не по чему щелкать. После нажатия кнопки происходит обратное переключение в сессию 1 и выход из consent.exe.

AppInfo берет результат выполнения consent.exe и определяет, нужно ли запускать новый процесс (например, если вы выбрали запуск с расширенными правами). AppInfo создает процесс, используя полный административный токен (помните эти половинки токенов?) текущего пользователя, в сессии 1 с IL High. Если запустить Task Manager, можно увидеть, что elevated-процессы действительно запущены от имени текущего пользователя. Мы знаем, что они исполняются в сессии 1, потому что можно создавать GUI-объекты и взаимодействовать с ними.

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

  1. AppInfo идет к Local Security Authority и просит у него elevated-токен текущего пользователя сессии 1.
  2. AppInfo загружает структуру STARTUPINFOEX (новинка Vista) и вызывает новую функцию InitializeProcThreadAttributeList(), появившуюся в Vista, с местом для одного атрибута.
  3. Вызывается OpenProcess(), чтобы получить хэндл процесса, инициировавшего RPC-вызов.
  4. Вызывается UpdateProcThreadAttribute() с PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, который использует хэндл, полученный на шаге 3.
  5. Вызывается CreateProcessAsUser() с EXTENDED_STARTUPINFO_PRESENT и результатами шагов 1 и 4.
  6. Вызывается DeleteProcThreadAttributeList().
  7. Результаты собираются, хэндлы удаляются.

Когда AppInfo сможет запустить процесс, оно передаст некоторую информацию обратно, вызвавшему ShellExecuteEx() приложению, через RPC-интерфейс. ShellExecuteEx() немножко походит вокруг да около, приберется за собой и в конечном счете двинется назад, к вызывавшей стороне, через целую кучу вызовов функций.

CreateProcessElevated() без ShellExecuteEx()?

Когда я в общих чертах разобрался, где в ShellExecuteEx() производится RPC-вызов AppInfo, и как AppInfo работает, мне захотелось узнать, можно ли создать полнофункциональное CreateProcessElevated() без бешено дорогого вызова ShellExecuteEx(). Под «полной функциональностью» я имею в виду полный контроль над структурой STARTUPINFO с поддержкой флага STARTF_USESTDHANDLES.

Это целиком заняло у меня три дня. Я в конце концов сократил всё до нескольких строк ассемблера внутри AicLaunchAdminProcess():

mov     edi, [ebp+VarStartupInfo]
mov     eax, [edi+_STARTUPINFOW.lpTitle]
mov     [ebp+VarStartupInfo_Title], eax
mov     eax, [edi+_STARTUPINFOW.dwX]
mov     [ebp+VarStartupInfo_X], eax
mov     eax, [edi+_STARTUPINFOW.dwY]
mov     [ebp+VarStartupInfo_Y], eax
mov     eax, [edi+_STARTUPINFOW.dwXSize]
mov     [ebp+VarStartupInfo_XSize], eax
mov     eax, [edi+_STARTUPINFOW.dwYSize]
mov     [ebp+VarStartupInfo_YSize], eax
mov     eax, [edi+_STARTUPINFOW.dwXCountChars]
mov     [ebp+VarStartupInfo_XCountChars], eax
mov     eax, [edi+_STARTUPINFOW.dwYCountChars]
mov     [ebp+VarStartupInfo_YCountChars], eax
mov     eax, [edi+_STARTUPINFOW.dwFillAttribute]
mov     [ebp+VarStartupInfo_FillAttr], eax
mov     eax, [edi+_STARTUPINFOW.dwFlags]
mov     [ebp+VarStartupInfo_Flags], eax
mov     cx, [edi+_STARTUPINFOW.wShowWindow]
mov     [ebp+VarStartupInfo_ShowWindow], cx
...
push    eax
push    ebx
push    0FFFFFFFFh
push    [ebp+hwnd]
lea     eax, [ebp+VarStartupInfo_Title]
push    eax
push    [ebp+hMemToWinSta0_Desktop]
push    [ebp+VarExpandedCurrDir]
push    [ebp+ArgCreationFlags]
push    [ebp+arg_8]  ; Probably bInheritHandles
push    [ebp+VarExpandedCommandLine]
push    [ebp+VarExpandedApplicationName]
push    StaticBindingHandle
lea     eax, [ebp+pAsync]
push    eax
call    _RAiLaunchAdminProcess@52

RAiLaunchAdminProcess() принимает целую охапку параметров, а затем, за сценой, отправляет все это через очень гнусный MIDL-вызов. Вы увидите lea (Load Effective Address) в [ebp+VarStartupInfo_Title]. Для неассемблерных гуру: это в сущности передача адреса адреса блока данных из Title в ShowWindow. Так вот, это значит, что только ограниченное количество информации передается из STARTUPINFO в целевой процесс. Это говорит мне, что AppInfo обрабатывает не так уж много информации.

В этой точке я бросил идею создать собственную RPC-штуковину без AppInfo, две секунды вопил (жалко было нескольких потерянных дней), и начал выяснять, как приспособить для своих надобностей ShellExecuteEx().

Роутинг через ShellExecuteEx()

Я быстро решил использовать раздельный DLL/EXE-подход. Моей целевой аудиторией исходно была моя база пользователей, так что все должно было быть чисто и понятно. DLL должна экспортировать функции, выглядящие и ведущие себя как Win32 API. DLL должна также упаковывать данные от каждой функции и отправлять их EXE, который должен принять эти данные, распаковать и исполнить корректную функцию как elevated-процесс.

Сперва я хотел отправлять данные, используя член lpParameters структуры SHELLEXECUTEINFO. К моему удивлению, это привело к на редкость бессмысленному сообщению об ошибке "The data area passed to a system call is too small." (Error code 122). Длительные эксперименты показали, что к появлению этого сообщения приводит отправка более 2048 байт (2К) данных в lpParameters.

Решением было передавать ID процесса и ID потока Elevate.exe из Elevate.dll, использовать эту информацию для открытия именованного канала, и подключаться к серверу именованных каналов, работающему в Elevate.dll, для передачи информации.

Об именованных каналах

Я пришел к мысли об использовании именованных каналов, когда узнал об Integrity Levels и том факте, что объекты, созданные с более высоким IL, имеют всего лишь IL Medium... тот же уровень, что и не elevated-процессы.

Но каналы основаны на HANDLE. И в этом все дело. Помните все эти пляски с бубном, чтобы запустить elevated-процесс? Поскольку HANDLE наследуемы, процесс, который реально запускает elevated-процесс – это AppInfo, а вовсе не процесс, вызывающий ShellExecuteEx(). И, даже если бы HANDLE можно было передать, есть проблема барьера между сессиями 1 и 0. Так что простая передача значений HANDLE не сработает.

Кто-то может заметить, что способ AppInfo запускать процессы на самом деле наследует хэндлы исходных процессов и должен включать стандартные HANDLE (stdin, stdout, stderr). Однако, хотя это и правда, и HANDLE можно наследовать, AppInfo придется заполнять структуру STARTUPINFO HANDLE-ами, которые не могут пересечь барьер между сессиями 0 и 1.

Решение лежит в именованных каналах. Именованные каналы имеют, хм, имена. Иногда ANSI, иногда – UNICODE, но в любом случае – строки. И, в отличие от HANDLE, строки можно передавать через IPC. Вызов CreateNamedPipe() и CreateFile() с одной и той же строкой аналогичен использованию HANDLE, указывающего на тот же канал. И, поскольку IL обоих объектов – Medium, это работает.

MSDN подтверждает, что запуск процесса с использованием именованных каналов для перенаправления ввода/вывода возможен (http://msdn2.microsoft.com/en-us/library/ms682499.aspx).

Как работает Elevate

Предположим, что происходит вызов CreateProcessElevated(). Загружается Elevate.dll, и вызывается функция. Непосредственно перед вызовом ShellExecuteEx() создается один именованный канал и три объекта-события. Elevate.dll запускает Elevate.exe как elevated-процесс. После успешного завершения (процесс запущен), Elevate.dll ждет, когда Elevate.exe завершит инициализацию. Обратите внимание, что таймаут для событийных объектов – 10 секунд. Это во избежание зависания приложения.

Когда два процесса синхронизированы, Elevate.dll упаковывает данные, отправляемые функции, и отсылает их как пакет данных процессу Elevate.exe. Elevate.exe получает и распаковывает пакет.

Теперь Elevate.exe готовится запустить процесс. Оно подключается к консоли процесса, запустившего Elevate.exe. Кроме того, я часто использую флаг STARTF_USESTDHANDLES, и Elevate.exe берет имена именованных каналов и настраивает соответствующие HANDLE для stdin, stdout, и stderr наряду с построением остальной части структуры STARTUPINFO. Подключение к консоли дает доступ к хэндлам stdin, stdout, и stderr этой консоли.

После этого выполняется корректная функция с корректными параметрами для всего на свете. Единственное, что нужно отметить – использование флага CREATE_SUSPENDED. Этот флаг запускает поток приостановленным. Причина этого в том, что информация о процессе и основном потоке отправляется обратно Elevate.dll. Помните, сам HANDLE передать нельзя, но ID процесса и потока – можно. Также помните, что срок жизни запущенного процесса может оказаться столь кратким, что он завершится раньше, чем Elevate.dll откроет соответствующие HANDLE для синхронизации.

Затем Elevate.dll получает информацию о процессе, обрабатывает ее и создает событие, оповещающее об этом Elevate.exe.

Elevate.exe возобновляет исполнение процесса (если это возможно) и завершает свою работу. Elevate.dll также возвращает управление, и вызывающая сторона продолжает нормальную работу.

UAC Permanent Links

Elevation API DLL (Elevate.dll) экспортирует следующие функции:

Link_Create()
Link_CreateAsUser()
Link_CreateWithLogon()
Link_CreateWithToken()
Link_Destroy()
Link_CreateProcessA()
Link_CreateProcessW()
Link_ShellExecuteExA()
Link_ShellExecuteExW()
Link_ShellExecuteA()
Link_ShellExecuteW()
Link_LoadLibraryA()
Link_LoadLibraryW()
Link_SendData()
Link_GetData()
Link_SendFinalize()

Вы можете поинтересоваться, для чего они нужны? Термин UAC Permanent Link – это мое изобретение. Ничего ведь нет плохого в маленькой художественной лицензии, так?

Что же такое UAC Permanent Link? Предположим, у вас есть приложение, которое вы написали и которое прекрасно работало под Windows XP, но для Vista потребовалось создавать отдельную версию из-за проблем с правами. Вы приделали щит везде, где нужно, и выпустили обновленную версию.

Однако некоторые пользователи жалуются на избыточное использование диалога UAC. Тем не менее, вы не хотите конвертировать все приложение. Здесь на помощь приходят UAC Permanent Links.

Link_Create() создает экземпляр UAC Permanent Link. Она запускает Elevate.exe, которое выводит диалог UAC. Пользователь щелкает по "Allow", и Elevate.exe запускается. HANDLE, который возвращает Link_Create(), затем можно передать другим функциям Link_...(). Когда приложение завершит использование UAC Permanent Link (то есть закончит работу), вызывается Link_Destroy(). Elevate.exe завершает работу, и все ресурсы освобождаются.

Возможности для этого безграничны. После вывода диалога UAC у вашей программы появляются возможности работы в пространстве elevated-процессов.

CreateProcessElevated() и другие экспортируемые функции создают экземпляр UAC Permanent Link, вызывают Link_CreateProcess() или более подходящую функцию, а затем вызывают Link_Destroy().

Рассмотрим не запуск процессов, а создание DLL. Можно даже выводить диалоги, возвращающие данные не-elevated процессам. Это сложно, поскольку все посылаемые/получаемые данные должны быть сериализованы/десериализованы, но это может быть выгодным так как это позволит избежать многократного запуска процессов. Может также оказаться важной и двухсторонняя коммуникация с не-elevated приложением. Если вы выберете этот путь, рекомендую посмотреть исходный код Elevate, чтобы понять, как все это работает.

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

Хорошо и то, что UAC Permanent Links вполне укладывается в рекомендации Microsoft по использованию UAC.

Исходный код

Скачавшие исходный код, обратите внимание – исходный код представляет собой только Application Layer (в терминологии безопасного C++). Base Library не включена. Исходный код выложен, чтобы желающие могли поискать или даже исправить баги в нем. Кроме этого, он позволит вам более-менее следовать изложенному в этой статье.

Еще о .manifest

Как говорилось выше, ассоциированный с исполняемым файлом файл манифеста может иметь уровни requestedExecutionLevel «asInvoker», «requireAdministrator» или «highestAvailable». «asInvoker» значит, что процесс может запустить не-elevated токен (или имеющий более высокие права). «requireAdministrator» значит, что процесс может запустить elevated-токен (и выше). «highestAvailable» значит, что процесс будет запущен на самом высоком уровне, разрешенном разделенным токеном пользователя.

Я уже говорил об опции uiAccess. uiAccess должна давать избранному набору не-elevated процессов доступ к GUI elevated-процессов. Это предназначено для задач автоматизации UI (например, записи и воспроизведения макросов). В большинстве случаев значение этой опции должно быть «false».

Но что, если задать ей значение true? Что ж, приготовьтесь тратить деньги. Вы увидите рекомендации относительно Verisign Code Signing Certificates, за которые просят какие-то несуразные баксы ($400 в год). Вместо этого можно попробовать одну из компаний, перечисленных в http://msdn2.microsoft.com/en-us/library/ms995347.aspx, только убедитесь, что они поддерживают Code Signing (и, возможно, Time Stamping – чем больше, тем лучше).

Согласно комментариям (https://author.tucows.com/certs.php), Tucows.com предлагает сертификаты Comodo Code Signing за $75 в год.

Если вы хотите только попробовать, можно не тратить деньги, но потребуются последние Authenticode-утилиты и EXE, который надо подписать, с манифестом, содержащим uiAccess со значением true. Если все это есть, выполните следующие команды из elevated-командной строки.

1) Создайте доверенный корневой сертификат:


Перейдите в каталог, где должна находиться копию сертификата.
Из командной строки выполните следующее:

   makecert -r -pe -n "CN=Test Certificate" -ss PrivateCertStore testcert.cer

   certmgr.exe -add testcert.cer -s -r localMachine root

2) Подпишите файл:


   SignTool sign /v /s PrivateCertStore /n "Test Certificate"
            /t http://timestamp.verisign.com/scripts/timestamp.dll
            YourApp.exe

   Здесь YourApp.exe – ваше приложение.

Примечание: Если подписать исполняемый файл сертификатом из доверенного источника, оранжевый диалог UAC станет серым и скучным.

IShellExecuteHook vs. ShellExecuteWithHookElevated()

Те, кто использует IShellExecuteHook DLL, вероятно, в курсе, что Microsoft объявил этот СОМ-интерфейс «устаревшим» в Vista, и не предложил альтернативы. По умолчанию он выключен, из-за проблем со старыми хуками, вызывающими крах оболочки Vista. Включить его можно так:

[HKLM or HKCU]\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer

EnableShellExecuteHooks=1 (DWORD)

Альтернативой созданию IShellExecuteHook DLL является перехват ShellExecute(), ShellExecuteEx() и IsUserAnAdmin().Там, где эти функции вызывают IsUserAnAdmin(), замените вызов ShellExecute() вызовом ShellExecute...Elevated(). Этот метод несовершенен, поскольку различные СОМ-интерфейсы пропускают API на пути к механизму elevation. Вам также придется соблюдать осторожность, чтобы не попасть в бесконечный цикл (пакет Elevate вызывает ShellExecuteEx()).

Другой подход заключается в комбинации двух, упомянутых выше. Напишите IShellExecuteHook DLL и затем перехватите также IsUserAnAdmin(). Это дает лучший шанс отловить все, что касается UAC, но придется беспокоиться уже о том, заработает ли СОМ-интерфейс.

Ссылки


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

Copyright © 1994-2016 ООО "К-Пресс"