вторник, 19 ноября 2013 г.

Как мне запустить ограниченный процесс из моего повышенного процесса и наоборот?

Это перевод How can I launch an unelevated process from my elevated process and vice versa? Автор: Реймонд Чен.

Перейти от обычного процесса (unelevated) к процессу с повышенными правами (elevated) - легко. К примеру, вы можете запустить процесс с повышением прав, используя в Shell­Execute или Shell­Execute­Ex действие (verb) "runas".

А вот в обратную сторону всё намного сложнее. Во-первых, убрать из вашего токена его "повышенную" сущность - уже непросто. А, во-вторых, даже если вы сможете это сделать, это не будет корректным - потому что начальный пользователь мог быть не администратором.

Позвольте мне подробнее рассказать об этом.

Возьмите любого пользователя, не являющегося администратором. Когда этот пользователь хочет запустить программу с повышением прав (элевацией), система покажет диалоговое окно, которое говорит: "Эй, слушай, раз уж ты не администратор, то мне потребуется логин и пароль кого-то, кто является таковым". Когда такое происходит, то повышенная программа будет работать не под учётной записью оригинального пользователя, а под учётной записью другого пользователя: пользователя-администратора. Даже если программа, запущенная с повышением прав, попытается убрать это "повышение" из своего токена, то в результате она получит всего лишь обычный, ограниченный токен пользователя-администратора, а не токен исходного пользователя.

Предположим, в системе есть пользователи Алиса (Администратор) и Боб (обычный/ограниченный пользователь). В систему входит Боб и запускает приложение LitWare Dashboard, которое требует повышения прав. Появляется диалог с запросом учётных данных администратора, Боб зовёт Алису, чтобы она ввела свои данные. Алиса вводит свой логин и пароль, так что LitWare запускается и работает под учётной записью Алисы.

Теперь предположим, что LitWare Dashboard хочет запустить браузер, чтобы показать какое-то web-содержимое. Поскольку нет никакой причины для запуска браузера с повышенными правами для простого показа HTML, то приложение пытается понизить права (unelevate) браузера, чтобы уменьшить площадь для атаки на уязвимости кода. Если приложение просто "подчистит" свой токен и использует его для запуска браузера, то оно запустит браузер, работающий под ограниченной учётной записью Алисы. Но, вероятнее всего, LitWare Dashboard хочет запустить браузер для Боба - поскольку именно Боб запустил приложение.

Решение заключается в том, чтобы вернуться к Проводнику и попросить его запустить вам программу. Поскольку Проводник работает под учётной записью оригинального пользователя, то программа (в нашем случае - браузер) будет запущена под Бобом.

Это также важно для случаев, когда обработчик открываемого файла работает как in-process расширение а не отдельным процессом - потому что в этом случае попытка обрезать токен будет бессмысленна, поскольку никакого нового процесса запускаться вообще не будет (и если обработчик файла попытается установить связь с ограниченной копией самого себя, то ему это не удастся - из-за UIPI).

Ладно, я знаю, что у Маленьких Программ нет мотивации делать всё правильно, но я не смог сдержаться. Хватит болтовни. Берём в руки клавиатуру.

(Только не забудьте, что Маленькие Программы практически не выполняют проверку на ошибки - потому что "that's the way they roll")
procedure FindDesktopFolderView(const RIID: TGUID; var PPV);
var
  ShellWindows: IShellWindows;
  Browser: IShellBrowser;
  Disp: IDispatch;
  ServiceProvider: IServiceProvider;
  View: IShellView;
  Loc: OleVariant;
  Empty: OleVariant;
  Wnd: Integer;
begin
  CoCreateInstance(CLASS_ShellWindows, nil, CLSCTX_ALL, IShellWindows, ShellWindows);

  Loc := CSIDL_DESKTOP;
  VarClear(Empty);
  Disp := ShellWindows.FindWindowSW(Loc, Empty, SWC_DESKTOP, Wnd, SWFO_NEEDDISPATCH);

  Disp.QueryInterface(IServiceProvider, ServiceProvider);
  ServiceProvider.QueryService(SID_STopLevelBrowser, IShellBrowser, Browser);
  Browser.QueryActiveShellView(View);
  View.QueryInterface(RIID, PPV);
end;
Функция Find­Desktop­Folder­View извлекает Shell View Рабочего Стола. Мы уже видели похожий код ранее, так что в целом код должен быть знаком, за исключением вызова Find­Window­SW - потому что мы заинтересованы в одном конкретном окне, а не перечисляем все окна подряд.

Первый параметр для Find­Window­SW задаёт папку, которую мы ищем. Мы используем Рабочий Стол не потому, что он имеет какой-то особенный смысл, а лишь потому, что он всегда существует.

Второй параметр зарезервирован и всегда должен быть пустым (VT_EMPTY).

Третий параметр описывает тип окон, которые мы ищем. Мы используем специальный флаг SWC_DESKTOP (доступный, начиная с Windows Vista), чтобы сказать: "Эй, я знаю, что Рабочий Стол - это не совсем обычная вещь, которую имеют в виду люди, когда они ищут окна Проводника, но я знаю, про что я говорю, так что верните-ка мне его".

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

Пятый параметр задаёт настройки поиска. Мы используем опцию SWFO_NEED­DISPATCH, чтобы сказать: "Пожалуйста, помести в шестой параметр IDispatch".

И шестой параметр - это куда мы хотим поместить возвращаемый IDispatch.

procedure GetDesktopAutomationObject(const RIID: TGUID; var PPV);
var
  SV: IShellView;
  DispView: IDispatch;
begin
  FindDesktopFolderView(IShellView, SV);
  SV.GetItemObject(SVGIO_BACKGROUND, IDispatch, Pointer(DispView));
  DispView.QueryInterface(RIID, PPV);
end;
Функция Get­Desktop­Automation­Object ищет папку Рабочего Стола, а затем берёт у неё dispatch-объект для её View. После чего возвращает его в форме, запрошенной вызывающим. Этим dispatch-объектом является Shell­Folder­View, а соответствующим интерфейсом для него будет IShell­Folder­View­Dual, так что большинство вызывающих будут запрашивать именно его, но если вы - мазохист, то можете работать напрямую с IDispatch и его Invoke.
procedure ShellExecuteFromExplorer(const AFile: WideString; const AParameters: WideString = ''; const ADirectory: WideString = ''; const AOperation: WideString = ''; const AShowCmd: Cardinal = SW_SHOWNORMAL);
var
  FolderView: IShellFolderViewDual;
  DispShell: IDispatch;
  ShellDispatch: IShellDispatch2;
begin
  GetDesktopAutomationObject(IShellFolderViewDual, FolderView);
  FolderView.get_Application(DispShell);

  DispShell.QueryInterface(IShellDispatch2, ShellDispatch);
  ShellDispatch.ShellExecute(PWideChar(AFile), AParameters, ADirectory, AOperation, AShowCmd);
end;
Функция Shell­Execute­From­Explorer сначала получает объект automation папки Рабочего Стола. Как и с View Рабочего Стола, объект Shell­Folder­View не интересен нам сам по себе. Он интересует нас лишь как объект, созданный в процессе, который является сервером для View Рабочего Стола (иными словами, в главном процессе Проводника). Поэтому из Shell­Folder­View мы берём свойство Application, получая главный объект Shell.Application, у которого есть интерфейс IShell­Dispatch (а также его расширенные варианты IShell­Dispatch2-IShell­Dispatch6) и его родные эквиваленты. И здесь нас интересует метод IShell­Dispatch2.Shell­Execute.

"Ты никогда меня не любил. Ты просто использовал меня, чтобы вступить в мою семью" - рыдает View папки Оболочки.

И мы вызываем IShell­Dispatch2.Shell­Execute с подходящими параметрами. Заметьте, что параметры к IShell­Dispatch2.Shell­Execute передаются в другом порядке, чем параметры к Shell­Execute!

Окей, теперь давайте проверим этот код в небольшой демонстрационной программе:
program Scratch;

{$APPTYPE CONSOLE}

uses
  Windows,
  ActiveX,
  SysUtils,
  ShellExecuteUnelevated;

begin
  CoInitialize(nil);
  try
    try
      ShellExecuteFromExplorer(ParamStr(1), ParamStr(2), ParamStr(3), ParamStr(4), StrToIntDef(ParamStr(5), SW_SHOWNORMAL));
    except
      on E: Exception do
        WriteLn(E.ClassName + ': ' + E.Message);
    end;
  finally
    CoUninitialize;
  end;
end.
Программа требует не пустого первого аргумента командной строки - то, что будем открывать, будь это программа, документ или URL. Кроме обязательного первого есть ещё четыре опциональных параметра: аргументы, каталог, действие и положение окна.

Запустите CMD.exe под администратором, зайдите в папку со Scratch.exe и протестируйте её работу:

scratch http://www.msn.com/ Открывает страницу в браузере по умолчанию без повышения прав.
scratch cmd.exe "" C:\Users "" 3 Открывает командную строку без повышения прав в папке C:\Users, развёрнутую на весь экран.
scratch C:\Path\To\Image.bmp "" "" edit Редактирует рисунок в графическом редакторе без повышения прав.

Эта программа практически идентична программе-примеру Execute in Explorer.

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

3 комментария:

  1. Я обычно поступаю несколько иным способом. а именно:
    1. пользователь с ограниченными правами запускает приложение в обычном режиме
    2. приложение видит что ему требуется повышение прав и стартует свою копию через RUNAS, передавая новой копии некий указатель на себя и засыпает.
    3. из приложение с повышением, при необходимости делается запрос спящей копии, которая изначально была запущена из контекста обычного пользователя
    4. спящая копия выполняет переданную команду (в частности запускает что-то на исполнение).

    Дешево и сердито :)

    ОтветитьУдалить
  2. Все работает, но есть нюанс - при вызове из реального приложения вызываемый процесс появляется на заднем плане, т.е. уже за нашим окном...

    ОтветитьУдалить
  3. А если так:
    LogonUser(NewUserName, Password, ..., phToken);//в начале программы
    ...
    ImpersonateLoggedOnUser(phToken);
    try
    //что-то делать, имея права NewUserName
    finally
    RevertToSelf;
    end;
    ...
    CloseHandle(phToken); //в конце программы

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.