воскресенье, марта 15, 2009

Использование Vista UAC в Delphi: Часть 3

Это заключительная часть серии из трех частей о UAC и Delphi.
Перейти к части 1
Перейти к части 2

Здесь я затрону наиболее интересные подходы к обеспечению подлинного UAC соответствия – в том виде, в каком вероятно, следует реализовывать большинство Vista-совместимых приложений.

Повышение прав по запросу с использованием COM Elevation Moniker

Первая вещь, которую в которой вы должны быть уверены, это то, что исполняемый файл приложения содержит UAC манифест. Манифест, практически идентичный тому, который вы могли видеть в части 2, с одним, но очень важным отличием: атрибут requestedExecutionLevel должен иметь значение "asInvoker":

  1. <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  2. <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  3. <assemblyIdentity
  4. version="1.0.0.0"
  5. processorArchitecture="*"
  6. name="UACAwareApplication"
  7. type="win32"/>
  8. <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
  9. <security>
  10. <requestedPrivileges>
  11. <requestedExecutionLevel level="asInvoker"/>
  12. </requestedPrivileges>
  13. </security>
  14. </trustInfo>
  15. </assembly>

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

Прежде чем продолжить, я объясню, как повышение привилегий работает изнутри…

Когда процесс стартует, ему присваивается уровень доступа Windows, который контролирует то, что этот процесс может или не может сделать, с некоторыми объектами, такими как файлы и каталоги, имеющими списки управления доступом (Access Control Lists или ACL's, для краткости) к себе. Что здесь важно, так это то, что уровень доступа, который присваивается фактически единожды, в момент запуска процесса, не может быть изменен на более поздних этапах жизни процесса. Следовательно, повышение привилегий, возможно одним единственным способом - порождением нового процесса с более высоким уровнем привилегий. Зная об этом, менее требовательные приложения, которые не нуждаются в использовании сложных состояний пользовательского интерфейса, могут просто перезапускать себя в админском режиме и работать.

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


Вместо этого, есть другое решение для таких случаев. Код приложения может быть разбит на две части – не привилегированный код размещается в основном исполняемом файле, в то время, как код, требующий привилегий выносится в отдельную dll, оформленную как один или несколько COM объектов. COM объекты могут вызываться COM посредствам Elevation Moniker в контексте выделенного COM-сервера, который и повышает привилегии.

Довольно теории. Выполним некоторые действия.

Здесь мы должны сделать следующее:

написать dll, которая будет загружаться выделенным COM – сервером;

добавить COM-объект, реализующий функциональность, требующую привилегий;

добавить COM class factory, которая будет использоваться для создания экземпляра COM объекта и регистрации библиотеки, и

написать какой-то код для вызова COM объекта из главного исполняемого файла, который изначально не обладает привилегиями.

До настоящего момента, ничего не говорилось о создании dll. Эта обычная COM библиотека может быть создана в Delphi путем следующих действий:

"New" -> "Other..." -> "Delphi Projects"\"ActiveX" -> "ActiveX Library", как это показано на скриншоте.

clip_image002[1]

Как только мы создали и сохранили новый COM library проект, нам необходимо добавить новый COM объект, в котором мы реализуем функционал повышения привилегий.


clip_image004[1]

В демонстрационных целях мы определим единственный метод COM объекта, и опишем его с помощью интерфейса объекта по умолчанию:


  1. IMyPrivilegedObject = interface(IUnknown)
  2. ['{04004D01-2115-40D5-991F-D258C8CEF07E}']
  3. function CreateFile(const aFileName: WideString): HResult; stdcall;
  4. end;

Этот метод позволит нам проверить, действительно ли COM объект привязывается к процессу повышения привилегий.

Теперь нам нужно создать пользовательскую фабрику классов (Class Factory) для нашего COM объекта. Это нужно по той причине, что Class Factory отвечает за регистрацию и удаление (unregistering) COM объекта в системном реестре. Мы не можем просто по умолчанию использовать TTypedComObjectFactory, потому существуют некоторые специальные значения, которые должны быть прописаны в реестре, для того, чтобы сделать возможным размещение вне процесса и независимое повышение привилегий.

Приложения не могут просто запрашивать у Windows повышение привилегий подобно тому, как это делает любой OLE код. Если бы оно могло делать это, это была бы серьезная дыра в системе безопасности. Любой код, который должен быть выполнен с повышенными привилегиями, должен быть помещен в COM объект, расположенный в dll, который должен быть подготовлен к повышению привилегий.

Здесь основная часть реализации фабрики классов.

  1. type
  2. TPrivilegedClassFactory = class(TTypedComObjectFactory)
  3. private
  4. fResourceId: AnsiString;
  5. public
  6. constructor Create(
  7. const aResourceId: AnsiString;
  8. aComServer: TComServerObject;
  9. aTypedComClass: TTypedComClass; const aClassID: TGUID;
  10. aInstancing: TClassInstancing;
  11. aThreadingModel: TThreadingModel = tmSingle
  12. );
  13. procedure UpdateRegistry(aRegister: Boolean); override;
  14. end;
  15.  
  16. implementation
  17.  
  18. constructor TPrivilegedClassFactory.Create(const aResourceId: AnsiString;
  19. aComServer: TComServerObject; aTypedComClass: TTypedComClass;
  20. const aClassID: TGUID; aInstancing: TClassInstancing;
  21. aThreadingModel: TThreadingModel);
  22. begin
  23. inherited Create(
  24. aComServer, aTypedComClass, aClassID, aInstancing, aThreadingModel
  25. );
  26. { Save the id of the string resource, that holds the application name. }
  27. fResourceId := aResourceId;
  28. end;
  29.  
  30. procedure TPrivilegedClassFactory.UpdateRegistry(aRegister: Boolean);
  31. var
  32. ID, ClassKey, FullFileName, FileName: AnsiString;
  33. begin
  34. ID := GUIDToString(Self.ClassID);
  35. ClassKey := 'CLSID\' + ID;
  36. FullFileName := ComServer.ServerFileName;
  37. FileName := ExtractFileName(FullFileName);
  38. try
  39. if aRegister then begin
  40. inherited UpdateRegistry(aRegister);
  41. { DLL out-of-process hosting requirements. }
  42. CreateRegKey('AppID\' + ID, '', Description);
  43. CreateRegKey('AppID\' + ID, 'DllSurrogate', '');
  44. CreateRegKey('AppID\' + FileName, 'AppID', ID);
  45. { Over-The-Shoulder activation requirements. }
  46. SetAccessPermissionsForLUAServer('AppID\' + ID, 'AccessPermission');
  47. { COM object elevation requirements. }
  48. CreateRegKey(ClassKey, 'AppID', ID);
  49. CreateRegKey(ClassKey, 'LocalizedString', '@' + FullFileName + ',-' + fResourceId);
  50. CreateRegKeyEx(ClassKey + '\Elevation', 'Enabled', '1', nil, 0, REG_DWORD);
  51. end else begin
  52. DeleteRegKey(ClassKey + '\Elevation');
  53. DeleteRegKey('AppID\' + ID);
  54. DeleteRegKey('AppID\' + FileName);
  55. inherited UpdateRegistry(aRegister);
  56. end;
  57.  
  58. except
  59. on E: EOleRegistrationError do raise;
  60. on E: Exception do raise EOleRegistrationError.Create(E.Message);
  61. end;
  62. end;

Как отмечено в комментариях, существуют три требования, которые должны быть удовлетворены: регистрация размещения вне процесса, регистрация «обратной» (Over-The-Shoulder) активации, регистрация превышения привилегий. Вы можете найти объяснение каждого из требований здесь, поэтому нет необходимости дублировать информацию.

Я только хочу отметить, что такие фабрики классов являются типичными, и могут быть использованы в любых Delphi приложениях, требующих повышения привилегий. Параметр aResourceId хранит ID ресурсной строки, которая будет отображаться в запросе на превышение в качестве имени приложения. Будьте готовы использовать валидный ID из строковой таблицы dll!

Теперь мы должны использовать новую фабрику классов для создания COM объекта, и встроить строковый ресурс в нашу dll:

  1. initialization
  2. TPrivilegedClassFactory.Create(
  3. '101', // resource string id
  4. ComServer, TMyPrivilegedObject, Class_MyPrivilegedObject,
  5. ciMultiInstance, tmApartment
  6. );
Это файл ресурсов со строкой, которая будет отображаться в запросе на превышение привилегий.

STRINGTABLE

{

101, "Delphi Elevation Demo"

}

Последняя вещь, которую осталось сделать, это написать процедуру, которая будет вызываться из не привилегированного исполняемого файла в процессе вызова COM  объекта. Вот она:
  1. procedure CoCreateInstanceAsAdmin(
  2. aHWnd: HWND; // parent for elevation prompt window
  3. const aClassID: TGUID; // COM class guid
  4. const aIID: TGUID; // interface id implemented by class
  5. out aObj // interface pointer
  6. );
  7.  
  8. implementation
  9.  
  10. procedure CoCreateInstanceAsAdmin(aHWnd: HWND; const aClassID, aIID: TGUID;
  11. out aObj);
  12. var
  13. BO: BIND_OPTS3;
  14. MonikerName: WideString;
  15. begin
  16. if (not IsElevated) then begin
  17. { Request elevated out-of-process instance. }
  18. MonikerName := 'Elevation:Administrator!new:' + GUIDToString(aClassID);
  19. FillChar(BO, SizeOf(BIND_OPTS3), 0);
  20. BO.cbStruct := SizeOf(BIND_OPTS3);
  21. BO.dwClassContext := CLSCTX_LOCAL_SERVER;
  22. BO.hwnd := aHWnd;
  23. OleCheck(CoGetObject(PWideChar(MonikerName), @BO, aIID, aObj));
  24. end else
  25. { Request normal in-process instance. }
  26. OleCheck(CoCreateInstance(aClassID, nil, CLSCTX_ALL, aIID, aObj));
  27. end;

Здесь находится полный исходный код ElevationDemo в формате Delphi 2006 for Win32 проекта вмести с скомпилированными и подписанными бинарниками, так что вы можете протестировать их без компиляции. Конечно, вы для этого должны использовать Windows Vista. Когда вы запустите исполняемый файл, вы увидите нечто подобное:
clip_image006[1]

Чекбокс определяет будет ли вызываться COM объект, запрашивающий повышение привилегий или локальный COM объект, в случае вызова CreateFile(). Вы можете поэкспериментировать, вводя разные имена файлов в поле ввода.

Исходный код создан с использованием некоторых специфических Vista-структур, но для простоты я не включал код проверки ОС, пусть это будет домашним заданием для читателя.

Не забудьте сначала зарегистрировать DLL (для этого вам понадобятся привилегии Admin’а), в противном случае приложение не заработает. Обычно, это задача инсталлятора, но вы это можете сделать вручную:

regsvr32.exe PrivilegedLib.dll

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


  1. HKEY_LOCAL_MACHINE\SOFTWARE\Classes
  2. {
  3. AppID\PrivilegedLib.dll
  4. {
  5. (Default): REG_SZ = null
  6. AppID: REG_SZ = '{6BCFB187-C1DD-4807-96AD-F91AB4AB08AC}'
  7. }
  8. AppID\{6BCFB187-C1DD-4807-96AD-F91AB4AB08AC}
  9. {
  10. (Default): REG_SZ = 'MyPrivilegedObject'
  11. AccessPermission: REG_BINARY = <BINARY VALUE>
  12. DllSurrogate: REG_SZ = ''
  13.  
  14. }

Если все прошло нормально, вы должны будете увидеть один из следующих диалогов, после того, как вы выполните вызов запроса на превышение привилегий – вид диалога зависит от ваших настроек безопасности.

clip_image007[1]

clip_image008[1]

Ну вот и все. Я надеюсь, что вам понравилась эта серия. Я уверен, что показал все важнейшие шаги по созданию UAC-aware приложения в Delphi. Естественно, что я опустил массу деталей, чтобы не слишком вас утомлять – но это тема для отдельной публикации на будущее. Как всегда, присылайте свои комментарии и пожелания.