Защита в DCOM/COM+ Часть 3

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.

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

  • CoQueryProxyBlanket
  • CoSetProxyBlanket
  • CoCopyProxy

Их описание практически полностью совпадает с описанием аналогичных методов интерфейса 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. Вот возможные значения этого параметра:

  • RPC_C_AUTHN_NONE – Аутентификация отсутствует.
  • RPC_C_AUTHN_DCE_PRIVATE – DCE-аутентификация с закрытым ключом (DCE private key authentication). Успешно побежденное Microsoft наследие. :)
  • RPC_C_AUTHN_DCE_PUBLIC – DCE-аутентификация с открытым ключом (DCE public key authentication). Как и предыдущее значение, никогда не применялось в COM (и вряд ли будет применяться в будущем).
  • RPC_C_AUTHN_DEC_PUBLIC – DEC-аутентификация с открытым ключом. Зарезервировано для испытаний новых версий компиляторов (на объем обрабатываемой информации). То есть столь же полезное значение, как и два предыдущих. :(
  • RPC_C_AUTHN_GSS_NEGOTIATE – провайдер поддержки безопасности Snego. Snego реально не предоставляет сервисов аутентификации. Вместо этого он берет список служб аутентификации и подбирает сервис, который будет приемлем и для клиента, и для сервера. Параметры аутентификации не используются Snego, а передаются выбранной службе аутентификации, которая и производит реальные проверки. В W2k этот провайдер используется по умолчанию.
  • RPC_C_AUTHN_WINNT – NTLMSSP (Windows NT LAN Manager Security Support Provider). Всегда используется при локальных вызовах и в NT 4. Прекрасно работает и в W2k.
  • RPC_C_AUTHN_GSS_SCHANNEL – провайдер поддержки безопасности SChannel. Этот сервис аутентификации поддерживает SSL 2.0, SSL 3.0, TLS 1.0 и PCT 1.0. Это значение может использоваться только в W2k и более поздних версиях ОС.
  • RPC_C_AUTHN_GSS_KERBEROS – заставляет COM использовать провайдер защиты Kerberos. Это значение можно применять только в W2k или более поздних версиях ОС. Если используется маскировка (cloacking), то должен применяться именно Kerberos-провайдер. Однако если задать значение RPC_C_AUTHN_DEFAULT, Windows прекрасно сама справится с задачей выбора провайдера защиты.
  • RPC_C_AUTHN_MSN, RPC_C_AUTHN_DPA, RPC_C_AUTHN_MQ – не имеют исторических корней и созданы исключительно в целях группы разработчиков компиляторов. :)
  • RPC_C_AUTHN_DEFAULT – заставляет W2k использовать «Security Blanket Negotiation алгоритм» о котором говорилось выше. Под другими Win32-ОС практически всегда приводит к тому, что ОС выбирает значение RPC_C_AUTHN_WINNT.

Реально под 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 наплюет на установки и выберет то, что ей нужно. Но проблем это не вызовет. Ниже перечислены допустимые значения этого параметра.

  • RPC_C_AUTHZ_NONE – Сервер не производит аутентификации. Сейчас RPC_C_AUTHN_WINNT, RPC_C_AUTHN_GSS_SCHANNEL и RPC_C_AUTHN_GSS_KERBEROS используют только RPC_C_AUTHZ_NONE.
  • RPC_C_AUTHZ_NAME – Сервер производит аутентификацию на основе имени принципала клиента
  • RPC_C_AUTHZ_DCE – Сервер производит аутентификацию используя сертификат атрибутов привилегий DCE (DCE privilege attribute certificate ,PAC), посылаемый серверу при каждом удаленном вызове. Традиционно не работает.
  • RPC_C_AUTHZ_DEFAULT – заставляет W2k использовать «Security Blanket Negotiation алгоритм» о котором говорилось выше. Под другими Win32-ОС практически всегда приводит к тому, что ОС выбирает значение RPC_C_AUTHN_WINNT.

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.

Защита в службах Windows и DCOM

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

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

Защита в службах Windows и DCOM

Рисунок 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) дважды. Один раз до имперсонации, а второй - после. В моем случае сервер запускался под учетной записью, которой был дан полный доступ к реестру и, естественно, в режиме без имперсонации ключ всегда открывался без проблем.

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

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

Категория: Службы и консоли | Добавил: masterov (01.12.2017) | Автор: Андрей Мастеров E W
Просмотров: 23 | Теги: Windows, службы | Рейтинг: 0.0/0
Всего комментариев: 0
avatar