Перейти от обычного процесса (unelevated) к процессу с повышенными правами (elevated) - легко. К примеру, вы можете запустить процесс с повышением прав, используя в
ShellExecute
или ShellExecuteEx
действие (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;Функция
FindDesktopFolderView
извлекает Shell View Рабочего Стола. Мы уже видели похожий код ранее, так что в целом код должен быть знаком, за исключением вызова FindWindowSW
- потому что мы заинтересованы в одном конкретном окне, а не перечисляем все окна подряд.Первый параметр для
FindWindowSW
задаёт папку, которую мы ищем. Мы используем Рабочий Стол не потому, что он имеет какой-то особенный смысл, а лишь потому, что он всегда существует.Второй параметр зарезервирован и всегда должен быть пустым
(VT_EMPTY)
.Третий параметр описывает тип окон, которые мы ищем. Мы используем специальный флаг
SWC_DESKTOP
(доступный, начиная с Windows Vista), чтобы сказать: "Эй, я знаю, что Рабочий Стол - это не совсем обычная вещь, которую имеют в виду люди, когда они ищут окна Проводника, но я знаю, про что я говорю, так что верните-ка мне его".Четвёртый параметр принимает описатель найденного окна, который бесполезен для наших целей, но он обязательный, поэтому нам нужно его задать.
Пятый параметр задаёт настройки поиска. Мы используем опцию
SWFO_NEEDDISPATCH
, чтобы сказать: "Пожалуйста, помести в шестой параметр 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;Функция
GetDesktopAutomationObject
ищет папку Рабочего Стола, а затем берёт у неё dispatch-объект для её View. После чего возвращает его в форме, запрошенной вызывающим. Этим dispatch-объектом является ShellFolderView, а соответствующим интерфейсом для него будет IShellFolderViewDual, так что большинство вызывающих будут запрашивать именно его, но если вы - мазохист, то можете работать напрямую с 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;Функция
ShellExecuteFromExplorer
сначала получает объект automation папки Рабочего Стола. Как и с View Рабочего Стола, объект ShellFolderView
не интересен нам сам по себе. Он интересует нас лишь как объект, созданный в процессе, который является сервером для View Рабочего Стола (иными словами, в главном процессе Проводника). Поэтому из ShellFolderView мы берём свойство Application
, получая главный объект Shell.Application
, у которого есть интерфейс IShellDispatch
(а также его расширенные варианты IShellDispatch2
-IShellDispatch6
) и его родные эквиваленты. И здесь нас интересует метод IShellDispatch2.ShellExecute
."Ты никогда меня не любил. Ты просто использовал меня, чтобы вступить в мою семью" - рыдает View папки Оболочки.
И мы вызываем
IShellDispatch2.ShellExecute
с подходящими параметрами. Заметьте, что параметры к IShellDispatch2.ShellExecute
передаются в другом порядке, чем параметры к ShellExecute
!Окей, теперь давайте проверим этот код в небольшой демонстрационной программе:
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\ |
Редактирует рисунок в графическом редакторе без повышения прав. |
Эта программа практически идентична программе-примеру Execute in Explorer.
Примечание переводчика: финальный код с обработкой ошибок можно взять здесь. Кроме того, вам может пригодиться готовый код по запуску частей самого себя с повышением (например, для установки обновлений или регистрации глобальных файловых ассоциаций). Взять его можно тут. Код написан с претензией на качество, но на коленке - так что перепроверьте на всякий случай.
Я обычно поступаю несколько иным способом. а именно:
ОтветитьУдалить1. пользователь с ограниченными правами запускает приложение в обычном режиме
2. приложение видит что ему требуется повышение прав и стартует свою копию через RUNAS, передавая новой копии некий указатель на себя и засыпает.
3. из приложение с повышением, при необходимости делается запрос спящей копии, которая изначально была запущена из контекста обычного пользователя
4. спящая копия выполняет переданную команду (в частности запускает что-то на исполнение).
Дешево и сердито :)
Все работает, но есть нюанс - при вызове из реального приложения вызываемый процесс появляется на заднем плане, т.е. уже за нашим окном...
ОтветитьУдалитьА если так:
ОтветитьУдалитьLogonUser(NewUserName, Password, ..., phToken);//в начале программы
...
ImpersonateLoggedOnUser(phToken);
try
//что-то делать, имея права NewUserName
finally
RevertToSelf;
end;
...
CloseHandle(phToken); //в конце программы