Защита в DCOM/COM+

Автор: Владислав Чистяков
The RSDN Group

Источник: RSDN Magazine #0
Опубликовано: 28.01.2002
Версия текста: 1.2.1
Защита в Windows
Дескриптор защиты
Защита в COM
Права активации
Права доступа
CoInitializeSecurity
Алгоритм вычисления атрибутов безопасности для proxy (Security Blanket Negotiation)
Dcomcnfg.exe
IClientSecurity
IServerSecurity
Демонстрационные приложения
Установка примера ComSec
SecTest
Имперсонация
COM+
Ролевая безопасность
Контекст вызова (call context)
Подводные камни

Исполняемые файлы для примеров ComSec и ComSrvEvents (ComSecModules.zip - 109 KB)
Исходные тексты для примера ComSec - 32 KB
Исходные тексты для примера ComSrvEvents - 34 KB

COM+ в W2k радикально упростил работу с защитой, но, как ни крути, она базируется на защите DCOM, а та, в свою очередь, основывается на защите Windows. Для полного понимания некой предметной области нужно начинать с основ. Поэтому стоит последовательно разобрать систему защиты на этих трёх уровнях. Начнем с защиты Windows.

Защита в Windows

Так как в Windows 9x состояние подсистемы безопасности близко к коматозному, то далее в этом разделе речь пойдет только о Windows NT (и, разумеется, ее потомках – Windows 2000 (W2k), Windows XP и Windows.Net). Если речь все-таки зайдет о Windows 9x, то об этом будет сказано отдельно.

Итак, Windows NT (далее просто NT) – это ОС, поддерживающая защиту класса C2 (начиная с W2k даже чуть больше, за счет возможности шифрования файлов). Это означает, что она обеспечивает защиту в момент, когда компьютер включен и данная ОС загружена. Если компьютер запущен под управлением другой ОС, безопасность не гарантируется.

Как же устроена защита в NT? Без ответа на этот вопрос практически невозможно объяснить, как устроена безопасность в COM. Но полный и развернутый ответ приведет к слишком большому объему информации, который не смогут стерпеть не только страницы этого журнала, но и большинство читателей. :) Поэтому я попробую дать краткое и поверхностное представление о защите в NT. Те, кто уже знаком с этим вопросом, могут или пропустить данный раздел, или просмотреть его мельком, попутно обновив знания и воспроизведя общую картину в своем сознании.

Первое, что нужно понять - NT автоматически обеспечивает защиту только объектов ядра. Остальные объекты могут быть защищены опосредованно через объекты ядра. Например, окно не является объектом ядра и не может иметь самостоятельных атрибутов защиты. Однако, и окна можно защитить, но об этом чуть ниже.

Для начала взгляните на рисунок 1.


Рисунок 1. Схематичное представление защищенных объектов в Windows.

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

Итак перечислим эти сущности:

Я не упомянул еще одну сущность – Desktop (рабочий стол). Он также может участвовать в системе защиты ОС, но практически не оказывает влияния на работу серверных приложений, так как больше относится к взаимодействию с пользователем.

Кратко пройдемся по перечисленным выше понятиям...

Logon session – я намеренно не переводил это понятие на русский, так как самое близкое по звучанию словосочетание «вход в систему» больше ассоциируется с физическим входом в систему интерактивного пользователя, в то время как logon – чисто логическое понятие, подразумевающее всего лишь процесс аутентификации некоторого пользователя в системе, и не подразумевающее обязательного входа в систему интерактивного (и даже подключенного по сети) пользователя. По сути, logon может произойти и вообще без участия пользователя. Например, logon происходит каждый раз, когда система загружает сервис или COM-сервер, которому указан вариант загрузки, отличный от «Interactive User». В результате этого процесса создается logon-сессия. В терминах систем безопасности участвующая в этом процессе сущность (человек или компьютер) называется принципалом (principal). В системе информация о принципале хранится в его учетной записи. Учетная запись может быть создана не только для принципалов, но и для групп. Однако группа не может быть аутентифицирована, так как она может объединять нескольких принципалов, в том числе опосредованно, через другие группы.

Windows-станция (Windows station или, как ее иногда называют, WinSta) – это логическое пространство, позволяющее защитить окна некоторых процессов от воздействия на них извне. Windows-станции нужны потому, что окна не являются объектами ядра NT, а значит, не могут иметь самостоятельных атрибутов защиты. Любое окно в Windows ассоциировано с некоторым процессом (в котором оно было создано), а процесс, в свою очередь, ассоциирован с некоторой Windows-станцией (реально окна ассоциируются с потоками, а потоки - с десктопами, так что связь окон с оконными станциями опосредованная. Но для простоты восприятия эту особенность можно опустить). В NT только одна Windows-станция может напрямую взаимодействовать с пользователем. Эта станция имеет имя WinSta0. Windows XP и терминальный сервер позволяют создавать несколько интерактивных Windows-станций, каждая из которых, тем не менее, называется WinSta0. Эти станции работают в своих независимых пространствах имен. Неинтерактивные Windows-станции могут создавать окна и манипулировать ими так, как будто бы это нормальные окна, созданные в интерактивной Windows-станции, но эти окна не смогут быть отображены на экране без дополнительных усилий (в принципе существует возможность "на лету" подключить приложение к интерактивной Windows-станции). Как вы, наверное, уже догадались, Windows-станции могут иметь собственные атрибуты защиты. Более того, через них опосредованно и осуществляется защита окон в Windows. Если ваш процесс запущен в Windows-станции A, вы не сможете беспрепятственно послать сообщение окну из Windows-станции B. Честно говоря, в принципе это возможно, но данный процесс связан с большим количеством затруднений и полностью контролируется защитой NT. Кстати сказать, в W2k (без сервис-паков) была дыра в защите Windows, которая позволяла посылать сообщения окнам, находящимся в других Windows-станциях. COM осуществляет синхронизацию межапартаментных вызовов с помощью окон, и, стало быть, Windows-станции имеют непосредственное отношение к защите в COM, так как если бы любой желающий мог посылать сообщения в апартаментные окна, было бы возможно симулировать COM-вызовы.

Процесс – думаю, мало кому нужно объяснять, что такое процесс. Однако краткое определение дать все же стоит. Процесс – это запущенное приложение, которое содержит защищенное (принадлежащее только ему) виртуальное адресное пространство, куда загружаются код и данные. Процессу могут принадлежать некоторые ресурсы операционной системы, такие как открытые файлы, pipe-ы, синхронизирующие объекты (мьютексы, семафоры...), окна и т.п. Процесс также содержит один или более потоков исполнения (нитей), которые могут выполнять код, принадлежащий процессу. Процесс обязательно принадлежит некоторому logоn-у и также может быть ассоциирован с некоторой Windows-станцией.

Поток – логический поток выполнения программы. Два потока могут выполнять код параллельно (с помощью квантования времени или на разных процессорах). Вопреки распространенному заблуждению, код не принадлежит потокам, и функция, выполняемая в одном потоке, не может вызвать функцию в другом. По сути, так даже нельзя говорить (но говорят же!). Однако потоки можно синхронизировать друг с другом. За счет этого можно передавать управление от одного потока другому. Передача управления также возможна через каналы связи (например, pipe-ы или каналы связи COM, в COM под это дело подведена концепция апартаментов). Физически синхронизация осуществляется либо с помощью синхронизирующих примитивов, либо с помощью различного рода очередей, в частности, оконных (с помощью сообщений). При вызове функции передачи сообщения оно ставится в очередь, которая выбирается в потоке, ассоциированном с окном. COM использует эту особенность для организации апартаментов.

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

Как же организован доступ и проверка информации о защите для этих сущностей? Начнем с logon-а и разберем вход интерактивного пользователя в систему. Для начала работы пользователь должен подойти к компьютеру и... очевидно, включить его (по крайней мере один раз это сделать придется точно). :) Через некоторое время перед ним появится диалог входа в систему. Чтобы войти в систему, пользователь должен идентифицировать себя, введя пароль и имя своей (или чужой :) ) учетной записи.

Что же происходит далее?.. А далее происходит создание той самой logon-сессии. Так как logon-сессия – это абстракция, не имеющая вещественного воплощения, для взаимодействия с ней нужно некоторое её физическое представление. Таким представлением является токен. Токен содержит в себе информацию о logon-сессии пользователя, его правах и привилегиях (об этом мы поговорим немного позже).

Далее происходит создание первого процесса. Для интерактивного logon-а таким процессом является оболочка ОС (shell), имя которого записано в ветке реестра HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon. На самом деле, сначала запускается процесс, указанный в значении UserInit (userinit.exe), а уже этот процесс запускает оболочку, прописанную в значении

Shell. Обычно (но не обязательно) такой оболочкой является Windows Explorer. Первому процессу присваивается токен logon-сессии, после чего ему передается управление. С этого момента токен процесса можно узнать с помощью функции OpenProcessToken. Данной функции передается handle процесса (handle текущего можно узнать с помощью GetCurrentProcess) и информация о целях получения данного handle токена (для изменения атрибутов защиты, активации/деактивации разрешенных привилегий, подключения к процессу, дублирования, заимствования прав (имперсонации), запроса информации). Подробное описание данной функции можно найти в MSDN. Скажу только, что для самых любопытных операций всегда нужны привилегии, отсутствующие у обычных пользователей, но имеющиеся у учетной записи SYSTEM, под которой запускаются многие сервисы и сама ОС. Собственно, поэтому ОС и может выполнять столько хитрых операций, не вступая в конфликт с защитой.

Если в текущем процессе создать новый (с помощью функции CreateProcess), то ему будет присвоена копия токена. Хотя атрибуты защиты нового процесса могут и отличаться от атрибутов родительского процесса, токен, определяющий учетную запись, под которой запустится процесс, будет таким же. Такое поведение гарантирует, что новый процесс создается в рамках той же logon-сессии. Таким образом, создается иерархия процессов, находящихся в рамках одного logon-а.

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

Ассоциация потока с токеном, отличным от токена процесса, позволяет потоку прикинуться другим пользователем. Эта технология называется имперсонацией. Имея токен, ассоциированный с другой учетной записью, можно осуществлять операции над защищенными объектами так, как будто работа происходит в процессе, запущенном под другим logon-ом.

Для упрощения имперсонации в Windows имеется функция ImpersonateLoggedOnUser. Для отмены имперсонации можно воспользоваться функцией RevertToSelf. Можно также задавать вручную токен для потока (с помощью функции SetThreadToken).

В Win32 существует функция, позволяющая создать процесс от имени другого пользователя. Эта функция называется CreateProcessAsUser. Именно ею и пользуется система, когда создает первый процесс для интерактивного logon-а, SCM в момент запуска сервиса или COM SCM при запуске COM-сервера. Данная функция среди прочих параметров получает токен, содержащий информацию об учетной записи, под которой будет создан новый процесс. Токен, используемый для создания нового процесса, должен быть так называемым первичным (primary) токеном, т.е. токен, полученный для имперсонации, не может быть использован в данном контексте. Токен можно скопировать с помощью функции DuplicateTokenEx. При этом можно изменить тип токена, но на данную операцию тоже нужно иметь права. В частности, исходный токен должен быть открыт с правом доступа TOKEN_DUPLICATE. Забегая вперед, скажу, что последовательность вызовов DuplicateTokenEx, CreateProcessAsUser позволяет серверному приложению (сервису или COM-серверу) создавать процессы под учетной записью вызывающего клиента.

Для получения токена можно воспользоваться функцией LogonUser. Она создает новую logon-сессию и возвращает токен, содержащий информацию о logon-сессии. Чтобы воспользоваться ей, нужно иметь сведения об учетной записи (имя учетной записи и пароль). Кроме этого, процесс, вызывающий функцию LogonUser, должен обладать привилегией SE_TCB_NAME (известной администраторам как «Act as part of the Operating System»). Это очень мощная привилегия и обычно ее не назначают учетным записям обычных пользователей, но учетной записи сервера она вполне может быть назначена. Учетная запись SYSTEM автоматически обладает данной привилегией. (SYSTEM вообще является самой мощной записью на локальном компьютере, однако она беспомощна, если речь заходит о сети.). В разделе "Вопросы и Ответы" этого номера есть несколько Q&A, описывающих тонкости данного вопроса. Я же позволю себе пропустить эту, безусловно интересную, тему. Замечу только, что использование программного создания logon-сессии с открытым паролем, да и вообще манипуляции с паролем в программе, являются показателем «дурного тона» и неграмотного построения системы защиты. Под NT можно писать большие распределенные приложения, вообще не сталкиваясь с открытыми паролями.

Главное правило в защите под Windows – чем меньше вы ее настраиваете, тем надежнее она будет работать ;). Я, конечно, утрирую, но доля правды в этом высказывании довольно велика.

Дескриптор защиты

Каждый объект ядра обладает дескриптором защиты (Security Descriptor, SD), который, кроме всего прочего, включает в себя список идентификаторов защиты (Security Identifier, SID), представляющих собой идентификаторы учетных записей, которым разрешен или запрещен доступ к объекту.

Этот список называется DACL (читается как дакл или даже джакл). DACL расшифровывается как «Discretionary Access-Control List». По-русски это можно перевести примерно как «список, определяющий контроль над доступом». Каждая запись такого списка содержит информацию о предоставляемых правах, об учетной записи, которой предоставляются данные права (SID) и признак, определяющий, будет ли данная запись «разрешающей» или «запрещающей». Разрешающая запись обычно называемая положительной, а запрещающая – отрицательной.

Идентификаторы защиты, входящие в DACL, могут представлять как конкретного пользователя (или, начиная с W2k, компьютер), так и группу (как домена, так и локальную). Применение групп позволяет уменьшить размер DACL (тем самым ускоряя проверки защиты) и упростить администрирование.

Группа – это сущность, позволяющая объединить несколько учетных записей под одним именем (представляемым SID-ом). Например, если на машине должны работать несколько пользователей, то интерактивный вход можно разрешать не для каждого из них в отдельности, а объединить их в группу и дать такую привилегию только группе. При этом все пользователи получат возможность входить в систему интерактивно.

Пример дескриптора защиты схематично представлен на рисунке 2.


Рисунок 2. Схематичное представление дескриптора защиты.

«SID Владельца» (или Owner SID) определяет, кому принадлежит данный объект. Только один человек может быть владельцем объекта. Учетная запись, под которой создается объект, становится владельцем этого объекта. Владельцу автоматически разрешается изменять права доступа к объекту. Администратор системы (или другая учетная запись, которой это позволено) может взять на себя владение объектом. Но информация о том, что кто-то взял на себя владение объектом, будет доступна, и администратор не сможет вернуть владение учетной записи, владевшей объектом ранее. Администратор не может передать кому-либо владение объектом, он может только присвоить его себе. Таким образом, администратор не сможет поизгаляться над объектом и замести следы, вернув все атрибуты защиты в первоначальное состояние.

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

«SID первичной группы» (или, в оригинале, «Primary Group SID») – это группа, которой разрешен доступ к объекту из POSIX-совместимых приложений. Это поле дескриптора защиты нужно исключительно для совместимости со стандартом POSIX. Ввиду того, что COM-приложения и POSIX имеют мало общего, я не буду рассматривать данный вопрос.

SACL – «System Access-Control List». SACL определяет, будет ли проводиться аудит доступа к объекту. По структуре SACL аналогичен DACL за исключением того, что признак «положительности/отрицательности» определяет не разрешение или запрет доступа, а тип аудита. «+» означает, что аудит будет производиться при успешном доступе к объекту, а «-» – что аудит будет производиться при неудачной попытке доступа к объекту. Таким образом, с помощью SACL можно определять, за кем и в каком случае нужно следить.

Права

Права (access rights/permissions) – это маска доступа к объекту. Разные принципалы в Windows могут иметь разные права. Конкретные права обычно задаются администратором для каждого идентификатора защиты (учетной записи). Настраивать права можно программно или визуально. Например, задать права доступа к файлу или директории можно на странице свойств «Security» (в Explorer или другом файловом менеджере).

Для получения доступа к некоторому объекту его нужно предварительно «открыть». При открытии объекта задаётся тип доступа, который может понадобиться при работе с объектом. При этом происходит сравнение прав, назначенных администратором данному пользователю, с затребованными правами доступа. Если все нормально, ОС возвращает handle на открываемый объект. Данный handle позволяет производить операции, допускаемые затребованными при открытии правами. Если права на некоторую операцию не были затребованы при открытии handle-а, то данная операция запрещается, даже если пользователю разрешен данный вид доступа. Например, некоторому пользователю может быть разрешена запись в некоторый файл, но если файл открыт «на чтение» (с правами, допускающими только чтение), то пользователю не дадут произвести запись в файл через данный handle.

Заметьте, что после открытия некоторого handle-а проверка прав пользователя больше не производится. Этим handle-ом можно пользоваться даже из потока, выполняемого под другой учетной записью, возможно, с меньшими правами. Учитывая это, нужно быть аккуратным при построении политики безопасности серверного приложения. Нелишне напомнить, что сервер, как правило, работает под учетной записью, обладающей большими правами и привилегиями. Политика безопасности должна учитывать, что сервер предоставляет клиенту некий сервис, определяемый интерфейсом сервера. То есть клиент не должен иметь возможности выполнять какие-либо незапланированные действия. Так, с точки зрения безопасности ошибками являются создание универсального интерфейса (позволяющего обойти защиту), запуск клиентских процедур (скриптов), прямое выполнение SQL-запросов, загрузка внешних модулей и т.д.

Привилегии

Привилегии – одна из возможностей в Windows позволить некоторой учетной записи производить разные действия над системой.

Привилегии – это права учетной записи пользователя или группы производить с системой различные операции на локальном компьютере, например, выключение компьютера, загрузку драйверов устройств или изменение системного времени. Привилегии и права доступа – это разные вещи:

  1. Привилегии управляют доступом к действиям на системном уровне, а права – доступом к ресурсам.
  2. Привилегии назначаются учетным записям групп и отдельных пользователей, а права доступа – защищаемым объектам на основании записей в DACL объекта.

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

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

Перед выполнением привилегированной операции нужно активизировать нужные привилегии в токене. Для этого нужно вызвать функцию OpenThreadToken или OpenProcessToken, получить от нее handle токена, а затем активизировать нужные привилегии с помощью функции AdjustTokenPrivileges. После выполнения привилегированной операции вызовите AdjustTokenPrivileges снова, чтобы деактивировать привилегии.

В Win32 API определены строковые константы для каждой привилегии.

Администратор может назначать привилегии через User Manager в NT4 и snap-in Security Settings в W2k. Программно их можно назначать с помощью функций LsaAddAccountRights и LsaRemoveAccountRights , а просмотреть их список – с помощью LsaEnumerateAccountRights. Функция LsaEnumerateAccountsWithUserRight перечисляет учетные записи, обладающие указанной привилегией. В таблице 1 приведен список привилегий, поддерживаемых в NT4 и W2k и привилегии, назначенные по умолчанию для учетных записей Administrator (Admin) и System.

* Поддерживается только в Windows 2000

Проверки прав доступа к защищенным объектам

Итак, информация о принципале, с которым ассоциирован процесс или поток, хранится в токене, а информация о защите объектов (поддерживающих защиту) находится в дескрипторе безопасности, ассоциированном с этими объектами. Но как же осуществляется проверка прав данного процесса/потока на доступ к некоторому защищенному объекту? Ведь данная информация может быть задана опосредованно через группы! Неужели для каждой проверки приходится обращаться к контролеру домена (Domain Controller – DC)? Это же должно приводить к ужасным задержкам!

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

Сначала проверяется запретительный список (ACL с негативным признаком). При этом ищется запись, содержащая затребованные права и учетную запись, совпадающую с одной из содержащихся в токене. Если есть хоть одно совпадение, то ОС отказывает в доступе. Список разрешений при этом игнорируется, так как запрещение имеет приоритет над разрешением.

ПРИМЕЧАНИЕ

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

Далее (если совпадений в списке запрещений не обнаружено) аналогичным образом проверяется список разрешений. При этом проверяется, все ли запрошенные права имеются у данного пользователя. Права могут быть заданы раздельно. Например, учетной записи Mike может быть предоставлено только право записи текстового файла А. При этом Mike не сможет открыть этот файл, так как не обладает правом читать файл. Но если Mike входит в группу Users, которой разрешено чтение данного файла, он получит возможность и чтения, и записи – так как права суммируются. Однако если хоть одного запрошенного права нет в списке разрешений, в доступе отказывается, и возвращается ошибка.

Подчеркну еще раз, список запрещений имеет перевес над списком разрешений. Например, одной из часто делаемых ошибок является запрет доступа для группы Everyone (Все) при одновременном разрешении доступа некоторым учетным записям или группам (допустим, членам групп Administrators и Power Users). Делающий это неопытный администратор наивно полагает, что доступ будет дан всем администраторам и продвинутым пользователям с одновременным запретом доступа для всех остальных (нежелательных элементов). Однако в результате, вследствие приоритета запрета над разрешением, никто не сможет получить доступ к объекту с подобными настройками защиты. Правильным выходом в данной ситуации было бы просто убрать запрет для группы Everyone, поскольку никто, кроме администраторов и продвинутых пользователей и так не получит доступа к подопытному объекту.

Для чего же нужны запреты? А для того, чтобы организовать «исключения из правил». Предположим, что нужно дать доступ к объекту всем пользователям, но запретить доступ Васе и Пете. Для этого следует добавить в список разрешений группу Users, а в список запретов Васю и Петю. Вместо прямого добавления Васи и Пети в список запрещений можно создать группу Restricted Users (Пользователи с ограниченными правами), внести Васю и Петю в эту группу и запретить для неё доступ к объекту. Этот подход более гибок, так как защищаемых объектов может быть много. Если впоследствии появятся другие пользователи, которым нужно запретить доступ к этим объектам, не придется изменять права для каждого объекта в отдельности. Можно будет просто добавить необходимых пользователей в группу «Restricted Users».

Кэширование групп хотя и позволяет значительно поднять скорость проверок атрибутов безопасности, но имеет одно неприятное ограничение. Изменение атрибутов защиты учетной записи не отражается на открытых сессиях. Может возникнуть ситуация, когда вы изменили права пользователя (добавив его в группу или, наоборот, удалив его из группы) и считаете, что данный пользователь уже не сможет совершить «незаконных» действий. Но если сессия с данной учетной записью существовала во время изменения прав, данные изменения никак на ней не отразятся. Самое опасное, что администратор может сделать ошибку в настройках защиты, но она не будет заметна до перезапуска системы (так как многие сервисы обычно перезапускаются только вместе с ОС), а перезапуск может произойти со значительной отсрочкой. При этом создается впечатление, будто в ОС вкрался вирус или сверхъестественные силы. Такие ситуации нередко становятся предпосылками для шаманства и необдуманных действий. Как уже говорилось, COM-серверы и их клиенты пользуются теми же механизмами защиты, что и другие части Windows, а значит, имеют те же проблемы и особенности. Следует помнить об этом. Если появилось ощущение, что поведение сервера не соответствует ожидаемому, нужно перезапустить сервер, снова войти в систему (если сервер запускается под интерактивным пользователем), а может быть даже перезапустить систему.

Итак, беглый взгляд на систему безопасности Windows брошен. Перейдем к особенностям защиты в COM.

Защита в COM

Первые версии COM создавались как чисто локальные, и речи о защите как таковой не шло. С появлением DCOM вопрос защиты был включен в повестку дня. К счастью, о защите задумались уже в момент написания спецификации, а не как это зачастую бывает, задним числом.

В чем же заключаются особенности защиты в DCOM по сравнению с обычной защитой в Windows? Главное отличие в том, что COM – объектно-ориентированная модель, в которой не нашлось места для вездесущих handle-ов. Вместо handle-ов в COM используются указатели на интерфейсы, которые являются обыкновенными указателями на область памяти. По сути handle-ы являются некими черными ящиками, в которых можно скрывать разные особенности реализации, в том числе и защиту. Отказ от них привел к невозможности скрыть защиту в недрах ОС. В таких условиях просто невозможно организовать защиту, ведь программист имеет прямой указатель на объект и может делать с этим объектом всё, что угодно. Как же защитить объект? Если объект находится в том же апартаменте, то никак. Но если объект находится в другом апартаменте (для простоты восприятия будем подразумевать в другом процессе, но надо помнить, что это может быть и просто другой поток), то очень даже можно. В этом случае клиент получает указатель не на сам объект, а на proxy. Proxy – это небольшой объект, имеющий те же интерфейсы, что и основной объект, но реализация методов этих интерфейсов занимается всего лишь преобразованием параметров метода в последовательную форму (сериализацией), и пересылкой информации серверной заглушке (stub-у) которая восстанавливает параметры (формируя стековый фрейм) и вызывает настоящий метод объекта. Весь этот процесс в COM называется маршалингом. При возврате управления из метода происходит обратный процесс, вследствие которого клиенту передаются [out]-параметры, возвращаемое значение (HRESULT) и, если произошла ошибка, объект, содержащий описание ошибки.


Рисунок 3. Взаимодействие клиента с сервером через proxy/stub

Фактическая передача осуществляется так называемым RPC-каналом (далее просто канал). Такая архитектура чем-то сродни работе через handlе-ы, и прекрасно подходит для встраивания дополнительных сервисов наподобие поддержки транзакций или защиты. Собственно, программисты из Microsoft этим и воспользовались.

Внепроцессные COM-серверы запускаются внутри некоторой logon-сессии (под определенной учетной записью). Когда клиент (обычно запускаемый под другой учетной записью и, соответственно, в другой сессии) вызывает метод интерфейса, proxy этого интерфейса запаковывает и передает информацию об атрибутах защиты серверу (см. рис. 4).


Рисунок 4. Взаимодействие клиентского процесса и COM-сервера

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

Главное, что нужно понять – по умолчанию метод COM-объекта выполняется под учетной записью серверного процесса. Это очень важный момент! Не осознав его, дальше двигаться нельзя. Представьте себе, в клиентском процессе выполняется некоторый код... этот код выполняется под учетной записью (обычно) имеющей мало прав (по крайней мере на сервере). Код выполняется и в некоторый момент времени доходит до вызова метода удаленного COM-объекта. Управление передается в proxy, та сериализует переданные ей параметры, записывает информацию об учетной записи клиента, устанавливает необходимые параметры защиты и... передает управление DCOM. Тот в свою очередь делает сетевой вызов (с помощью RPC) и передает управление Stub-коду, который восстанавливает полученные данные и вызывает метод COM-объекта. Но (!) код COM-объекта выполняется в контексте защиты сервера, что дает ему (обычно) значительно больше свободы. Сервер выполняет некоторые действия и возвращает управление клиенту. После этого клиент продолжает выполнять свой код в своем контексте защиты. По сути, на время клиент получает права (а вместе с ними и мощь) более привилегированной учетной записи. Но эта мощь ограничена сервисом, предоставляемым сервером. Это очень важный момент, не позволяющий клиенту вмешаться в поведение системы и сделать что-либо нежелательное.

Теперь давайте представим, что может происходить на сервере? Когда вызов приходит на сервер, COM извлекает из полученного буфера информацию о защите и проверяет, можно ли учетной записи, от имени которой происходит вызов, обращаться к серверу. Если в доступе отказано, COM возвращает клиенту E_ACCESSDENIED. Если доступ разрешен, COM вызывает stub для продолжения работы. При этом с потоком, в котором производится вызов, ассоциируется некоторая информация о клиенте. Чуть позже будет рассказано, как получить и использовать эту информацию.

Нетрудно предположить, что информация о клиенте представляется в виде токена (олицетворяющего logon-сессию). Если клиент позволяет серверу использовать этот токен для имперсонации, сервер сможет обращаться к локальным ресурсам компьютера от имени клиента. Подчеркну, именно к локальным ресурсам, например, файлам. Имперсонация не распространяется на вызовы COM-методов или доступ к ресурсам других компьютеров. В W2k появилась новый уровень имперсонации – делегация. Он позволяет серверу вызывать методов удаленных интерфейсов от имени клиента (вызвавшего метод сервера). Подробнее обо всем этом будет рассказано ниже.

Права активации

Под Windows 9x COM-сервер не запускается автоматически по требованию клиента. В ОС этой линейки приходится запускать COM-серверы вручную. Ввиду практически полной незащищенности, плохой устойчивости к сбоям и недостаточной функциональности реализуемых сервисов, Windows 9x вообще лучше не использовать в качестве платформы для серверных приложений. В отличие от Windows 9x, ОС линейки NT прекрасно подходят для запуска COM-серверов. Так, эти ОС поддерживают автоматический запуск приложений удаленными клиентами. Например, если клиент вызвал функцию, активирующую COM-объект (скажем, CoCreateInstance), реализованный в некотором сервере, COM SCM проверяет, зарегистрирована ли для этого объекта в системе фабрика класса. Если не зарегистрирована, то COM SCM находит (через реестр) исполняемый модуль этого сервера, запускает его на выполнение и ждет некоторое время регистрации необходимой фабрики классов.

В ходе этого процесса COM SCM проверяет, имеет ли пользователь, осуществляющий запрос на создание объекта, право на запуск сервера. Сведения о тех, кому разрешено/запрещено запускать сервер, называются «правами активации» (или «launch permissions»). Они хранятся в именованном значении LaunchPermission, находящемся в ветке HKEY_CLASSES_ROOT\AppID\{AppID приложения} реестра. LaunchPermission имеет тип REG_BINARY и хранит сериализованный вид дескриптора безопасности. Настроить права активации можно из утилиты dcomcnfg. Для этого в dcomcnfg.exe нужно найти необходимое приложение, открыть его свойства, перейти на закладку «Security», выбрать пункт «Use custom launch permissions» и нажать на соответствующую (их там много) кнопку «Edit» (см. рисунок 5).


Рисунок 5. Настройка прав активации (dcomcnfg).

Дескриптор безопасности определяет учетные записи, которым разрешено или запрещено запускать сервер. В этот дескриптор обязательно должна входить учетная запись SYSTEM. Современные версии утилит dcomcnfg и OLEViewer добавляют эту запись автоматически. Однако старые версии не делали этого.

Если права активации не были явно заданы, будут действовать права доступа «по умолчанию». Они распространяются на всю машину и задаются в именованном значении DefaultLaunchPermission ветки реестра HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Ole. Это значение, так же как и LaunchPermission (описанное выше), содержит дескриптор защиты.

То, что кому-то запрещено запускать сервер, еще не означает, что ему запрещено создавать объекты в этом сервере и вызывать их методы. Это всего лишь означает, что если сервер не запущен, этот кто-то не сможет его запустить посредством удаленной активации. Это свойство защиты COM-серверов особенно полезно, если сервер является сервисом, и управлять временем жизни сервера нужно вручную (администратору). Права доступа к объектам сервера задаются отдельно.

Права доступа

Каждое COM-приложение (и клиентское, и серверное) может определить настройки защиты по умолчанию. Такие настройки задают провайдеров безопасности и минимальные значения для параметров защиты. Умолчания задаются в момент, когда внутри приложения первый раз вызывается CoInitializeSecurity. Это может происходить неявно (когда сервер, не вызывая CoInitializeSecurity, пытается создать первый COM-объект, или в COM+, где это делает рантайм-подсистема) и явно (вручную) при старте приложения/сервиса.

Очень распространенной ошибкой является попытка вызвать эту функцию после того, как она уже была вызвана. Эту ошибку особенно трудно осознать, если CoInitializeSecurity в первый раз была вызвана неявно. Так, не стоит пытаться вызвать эту функцию из COM-сервера, расположенного в DLL, даже если DLL зарегистрирована в COM+-приложении, ибо к моменту создания пользовательского объекта данная функция точно уже вызвана самим COM.

Из книг по COM многие знают, что права доступа к объектам сервера и другая информация о защите хранятся в реестре и обычно задаются с помощью утилиты dcomcnfg (закладка Security). Реально же атрибуты защиты (кроме прав на активацию) задаются функцией CoInitializeSecurity. Для чтения из реестра настроек защиты нужно вызвать CoInitializeSecurity с определенными значениями параметров.

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

Еще одну проблему, как ни странно, вносит библиотека ATL (призванная упростить создание COM-серверов и сервисов). Происходит это из-за того, что визард генерирует следующий код:

// This provides a NULL DACL which will allow access to everyone.
CSecurityDescriptor sd;
sd.InitializeFromThreadToken();
hr = CoInitializeSecurity(sd, -1, NULL, NULL,
   RPC_C_AUTHN_LEVEL_PKT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL);

Этот код говорит COM, что к объекту может иметь доступ кто угодно. При этом настройки безопасности доступа в реестре не влияют на реальную политику безопасности. Есть и еще один фактор, который может повлиять на безопасность доступа – это атрибуты защиты, заданные для исполняемого файла сервера. Так что если поместить exe-файл COM-приложения в папку, к которой запрещен доступ некоторой учетной записи, при обращении к приложению обладатель этой учетной записи столкнется с сообщением «access denied». При этом можно долго задавать «custom access permissions» в DCOMCNFG и не понимать, почему сервер не работает.

Чтобы заставить ATL-сервер работать так, как написано в большинстве книг по COM, т.е. чтобы правами доступа можно было управлять через dcomcnfg, нужно изменить инициализацию защиты так, чтобы настройки считывались из ветки реестра, соответствующей AppID. Для этого нужно заменить приведенные выше строки кода на следующие:

struct __declspec(uuid("8030105E-9B2A-4758-9AF9-12CCC4507468")) CAppID;
GUID gAppID = __uuidof(CAppID);
hr = CoInitializeSecurity(&gAppID, -1, NULL, NULL, NULL, NULL, NULL, 
                          EOAC_APPID, NULL);

Здесь 8030105E-9B2A-4758-9AF9-12CCC4507468 – AppID. Это значение генерирует ATL-визард, его можно найти в коде функции _tWinMain и RegisterServer.

Можно также передать NULL вместо AppID. При этом реальный AppID будет искаться по имени исполняемого модуля. Но такой подход менее предпочтителен, поскольку в случае использования длинных имен файлов или пути к исполняемому файлу могут возникнуть проблемы.

Иногда защита не только не нужна, но и вредна, так как на нее приходится тратить время и нервы. Особенно это справедливо по отношению к предоставляющим callback-интерфейсы клиентским приложениям, запускаемым под управлением Windows 9x. В таком случае имеет смысл отключить защиту вообще. Для этого нужно вызвать CoInitializeSecurity следующим образом:

hr = CoInitializeSecurity(
    NULL, -1, NULL, NULL,
RPC_C_AUTHN_LEVEL_NONE, RPC_C_IMP_LEVEL_IDENTIFY, NULL, EOAC_NONE, NULL ); ...

Никакой защиты... но и никакой с ней мороки! Более детально этот вопрос разбирается в конце статьи.

Чтобы понять, как все это работает, рассмотрим поподробнее функцию CoInitializeSecurity.

CoInitializeSecurity

Как уже говорилось ранее, эта функция задает значения по умолчанию для процесса, в котором она вызывается. Вот ее описание:

HRESULT CoInitializeSecurity(

  // реально указатель на void-значение зависит
  // от значения параметра dwCapabilities
  PSECURITY_DESCRIPTOR pVoid,   

  // Количество элементов в asAuthSvc. Если задать -1,
  // COM будет сам выбирать сервис аутентификации.
  LONG cAuthSvc, 
  
  // Массив сервисов аутентификации, которые приложение 
  // хочет использовать при приеме вызовов. 
  SOLE_AUTHENTICATION_SERVICE * asAuthSvc, 

  void * pReserved1, //Зарезервирован. Должен содержать NULL

  // Выбираемый по умолчанию (для proxy) уровень аутентификации.
  DWORD dwAuthnLevel,

  // Выбираемый по умолчанию (для proxy) уровень имперсонации.
  DWORD dwImpLevel,

  // NULL в NT 4. Под W2k может содержать информацию для 
  // сервисов аутентификации.
  SOLE_AUTHENTICATION_LIST * pAuthList,

  // Определяет, как будет производиться настройка прав доступа.
  DWORD dwCapabilities,

  void * pReserved3 //Зарезервирован. Должен содержать NULL
);

Первый параметр применяется для задания списка учетных записей (ACL), которым будет разрешен доступ к объектам данного сервера. Для клиента этот список значения не имеет. Впрочем, не надо забывать, что клиент и сервер могут легко поменяться местами (например, если сервер реализует события, а клиент подключается к ним).

Данный список можно инициализировать несколькими способами:

через AppID. Пожалуй, это лучший способ инициализации, т.к. он позволяет администратору настраивать атрибуты защиты с помощью dcomcnfg или OLEViewer.

через интерфейс IAccessControl. Данный способ инициализации может пригодиться, если в качестве сервера выступает Windows 9x или Unix-машина. Последнее хотя и маловероятно, но возможно – CA перенесла COM на большое количество Unix-совместимых платформ. Для серверов, запускаемых под управлением NT, применение данного способа инициализации списка защиты нежелательно.

и традиционный для Windows NT способ, через заполнение структуры SECURITY_DESCRIPTOR

То, как будет производиться инициализация, определяет восьмой по счету параметр dwCapabilities. Вот его возможные значения:

Второй параметр этой функции, cAuthSvc, позволяет задать количество элементов, содержащихся в списке сервисов аутентификации, задаваемом третьим параметром (asAuthSvc). Если передать в cAuthSvc ноль, не одного сервиса аутентификации не будет зарегистрировано и сервер не сможет обрабатывать защищенных вызовов. Если передать в cAuthSvc -1, COM будет самостоятельно выбирать сервисы аутентификации для регистрации.

Третий параметр (asAuthSvc) позволяет задать список сервисов, которые могут применяться при вызове методов сервера. Если значение параметра cAuthSvc равно 0 или -1, asAuthSvc должен содержать NULL.

Параметр dwAuthnLevel задает уровень аутентификации, используемый приложением.

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

При вызове в клиентском процессе данный параметр задает уровень аутентификации, который будет использоваться по умолчанию. COM использует данное значение в алгоритме вычисления атрибутов безопасности (о котором речь пойдет ниже) при расчете уровня аутентификации для proxy.

Уровень аутентификации можно переопределить для конкретной proxy/stub (с помощью IClientSecurity::SetBlanket, о чем пойдет речь ниже). Это позволяет задать с помощью функции CoInitializeSecurity общий (минимальный) уровень аутентификации, а в случае необходимости изменить данный параметр для конкретной proxy/ stub.

dwAuthnLevel может принимать следующие значения:

ПРЕДУПРЕЖДЕНИЕ

Хотя Windows 95 может производить вызовы на любом уровне аутентификации, принимать вызовы она может только с уровнем RPC_C_AUTHN_LEVEL_NONE или RPC_C_AUTHN_LEVEL_CONNECT.

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

pAuthList – под W2k этот параметр является указателем на SOLE_AUTHENTICATION_LIST, массив структур SOLE_AUTHENTICATION_INFO. Этот список содержит информацию для каждой службы аутентификации, которую может использовать клиент при вызове сервера. Этот параметр используется, только когда в клиентском приложении вызывается CoInitializeSecurity. В NT4 pAuthList должен иметь значение NULL.

dwCapabilities – этот параметр задает дополнительные характеристики (как для клиента, так и для сервера). Характеристики задаются с помощью флагов, объявленных в перечислении EOLE_AUTHENTICATION_CAPABILITIES. О некоторых из них уже упоминалось при описании первого параметра этой функции. Ниже приведено описание остальных флагов:

Функция CoInitializeSecurity не задает конкретных параметров безопасности. Она только указывает COM, какие значения максимально (или минимально) допустимы. Конкретные значения, назначаемые новой proxy вычисляются COM-ом в процессе «Security Blanket Negotiation».

Алгоритм вычисления атрибутов безопасности для proxy (Security Blanket Negotiation)

Security blanket – это настройки безопасности, применяемые к proxy. Это понятие включает следующие значения:

Как не трудно догадаться, эти значения соответствуют параметрам функции CoInitializeSecurity.

Security blanket negotiation – это алгоритм, который применяется COM для вычисления конкретных значений атрибутов защиты, присваиваемых новой proxy. Этот алгоритм сравнивает Security blanket сервера и клиента, и выбирает подходящие обеим сторонам атрибуты защиты (которые как раз и задаются при вызове CoInitializeSecurity).

Некоторые параметры функции CoInitializeSecurity применяются, если приложение является сервером, а некоторые – если оно является клиентом.

Когда CoInitializeSecurity вызывается для сервера, учитываются: ACL, список сервисов аутентификации/авторизации, принципал и уровень аутентификации.

Для клиента используются: уровень аутентификации, уровень имперсонации, информация о пользователе, подтверждающая его личность, и дополнительные параметры (задающиеся в параметре dwCapabilities). При этом клиент определяет, какие установки защиты он хочет применять при вызове удаленных методов.

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

После создания proxy её настройки защиты можно изменить с помощью IClientSecurity::SetBlanket. Значение, задаваемое функцией SetBlanket, не вычисляется, как описывалось ранее, а просто применяется к proxy. Однако если функции SetBlanket передается значение ХХХ_DEFAULT, то реальное значение вычисляется по описанному выше алгоритму.


Рисунок 6.

Dcomcnfg.exe

Как уже было сказано выше, с помощью CoInitializeSecurity можно заставить DCOM считывать настройки безопасности из реестра. Задать эти настройки можно с помощью утилиты dcomcnfg. Эта утилита позволяет задавать настройки защиты для конкретного приложения, а также настройки защиты, используемые по умолчанию.

Проще всего запустить dcomcnfg из командной строки, так как по умолчанию эта утилита не имеет ярлыка.


Рисунок 7. Утилита dcomcnfg.

Dcomcnfg –небольшое диалоговое приложение с четырьмя закладками-страницами (похожий диалог можно увидеть, если открыть свойства локального компьютера в консоли Component Services):

Applications – содержит список COM-приложений. Выбрав одно из них, можно открыть диалог его настройки (кнопка Properties).

Default Properties – позволяет задать значения по умолчанию для уровней аутентификации и имперсонации. На этой странице можно также включить или выключить DCOM для текущей машины. Если DCOM выключен, работа с COM-объектами по сети невозможна. Значения этого переключателя можно найти в реестре по адресу HKEY_LOCAL_MACHINE\Software\Microsoft\OLE. На этой же странице есть еще одна опция, которая относится к защите. Она называется "Provide additional security for reference tracking". Эта опция заставляет COM защищать вызовы методов интерфейса IUnknown, отвечающие за подсчет ссылок. Это может предотвратить несанкционированное уничтожение объекта, например, вследствие вмешательства злоумышленников. (рисунок 7)

Default Security – На этой странице можно задать значения по умолчанию для прав доступа и прав активации. Эти значения тоже хранятся в ключе HKEY_LOCAL_MACHINE\Software\Microsoft\OLE, в значениях DefaultAccessPermission и DefaultLaunchPermission, соответственно. Их содержимое – не что иное, как сериализованный дескриптор защиты. Эти настройки будут использоваться обычными DCOM-приложениями, если настройки для этих приложений не заданы явно. При задании прав нужно помнить, что список тех, кому разрешен доступ, обязательно должен содержать учетную запись SYSTEM! Иначе вы будете получать загадочные сообщения об ошибке (например, "не хватает памяти" или "не могу загрузить user.dll"). Новые утилиты (dcomcnfg и OLEViewer) автоматически добавляют ее, но старые этого не делали, так что будьте осторожны. На COM+-приложения эти настройки не распространяются.

На этой закладке есть еще одна кнопка - Default Configuration Permissions. Она позволяет настроить ACL разрешений/запрещений для ключа HKEY_CLASSES_ROOT. В этот ключ помещается информация о COM-объектах, регистрируемых в системе. Если запретить кому-нибудь доступ на запись в этот ключ реестра, этот кто-то не сможет устанавливать в систему COM-приложения. Будьте осторожны, так как HKEY_CLASSES_ROOT может быть довольно большим и изменение прав доступа к нему может занять довольно много времени.

Default Protocols – эта закладка позволяет настроить список и приоритет использования для сетевых протоколов, используемых DCOM. Ее рассмотрение выходит за рамки этой статьи.

Для задания прав доступа к ключевым (с точки зрения COM) веткам реестра можно использовать regedt32 (regedit в Windows XP). Это позволит избежать лишних проблем с излишне продвинутыми пользователями или, наоборот, делегировать часть прав по администрированию обладателям менее привилегированных учетных записей.

Настройка свойств отдельного DCOM-приложения производится в отдельном диалоговом окне, которое (как уже говорилось выше) можно вызвать на страницы Applications. Все установки, которые можно задать в этом диалоге, хранятся в ключе реестра HKEY_CLASSES_ROOT\AppID\{AppID приложения}. На рисунке 8 показано содержимое этого ключа для COM-сервера SampleServerWithEvents (из второго теста).


Рисунок 8. Настройки защиты COM-объекта (вид из реестра).

Ниже кратко описаны страницы этого диалога, имеющие отношение к делу:

General – позволяет задать минимальный уровень аутентификации и получить кое-какую (малоинтересную) информацию. К сожалению, из dcomcnfg нельзя задать минимальный уровень имперсонации (как это можно сделать из диалога настройки COM+-приложения). Если нужно, можете сделать это программно (с помощью функции CoInitializeSecurity).

Location – к защите прямого отношения не имеет. Позволяет указать, где будет запускаться сервер (на удаленном сервере или на данной машине). Обычно используется для указания компьютера- сервера. Данная установка не действует, если объект создается с помощью CoCreateInstanceEx (CreateObject в VB6), и имя сервера задано явно.

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

Identity – эта страница позволяет указать, под какой учетной записью и в какой Windows-станции будет запускаться DCOM-приложение. Dcomcnfg помещает настройки, сделанные на этой странице, в значение реестра RunAs. Вот возможные значения этого параметра:

Если сервер должен взаимодействовать с интерактивным пользователем, нужно или написать отдельное клиентское приложение, или создать и зарегистрировать COM-сервер как сервис, указав при регистрации сервиса, что он должен иметь право взаимодействовать с рабочим столом. Сделать это можно в диалоге свойств сервиса (см. рисунок 9). Какой способ лучше? Я считаю – первый. По-моему, негоже серверу выпендриваться и лезть к незнакомым людям с вопросами и предложениями. Если пользователю будет нужно, он запустит утилиту администрирования и всласть наобщается с сервером. Если нужно оповещать пользователя, то, например, можно поместить клиентское приложение в папку Startup и вывести иконку в панели задач. При этом, если кому-то такой сервис не понравится, он сможет выгрузить и удалить надоеду.

Если нужно просто выдать сообщение, например, о критичном сбое, можно воспользоваться функцией MessageBox, передав ей в качестве последнего параметра флаг MB_SERVICE_NOTIFICATION. Это заставит выдавать сообщение на default desktop (не обращая внимания на Windows-станцию, в которой создан сервер). Будьте при этом осторожны, так как окна сообщений, показываемые с флагом MB_SERVICE_NOTIFICATION модальны по отношению к системе, то есть если пользователь не закроет такое окно, второе не будет выведено.


Рисунок 9. Если включить переключатель, обведенный красной линией, ваш сервис сможет взаимодействовать с интерактивным пользователем.

Dcomcnfg не очень мощная, но порою незаменимая утилита. Нередко программист хочет избавить пользователя от использования этой утилиты, например, задавая настройки прямо из сервера (с помощью функции CoInitializeSecurity). Однако права активации всегда задаются через реестр, и их считывание происходит до загрузки сервера. Если хочется упростить жизнь пользователя, то следует или использовать COM+, или задавать необходимые настройки в момент инсталляции программы.

IClientSecurity

IClientSecurity позволяет изменять (или считывать) установки, сделанные функцией CoInitializeSecurity. Но изменения эти распространяются только на конкретную proxy. Получить указатель на этот интерфейс можно, вызвав QueryInterface у указателя на proxy некоторого объекта:

IClientSecurity, * pIClientSecurity;
hr = pSomeItf->QueryInterface(IID_ IClientSecurity, (void**)& pIClientSecurity);
if(SUCCEEDED(hr))
{
  pIClientSecurity->SetBlanket(pSomeItf, ...);
  ...
  pIClientSecurity->Release();
}

Физически IClientSecurity реализуется proxy-менеджером. Вот описание этого интерфейса:

[
  local,
  object,
  uuid(0000013D-0000-0000-C000-000000000046)
]
interface IClientSecurity : IUnknown
{

  typedef struct tagSOLE_AUTHENTICATION_SERVICE
  {
    DWORD     dwAuthnSvc;
    DWORD     dwAuthzSvc;
    OLECHAR * pPrincipalName;
    HRESULT   hr;
  } SOLE_AUTHENTICATION_SERVICE;

  typedef SOLE_AUTHENTICATION_SERVICE *PSOLE_AUTHENTICATION_SERVICE;

  typedef enum tagEOLE_AUTHENTICATION_CAPABILITIES
  {
    EOAC_NONE           = 0x0,
    EOAC_MUTUAL_AUTH    = 0x1,
    EOAC_CLOAKING       = 0x10,

    // These are only valid for CoInitializeSecurity
    EOAC_SECURE_REFS    = 0x2,
    EOAC_ACCESS_CONTROL = 0x4,
    EOAC_APPID          = 0x8
  } EOLE_AUTHENTICATION_CAPABILITIES;

  HRESULT QueryBlanket(
    [in]  IUnknown   * pProxy,
    [out] DWORD      * pAuthnSvc,
    [out] DWORD      * pAuthzSvc,
    [out] OLECHAR   ** pServerPrincName,
    [out] DWORD      * pAuthnLevel,
    [out] DWORD      * pImpLevel,
    [out] void      ** pAuthInfo,
    [out] DWORD      * pdwCapabilities
  );

  HRESULT SetBlanket (
    [in] IUnknown    * pProxy,
    [in] DWORD         AuthnSvc,
    [in] DWORD         AuthzSvc,
    [in] OLECHAR     * pServerPrincName,
    [in] DWORD         AuthnLevel,
    [in] DWORD         ImpLevel,
    [in] void        * pAuthInfo,
    [in] DWORD         dwCapabilities
  );

  HRESULT CopyProxy(
    [in]  IUnknown   * pProxy,
    [out] IUnknown  ** ppCopy
  );
}

Первое, что бросается в глаза при взгляде на этот интерфейс – это то, что он помечен атрибутом «local». Этот атрибут говорит, что данный интерфейс нельзя передавать за пределы апартамента. В самом деле, интерфейс, предоставляемый proxy-менеджером, имеет мало смысла в другом контексте.

Метод QueryBlanket позволяет считывать текущие настройки безопасности для proxy некоторого интерфейса. Параметры этого метода аналогичны параметрам метода SetBlanket (того же интерфейса) с той лишь разницей, что SetBlanket позволяет задавать атрибуты защиты, а QueryBlanket – читать их. На практике QueryBlanket применяется реже, чем SetBlanket. Поэтому дальше будет приведено описание только метода SetBlanket.

Для упрощения использования методов этого интерфейса были созданы вспомогательные функции:

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

Функция CoCopyProxy (IClientSecurity::CopyProxy) предназначена для копирования proxy. Реальное применение этой функции придумать трудно. Совершенно непонятно, зачем нужна копия proxy! В MSDN этот метод упоминается только при описании интерфейса IClientSecurity. Причем там сказано, что CoCopyProxy позволяет создать скрытую копию proxy, дающую возможность скрыть изменения, вносимые с помощью CoSetProxyBlanket. Но того же эффекта можно добиться, запоминая и восстанавливая исходные значения, заданные для proxy. Ко всему прочему, применение этой функции может привести к довольно неожиданным ошибкам. Вызов QueryInterface от клона приведет к возврату указателя на другую proxy, которая (заметьте!) не была клонирована. Причем это происходит, даже если попытаться запросить тот же самый интерфейс. Допустим, вам захотелось написать вспомогательную функцию, изменяющую параметры защиты, для применения в VB-приложениях. Вспомогательная функция нужна, так как IClientSecurity – низкоуровневый интерфейс, не слишком пригодный для использования в этой среде. Так вот, использование CopyProxy в этом случае – занятие неблагодарное. Дело в том, что высокоуровневые среды, такие, как VB, неявно вызывают QueryInterface, и можно быть уверенным, что вашу замечательную работу никто не сможет оценить по достоинству.

Можно было бы подумать, что CoCopyProxy позволяет ускорить работу распределенного приложения за счет сокращения количества изменений установок proxy, и, стало быть, общения с контролером домена, но кэширование, применяемое в последних версиях COM, сводит на нет весь гипотетический выигрыш. В общем, если у кого-нибудь возникнут мысли по поводу вопроса "для чего может пригодиться эта функция?", найдите онлайн-версию этой статьи (на rsdn) и напишите об этом в комментариях к статье. :)

CoSetProxyBlanket

Итак, подошло время заняться рассмотрением самой интересной функции – CoSetProxyBlanket.

Первый ее параметр – это указатель на интерфейс proxy-объекта. В него можно передавать указатель на интерфейс, полученный из другого апартамента (это автоматически подразумевает наличие proxy).

pAuthnSvc – позволяет задать, какой сервис аутентификации использовать при вызове методов через настраиваемую proxy. Вот возможные значения этого параметра:

Реально под NT 4 доступен только RPC_C_AUTHN_WINNT, а под W2k еще и RPC_C_AUTHN_GSS_NEGOTIATE и RPC_C_AUTHN_GSS_KERBEROS. Приложив некоторые усилия (связанные с получением и настройкой сертификатов), под W2k удастся заставить работать SChannel (RPC_C_AUTHN_GSS_SCHANNEL).

Обычно для этого параметра подходит значение RPC_C_AUTHN_DEFAULT (при этом система сама подбирает наиболее подходящий сервис аутентификации), но в системах, строящихся с использованием ОС разных версий, может оказаться полезным вручную указать сервис аутентификации (обычно RPC_C_AUTHN_WINNT).

Нужно также заметить, что если и клиент, и сервер запущены на одной машине, то всегда будет использоваться RPC_C_AUTHN_WINNT. Причем попытка указать значения, отличные от RPC_C_AUTHN_WINNT или RPC_C_AUTHN_DEFAULT, приведет к ошибке (0x800706D3 – «The authentication service is unknown.», неизвестный сервис аутентификации). И вообще, если защита работает на локальном компьютере – это не означает, что она автоматически заработает и в сетевом режиме. Так что всегда следует тестировать приложение в условиях, как можно более приближенных к условиям реального использования.

pAuthzSvc – определяет, какой сервис авторизации будет использоваться. Если задано значение RPC_C_AUTHZ_DEFAULT, Windows сама займется определением оптимального значения для этого параметра (см. «Security Blanket Negotiation алгоритм»). Реально под управлением W2k бесполезно заполнять этот параметр, так как Windows наплюет на установки и выберет то, что ей нужно. Но проблем это не вызовет. Ниже перечислены допустимые значения этого параметра.

pServerPrincName – указатель на строку WCHAR, указывающую имя принципала сервера, используемое со службой аутентификации. Если указано COLE_DEFAULT_PRINCIPAL, DCOM выберет имя принципала, используя «алгоритм Security Blanket Negotiation». Если в качестве службы аутентификации используется Kerberos, это значение должно быть равно NULL.

Если используется служба аутентификации SChannel, это значение должно иметь формат msstd или fullsic. Если взаимная аутентификация не нужна, это значение должно быть равно NULL.

Задание NULL не переопределит имя принципала сервера для proxy; напротив, сохранятся имеющиеся настройки. При использовании NULL в качестве pServerPrincName при изменении службы аутентификации, используемой proxy, нужно соблюдать осторожность, так как заданное ранее имя принципала не обязательно подойдет для новой службы аутентификации.

Сервер регистрирует имя принципала у провайдера защиты. SSP диктует формат имени принципала. Например, протокол Kerberos требует, чтобы имя принципала имело формат "servername" или "domain\servername".

SSP SCHANNEL принимает имена принципалов в одном из двух форматов. Первая – форма msstd. Имена в формате msstd должны формироваться по принципу servername@serverdomain.com. Оно должно содержаться в свойстве сертификата "email-адрес". Если сертификат содержит свойство "email-адрес", и оно содержит знак "at" (@), имя принципала – msstd:email. Иначе сертификат должен иметь свойство "обычное имя" (common name). Если нет ни того, ни другого, SSP SCHANNEL вернет сообщение ERROR_INVALID_PARAMETER. Содержащиеся знаки обратной косой черты дублируются.

Второй формат имени принципала SCHANNEL – формат fullsic. Это серия RFC1779-совместимых имен, заключенных в скобки и разделенных знаком обратной косой черты. Обычно они соответствуют образцу fullsic:\<\Authority\SubAuthority\.....\Person> или fullsic:\<\Authority\SubAuthority\.....\ServerProgram>.

dwAuthnLevel – задает уровень проверки безопасности. Для этого параметра допустимы те же значения, что и для аналогичного параметра функции CoInitializeSecurity (см. описание этой функции).

pAuthInfo – указатель на значение RPC_AUTH_IDENTITY_HANDLE, позволяющее задать учетную запись клиента. Этот параметр не используется при локальных вызовах. Формат структуры, на которую ссылается handle, зависит от провайдера службы аутентификации.

Для NTLMSSP или Kerberos – это структура SEC_WINNT_AUTH_IDENTITY или SEC_WINNT_AUTH_IDENTITY_EX. Клиент должен обеспечить сохранность памяти до изменения этих установок или до освобождения всех proxy объекта. Если указан NULL, DCOM считывает информацию о клиенте из токен процесса или потока.

Для SChannel этот параметр – указатель на CERT_CONTEXT, содержащий клиентский сертификат X.509, или NULL, если клиент производит анонимное подключение к серверу. Если указан сертификат, вызывающая сторона не должна освобождать его, пока в данном апартаменте существуют какие-либо proxy объекта.

Для Snego этот параметр – либо NULL, либо указатель на структуру SEC_WINNT_AUTH_IDENTITY_EX. В первом случае Snego использует список служб аутентификации, имеющихся на клиентской машине. Иначе член PackageList структуры должен указать строку, содержащую разделенный запятыми список названий служб аутентификации, а член PackageListLength – количество байтов в этой строке. Если PackageList содержит NULL, вызвать Snego не получится.

Если указано COLE_DEFAULT_AUTHINFO, DCOM будет использовать информацию об аутентификации, используя «алгоритм Security Blanket Negotiation».

CoSetProxyBlanket вернет ошибку, если при заданном pAuthInfo в dwCapabilities задан один из флагов маскировки.

dwCapabilities – DWORD, определяющий флаги, указывающие способности proxy, определенные в перечислении EOLE_AUTHENTICATION_CAPABILITIES (описанные выше). Для этой функции доступны флаги EOAC_MUTUAL_AUTH, EOAC_STATIC_CLOAKING, EOAC_DYNAMIC_CLOAKING, EOAC_ANY_AUTHORITY, EOAC_MAKE_FULLSIC и EOAC_DEFAULT. Совместно с pAuthInfo и SChannel можно использовать флаги EOAC_STATIC_CLOAKING или EOAC_DYNAMIC_CLOAKING. Если задать любые другие флаги, CoSetProxyBlanket вернет ошибку.

IServerSecurity

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

[
    local,
    object,
    uuid(0000013E-0000-0000-C000-000000000046)
]
interface IServerSecurity : IUnknown
{
    HRESULT QueryBlanket
    (
        [out] DWORD    *pAuthnSvc,
        [out] DWORD    *pAuthzSvc,
        [out] OLECHAR **pServerPrincName,
        [out] DWORD    *pAuthnLevel,
        [out] DWORD    *pImpLevel,
        [out] void    **pPrivs,
        [out] DWORD    *pCapabilities
    );
    HRESULT ImpersonateClient();
    HRESULT RevertToSelf();
    BOOL IsImpersonating();
}

Как и IServerSecurity, этот интерфейс помечен атрибутом local, а стало быть, не может быть вызван за пределами апартамента, в котором он был получен. Получить указатель на этот интерфейс можно только из метода COM-объекта (при условии, что вызов производится удаленным клиентом). Делается это с помощью функции CoGetCallContext. Эта функция принимает IID необходимого интерфейса (в нашем случае – IID_IServerSecurity) и возвращает указатель на интерфейс. С помощью этой функции можно получать указатель не только на IServerSecurity, но и на интерфейсы ISecurityCallContext и ISecurityIdentityColl. Впрочем, последние два интерфейса относится к подсистеме COM+, а значит, время их обсуждения еще не настало. Ниже приведен код, позволяющий получить указатель на IServerSecurity:

IServerSecurity * pIServerSecurity;
hr = CoGetCallContext(IID_IServerSecurity, (void**)&pIServerSecurity);

QueryBlanket – позволяет получить информацию о реальных настройках proxy, через которую клиент вызвал данный метод. К сожалению, через этот метод можно получить не всю желаемую информацию.

ImpersonateClient – позволяет ассоциировать токен клиента с текущим потоком. Заметьте, что выполнение этого метода происходит успешно даже при минимальном уровне имперсонации (RPC_C_IMP_LEVEL_IDENTIFY). Это позволяет читать информацию о токене и производить проверки ACL. Чуть позже, в примере, я продемонстрирую, как это делается.

RevertToSelf – если текущий поток имперсонирован, эта функция снимает имперсонацию. Что забавно, в случае отсутствия имперсонации эта функция все равно возвращает S_OK.

IsImpersonating – предназначена для борьбы с забавной особенностью предыдущей функции. IsImpersonating возвращает ненулевое значение, если поток находится в режиме имперсонации, и ноль – в обратном случае.

Как и в случае с IClientSecurity, методы интерфейса IServerSecurity имеют вспомогательные функции, позволяющие упростить код. Вот их список (сзади указаны соответствующие методы):

CoQueryClientBlanket – IServerSecurity::QueryBlanket

CoImpersonateClient – IServerSecurity::ImpersonateClient

CoRevertToSelf – IServerSecurity::RevertToSelf

К сожалению, метод IServerSecurity::IsImpersonating не имеет аналогичной API-функции.

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

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

Я создал два примера, демонстрирующих принципы работы с защитой COM. Для их работы вы должны обладать двумя компьютерами, на которые установлена ОС Windows 2000 (и, возможно, Windows 95 с DCOM 1.3), объединенными в сеть, в которой имеется домен (доменом может быть одна из испытательных машин). Естественно, оба компьютера должны быть подключены к домену. Вам также понадобятся права администратора на обеих машинах.

Первое приложение состоит из двух частей – in-process COM+-сервера (ComSec.DLL, содержащей тестовый COM-объект) и клиентского приложения (SecTest), реализованного на WTL. Тестовый COM-объект имеет один метод:

HRESULT GetInfo([out, retval] BSTR * pbsInfo);

Как видно из описания, он имеет один out-параметр, через который клиенту возвращается строка. В эту строку помещается информация, добываемая на сервере. Клиент просто получает строку и выводит ее в окне сообщения. Единственное, чем клиент отличается от некоторых «профессиональных» приложений – в нем обрабатываются ошибки :). Это важный момент, так как приложение является испытательным стендом. С его помощью без перекомпиляции можно протестировать разные комбинации параметров функции SetBlanket. А так как оно выводит большое количество диагностической информации (получаемой с сервера), его можно использовать и для тестирования подсистемы защиты COM+-приложений.

Второе – ATL EXE-сервер, содержащий COM-объект, и простенький клиент. COM-объект поддерживает рассылку уведомлений о событиях (IConnectionPoint) и имеет метод Method1, который производит рассылку уведомлений. Клиент же создает экземпляр объекта, подключается к уведомлениям о событиях сервера и вызывает метод сервера, производящий рассылку уведомлений. Клиент получает уведомления и выводит информацию о них в виде окон сообщений. Собственно, этот пример я не создавал, его прислал мне один из посетителей форума «COM/DCOM/COM+» сайта www.rsdn.ru. Этот посетитель не мог запустить данный пример на удаленном сервере. Я только подчистил этот пример и добавил в него код, позволяющий обойти проблемы, возникающие с защитой при осуществлении callback-вызовов. Этот пример интересен тем, что при получении уведомлений клиент на некоторое время становится сервером. В результате клиент должен позаботиться о инициализации защиты в стиле сервера или (как в данном примере) о выключении защиты (снижении ее уровня) для обратных вызовов. Задача усложняется тем, что клиент может исполняться на ОС, поддерживающих защиту очень условно. Догадываетесь, о каких ОС я говорю?

Оба примера вызывают функцию CoSetProxyBlanket. На этом этапе более интересен первый пример, так как он позволяет поэкспериментировать с вызовом этой функции и лучше понять, как она работает.

Для начала о том, как установить и запустить этот пример.

Установка примера ComSec

Если вы решили самостоятельно скомпилировать и зарегистрировать модули, входящие в этот пример, откомпилируйте проекты входящие в Workspace «ComSec» в debug-режиме. Затем откройте snap-in Component Services, создайте новое COM+-приложение и подключите к нему ComSec.dll.

Чтобы создать COM+-приложение, откройте папку «Component Services\Computers\My Computer\COM+ Applications», выделите ее, и из контекстного меню выберите пункт «New\Application». В появившемся визарде выберите «Create an empty application» (создать пустое приложение), задайте «ComSec» в качестве имени нового приложения и нажимайте Next (не изменяя больше ничего) до тех пор, пока визард не закроется, и не будет создано новое COM+-приложение.

Чтобы добавить ComSec.dll в новое приложение, откройте папку этого приложения, выделите папку Components, и из контекстного меню выберите пункт «New\Component». В появившемся визарде выберите «&Install new component(s)» и в появившемся диалоге выбора файлов найдите и выберите ComSec.dll. Далее нажимайте на Enter до тех пор, пока визард не закроется и в папке Components не появится ветка с именем «ComSec.Obj1.1».

Для проведения испытаний необходимо иметь доступ к двум учетным записям домена. Под первой вы должны зайти в систему. От второй вы должны знать только пароль. Обоим записям необходимо дать администраторские права на обе испытательные машины.

Теперь нужно добавить две «роли». Чтобы это сделать, выберите папку «Roles» созданного приложения и из контекстного меню выберите пункт «New\Role». В появившемся диалоге введите «Test1». Аналогичным образом добавьте роль «Test2». Теперь раскройте подпапки «Users» для обеих ролей и добавьте в роль Test1 обе учетные записи, а в роль Test2 только одну. Роли нам сейчас не нужны, но лучше уж сразу все настроить как следует.

Теперь необходимо сгенерировать инсталлятор proxy для COM+-приложения. Для этого выберите из контекстного меню приложения пункт «Export...» и на шаге «Application Export Information» выделите опцию «Application proxy». Введите имя файла инсталлятора и полный путь к папке, куда его необходимо поместить. Нажмите пару раз Enter. Если все прошло успешно, то в указанном вами каталоге будет лежать msi-файл инсталлятора proxy. Его нужно скопировать на второй компьютер и проинсталлировать.

Осталось только скопировать на клиентскую машину SecTest.exe.

Теперь все готово для запуска клиентского приложения.

SecTest

Запустите SecTest.exe. Перед вами должен появиться диалог, аналогичный приведенному на рисунке 10.


Рисунок 10. Клиент тестового приложения ComSec

С помощью этого приложения можно задать большую часть параметров функции CoSetProxyBlanket. Нажатие на кнопку «Call secure method» приводит к тому, что информация, введенная пользователем, передается в CoSetProxyBlanket. После этого вызывается метод GetInfo удаленного объекта. Таким образом, можно интерактивно задавать параметры функции CoSetProxyBlanket и смотреть на получившийся результат. Если запустить SecTest.exe на компьютере, где расположен сервер, то окно сообщения будет выглядеть примерно так, как изображено на рисунке 10a.


Рисунок 10a. Окно сообщений.

Не обращайте внимание на текст перед строкой «----------- !!! -----------». Эта информация относится к COM+, и смысл ее я объясню позже. Сейчас нас интересует только часть, идущая сразу за этой строкой.

Чтобы получить необходимую информацию, в методе GetInfo вызывается функция CoQueryClientBlanket:

LPWSTR Privs;
DWORD dwAuthnLevel = 0;
DWORD dwCapabilities = 0;
DWORD dwAuthnSvc = 0;
DWORD dwAuthzSvc = 0;
hr = CoQueryClientBlanket(&dwAuthnSvc, &dwAuthzSvc, NULL, &dwAuthnLevel, 
       NULL, (RPC_AUTHZ_HANDLE*)&Privs, &dwCapabilities);

Далее полученные значения переводятся в строковый вид и конкатенируются к возвращаемой строке. Этот код малоинтересен. Вы можете изучить его самостоятельно.

Попробуйте изменить сервис аутентификации на что-то отличное от RPC_C_AUTHN_DEFAULT или RPC_C_AUTHN_WINNT, и вы получите сообщение об ошибке «The authentication service is unknown». Это происходит потому, что при вызове в рамках одного компьютера всегда используется NTLMSSP-провайдер. При вызове через сеть (между двумя машинами, запущенными под W2k) вам будет также доступен Kerberos-провайдер (RPC_C_AUTHN_GSS_KERBEROS) и Snego (RPC_C_AUTHN_GSS_NEGOTIATE).

Заметьте также, что изменение уровня аутентификации не приводит к изменениям значения, считываемого на сервере (при локальном вызове). Дело в том, что при локальном вызове для коммуникации между процессами вместо RPC используется LPC (Local Process Call). При этом невозможно вмешательство по сети, и COM автоматически устанавливает значение уровня аутентификации в RPC_C_AUTHN_LEVEL_PKT_PRIVACY (максимальное). При сетевом вызове всегда берется значение, заданное через CoSetProxyBlanket. Заметьте, что если на клиенте будет установлено значение ниже, чем заданное на сервере, вы получите сообщение об ошибке «Access Denied». Уровень аутентификации для COM+-приложения задается в выпадающем списке «Authentication level for calls» на закладке Security.

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

Имперсонация

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

В процессе имперсонации потоку сервера, в котором осуществляется обработка вызова, присваивается токен клиента производящего вызов.

Итак, чтобы имперсонировать поток, нужно всего лишь вызвать функцию CoImpersonateClient. Она не имеет параметров и практически всегда возвращает S_OK (по крайней мере, я другого результата добиться не смог).

Отменить имперсонацию можно с помощью функции CoRevertToSelf. Как и CoImpersonateClient, она тоже старается не возвращать ничего, кроме S_OK. Если вы забудете вызвать эту функцию, по окончании вызова серверного метода COM сделает это за вас.

Зачем же серверу выполнять действия от имени клиента? Для начала, это существенно упрощает осуществление всякого рода проверок. При имперсонации токен клиента ассоциируется с потоком и становится доступным для использования обычными средствами Win32 API.

Так, если токен ассоциирован с потоком, мы можем открыть этот токен и прочитать из него нужную информацию. Вот кусок кода функции GetInfo из первого примера (напомню, это единственная функция единственного COM-объекта, зарегистрированного в COM+-приложении):

hr = CoImpersonateClient();
...
HANDLE hHandle = NULL;
if(OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &hHandle))
{
  DWORD cbBuffer = 1000; // Занимаем заведомо большой буфер.

  // Для начала считаем SID клиента

  PTOKEN_USER ptUser = (PTOKEN_USER)alloca(cbBuffer);
  BOOL bOK = GetTokenInformation(hHandle, TokenUser, ptUser, 
                                 cbBuffer, &cbBuffer);
  if(bOK)
    TestGetAccountInfoStrForSid(_T("\nПоток имперсонировала"), 
      ptUser->User.Sid, sbsMsg);
  else
    TestAppеndSysErrorText(_T("\nGetTokenInformation(TokenUser) Failed!"), 
                           sbsMsg);

  // Теперь узнаем уровень имперсонации токена

  SECURITY_IMPERSONATION_LEVEL tokImpLevel = (SECURITY_IMPERSONATION_LEVEL)-1;
  bOK = GetTokenInformation(hHandle, TokenImpersonationLevel, 
    &tokImpLevel, sizeof(tokImpLevel), &cbBuffer);
  if(bOK)
    TestAppendImpersonationLevel(sbsMsg, tokImpLevel);
  else
    TestAppеndSysErrorText(
       _T("\nGetTokenInformation(TokenImpersonationLevel) Failed!"), sbsMsg);
  
  CloseHandle(hHandle);
}
else
  TestAppеndSysErrorText(_T("\nOpenThreadToken() Failed!"), sbsMsg);

Главное, что демонстрирует этот код – токен потока можно открыть для считывания информации функцией OpenThreadToken. Первый параметр этой функции задает handle потока, второй – тип доступа, в четвертом (в случае успеха) возвращается handle токена, а вот на третьем параметре (выделенный жирным шрифтом) хотелось бы остановиться поподробнее. Он называется OpenAsSelf и имеет тип BOOL. Если этот параметр задать в FALSE, проверка прав доступа к открываемому токену будет производиться для токена потока (т.е. с правами клиента, так как мы только, что ассоциировали его токен с потоком). Поэтому при вызове с низким уровнем имперсонации, или с недостаточными правами пользователя, будет получена ошибка E_ACCESSDENIED. Если же установить этот параметр в TRUE, проверки будут производиться под токеном процесса. Чтобы иметь возможность гарантированно читать информацию о токене клиента, необходимо передать в этот параметр именно TRUE.

Далее дважды вызывается функция GetTokenInformation для определения SID клиента и типа имперсонации.

Все методы и функции, реализованные мною (не API-функции), начинаются с префикса Test. Функции TestAppеndSysErrorText и TestAppеndErrorText добавляют в строку сообщение о системной и COM-ошибках соответственно. Их рассмотрение выходит за рамки данной статьи. Если вас интересует их реализация, вы можете найти их в файле shared.h в прилагаемых проектах. А вот функция TestGetAccountInfoStrForSid заслуживает более пристального внимания. Она преобразует SID в строку и добавляет ее к общей строке сообщения вместе с комментарием. Вот ее код:

void TestGetAccountInfoStrForSid(
    LPCTSTR szNote, 
    PSID pSID, 
    CComBSTR & sbsOutMsg
)
{

  const int ciNameSize = 100;
  DWORD cbDomain = ciNameSize, cbUserName = ciNameSize;
  TCHAR szUserName[ciNameSize], szDomain[ciNameSize];
  SID_NAME_USE SidNameUse;

  // В режиме со слабым уровнем имперсонации нам не дадут вызвать 
  // LookupAccountSid. Поэтому мы пытаемся временно отменить имперсонацию...
  CComPtr<IServerSecurity> spIServerSecurity;
  HRESULT hr = CoGetCallContext(__uuidof(spIServerSecurity), 
                                (void**)&spIServerSecurity);
  if(S_OK != hr)
  {
    sbsOutMsg.Append(OLESTR("\nНевозможно получить IServerSecurity!"));
    return;
  }

  // Запоминаем, был ли включен режим имперсонации...
  BOOL bImpersonated = spIServerSecurity->IsImpersonating();
  if(bImpersonated) // ...если был выключаем его
    spIServerSecurity->RevertToSelf();
  if(!LookupAccountSid(NULL, pSID, szUserName, &cbUserName, 
                       szDomain, &cbDomain, &SidNameUse))
    return;
  // Если до этого выполнение происходило в режиме имперсонации,
  // то нужно вернуться в этот режим.
  if(bImpersonated)
    spIServerSecurity->ImpersonateClient();
  TCHAR szBuf[300];
  wsprintf(szBuf, _T("%s учетная запись: %s\\%s имеющая тип: %s"), 
    szNote, szDomain, szUserName, GetSidTypeStr(SidNameUse));
  sbsOutMsg.Append(szBuf);
}

Эта функция интересна по двум причинам. Во-первых, из-за решаемой задачи – она преобразует SID в пригодный для чтения вид (посредством Win32 API-функции LookupAccountSid), а во-вторых тем, что эта функция может вызываться в то время, когда поток имперсонирован токеном клиента.

Как пользоваться функцией LookupAccountSid, ясно из кода, а вот проблема вызова ее из имперсонированного потока требует разъяснения. Как и в случае с вызовом OpenThreadToken с параметром OpenAsSelf, заданным в FALSE, функция LookupAccountSid начинает пользоваться токеном, ассоциированным с потоком, но, в отличие от OpenThreadToken, функция LookupAccountSid не имеет спасительного параметра. Чтобы в очередной раз не наткнуться на навязчивое сообщение «Access Denied», необходимо определить, не имперсонирован ли текущий поток, и, если он имперсонирован, временно прервать имперсонацию, вызвать LookupAccountSid и восстановить имперсонацию. В результате функция LookupAccountSid будет всегда выполняться под токеном основного процесса (сервера), и проблем с недостаточностью прав не будет. Ввиду того, что для метода IServerSecurity::IsImpersonating не было создано вспомогательной функции, а также для сокращения количества вызовов QueryInterface (IServerSecurity), здесь используются не вспомогательные функции, а сам интерфейс.

Кроме чтения информации из клиентского токена, вы можете производить проверки SID клиента на соответствие ACL. Для этого нужно будет сформировать новый или считать уже имеющийся ACL, и воспользоваться функцией AccessCheck. Я не буду рассматривать эту функцию, так как она довольно сложна и практически бесполезна в COM-приложениях (если есть желание, ее описание и примеры применения можно найти в MSDN). Бесполезна потому, что есть более простые способы проверки прав пользователя. Так, в COM+ то же самое можно сделать с помощью «ролевой безопасности» (о ней речь пойдет далее). А в простом COM-е можно воспользоваться простым трюком, о котором я сейчас расскажу.

Можно создать объект ядра. Для этой цели прекрасно подходит ветка реестра (естественно, в NT, Windows 9x не поддерживает ни защищенных объектов, ни вообще какой-либо защиты). Можно создать необходимое количество веток реестра и назначить некоторым пользователям права на эти ветки. Сделать это можно из regedit.exe в Windows XP и более новых версиях, или в regedt32.exe в более старых ОС. В коде сервера нужно включать режим имперсонации и пытаться открыть ветку.

В примере ComSec есть код, который пытается открыть ветку реестра до и после имперсонации. Но для того, чтобы его протестировать, нужно сделать некоторые приготовления. Откройте на машине, которая выступает в качестве сервера, regedt32.exe (или regedit.exe в Windows XP), и добавьте в ветку HKEY_LOCAL_MACHINE\SOFTWARE подветку с именем ComSec. Теперь назначьте для этой ветки права на полный доступ для одной из учетных записей (но не для обеих!).

Теперь можно запустить клиентское приложение под разными учетными записями. Это можно сделать с удаленной машины. Можно также запустить процесс под другим пользователем, сделав ярлык на клиентское приложение и включив для этого ярлыка опцию «Run as different user». Если установки объекта в COM+-приложении имеют минимальные установки имперсонации, а на клиенте в качестве этого параметра задано значение RPC_C_IMP_LEVEL_IDENTIFY, оба клиента не смогут открыть ключ (с сообщением, что текущий уровень имперсонации недостаточен). Но если поднять уровень имперсонации до RPC_C_IMP_LEVEL_IMPERSONATE, то пользователь, которому разрешен доступ к ветке, получит его, а второй получит отказ в доступе.

Вот выдержки из кода примера ComSec, производящего доступ к ветке реестра:

void TestTryOpenRegKey(LPCTSTR szMsg, CComBSTR & sbsOutMsg)
{
  HKEY hKey = NULL;
  sbsOutMsg.Append(
     OLESTR("\nRegOpenKeyEx(HKEY_LOCAL_MACHINE,... \"SOFTWARE\\ComSec\")"));

  HRESULT hr = RegOpenKeyEx(HKEY_LOCAL_MACHINE, 
    _T("SOFTWARE\\ComSec"), 0, KEY_QUERY_VALUE | KEY_READ, &hKey);
  hr = HRESULT_FROM_WIN32(hr);
  if(SUCCEEDED(hr))
  {
    sbsOutMsg.Append(OLESTR(" OK. "));
    RegCloseKey(hKey);
  }
  else
    TestAppеndErrorText(hr, _T(" Failed! "), sbsOutMsg);
  sbsOutMsg.Append(szMsg);
}
...

// Пытаемся открыть защищенный ключ реестра для учетной записи,
// под которой запущен сервер.
TestTryOpenRegKey(_T("(без имперсонации)") , sbsMsg);
  
// Применяем к потоку токен клиента.
hr = CoImpersonateClient();
...

// Пытаемся открыть ключ реестра для учетной записи клиента, которая
// в данный момент ассоциирована с потоком.
TestTryOpenRegKey(_T("(с имперсонацией!)") , sbsMsg);

TestTryOpenRegKey пытается открыть ключ реестра и выводит диагностические сообщения. TestTryOpenRegKey вызывается в основной функции (GetInfo) дважды. Один раз до имперсонации, а второй - после. В моем случае сервер запускался под учетной записью, которой был дан полный доступ к реестру и, естественно, в режиме без имперсонации ключ всегда открывался без проблем.

Описанный мною подход позволяет выбирать одну из двух стратегий. Можно просто создавать ключи реестра (или других объектов ядра), логически ассоциируя с каждым из них некоторую роль. При этом вы проверяете, может ли тот или иной клиент получить доступ к ключу реестра. Если может, ставите виртуальную галочку, что клиент поддерживает некоторую роль. А можно просто заложить в реестр некоторую важную для работы программы информацию, например, привилегированную строку соединения с БД.

Естественно, что если речь идет просто о доступе к некоторым защищенным объектам, то вообще ничего, кроме имперсонации, делать не надо.

Делегация

До W2k, да и в W2k по умолчанию вызов COM-сервера из другого сервера производится от лица учетной записи первого сервера. Если строится распределенная система, содержащая множество серверов, появляется необходимость изменить это положение. Цепь вызовов может соединять несколько серверов, причем каждый из них должен иметь возможность работать от имени клиента, то есть поток сервера иметь возможность имперсонации клиента. Такую возможность предоставляет уровень имперсонации RPC_C_IMP_LEVEL_DELEGATE. Делегация – наиболее мощный уровень имперсонации. Microsoft справедливо боится этого уровня, поскольку непродуманное использование делегации может пробить обширную брешь в защите. Однако препоны, чинимые Microsoft, сильно напоминают паранойю. Включить этот уровень имперсонации – задача не для слабых духом, поэтому имеет смысл уделить ему некоторое внимание.

Аутентификация по протоколу Kerberos создает токен этого уровня при следующих условиях:

Учетная запись, которую вы пытаетесь делегировать, не должна быть помечена в Active Directory как "sensitive and cannot be delegated" и должна быть помечена как "Trusted for delegation".

Учетная запись, под которой работает серверный процесс, должна быть помечена в Active Directory как "Trusted for delegation".

Как этого добиться? Начнем с учетной записи клиента. Сначала запустите на контроллере домена snap-in Active Directory Users and Computers. В свойствах учетной записи на закладке Account включите опцию Trusted for delegation, а опцию Account is sensitive cannot be delegated, наоборот, выключите.

Теперь перейдем к серверному процессу. Если он исполняется под системной учетной записью, принципалом является компьютер, на котором работает этот процесс. Поэтому настройки нужно производить для учетной записи компьютера. Иначе нужно настраивать ту учетную запись, под которой был запущен сервер. Делается это так же, как и для клиента.

Все компьютеры в цепочке вызовов должны работать под W2k или более поздней версией ОС в домене W2k.

Уровень делегации должен быть назначен для proxy, через которую производится вызов либо напрямую, через CoSetProxyBlanket, либо выбираться через алгоритм Security Blanket Negotiation. Во втором случае клиент не должен явно задавать уровень имперсонации, а на сервере должен быть задан именно уровень Delegate.

Делегация доступна только при использовании протокола Kerberos. Как вы помните, в пределах одного компьютера в W2k всегда используется NTLMSSP. Но это не препятствует использованию уровня имперсонации RPC_C_IMP_LEVEL_DELEGATE. Напротив, использовать этот уровень с NTLMSSP даже проще, поскольку не нужна настройка учетных записей. SChannel не поддерживает делегации вообще.

Cloaking

В определении поведения имперсонации участвуют два ингредиента: полномочия, предоставляемые клиентом серверу через уровень имперсонации, и способность сервера скрывать свою идентичность при действиях от лица клиента. Эта способность называется маскировкой (cloaking). Достаточно подробно маскировка рассмотрена в описаниях CoInitializeSecurity и CoSetProxyBlanket.

Cloaking – это просто новый термин для того, что в named pipes и RPC называлось "управление identity tracking (отслеживание учетной записи клиента, от имени которой производится вызов)".

W2k поддерживает и статическую, и динамическую модель маскировки (непонятно, почему в NT 4 это не поддерживалось, ведь MSRPC давным-давно поддерживает эту модель). Интересно, что в W2k появилась третья опция, которую RPC, в сущности, не поддерживает, причем эта опция используется по умолчанию. Я бы назвал ее "игнорировать токен потока", поскольку именно это она и делает: определяя, от какого лица производить исходящий вызов, COM полностью игнорирует токены потоков. Вместо них всегда используется токен процесса. Рисунок 11 показывает различия между тремя моделями маскировки.


Рисунок 11.

COM+

Прежде чем говорить о защите в COM+ нужно, наверное, ответить на вопрос – что же такое COM+. Queued-компоненты, поддержка событий, распределенные транзакции – это, хотя и важные и очень полезные, но все же не главные возможности COM+. Прародитель COM+ – MTS сыграл плохую роль. Название "MTS" четко ассоциировало его с менеджерами транзакций. На самом деле MTS, и в особенности COM+ – это замечательный сервер приложений. AppCenter, новый продукт Microsoft, поднимает возможности COM+ до заоблачных высот.

Так что же нового может предложить COM+ в области защиты? И не приведет ли это к еще большему усложнению и без того сложного механизма защиты? Да, в COM+ появился ряд новшеств, расширяющих возможности DCOM, но, к счастью, это не привело к усложнению жизни программистов. Напротив, применение защиты в COM+ упростилось! Да-да, упростилось, несмотря на усложнение. :)

Упростилось оно за счет трех факторов. Во-первых, появилось настоящее средство администрирования. О нем уже говорилось выше – это snap-in Component Services. Во-вторых, многие проблемы, которые раньше можно было решить, только написав море сложного кода, теперь можно решать, задавая атрибут. Атрибуты – это новшество, появившееся в MTS и доведенное до совершенства в COM+. Самое приятное, что их можно задавать как программно, так и декларативно – через средства администрирования. В-третьих, появилась так называемая ролевая безопасность – концепция, похожая на группы в NT, но с несколько иной идеологической подоплекой и преимущественно ориентированная на декларативный стиль работы.

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

Еще одной новостью стало то, что COM+ предоставляет дополнительный сервис через контекст объекта. В этот список вошел и сервис расширенного управления безопасностью.

Ну и, наконец, в COM+ введена замечательная система администрирования. Теперь администратору не нужно бегать от одной машины к другой, копировать нужные файлы и задавать настройки для каждого COM-приложения. COM+ позволяет создавать инсталляторы так называемых COM+-proxy (не путайте их с proxy интерфейсов). А возможность получить программный доступ к настройкам COM+-приложения и клиентских компьютеров дает небывалую гибкость. При этом все администрирование (в том числе установка серверов и клиентских COM+-proxy) может осуществляться удаленно (централизовано).

Ролевая безопасность

Сначала разберемся, как декларативно задать безопасность, и что из этого получится. Откройте snap-in Component Services, найдите в нем приложение ComSec и раскройте его иерархию. У вас должно получиться нечто похожее на рис. 12.


Рисунок 12. COM+-приложение ComSec.

Теперь необходимо настроить защиту приложения так, чтобы COM+ смог полностью показать свои возможности. Для этого откройте свойства приложения и перейдите на закладку Security (см. рис. 13).


Рисунок 13. Страница свойств настройки защиты

Security level позволяет указать, будут проверяться права при доступе ко всему приложению в целом (Perform access checks only at the process level) или при доступе к отдельным объектам и методам (Perform access checks at the process and component level). Первый вариант – это то, что было доступно в обычном DCOM. Второй вариант появился в MTS и был доведен до совершенства в COM+. Переключатель Enforce access checks for this application позволяет включить или выключить поддержку ролевой безопасности.

ПРЕДУПРЕЖДЕНИЕ

Ролевая безопасность может работать, только если выбрана опция Perform access checks at the process and component level. Будьте аккуратны, так как переключатель и данная опция не зависят друг от друга в интерфейсе настройки защиты COM+-приложения.

Выпадающие списки Authentication level for calls и Impersonation level позволяют настроить значения по умолчанию для уровня аутентификации и имперсонации соответственно. Эти значения COM+ подставит в функцию CoInitializeSecurity при запуске COM+-приложения. Их декларативное задание очень актуально для COM+-приложений, так как компоненты, реализуемые в DLL-сервере, не могут самостоятельно вызвать эту функцию.

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

Теперь настройте роли для объекта, его интерфейса или метода. Для этого выберите сущность, которой нужно назначить роль, откройте её свойства и на закладке Security отметьте необходимые роли. (Как я уже говорил, настроить сами роли и задать ассоциированных с ними пользователей можно в папке Roles COM+-приложения.). Принцип работы ролей таков: если включена ролевая безопасность, то, чтобы получить доступ к методу некоторого объекта, нужно, чтобы хотя бы одна роль, назначенная пользователю (от лица которого производится вызов), была явно включена для объекта, интерфейса или непосредственно метода. По существу, роли очень похожи на группы в NT. Но есть несколько оговорок. Во-первых, роль может быть ассоциирована только с принципалами, т.е. ни группа, ни другая роль не могут быть ассоциированы с ролью. Во-вторых, роль не может содержать запрещений, т.е. вы не можете запретить Иванову вызывать именно этот метод, для этого придется задавать доступ на уровне методов и просто не дать разрешение для ролей, с которыми ассоциирован Иванов. В-третьих, роль следует воспринимать несколько иначе, нежели группу. Надо рассуждать так: роль выделяет некоторые логические типы принципалов. Например, у вашего приложения может быть четыре вида пользователей:

Вы можете задать список принципалов, которым назначена эта роль (например, Интернет-пользователю дать роль Customer, а остальных назначить на роли в соответствии со штатным расписанием), а потом задать роли для отдельных методов. Роль Администратора применяется всюду (ну, возможно, за исключением некоторых методов, возвращающих конфиденциальные данные). Роль Клиента назначается только тем методам, которые предназначены для принятия заказов и возврата данных клиенту. Служащие ассоциируются с большинством неконфиденциальных методов и объектов. Начальник – с методами и объектами, позволяющими получать конфиденциальную информацию, но не с методами, которые позволяют вносить изменения в работу, выполняемую персоналом. И список ролей, и их назначения могут изменяться со временем, причем делать это может администратор (без участия программиста).

Честно говоря, ролевая безопасность уступает по возможностям и гибкости парадигме работы с группами в NT, но благодаря простоте восприятия, легкости декларативной настройки и простоте программной проверки она существенно облегчает создание защищенных распределенных приложений.

Контекст вызова (call context)

Контекст вызова – это некоторая абстракция, через которую COM+ предоставляет свои сервисы серверному коду. Все нововведения в COM+ так или иначе связаны с контекстом вызова. Именно через этот самый контекст и осуществляется предоставление сервисов, связанных с защитой. COM+ предоставляет несколько интерфейсов, связанных с защитой. ISecurityCallContext позволяет легко получить расширенную информацию о текущем клиенте. IObjectContext в основном предназначен для управления транзакциями, но содержит также и методы, аналогичные ISecurityCallContext. Кроме них появился набор Automation-совместимых интерфейсов для работы из VB6 и скриптовых языков. Стоит отметить, что они значительно удобнее в использовании, чем их предшественники.

GetSecurityCallContext

Чтобы в VB получить доступ к свойствам контекста вызова, можно воспользоваться функцией GetSecurityCallContext. Она возвращает ссылку на объект SecurityCallContext, реализующий интерфейс ISecurityCallContext.

ISecurityIdentityColl

C и C++-программисты для тех же целей могут воспользоваться интерфейсом ISecurityIdentityColl. Для его получения нужно воспользоваться функцией CoGetCallContext.

ISecurityCallContext

Интерфейс ISecurityCallContext предоставляет доступ к методам защиты и контексту безопасности вызова. COM+-приложения, использующие ролевую безопасность, получают доступ к коллекции, содержащей свойства контекста вызова. С помощью этого интерфейса можно получить информацию о любом из участников цепочки вызовов.

Ниже перечислены свойства, содержащиеся в этой коллекции.

Этот интерфейс также содержит методы IsSecurityEnabled (позволяющий узнать, включена ли ролевая безопасность), IsCallerInRole (позволяющий определить, назначена ли клиенту та или иная роль) и IsUserInRole, позволяющий определить, назначена ли пользователю та или иная роль.

IObjectContext

Этот интерфейс в в основном предназначен для управления транзакциями, но со содержит также и методы для работы с ролевой безопасностью. Взгляните на следующий код, взятый из метода GetInfo (серверного объекта из примера ComSec):

void TestIsCallerInRole(IObjectContext * pIObjectContext, 
         CComBSTR & sbsOutMsg, LPCTSTR szRole
)
{
  USES_CONVERSION;
  BOOL bInRole = FALSE;
  LPTSTR szMsg = NULL;
  TCHAR szBuf[1000];

  // Проверяем принадлежность ноли szRole.
  HRESULT hr = pIObjectContext->IsCallerInRole(CComBSTR(szRole), &bInRole);
  if(SUCCEEDED(hr))
  {
    // Формируем строку сообщения
    if(bInRole)
      szMsg = _T("\nВызывающая сторона в роли %s.");
    else
      szMsg = _T("\nВызывающая сторона НЕ (!) в роли %s.");

    // Подставляем в сформированную строку сообщения название роли
    wsprintf(szBuf, szMsg, szRole);
    sbsOutMsg.Append(szBuf);
  }
  else
  {
    szMsg = _T("\nОшибка: IsCallerInRole при проверке на роль %s");
    wsprintf(szBuf, szMsg, szRole);

    TestAppendErrorText(hr, szBuf, sbsOutMsg);
  }
}

...
CComPtr<IObjectContext> spIObjectContext;

HRESULT hr = GetObjectContext(&spIObjectContext);
if(SUCCEEDED(hr))
{
  sbsMsg.Append(OLESTR("Object-Context присутствует!"));
  
  // Проверяем, включена ли ролевая безопасность.
  if(spIObjectContext->IsSecurityEnabled())
    sbsMsg.Append(OLESTR("\nРолевая безопасность COM+ включена!\n"));
  else
    sbsMsg.Append(OLESTR("\nРолевая безопасность COM+ ВЫКЛЮЧЕНА!\n"));
  
  // Проверяем принадлежность к ролям...
  TestIsCallerInRole(spIObjectContext, sbsMsg, _T("Test1"));
  TestIsCallerInRole(spIObjectContext, sbsMsg, _T("Test2"));
  TestIsCallerInRole(spIObjectContext, sbsMsg, _T("Test3"));

В этом коде сначала с помощью функции GetObjectContext получается указатель на IObjectContext, а затем в функции TestIsCallerInRole происходит проверка ассоциации роли с данным клиентом. Она осуществляется с помощью метода IObjectContext::IsCallerInRole. Этот метод получает имя роли через свой первый параметр, а через последний параметр возвращает булево значение. TRUE означает, что пользователю назначена указанная роль, а FALSE – что нет. Если указанной роли вообще нет в рамках COM+-приложения, возвращается соответствующая ошибка.

Интересной особенностью метода IsCallerInRole является то, что TRUE возвращается также и в случае, если ролевая безопасность выключена в настройках приложения (о том, как это сделать, написано выше). Проверить, включена ли в данный момент ролевая безопасность, можно с помощью метода IObjectContext::IsSecurityEnabled.

Динамические проверки принадлежности клиента к той или иной роли нужны, если метод COM-объекта осуществляет доступ к некоторому ресурсу, и разным клиентам нужно давать разные права доступа к этому ресурсу (или организовать любую другую сложную логику). Например, вы можете создать метод, который изменяет бухгалтерскую проводку. Этот метод должен позволять рядовым бухгалтерам исправлять данные только в текущем периоде, а главному бухгалтеру – в предыдущем. Вместо того, чтобы создавать дополнительный метод, можно программно проверить клиента на принадлежность к группе «Главный бухгалтер». После установки приложения у заказчика администратор сможет назначить некоторым пользователям роль главного бухгалтера, позволяя изменять данные прошлого периода, и, тем самым, спокойно работать над сдачей баланса в то время, как остальные бухгалтеры будут защищены от непреднамеренной (а может, и преднамеренной) порчи данных. Подчеркну еще раз, что программные проверки не должны быть доминирующими. К ним нужно прибегать, только если декларативные средства не дают необходимой свободы действий.

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

Интерфейс ISecurityProperty

Интерфейс ISecurityProperty позволяет получить из контекста объекта информацию о клиентах, то есть SID создавшего объект клиента, и SID клиента, вызывающего объект в текущий момент.

Методы GetDirectCreatorSID и GetOriginalCreatorSID использовать не стоит. Дело в том, что результат их использования в COM+ непредсказуем – они разрабатывались для MTS и сохранены для совместимости.

Пример ComSec демонстрирует работу методов интерфейса ISecurityProperty, преобразуя полученные SID в текстовый вид. Однако сам пример не рассчитан на множественные вызовы. Поэтому, чтобы получить разные результаты вызовов, вам придется самостоятельно изменить пример.

На практике чаще всего используется метод GetDirectCallerSID – в качестве простого способа получения SID клиента.

Подводные камни

Напоследок рассмотрим несколько часто встречающихся проблем и пути их решения.

В различных конференциях и форумах то и дело появляются вопросы типа – «Мой DCOM/COM+-сервер прекрасно работает на одной машине (обычно W2k или NT 4) и наотрез отказывается работать на удаленном сервере. Что я делаю не так и главное(!), как определить в чем проблема?».

Большинство таких вопросов вызваны непониманием, а значит неправильным обращением с системой безопасности COM/Windows. Причем конкретных проблем может быть довольно много. Что же нужно сделать, чтобы найти ошибки?

Обработка ошибок

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

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

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

#define CHK(hr) \
  do \
  { \
    HRESULT hr_tmp = hr; \
    if(FAILED(hr_tmp)) \
    { \
      TestDisplayError(hr_tmp, \
        _T("При выполнени: \nCHK(")_T(#hr)_T(");")); \
      return hr_tmp; \
    } \
  } \
  while(0)

Этот макрос анализирует (с помощью стандартного макроса FAILED) HRESULT передаваемый ему в качестве параметра и, если произошла ошибка, вызывает функцию TestDisplayError, которая извлекает и показывает сообщение об ошибке вместе с передаваемым ей сообщением. Сообщение формируется из текста вызываемой строки кода и сообщения об ошибке COM. Таким образом, пользователь видит код, вызвавший ошибку, и сообщение, описывающее эту ошибку. После того, как пользователь закроет окно сообщения, происходит выход из функции с возвратом HRESULT, содержащего ошибку. Это подразумевает, что данный макрос будет использоваться из функций, возвращающих HRESULT. Реализацию этого макроса и функций, используемых для извлечения информации об ошибках, можно найти в файле shared.h, который находится в прилагаемых примерах. Обработка ошибок в COM заслуживает отдельной статьи, которая, возможно, появится на страницах этого журнала и сайте www.rsdn.ru.

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

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

Избегайте излишней вложенности. Это упрощает код, а значит, уменьшает вероятность появления ошибки. Чтобы избежать излишней вложенности, можно пользоваться простенькой хитростью. Вместо проверки на успех (макросом SUCCEEDED), старайтесь проверять на ошибку (макросом FAILED). Мой пример не придерживается этого правила, исключительно из-за того, что мне нужно было продемонстрировать логику работы COM даже в ситуации, когда большинство функций завершаются неудачно.

Понятно что обработка ошибок дело скучное, но главное – помнить, что время, потраченное на это занятие, окупится сторицей на стадии отладки и при запуске системы в эксплуатацию. Так, во втором тесте, чтобы понять, в чем дело, мне хватило двадцати минут. А задававший вопрос программист маялся с ним несколько дней. А ведь единственное, что я сделал – это добавил обработку ошибок!

Уточнение причин...

Итак, вы добавили обработчики ошибок всюду, где это было возможно, переписали клиентское приложение и сервер на клиентскую машину (или зарегистрировали proxy для COM+-приложения), но при запуске получили сообщение об ошибке. Что делать? Первое, что нужно проверить – а загружается ли вообще сервер? Есть простой, но исключительно эффективный способ – добавьте некоторый звук на загрузку процесса в настройках подопытного сервера. Это можно сделать с помощью одного простого трюка. Можно ассоциировать звуковой сигнал с запуском приложения (в «Control Panel» > «Sounds and Multimedia Properties» > «Sound Events» > «Windows» > «Open program»). Если звука нет, то проблема или в правах активации (см. выше) или неправильных настройках DCOM по умолчанию.

Первое, что стоит сделать – это убедиться, включен ли на машине DCOM. В разделе, посвященном dcomcnfg, рассказано, как это сделать.

Если все в порядке, но сервер все же не запускается, или запускается (звук есть), но не работает, следует проверить, на чем, собственно, вы пытаетесь запускать сервер и клиентское приложение?

Первое правило – не используйте в качестве сервера системы на базе Windows 9x. Даже под страхом увольнения. Если уж обстоятельства таковы, что не в вашей воле выбирать, и нужно использовать Windows 9x, даже не надейтесь работать с защитой. Постарайтесь ее отключить. В крайнем случае, знайте, что дескрипторы защиты незнакомы Windows 95. Прочтите все, что сможете найти про IAccessControl, и приготовьтесь к большому количеству разочарований. Но намного правильнее будет показать эти строки своему начальству и попросить денег на покупку W2k (или одного из ее отпрысков).

Используя Windows 9x в качестве клиента, не забудьте установить DCOM 9x 1.3 или более совершенную версию. Учтите, что без установки этого пакета использовать Windows 9x лучше не стоит. Да и с DCOM 9x потомки Windows 95 проигрывают во всех отношениях (кроме объемов необходимой памяти) ОСям линейки NT.

Если в качестве ОС вы используете NT4, постарайтесь обеспечить, чтобы на ОС был установлен SP6a (или более свежий пакет обновления, если таковой появится). DCOM/MTS-приложения (и COM+-клиенты) будет работать и с более старым пакетом обновления. Но, во-первых, не все возможности будет доступны. А во-вторых, вы можете столкнуться с ненужными проблемами, которых легко избежать, потратив двадцать минут на установку SP6.

Даже с установленным SP6 NT4 все же уступает W2k. Так что если у вас есть выбор, то всегда выбирайте W2k.

Если сервер (установленный на NT4) не запускается, а клиенту возвращается ошибка E_OUTOFMEMORY, с большой долей уверенности можно сказать, что вы пользуетесь устаревшей версией dcomcnfg или OLEView, которые не добавляют учетную запись SYSTEM в права доступа (или права доступа по умолчанию). В правах доступа всегда должна присутствовать учетная запись SYSTEM. Это связано с тем, что запуск COM-объектов осуществляет COM SCM, и, естественно, если у него нет прав на доступ к объекту, ничего хорошего не выйдет. Странное же сообщение об ошибке (E_OUTOFMEMORY) является всего лишь банальной ошибкой в DCOM (которая исправлена в более новых версиях DCOM, но вам от этого легче не станет).

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

Если вы получаете сообщение E_ACCESSDENIED, сначала нужно убедиться в том, что оба компьютера находятся в одном домене, или что домены, к которым они относятся, соединены доверительными отношениями. При этом нужно пользоваться именно учетными записями домена, а не локальными. Частой ошибкой является использование некоторой локальной учетной записи. Надо понимать, что, например, учетные записи Administrator на одной машине и на другой – это две совершенно разные записи. Administrator на машинах VLAD и MIKE, и администратор домена OPT будут восприниматься подсистемой защиты NT как VLAD\Administrator, MIKE\Administrator и OPT\Administrator! Если домена вообще нет (и только в этом случае!), можно воспользоваться предоставляемой NT возможностью защиты на уровне рабочих групп. Основная идея заключается в том, что если учетные записи на разных машинах имеют одинаковые имя и пароль, то NT будет автоматически авторизовать доступ с удаленного компьютера, как будто он производится от имени локальной учетной записи. Но это происходит, только если компьютеры не подключены к домену.

Вообще, если есть возможность использовать доменную подсистему NT, это делать нужно обязательно. Как минимум, вы выиграете в скорости.

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

Если вы получили сообщение об ошибке, говорящее, что User32.dll не инициализирована или не может быть загружена, знайте, что ваша программа пытается вывести окно, послать сообщение или сделать еще что-либо с десктопом, но у учетной записи, под которой она запущена, недостаточно прав на взаимодействие с Windows-станцией или десктопом.

Если в сообщении фигурирует Shell32.dll, скорее всего, приложение запущено под учетной записью пользователя, которому запрещен локальный доступ в систему, а приложение пытается обратиться к его профайлу.

Локализовать проблему, возникающую при вызове, можно следующим образом.

Используйте отладчик, чтобы проверить, достигает ли вызов серверного метода. Если этого не происходит, проанализируйте ошибку, возвращаемую при вызове. Проблема может быть, например, в несоответствии уровней аутентификации или авторизации. Проблема несоответствия уровня аутентификации особенно обостряется, если клиентом (или еще хуже, сервером) выступает Windows 9x. В этом случае при вызовах методов сервера проблемы беспокоят не так сильно, но если возникает необходимость произвести обратный вызов (например, принять оповещение клиента о событии), без ручного вмешательства в настройки защиты не обойтись. Причем в большинстве случаев обратные вызовы совершенно не нужно защищать (ведь подключаться к интерфейсам клиента не только сложно, но и довольно бессмысленно).

Я решил рассмотреть вопрос с обратными вызовами более подробно. Для этого к статье прилагается тестовый проект ComSrvEvents, в котором имеется DCOM-сервер с объектом, реализующим события (через IConnectionPoint/ IConnectionPointContainer), и простенький клиент, подключающийся к событиям сервера и вызывающий его метод Method1. Этот метод инициирует рассылку уведомлений (обратных вызовов).

Первая версия этого теста не предпринимала никаких действий и вылетала с ошибкой при попытке подключиться к событиям сервера. Реально сбой происходил при попытке вызова QueryInterface внутри реализации серверного метода IConnectionPoint::Advise. Причем проблема проявлялась только при запуске DCOM-приложения на удаленном сервере (локально все работало).

Что делать в такой ситуации?

Есть два пути. Первый – отключить (понизить до минимума установки) защиту на сервере и клиенте. Это можно сделать, вызвав на сервере и клиенте функцию CoInitializeSecurity следующим образом:

CoInitializeSecurity(NULL, -1, NULL, NULL,
    RPC_C_AUTHN_LEVEL_NONE, RPC_C_IMP_LEVEL_IDENTIFY, 
    NULL, EOAC_NONE, NULL);

Главное здесь – минимальный уровень аутентификации. Так как уровень аутентификации и у клиента, и у сервера по умолчанию NONE, никаких проверок прав доступа производиться не будет, и следовательно, вызовы в обе стороны будут производиться без пробоем.

Но такой вариант подходит, только если вам глубоко наплевать на защиту и попросту нужно от нее избавиться. Если защита для сервера все же нужна, то можно понизить или отключить защиту для клиента, но оставить достаточный уровень защиты сервера. Можно проинициализировать защиту приведенным выше образом только для клиента. Но при попытке обратного вызова (того же самого QueryInterface в IConnectionPoint::Advise) вы снова получите сообщение об ошибке. Что же происходит? Дело в том, что вне зависимости от того, делается прямой вызов или обратный, уровни аутентификации и имперсонации подбираются COM-ом путем анализа установок клиента и сервера. Этот процесс подробно описан выше. При этом клиентский объект вызывается не с минимальными настройками, а с настройками, заданными при вызове функции CoInitializeSecurity на сервере. Чтобы избавиться от этого эффекта, можно задавать настройки этих параметров напрямую для proxy клиентского интерфейса (непосредственно перед вызовом). Сервер из проекта ComSrvEvents реализован на ATL. ATL предоставляет визард, помогающий упростить реализацию IConnectionPoint. Этот визард предлагает выбрать библиотеку типов и имя интерфейса, и по описанию этого интерфейса генерирует файл, содержащий класс, реализующий IConnectionPoint для выбранного интерфейса.

Основная реализация IConnectionPoint находится в шаблоне IConnectionPointImpl<> (от которого наследуется генерируемый класс). В этом шаблоне находится и реализация метода IConnectionPoint::Advise. Чтобы понизить уровень аутентификации и другие параметры, я переопределил метод Advise в сгенерированном визардом классе. Вот что из этого получилось:

STDMETHODIMP Advise(IUnknown* pUnkSink, DWORD* pdwCookie)
{
  T* pT = static_cast<T*>(this);
  IUnknown* p;
  HRESULT hRes = S_OK;
  if (pUnkSink == NULL || pdwCookie == NULL)
    return E_POINTER;

  IID iid;
  GetConnectionInterface(&iid);

  CoSetProxyBlanket(pUnkSink, 
    RPC_C_AUTHN_NONE, 
    RPC_C_AUTHZ_NONE, 
    NULL,
    RPC_C_AUTHN_LEVEL_NONE,
    RPC_C_IMP_LEVEL_IDENTIFY,
    NULL,
    EOAC_NONE);

  hRes = pUnkSink->QueryInterface(iid, (void**)&p);
  if (SUCCEEDED(hRes))
  {
    pT->Lock();
    *pdwCookie = m_vec.Add(p);
    hRes = (*pdwCookie != NULL) ? S_OK : CONNECT_E_ADVISELIMIT;
    pT->Unlock();
    if (hRes != S_OK)
      p->Release();
  }
  else if (hRes == E_NOINTERFACE)
    hRes = CONNECT_E_CANNOTCONNECT;
  if (FAILED(hRes))
    *pdwCookie = 0;
  return hRes;
}

Единственное изменение, которое мне пришлось сделать –добавить вызов CoSetProxyBlanket (выделено жирным).

Такой же код пришлось добавить в методы рассылки сообщений, сгенерированные визардом:

HRESULT Fire_Event1(BSTR str)
{
  CComVariant varResult;
  T* pT = static_cast<T*>(this);
  CComVariant pvars[1];
  int nConnections = m_vec.GetSize();
  
  int nConnectionIndex = 0;
  for (; nConnectionIndex < nConnections; nConnectionIndex++)
  {
    pT->Lock();
    CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);
    pT->Unlock();
    IDispatch* pDispatch = reinterpret_cast<IDispatch*>(sp.p);
    if (pDispatch != NULL)
    {
      VariantClear(&varResult);
      pvars[0] = str;
      DISPPARAMS disp = { pvars, NULL, 1, 0 };

#ifdef _TEST_WITH_SERVER_SECURE
      CoSetProxyBlanket(pDispatch, 
        RPC_C_AUTHN_NONE, 
        RPC_C_AUTHZ_NONE, 
        NULL,
        RPC_C_AUTHN_LEVEL_NONE,
        RPC_C_IMP_LEVEL_IDENTIFY,
        NULL,
        EOAC_NONE);
#endif // _TEST_WITH_SERVER_SECURE

      HRESULT hr = pDispatch->Invoke(0x1, IID_NULL, LOCALE_USER_DEFAULT,
        DISPATCH_METHOD, &disp, &varResult, NULL, NULL);
      if(FAILED(hr))
        if(DISP_E_EXCEPTION != hr)
        { // Это исправление бага ATL-визарда. 
          // Без этого исправления ошибки при обратных вызовах
          // попросту "замалчиваются"!
          varResult.scode = hr;
          break;
        }
    }
  }
  return varResult.scode;

}

Я также исправил ошибку, вносимую визардом – некорректную обработку возвращаемого значения метода Invoke.

Этих действий достаточно, чтобы заставить сервер правильно работать с клиентами, запускаемыми под различными ОС (в том числе и под Windows 9x), и при этом не отказываться от защиты на сервере.

Если совсем ничего не получается, откройте Event Log. Многие ошибки, возникающие при работе COM, помещаются именно туда.

Следует включить аудит событий logon/logoff. Это позволит определить, под какой учетной записью клиент обращается к серверу, и, если проблема в аутентификации (например, учетная запись клиента неизвестна на серверном компьютере, или задан неверный пароль), это сразу же проявится.

Если же ничего из перечисленного выше не помогло, придется искать помощь на стороне. Например, вы можете обратиться в форум COM/DCOM/COM+ на www.rsdn.ru. Возможно, ваш вопрос дойдет и до меня. :)


Эта статья опубликована в журнале RSDN Magazine #0. Информацию о журнале можно найти здесь