int process_id == fork();
if (process_id == 0)
exec(«…/bin/serverd»);
В предыдущей главе были представлены основы апартаментов COM и проиллюстрирована COM-архитектура удаленного доступа с изрядным количеством деталей. Были исследованы правила управления ссылками на объекты COM в условиях многопоточной среды, а также методика реализации классов и объектов COM, работающих в потоках. В этой главе будут рассматриваться проблемы, возникающие при управлении процессами и приложениями при использовании COM. Основное внимание будет сосредоточено на том, как апартаменты соотносятся с локализацией ошибок, доверительными отношениями и контекстом защиты.
Итак, серверы COM были ранее представлены как внутрипроцессные модули кода, загружаемые в активизирующий их процесс с целью создания объектов и выполнения их методов. Для значительного класса объектов это является разумной стратегией развертывания. Эта стратегия, однако, не лишена недостатков. Одним из подводных камней при запуске объекта в клиентском процессе является отсутствие изоляции ошибок. Если объект вызывает нарушение условий доступа или другую фатальную ошибку во время исполнения, то клиентский процесс завершится вместе с объектом. Более того, если программа клиента вызовет какую-либо ошибку, то все объекты, созданные в его адресном пространстве, будут немедленно уничтожены без предупреждения. Эта проблема также относится к тем клиентам, которые решат осуществить нормальный выход, например, когда конечный пользователь закрывает одно из приложений. Когда клиентский процесс завершается, любые объекты, созданные в адресном пространстве клиента, будут уничтожены, даже если внешние клиенты вне процесса хранят легальные импортированные ссылки. Очевидно, что если клиентский процесс прекратится, то при активизации внутри процесса жизнь объекта может быть прервана преждевременно.
Другая возможная ловушка при выполнении клиентского процесса состоит в совместном использовании контекста защиты. Когда клиент активизирует объект внутри процесса, методы объекта выполняются с использованием мандата (credential ) защиты клиента. Это означает, что объекты, созданные привилегированными пользователями, могут нанести значительные повреждения. Кроме того, это означает, что объекты, созданные клиентами с относительно меньшей степенью доверия, могут не получить достаточных привилегий для доступа к ресурсам, необходимым для корректного функционирования объекта. К сожалению, нет простого способа обеспечения внутрипроцессного объекта его собственным контекстом защиты.
Еще один подводный камень при внутрипроцессной активации состоит в том, что она не позволяет производить распределенные вычисления. Если объект должен быть активирован в адресном пространстве клиента, то по определению он разделит CPU (central processor unit – центральный процессор) и другие локальные ресурсы с клиентом. Внутрипроцессная активация также делает затруднительным совместное использование одного и того же объекта несколькими клиентскими процессами. Хотя понятие апартамента и допускает экспорт объектных ссылок из любого процесса (включая и те процессы, которые по традиции рассматривались как клиентские), тем не менее, трудно представить себе семантику активации для совместного использования внутрипроцессного экземпляра.
Для решения этих проблем COM допускает активацию классов в отдельных процессах. При активации в отдельном процессе каждый класс (или группа классов) может иметь свой отдельный контекст защиты. Это означает, что разработчик класса сам контролирует, каким пользователям позволено связываться с его объектами. Кроме того, разработчик класса контролирует, какой набор мандатов защиты должен использовать процесс. В зависимости от фактической упаковки класса разработчик класса также управляет тем, когда окружающий процесс закончится (и закончится ли вообще). Наконец, активация класса в отдельном процессе обеспечивает уровень изоляции ошибок, достаточный для изоляции клиента и объектов от завершения в результате ошибок друг друга.
Диспетчер управления COM-сервисами (Service Control Manager – SCM ) обеспечивает связь между CLSID и серверными процессами в реестре. Это позволяет SCM запускать серверный процесс при поступлении клиентских запросов на активацию. Если предположить, что код для класса будет упакован как образ процесса (ЕХЕ), а не как DLL, то достаточно использовать ключ реестра LocalServer32 вместо InprocServer32, как показано в следующем примере:
[HKCR\CLSID\{27EE6A26-DF65-11d0-8C5F-0080C73925BA}] @="Gorillaquot;
[HKCR\CLSID\{27EE6A26-DF65-11d0-8C5F-0080C73925BA}\LocalServer32] @="C:\ServerOfTheApes.exe"
Ожидается, что внепроцессный сервер установит эти ключи во время самоинсталляции. В отличие от своих внутрипроцессных аналогов, внепроцессные серверы не экспортируют известные библиотеки DllRegisterServer и DllUnregisterServer. Вместо этого внепроцессный сервер должен проверить командную строку на наличие известных ключей /RegServer и /UnregServer[1]. Имея вышеуказанные элементы реестра, SCM начнет новый серверный процесс с использованием файла ServerOfTheApes.ехе, при первом запросе на активацию класса Gorilla. После этого извещение SCM о том, какие классы фактически являются доступными из нового процесса, будет обязанностью серверного процесса.
Как уже рассматривалось в главе 3, процессы могут контактировать с SCM для связывания ссылок на объекты класса, экземпляров класса и постоянных экземпляров. Для осуществления этого в COM предусмотрены три функции активации (CoGetClassObject, CoCreateInstanceEx и CoGetInstanceFromFile). Они, как и высокоуровневые моникеры, предназначены для того, чтобы скрыть детали реализации каждой стратегии связывания. В каждой из этих трех стратегий активации для вызова объекта к жизни используется объект класса. Как уже рассматривалось в главе 3, когда активация объекта осуществляется внутри процесса, DLL класса загружается самой COM, а для выборки соответствующих объектов класса используется известная точка входа DllGetClassObject. Однако пока не рассматривалось, как объекты могут быть активированы через границы процессов.
Процесс становится серверным процессом для определенного класса после явной саморегистрации с помощью SCM. После такой регистрации любые активационные запросы класса, для которых необходима внепроцессная активация, будут отосланы к зарегистрированному серверному процессу[2]. Серверные процессы саморегистрируются с помощью SCM API-функции CoRegisterClassObject:
HRESULT CoRegisterClassObject(
[in] REFCLSID rclsid,
// which class?
// какой класс?
[in] IUnknown *pUnkClassObject,
// ptr to class object
// указатель на объект класса
[in] DWORD dwClsCtx,
// locality
// локализация
[in] DWORD dwRegCls,
// activation flags
// флаги активации
[out] DWORD *pdwReg);
// association ID
// ID связи
При вызове CoRegisterClassObject библиотека COM сохраняет ссылку на объект класса, указанную в качестве второго параметра, и связывает объект класса с его CLSID в организованной внутри библиотеки таблице. В зависимости от флагов активации, использованных при вызове, библиотека COM может также сообщать локальному SCM, что вызывающий процесс является теперь серверным процессом для указанного класса. CoRegisterClassObject возвращает двойное слово (DWORD), которое представляет связь между CLSID и объектом класса. Это двойное слово можно использовать для завершения связи (а также для извещения SCM о том, что вызывающий процесс более не является серверным процессом для данного CLSID) путем вызова API-функции CoRevokeClassObject:
HRESULT CoRevokeClassObject([in] DWORD dwReg);
// association ID
// ID связи
Два параметра типа DWORD являются примером тонкого устройства CoRegisterClassObject . Эти параметры дают вызывающему объекту контроль над тем, как и когда объект класса является доступным.
А каким образом и на какой срок сделать доступным объект класса, вызывающему объекту позволяют решить флаги активации, передающиеся CoRegisterСlassObject в качестве четвертого параметра. COM предусматривает следующие константы для использования в этом параметре:
typedef enum tagREGCLS {
REGCLS_SINGLEUSE = 0,
// give out class object once
// выделяем объект класса однократно
REGCLS_MULTIPLEUSE = 1,
// give out class object many
// выделяем объект класса многократно
REGCLS_MULTI_SEPARATE = 2,
// give out class object many
// выделяем объект класса многократно
REGCLS_SUSPENDED = 4,
// do not notify SCM (flag)
// не извещаем SCM (флаг) REGCLS_SURROGATE = 8 // used with DLL Surrogates // используется с суррогатами DLL } REGCLS;
Значение REGCLS_SURROGATE используется в реализациях суррогатов DLL, которые будут рассматриваться позднее в данной главе. Двумя основными значениями являются REGCLS_SINGLEUSE и REGCLS_MULTIPLEUSE. Первое предписывает библиотеке COM использовать объект класса для обслуживания только одного активационного запроса. Когда происходит первый активационный запрос, COM удаляет зарегистрированный объект класса из области открытой видимости (public view). Если придет второй активационный запрос, COM должна использовать другой зарегистрированный объект класса. Если больше не доступен ни один объект класса с тем же CLSID, то для удовлетворения этого запроса COM создаст другой серверный процесс.
Напротив, флаг REGCLS_MULTIPLEUSE показывает, что объект класса может быть использован многократно, до тех пор, пока вызов функции CoRevokeСlassObject не удалит его элемент из таблицы класса библиотеки COM. Флаг REGCLS_MULTI_SEPARATE адресует последующие внутрипроцессные активационные запросы, которые могут произойти в процессе вызывающего объекта. Если вызывающий объект регистрирует объект класса с флагом REGCLS_MULTIPLEUSE, то COM допускает, что любые внутрипроцессные активационные запросы от процесса вызывающего объекта не будут загружать отдельный внутрипроцессный сервер, а будут вместо этого использовать зарегистрированный объект класса. Это означает, что даже если вызывающая программа только зарегистрировала объект класса с флагом CLSCTX_LOCAL_SERVER, то для удовлетворения внутрипроцессных запросов от того же процесса будет использован зарегистрированный объект класса. Если такое поведение неприемлемо, вызывающая программа может зарегистрировать объект класса, используя флаг REGCLS_MULTI_SEPARATE. Флаг инструктирует COM использовать зарегистрированный объект класса для внутрипроцессных запросов только в случае, если для регистрации этого класса был использован флаг CLSCTX_INPROC_SERVER . Это означает, что следующий вызов CoRegisterClassObject:
hr = CoRegisterClassObject(CLSID_Me, &g_coMe, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &dw);
эквивалентен следующему:
hr = CoRegisterClassObject(CLSID_Me, &g_coMe, CLSCTX_LOCAL_SERVER | CLSCTX_INPROC, REGCLS_MULTI_SEPARATE, &dw);
В любом случае, если бы из процесса вызывающего объекта был осуществлен такой вызов:
hr = CoGetClassObject(CLSID_Me, CLSCTX_INPROC, 0, IID_IUnknown, (void**)&pUnkCO);
то никакая DLL не была бы загружена. Вместо этого COM удовлетворила бы запрос, используя объект класса, зарегистрированный посредством CoRegisterClassObject. Если, однако, серверный процесс вызвал CoRegisterClassObject таким образом:
hr = CoRegisterClassObject(CLSID_Me, &g_coMe, CLSCTX_LOCAL_SERVER, REGCLS_MULTI_SEPARATE, &dw);
то любые внутрипроцессные запросы на активацию CLSID_Me, исходящие изнутри серверного процесса, заставят DLL загрузиться.
CoRegisterClassObject связывает зарегистрированный объект класса с апартаментом вызывающего объекта. Это означает, что все поступающие запросы методов будут выполняться в апартаменте вызывающей программы. В частности, это означает, что если объект класса экспортирует интерфейс IClassFactory, то метод CreateInstance будет выполняться в апартаменте вызывающей программы. Результаты метода CreateInstance будут маршалированы из апартамента объекта класса, а это, в свою очередь, означает, что экземпляры класса будут принадлежать к тому же апартаменту, что и объект класса[3].
Серверные процессы могут регистрировать объекты класса для более чем одного класса. Если объекты класса зарегистрированы для выполнения в МТА процесса, то это означает, что поступающие запросы на активацию могут быть обслужены, как только будет завершен первый вызов CoRegisterClassObject. Во многих серверных процессах, основанных на МТА, это может вызвать проблемы, так как бывает, что процесс должен выполнить дальнейшую инициализацию. Чтобы избежать этой проблемы, в реализации COM под Windows NT 4.0 введен флаг REGCLS_SUSPENDED. При добавлении этого флага в вызов CoRegisterСlassObject библиотека COM не извещает SCM о том, что класс доступен. Это предотвращает поступление в серверный процесс входящих активационных запросов. Библиотека COM связывает CLSID с объектом класса; однако она помечает этот элемент в таблице библиотеки класса как отложенный. Для снятия этой пометки в COM предусмотрена API-функция CoResumeClassObjects:
HRESULT CoResumeClassObjects(void);
CoResumeClassObjects делает следующее. Во-первых, она помечает все отложенные объекты класса как легальные для использования. Во-вторых, она посылает SCM единственное сообщение, информируя его, что все ранее отложенные объекты класса теперь являются доступными в серверном процессе. Это сообщение обладает огромной силой, так как при его получении обновляется таблица класса SCM по всей машине и сразу для всех классов, зарегистрированных вызывающим объектом.
Получив три только что описанные API-функции, легко создать серверный процесс, экспортирующий один или более классов. Ниже приводится простая программа, которая экспортирует три класса из МТА сервера:
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
// define a singleton class object for each class
// определяем синглетон для каждого класса
static GorillaClass s_gorillaClass; static OrangutanClass s_orangutanClass;
static ChimpClass s_chimpClass; DWORD rgdwReg[3];
const DWORD dwRegCls = REGCLS_MULTIPLEUSE | REGCLS_SUSPENDED;
const DWORD dwClsCtx = CLSCTX_LOCAL_SERVER;
// enter the MTA
// входим в МТА
HRESULT hr = GoInitializeEx(0, COINIT_MULTITHREADED);
assert(SUCCEEDED(hr));
// register class objects with СОM library's class table
// регистрируем объекты класса с помощью
// таблицы класса библиотеки COM
hr = CoRegisterClassObject(CLSID_Gorilla, &s_gorillaClass, dwClsCtx, dwRegCls, rgdwReg);
assert(SUCCEEDED(hr));
hr = CoRegisterClassObject(CLSID_Orangutan, &s_orangutanClass, dwClsCtx, dwRegCls, rgdwReg + 1);
assert(SUCCEEDED(hr)) ;
hr = CoRegisterClassObject(CLSID_Chimp, &s_chimpClass, dwClsCtx, dwRegCls, rgdwReg + 2);
assert(SUCCEEDED(hr));
// notify the SCM
// извещаем SCM
hr = CoResumeClassObjects();
assert(SUCCEEDED(hr));
// keep process alive until event is signaled
// сохраняем процессу жизнь, пока событие не наступило
extern HANDLE g_heventShutdown; WaitForSingleObject(g_heventShutdown, INFINITE);
// remove entries from COM library's class table
// удаляем элементы из таблицы класса библиотеки COM
for (int n = 0; n < 3; n++)
CoRevokeClassObject(rgdwReg[n]);
// leave the MTA
// покидаем MTA CoUninitialize();
return 0;
}
В данном фрагменте кода предполагается, что событие (Win32 Event object) будет инициализировано где-нибудь еще внутри процесса таким образом:
HANDLE g_heventShutdown = CreateEvent(0, TRUE, FALSE, 0);
Имея данное событие, сервер может быть мирно остановлен с помощью вызова API-функции SetEvent:
SetEvent(g_heventShutdown);
которая запустит последовательность выключения в главном потоке. Если бы сервер был реализован как сервер на основе STA, то главный поток должен был бы вместо ожидания события Win32 Event запустить конвейер обработки оконных сообщений (windows message pump). Это необходимо для того, чтобы позволить поступающим ORPC-запросам входить в апартамент главного потока.
В примере, показанном в предыдущем разделе, не было точно показано, как и когда должен прекратить работу серверный процесс. В общем случае серверный процесс сам контролирует свое время жизни и может прекратить работу в любой выбранный им момент. Хотя для серверного процесса и допустимо неограниченное время работы, большинство из них предпочитают выключаться, когда не осталось неосвобожденных ссылок на их объекты или объекты класса. Это аналогично стратегии, используемой большинством внутрипроцессных серверов в их реализации DllCanUnloadNow. Напомним, что в главе 3 говорилось, что обычно сервер реализует две подпрограммы, вызываемые в качестве интерфейсных указателей, которые запрашиваются и освобождаются внешними клиентами:
// reasons to remain loaded
// причины оставаться загруженными
LONG g_cLocks = 0;
// called from AddRef + IClassFactory::LockServer(TRUE)
// вызвано из AddRef + IClassFactory::LockServer(TRUE)
void LockModule(void) {
InterlockedIncrement(&g_cLocks);
}
// called from Release + IClassFactory::LockServer(FALSE)
// вызвано из Release + IClassFactory::LockServer(FALSE)
void UnlockModule(void) {
InterlockedDecrement(&g_cLocks);
}
Это сделало реализацию DllCanUnloadNow предельно простой:
STDAPI DllCanUnloadNow() { return g_cLocks ? S_FALSE : S_OK; }
Подпрограмму DllCanUnloadNow нужно вызывать в случаях, когда клиент решил «собрать мусор» в своем адресном пространстве путем вызова CoFreeUnusedLibraries для освобождения неиспользуемых библиотек.
Имеются некоторые различия в том, как ЕХЕ-серверы прекращают работу серверов. Во-первых, обязанностью серверного процесса является упреждающее инициирование процесса своего выключения. В отличие от внутрипроцессных серверов, здесь не существует «сборщика мусора», который запросил бы внепроцессный сервер, желает ли он прекратить работу. Вместо этого серверный процесс должен в подходящий момент явно запустить процесс своего выключения. Если для выключения сервера используется событие Win32 Event, то процесс должен вызвать API-функцию SetEvent:
void UnlockModule(void) {
if (InterlockedDecrement(&g_cLocks) ==0)
{
extern HANDLE g_heventShutdown;
SetEvent(g_heventShutdown);
}
}
Если вместо серверного основного потока обслуживается очередь событий Windows MSG , то для прерывания цикла обработки сообщений следует использовать некоторые из API-функций. Проще всего использовать PostThreadMessage для передачи в основной поток сообщения WM_QUIT:
void UnlockModule(void) {
if (InterlockedDecrement(&g_cLocks) == 0) {
extern DWORD g_dwMainThreadID;
// set from main thread
// установлено из основного потока
PostThreadMessage(g_dwMainThreadID, WNLQUIT, 0, 0);
}
}
Если серверный процесс на основе STA знает, что он никогда не будет создавать дополнительные потоки, то он может использовать несколько более простую API-функцию PostQuitMessage:
void UnlockModule(void) {
if (InterlockedDecrement(&g_cLocks) == 0) PostQuitMessage(0);
}
Этот способ работает только при вызове из главного потока серверного процесса.
Второе различие в управлении временем жизни внутрипроцессного и внепроцессного сервера связано с тем, что должно поддерживать сервер в загруженном или работающем состоянии. В случае внутрипроцессного сервера такой силой обладают неосвобожденные ссылки на объекты и неотмененные вызовы IClassFactory::LockServer(TRUE). Неосвобожденные ссылки на объекты необходимо рассмотреть в контексте внепроцессного сервера.
Безусловно, сервер должен оставаться доступным до тех пор, пока внешние клиенты имеют неосвобожденные ссылки на объекты класса сервера. Для внутрипроцессного сервера это реализуется следующим образом:
STDMETHODIMP_(ULONG) MyClassObject::AddRef(void) {
LockModule();
// note outstanding reference
// отмечаем неосвобожденную ссылку
return 2;
// non-heap-based object
// объект, размещенный не в «куче»
}
STDMETHODIMP_(ULONG) MyClassObject::Release(void) {
UnlockModule();
// note destroyed reference
// отмечаем уничтоженную ссылку
return 1;
// non-heap-based object
// объект, размещенный не в «куче»
}
Такое поведение является обязательным, поскольку если DLL выгружается, несмотря на оставшиеся неосвобожденные ссылки на объекты класса, то даже последующие вызовы метода Release приведут клиентский процесс к гибели.
К сожалению, предшествующая реализация AddRef и Release не годится для внепроцессных серверов. Напомним, что после входа в апартамент COM первое, что делает типичный внепроцессный сервер, – регистрирует свои объекты класса с помощью библиотеки COM путем вызова CoRegisterClassObject. Тем не менее, пока таблица класса сохраняет объект класса, существует по меньшей мере одна неосвобожденная ссылка COM на объект класса. Это означает, что после регистрации своих объектов класса счетчик блокировок всего модуля будет отличен от нуля. Эти самоустановленные (self-imposed) ссылки не будут освобождены до вызова серверным процессом CoRevokeClassObject. К сожалению, типичный серверный процесс не вызовет CoRevokeClassObject до тех пор, пока счетчик блокировок всего модуля не достигнет нуля, что означает, что серверный процесс никогда не прекратится.
Чтобы прервать циклические отношения между таблицей класса и временем жизни сервера, большинство внепроцессных реализации объектов класса попросту игнорируют неосвобожденные ссылки на AddRef и Release:
STDMETHODIMP_(ULONG) MyClassObject::AddRef(void) {
// ignore outstanding reference
// игнорируем неосвобожденную ссылку
return 2;
// non-heap-based object
// объект, размещенный не в «куче»
}
STDMETHODIMP_(ULONG) MyClassObject::Release(void) {
// ignore destroyed reference
// игнорируем уничтоженную ссылку
return 1;
// non-heap-based object
//объект, размещенный не в «куче»
}
Это означает, что после регистрации объектов своего класса счетчик блокировок всего модуля останется на нуле.
На первый взгляд такая реализация означает, что серверный процесс может прекратить работу, несмотря на то, что существуют неосвобожденные ссылки на объекты его класса. Такое поведение фактически зависит от реализации объекта класса. Напомним, что сервер должен продолжать работу до тех пор, пока на объекты его класса есть внешние ссылки. Предшествующие модификации AddRef и Release влияют только на внутренние ссылки, которые хранятся в таблице классов библиотеки COM и поэтому игнорируются. Когда внешний клиент запрашивает ссылку на один из объектов класса серверного процесса, SCM входит в апартамент объекта класса для отыскания там ссылки на объект класса. В это время делается вызов CoMarshalInterface для сериализации объектной ссылки с целью использования ее клиентом. Если объект класса реализует интерфейс IExternalConnection, то он может заметить, что внешние ссылки являются неосвобожденными, и использовать эти сведения для управления временем жизни сервера. Если предположить, что объект класса реализует интерфейс IExternalConnection, тo следующий код достигает желаемого эффекта:
STDMETHODIMP_(DWORD) MyClassObject::AddConnection(DWORD extconn, DWORD) {
DWORD res = 0;
if (extconn & EXTCONN_STRONG) {
LockModule();
// note external reference
// записываем внешнюю ссылку
res = InterlockedIncrement(&m_cExtRef);
}
return res;
}
STDMETHODIMP_(DWORD) MyClassObject::ReleaseConnection(DWORD extconn, DWORD, BOOL bLastReleaseKillsStub)
{
DWORD res = 0;
if (extconn & EXTCONN_STRONG) {
UnlockModule();
// note external reference
// записываем внешнюю ссылку
res = InterlockedDecrement(&m_cExtRef);
if (res == 0 & bLastReleaseKillsStub)
CoDisconnectObject((IExternalConnection*)this, 0);
}
return res;
}
Отметим, что счетчик блокировок модуля будет ненулевым до тех пор, пока существуют неосвобожденные внешние ссылки на объект класса, в то время как внутренние ссылки, удержанные библиотекой COM, игнорируются.
Хотя технология использования IExternalConnection для объектов класса существовала в COM с самых первых дней, лишь немногие разработчики используют ее на деле. Вместо этого большинство серверов обычно игнорируют неосвобожденные внешние ссылки на объекты класса и завершают серверные процессы преждевременно. Этому положению способствовало присутствие метода LockServer в интерфейсе IClassFactory, который внушает разработчикам мысль, что клиенты будто бы способны в действительности обеспечить выполнение сервера. В то время как большинство разработчиков серверов успешно запирают модуль в методах LockServer, для клиента не существовало надежного способа вызвать данный метод. Рассмотрим следующий клиентский код: IClassFactory *pcf = 0;
HRESULT hr = CoGetClassObject(CLSID_You, CLSCTX_LOCAL_SERVER, О, IID_IClassFactory, (void**)&pcf);
if (SUCCEEDED(hr)) hr = pcf->LockServer(TRUE);
// keep server running?
// поддерживать выполнение сервера?
В первых версиях COM этот фрагмент кода находился бы в условиях серьезной гонки. Отметим, что существует интервал между вызовами CoGetClassObject и IClassFactory::LockServer. В течение этого периода времени другие клиенты могут уничтожить последний остающийся экземпляр класса. Поскольку неосвобожденная ссылка на объект класса игнорируется наивными реализациями серверов, серверный процесс прекратит работу раньше исходного вызова клиентом метода LockServer . Теоретически это можно было бы преодолеть следующим образом:
IClassFactory *pcf = 0;
HRESULT hr = S_OK;
do {
if (pcf) pcf->Release();
hr = CoGetClassObject(CLSID_You, CLSCTX_LOCAL_SERVER, 0, IID_IClassFactory, (void**)&pcf);
if (FAILED(hr)) break;
hr = pcf->LockServer(TRUE);
// keep server running?
// поддерживать выполнение сервера?
} while (FAILED(hr));
Отметим, что данный фрагмент кода периодически пытается подсоединиться к объекту класса и заблокировать его, пока вызов LockServer проходит успешно. Если сервер завершит работу преждевременно – между вызовами CoGetClassObject и LockServer , то вызов LockServer возвратит сообщение об ошибке, извещающее об отсоединенном заместителе, что вызовет повтор последовательности. Под Windows NT 3.51 и в более ранних версиях этот нелепый код был единственным надежным способом получения ссылки на объект класса.
Был признан тот факт, что многие реализации серверов не использовали IExternalConnection для должного управления временем жизни сервера, и в версии COM под Windows NT 4.0 введена следующая модернизация для замены этих наивных реализаций. При маршалинге ссылки на объект класса в ответ на вызов CoGetClass0bject SCM вызовет метод объекта класса IClassFactory::LockServer. С тех пор как значительное большинство серверов реализуют IClassFactory в своих объектах класса, эта модернизация исполняемых программ COM исправляет значительное количество дефектов. Однако если объект класса не экспортирует интерфейс IClassFactoryили если сервер должен выполняться и в более ранних версиях COM, чем Windows NT 4.0, то необходимо использовать технологию IExternalConnection.
Следует обсудить еще одну проблему, относящуюся ко времени жизни сервера. Отметим, что когда сервер решает прекратить работу, то он сообщает о том, что главный поток серверного приложения должен начать свою последовательность операций останова (shutdown sequence ) до выхода из процесса. Частью этой последовательности операций останова является вызов CoRevokeClassObject для отмены регистрации его объектов класса. Если, однако, были использованы показанные ранее реализации UnlockModule, то появляются условия серьезной гонки. Возможно, что в промежутке между тем моментом, когда сервер сигнализирует главному потоку посредством вызова SetEvent или PostThreadMessage, и тем моментом, когда сервер аннулирует объекты своего класса, вызывая CoRevokeClassObject , в серверный процесс поступят дополнительные запросы на активацию. Если в этот интервал времени создаются новые объекты, то уже нет способа сообщить главному потоку, что прекращение работы – плохая идея и что у процесса появились новые объекты для обслуживания. Для устранения этих условий гонки в COM предусмотрены две API-функции: ULONG CoAddRefServerProcess(void); ULONG CoReleaseServerProcess(void);
Эти две подпрограммы управляют счетчиком блокировок модуля от имени вызывающего объекта. Эти подпрограммы временно блокируют любой доступ к библиотеке COM, чтобы гарантировать, что во время установки счетчика блокировок новые активационные запросы не будут обслуживаться. Кроме того, если функция CoReleaseServerProcess обнаружит, что удаляется последняя блокировка в процессе, то она изнутри пометит все объекты класса в процессе как приостановленные и сообщит SCM, что процесс более не является сервером для его CLSID.
Следующие подпрограммы корректно реализуют время жизни сервера во внепроцессном сервере:
void LockModule(void) {
CoAddRefServerProcess();
// COM maintains lock count
// COM устанавливает счетчик блокировок
}
void UnlockModule(void) {
if (CoReleaseServerProcess() == 0)
SetEvent(g_heventShutdown);
}
Отметим, что прекращение работы процесса в должном порядке по-прежнему остается обязанностью вызывающей программы. Однако после принятия решения о прекращении работы ни один новый активационный запрос не будет обслужен этим процессом.
Даже при использовании функций CoAddRefServerProcess / CoReleaseServerProcess все еще остаются возможности для гонки. Возможно, что во время выполнения CoReleaseServerProcess на уровне RPC будет получен входящий запрос на активацию от SCM. Если вызов от SCM диспетчеризован после того, как функция CoReleaseServerProcess снимает свою блокировку библиотеки COM, то активационный запрос отметит, что объект класса уже помечен как приостановленный, и в SCM будет возвращено сообщение об ошибке со специфическим кодом (CO_E_SERVER_STOPPING ). Когда SCM обнаруживает этот специфический код, он просто запускает новый экземпляр серверного процесса и повторяет запрос, как только новый серверный процесс зарегистрирует себя. Несмотря на системы защиты, используемые библиотекой COM, остается вероятность того, что поступающий активационный запрос будет выполняться одновременно с заключительным вызовом функции CoReleaseServerProcess. Чтобы избежать этого, сервер может явно возвратить CO_E_SERVER_STOPPING как из IClassFactory::Create Instance, так и из IPersistFile::Load в том случае, если он определит, что по окончании запроса на прекращение работы был сделан еще какой-то запрос. Следующий код демонстрирует этот способ:
STDMETHODIMP MyClassObject::CreateInstance(IUnknown *puo, REFIID riid, void **ppv) {
LockModule();
// ensure we don't shut down while in call
// убеждаемся в том, что не прекращаем работу
// во время вызова
HRESULT hr; *ppv = 0;
// shutdown initiated?
// процесс останова запущен?
DWORD dw = WaitForSingleObject(g_heventShutdown, 0);
if (dw == WAIT_OBJECT_0) hr = CO_E_SERVER_STOPPING;
else {
// normal CreateInstance implementation
// нормальная реализация CreateInstance
}
UnlockModule();
return hr;
}
Во время написания этого текста ни одна из коммерческих библиотек классов COM не реализовывала этот способ.
В примере, показанном в предыдущем разделе, не было точно показано, как и когда должен прекратить работу серверный процесс. В общем случае серверный процесс сам контролирует свое время жизни и может прекратить работу в любой выбранный им момент. Хотя для серверного процесса и допустимо неограниченное время работы, большинство из них предпочитают выключаться, когда не осталось неосвобожденных ссылок на их объекты или объекты класса. Это аналогично стратегии, используемой большинством внутрипроцессных серверов в их реализации DllCanUnloadNow. Напомним, что в главе 3 говорилось, что обычно сервер реализует две подпрограммы, вызываемые в качестве интерфейсных указателей, которые запрашиваются и освобождаются внешними клиентами:
// reasons to remain loaded
// причины оставаться загруженными
LONG g_cLocks = 0;
// called from AddRef + IClassFactory::LockServer(TRUE)
// вызвано из AddRef + IClassFactory::LockServer(TRUE)
void LockModule(void) {
InterlockedIncrement(&g_cLocks);
}
// called from Release + IClassFactory::LockServer(FALSE)
// вызвано из Release + IClassFactory::LockServer(FALSE)
void UnlockModule(void) { InterlockedDecrement(&g_cLocks);
}
Это сделало реализацию DllCanUnloadNow предельно простой:
STDAPI DllCanUnloadNow()
{
return g_cLocks ? S_FALSE : S_OK;
}
Подпрограмму DllCanUnloadNow нужно вызывать в случаях, когда клиент решил «собрать мусор» в своем адресном пространстве путем вызова CoFreeUnusedLibraries для освобождения неиспользуемых библиотек.
Имеются некоторые различия в том, как ЕХЕ-серверы прекращают работу серверов. Во-первых, обязанностью серверного процесса является упреждающее инициирование процесса своего выключения. В отличие от внутрипроцессных серверов, здесь не существует «сборщика мусора», который запросил бы внепроцессный сервер, желает ли он прекратить работу. Вместо этого серверный процесс должен в подходящий момент явно запустить процесс своего выключения. Если для выключения сервера используется событие Win32 Event, то процесс должен вызвать API-функцию SetEvent:
void UnlockModule(void) {
if (InterlockedDecrement(&g_cLocks) ==0) {
extern HANDLE g_heventShutdown;
SetEvent(g_heventShutdown);
}
}
Если вместо серверного основного потока обслуживается очередь событий Windows MSG, то для прерывания цикла обработки сообщений следует использовать некоторые из API-функций. Проще всего использовать PostThreadMessage для передачи в основной поток сообщения WM_QUIT:
void UnlockModule(void) {
if (InterlockedDecrement(&g_cLocks) == 0) {
extern DWORD g_dwMainThreadID;
// set from main thread
// установлено из основного потока
PostThreadMessage(g_dwMainThreadID, WNLQUIT, 0, 0);
}
}
Если серверный процесс на основе STA знает, что он никогда не будет создавать дополнительные потоки, то он может использовать несколько более простую API-функцию PostQuitMessage:
void UnlockModule(void) {
if (InterlockedDecrement(&g_cLocks) == 0) PostQuitMessage(0);
}
Этот способ работает только при вызове из главного потока серверного процесса.
Второе различие в управлении временем жизни внутрипроцессного и внепроцессного сервера связано с тем, что должно поддерживать сервер в загруженном или работающем состоянии. В случае внутрипроцессного сервера такой силой обладают неосвобожденные ссылки на объекты и неотмененные вызовы IClassFactory::LockServer(TRUE). Неосвобожденные ссылки на объекты необходимо рассмотреть в контексте внепроцессного сервера.
Безусловно, сервер должен оставаться доступным до тех пор, пока внешние клиенты имеют неосвобожденные ссылки на объекты класса сервера. Для внутрипроцессного сервера это реализуется следующим образом:
STDMETHODIMP_(ULONG) MyClassObject::AddRef(void) {
LockModule();
// note outstanding reference
// отмечаем неосвобожденную ссылку
return 2;
// non-heap-based object
// объект, размещенный не в «куче»
}
STDMETHODIMP_(ULONG) MyClassObject::Release(void) {
UnlockModule();
// note destroyed reference
// отмечаем уничтоженную ссылку
return 1;
// non-heap-based object
// объект, размещенный не в «куче»
}
Такое поведение является обязательным, поскольку если DLL выгружается, несмотря на оставшиеся неосвобожденные ссылки на объекты класса, то даже последующие вызовы метода Release приведут клиентский процесс к гибели.
К сожалению, предшествующая реализация AddRef и Release не годится для внепроцессных серверов. Напомним, что после входа в апартамент COM первое, что делает типичный внепроцессный сервер, – регистрирует свои объекты класса с помощью библиотеки COM путем вызова CoRegisterClassObject. Тем не менее, пока таблица класса сохраняет объект класса, существует по меньшей мере одна неосвобожденная ссылка COM на объект класса. Это означает, что после регистрации своих объектов класса счетчик блокировок всего модуля будет отличен от нуля. Эти самоустановленные (self-imposed) ссылки не будут освобождены до вызова серверным процессом CoRevokeClassObject. К сожалению, типичный серверный процесс не вызовет CoRevokeClassObject до тех пор, пока счетчик блокировок всего модуля не достигнет нуля, что означает, что серверный процесс никогда не прекратится.
Чтобы прервать циклические отношения между таблицей класса и временем жизни сервера, большинство внепроцессных реализации объектов класса попросту игнорируют неосвобожденные ссылки на AddRef и Release:
STDMETHODIMP_(ULONG) MyClassObject::AddRef(void) {
// ignore outstanding reference
// игнорируем неосвобожденную ссылку
return 2;
// non-heap-based object
// объект, размещенный не в «куче»
}
STDMETHODIMP_(ULONG) MyClassObject::Release(void) {
// ignore destroyed reference
// игнорируем уничтоженную ссылку
return 1;
// non-heap-based object
//объект, размещенный не в «куче»
}
Это означает, что после регистрации объектов своего класса счетчик блокировок всего модуля останется на нуле.
На первый взгляд такая реализация означает, что серверный процесс может прекратить работу, несмотря на то, что существуют неосвобожденные ссылки на объекты его класса. Такое поведение фактически зависит от реализации объекта класса. Напомним, что сервер должен продолжать работу до тех пор, пока на объекты его класса есть внешние ссылки. Предшествующие модификации AddRef и Release влияют только на внутренние ссылки, которые хранятся в таблице классов библиотеки COM и поэтому игнорируются. Когда внешний клиент запрашивает ссылку на один из объектов класса серверного процесса, SCM входит в апартамент объекта класса для отыскания там ссылки на объект класса. В это время делается вызов CoMarshalInterface для сериализации объектной ссылки с целью использования ее клиентом. Если объект класса реализует интерфейс IExternalConnection, то он может заметить, что внешние ссылки являются неосвобожденными, и использовать эти сведения для управления временем жизни сервера. Если предположить, что объект класса реализует интерфейс IExternalConnection, тo следующий код достигает желаемого эффекта:
STDMETHODIMP_(DWORD) MyClassObject::AddConnection(DWORD extconn, DWORD) {
DWORD res = 0;
if (extconn & EXTCONN_STRONG) {
LockModule();
// note external reference
// записываем внешнюю ссылку
res = InterlockedIncrement(&m_cExtRef);
}
return res;
}
STDMETHODIMP_(DWORD) MyClassObject::ReleaseConnection(DWORD extconn, DWORD, BOOL bLastReleaseKillsStub) {
DWORD res = 0;
if (extconn & EXTCONN_STRONG) {
UnlockModule();
// note external reference
// записываем внешнюю ссылку
res = InterlockedDecrement(&m_cExtRef);
if (res == 0 & bLastReleaseKillsStub) CoDisconnectObject((IExternalConnection*)this, 0);
}
return res;
}
Отметим, что счетчик блокировок модуля будет ненулевым до тех пор, пока существуют неосвобожденные внешние ссылки на объект класса, в то время как внутренние ссылки, удержанные библиотекой COM, игнорируются.
Хотя технология использования IExternalConnection для объектов класса существовала в COM с самых первых дней, лишь немногие разработчики используют ее на деле. Вместо этого большинство серверов обычно игнорируют неосвобожденные внешние ссылки на объекты класса и завершают серверные процессы преждевременно. Этому положению способствовало присутствие метода LockServer в интерфейсе IClassFactory, который внушает разработчикам мысль, что клиенты будто бы способны в действительности обеспечить выполнение сервера. В то время как большинство разработчиков серверов успешно запирают модуль в методах LockServer, для клиента не существовало надежного способа вызвать данный метод. Рассмотрим следующий клиентский код:
IClassFactory *pcf = 0;
HRESULT hr = CoGetClassObject(CLSID_You, CLSCTX_LOCAL_SERVER, О, IID_IClassFactory, (void**)&pcf);
if (SUCCEEDED(hr)) hr = pcf->LockServer(TRUE);
// keep server running?
// поддерживать выполнение сервера?
В первых версиях COM этот фрагмент кода находился бы в условиях серьезной гонки. Отметим, что существует интервал между вызовами CoGetClassObject и IClassFactory::LockServer. В течение этого периода времени другие клиенты могут уничтожить последний остающийся экземпляр класса. Поскольку неосвобожденная ссылка на объект класса игнорируется наивными реализациями серверов, серверный процесс прекратит работу раньше исходного вызова клиентом метода LockServer . Теоретически это можно было бы преодолеть следующим образом:
IClassFactory *pcf = 0;
HRESULT hr = S_OK;
do {
if (pcf) pcf->Release();
hr = CoGetClassObject(CLSID_You, CLSCTX_LOCAL_SERVER, 0, IID_IClassFactory, (void**)&pcf);
if (FAILED(hr)) break;
hr = pcf->LockServer(TRUE);
// keep server running?
// поддерживать выполнение сервера?
}
while (FAILED(hr));
Отметим, что данный фрагмент кода периодически пытается подсоединиться к объекту класса и заблокировать его, пока вызов LockServer проходит успешно. Если сервер завершит работу преждевременно – между вызовами CoGetClassObject и LockServer, то вызов LockServer возвратит сообщение об ошибке, извещающее об отсоединенном заместителе, что вызовет повтор последовательности. Под Windows NT 3.51 и в более ранних версиях этот нелепый код был единственным надежным способом получения ссылки на объект класса.
Был признан тот факт, что многие реализации серверов не использовали IExternalConnection для должного управления временем жизни сервера, и в версии COM под Windows NT 4.0 введена следующая модернизация для замены этих наивных реализаций. При маршалинге ссылки на объект класса в ответ на вызов CoGetClass0bject SCM вызовет метод объекта класса IClassFactory::LockServer. С тех пор как значительное большинство серверов реализуют IClassFactory в своих объектах класса, эта модернизация исполняемых программ COM исправляет значительное количество дефектов. Однако если объект класса не экспортирует интерфейс IClassFactoryили если сервер должен выполняться и в более ранних версиях COM, чем Windows NT 4.0, то необходимо использовать технологию IExternalConnection.
Следует обсудить еще одну проблему, относящуюся ко времени жизни сервера. Отметим, что когда сервер решает прекратить работу, то он сообщает о том, что главный поток серверного приложения должен начать свою последовательность операций останова (shutdown sequence ) до выхода из процесса. Частью этой последовательности операций останова является вызов CoRevokeClassObject для отмены регистрации его объектов класса. Если, однако, были использованы показанные ранее реализации UnlockModule , то появляются условия серьезной гонки. Возможно, что в промежутке между тем моментом, когда сервер сигнализирует главному потоку посредством вызова SetEvent или PostThreadMessage, и тем моментом, когда сервер аннулирует объекты своего класса, вызывая CoRevokeClassObject, в серверный процесс поступят дополнительные запросы на активацию. Если в этот интервал времени создаются новые объекты, то уже нет способа сообщить главному потоку, что прекращение работы – плохая идея и что у процесса появились новые объекты для обслуживания. Для устранения этих условий гонки в COM предусмотрены две API-функции:
ULONG CoAddRefServerProcess(void);
ULONG CoReleaseServerProcess(void);
Эти две подпрограммы управляют счетчиком блокировок модуля от имени вызывающего объекта. Эти подпрограммы временно блокируют любой доступ к библиотеке COM, чтобы гарантировать, что во время установки счетчика блокировок новые активационные запросы не будут обслуживаться. Кроме того, если функция CoReleaseServerProcess обнаружит, что удаляется последняя блокировка в процессе, то она изнутри пометит все объекты класса в процессе как приостановленные и сообщит SCM, что процесс более не является сервером для его CLSID.
Следующие подпрограммы корректно реализуют время жизни сервера во внепроцессном сервере:
void LockModule(void) {
CoAddRefServerProcess();
// COM maintains lock count
// COM устанавливает счетчик блокировок
}
void UnlockModule(void) {
if (CoReleaseServerProcess() == 0) SetEvent(g_heventShutdown);
}
Отметим, что прекращение работы процесса в должном порядке по-прежнему остается обязанностью вызывающей программы. Однако после принятия решения о прекращении работы ни один новый активационный запрос не будет обслужен этим процессом.
Даже при использовании функций CoAddRefServerProcess / CoReleaseServerProcess все еще остаются возможности для гонки. Возможно, что во время выполнения CoReleaseServerProcess на уровне RPC будет получен входящий запрос на активацию от SCM. Если вызов от SCM диспетчеризован после того, как функция CoReleaseServerProcess снимает свою блокировку библиотеки COM, то активационный запрос отметит, что объект класса уже помечен как приостановленный, и в SCM будет возвращено сообщение об ошибке со специфическим кодом (CO_E_SERVER_STOPPING). Когда SCM обнаруживает этот специфический код, он просто запускает новый экземпляр серверного процесса и повторяет запрос, как только новый серверный процесс зарегистрирует себя. Несмотря на системы защиты, используемые библиотекой COM, остается вероятность того, что поступающий активационный запрос будет выполняться одновременно с заключительным вызовом функции CoReleaseServerProcess. Чтобы избежать этого, сервер может явно возвратить CO_E_SERVER_STOPPING как из IClassFactory::Create Instance, так и из IPersistFile::Load в том случае, если он определит, что по окончании запроса на прекращение работы был сделан еще какой-то запрос. Следующий код демонстрирует этот способ:
STDMETHODIMP MyClassObject::CreateInstance(IUnknown *puo, REFIID riid, void **ppv) {
LockModule();
// ensure we don't shut down while in call
// убеждаемся в том, что не прекращаем работу
// во время вызова HRESULT hr;
*ppv = 0;
// shutdown initiated?
// процесс останова запущен?
DWORD dw = WaitForSingleObject(g_heventShutdown, 0);
if (dw == WAIT_OBJECT_0) hr = CO_E_SERVER_STOPPING;
else {
// normal CreateInstance implementation
// нормальная реализация CreateInstance
}
UnlockModule();
return hr;
}
Во время написания этого текста ни одна из коммерческих библиотек классов COM не реализовывала этот способ.
В версии COM под Windows NT 4.0 введено понятие приложений COM (COM applications). Приложения COM идентифицируются с помощью GUID (называемых в этом контексте AppID – идентификаторы приложения) и представляют серверный процесс для одного или более классов. Каждый CLSID может быть связан с ровно одним идентификатором приложений. Эта связь фиксируется в локальном реестре, использующем именованное значение AppID:
[HKCR\CLSID\{27EE6A4E-DF65-11D0-8C5F-0080C73925BA}] @="Gorilla" AppID="{27EE6A4E-DF65-11D0-8C5F-0080C73925BA}"
Все классы, принадлежащие к одному и тому же приложению COM, будут иметь один и тот же AppID, а также будут использовать одни и те же установки удаленной активации и защиты. Эти установки записаны в локальном реестре под ключом HKEY_CLASSES_ROOT\AppID
Подобно CLSID, AppID могут быть зарегистрированы для каждого пользователя под Windows NT версии 5.0 или выше. Поскольку серверы, реализованные до появления Windows NT 4.0, не регистрируют явно свои AppID, инструментальные средства конфигурирования COM (например, DCOMCNFG.EXE, OLEVIEW.EXE) автоматически создадут новый AppID для этих старых серверов. Чтобы синтезировать AppID для старых серверов, эти программы автоматически добавляют именованное значение AppID ко всем CLSID, экспортируемым определенным локальным сервером. При добавлении этих именованных значений DCOMCNFG или OLEVIEW просто использует в качестве AppID первый встреченный CLSID для данного сервера. Те приложения, которые были разработаны после выпуска Windows NT 4.0, могут (и будут) использовать для своих AppID особый GUID.
Большинством установок AppID можно управлять с помощью программы DCOMCNFG.EXE, которая является стандартным компонентом Windows NT 4.0 или выше. DCOMCNFG.EXE предоставляет администраторам удобный для использования интерфейс для контроля установок удаленного доступа и защиты. Более мощный инструмент, OLEVIEW.EXE, осуществляет большинство функциональных возможностей DCOMCNFG.EXE и, кроме того, обеспечивает очень «COM-центрический» (COM-centric) взгляд на реестр. Обе эти программы интуитивно понятны при использовании и обе являются существенными для разработок с использованием COM.
Простейшим установочным параметром AppID является RemoteServerName. Эта именованная величина показывает, какую хост-машину следует использовать для удаленных запросов на активацию, если в них явно не указано с помощью COSERVERINFO удаленное хост-имя. Рассмотрим следующие установки реестра:
[HKCR\AppID\{27EE6A4D-DF65-11d0-8C5F-0080C73925BA}] @="Аре Server" RemoteServerName="www.apes.com"
[HKCR\AppID\{27EE6A4E-DF65-11d0-8C5F-0080C73925BA}] @="Gorilla" AppID={27EE6A4D-DF65-11d0-8C5F-0080C73925BA}
[HKCR\AppID\{27EE6A4F-DF65-11d0-8C5F-0080C73925BA}] @="Chimp" AppID={27EE6A4D-DF65-11d0-8C5F-0080C73925BA}
Если клиент осуществляет такой запрос на активацию:
IApeClass *рас = 0;
HRESULT hr = CoGetClassObject(CLSID_Chimp, CLSCTX_REMOTE_SERVER, 0, IID_IApeClass, (void**)&pac);
то SCM со стороны клиента направит этот запрос в SCM на www.apes.com , где этот запрос будет рассмотрен как локальный активационный запрос. Отметим, что если клиент предоставляет явное хост-имя:
IApeClass *рас = 0;
COSERVERINFO csi;
ZeroMemory(&csi, sizeof(csi));
csi.pwszName = OLESTR(www.dogs.com);
HRESULT hr = CoGetClassObject(CLSID_Chimp, CLSCTX_REMOTE_SERVER, &csi, IID_IApeClass, (void**)&pac);
то установка RemoteServerName игнорируется и запрос направляется в www.apes.com.
Чаще встречается ситуация, когда клиенты не указывают явно свои предпочтения относительно хост-имени и месторасположения. Рассмотрим следующий вызов CoGetClassObject
IApeClass *pac = 0;
HRESULT hr = CoGetClassObject(CLSID_Chimp, CLSCTX_ALL, 0, IID_IApeClass, (void*)&pac);
Поскольку не указано никакого хост-имени, то SCM сначала будет искать в локальном реестре следующий ключ:
[HKCR\AppID\{27EE6A4F-DF65-11d0-8C5F-0080C7392SBA}]
Если этот ключ локально не доступен, COM обратится к хранилищу класса (class store) под Windows NT 5.0, если оно доступно. Если с этой точки зрения ключ реестра доступен, то вслед за этим SCM будет искать подключ InpocServer32:
HKCR\CLSID\{27EE6A4F-DF65-11d0-8C5F-0080C73925BA}\InprocServer32] @="C:\somefile.dll"
Если этот ключ найден, класс будет активирован путем загрузки той DLL, которая указана в реестре. В противном случае SCM ищет подключ InprocHandler32 :
HKCR\CLSID\{27EE6A4F-DF65-11d0-8C5F-0080C73925BA}\InprocHandler32] @="C:\somefile.dll"
Если класс имеет ключ дескриптора (handler), то и тогда класс будет активирован путем загрузки той DLL, которая указана в реестре. Если ни один из внутрипроцессных подключей не доступен, то SCM предполагает, что активационный запрос должен быть внепроцессным. В таком случае SCM проверяет, имеет ли серверный процесс объект класса, зарегистрированный в настоящее время для запрашиваемого CLSID[1]. Если это так, то SCM входит в серверный процесс и маршалирует объектную ссылку из соответствующего объекта класса и возвращает ее в апартамент вызывающего объекта, где она демаршалируется до того, как управление возвращается к вызывающему объекту. Если объект класса был зарегистрирован серверным процессом с флагом REGCLS_SINGLEUSE, то SCM затем забывает, что класс доступен в серверном процессе и не будет использовать его для последующих запросов на активацию.
Только что описанный сценарий является корректным, если серверный процесс уже выполняется. Если, однако, SCM получает внепроцессный запрос на активацию, но под запрашиваемым CLSID не зарегистрировался ни один серверный процесс, то SCM запустит серверный процесс, который еще не запущен. COM поддерживает три модели для создания серверов: сервисы NT (NT Services), нормальные процессы и суррогатные процессы (surrogate processes). NT Services и нормальные процессы очень похожи, и причины, по которым один из них можно предпочесть другому, явятся предметом дальнейшего обсуждения в рамках этой главы. Суррогатные процессы используются в основном для возложения функции ведущего узла на старые внутрипроцессные серверы в отдельных серверных процессах. Это дает преимущества удаленной активации и локализации ошибок для старых DLL или для классов, которые должны быть упакованы как DLL (например, виртуальная машина Java). Независимо от того, какая модель используется для создания серверного процесса, у серверного процесса есть 120 секунд (или 30 секунд под Windows NT Service Pack 2 и более ранних версий) для регистрации запрошенного объекта класса с применением CoRegisterClassObject. Если серверный процесс не может вовремя зарегистрировать сам себя, то SCM откажет вызывающему объекту в запросе на активацию.
При создании серверного процесса SCM вначале проверяет, имеет ли AppID, соответствующий запрашиваемому классу, именованную величину LocalService:
[HKCR\AppID\{27EE6A4D-DF65-11d0-8C5F-0080C73925BA} LocalService="apesvc"
Если это именованное значение имеется, то SCM использует NT Service Control Manager (диспетчер управления сервисами) для запуска той службы NT, которая указана в реестре (например, apesvc). Если же именованная величина LocalService отсутствует, то в этом случае SCM ищет в указанном CLSID ключе подключ LocalServer32:
[HKCR\CLSID\{27EE6A4F-DF65-11d0-8C5F-0080C73925BA}\LocalServer32] @="C:\somefile.exe"
Если этот ключ присутствует, то SCM применит для запуска серверного процесса API-функцию CreateProcess (или CreateProcessAsUser). В случае отсутствия и LocalService, и LocalServer32, SCM ищет, определен ли для AppID -класса суррогатный процесс:
[HKCR\AppID\{27EE6A4D-DF6S-11d0-8CSF-0080C73925BA}] DllSurrogate=""
Если величина, именованная DllSurrogate, существует, но пуста, то SCM запустит суррогатный процесс по умолчанию (dllhost.exe). Если именованная величина DllSurrogate существует, но ссылается на легальное имя файла:
[HKCR\AppID\{27EE6A4D-DF65-11d0-8C5F-0080C7392SBA}] DllSurrogate="С:\somefile.exe"
то SCM запустит указанный серверный процесс. В любом случае суррогатный процесс саморегистрируется библиотекой COM (и SCM) с помощью API-функции CoRegisterSurrogate в качестве суррогатного процесса:
HRESULT CoRegisterSurrogate([in] ISurrogate *psg);
Эта API-функция предполагает, что суррогатный процесс предоставляет реализацию интерфейса ISurrogate:
[uuid(00000022-0000-0000-C000-000000000046), object] interface ISurrogate : IUnknown {
// SCM asking surrogate to load inprocess class object and
// call CoRegisterClassObject using REGCLS_SUSPENDED
// SCM просит суррогат загрузить внутрипроцессный
// объект класса и вызвать CoRegisterClassObject
// с флагом REGCLS_SUSPENDED
HRESULT LoadDllServer([in] REFCLSID rclsid);
// SCM asking surrogate to shut down
// SCM просит суррогат прекратить работу
HRESULT FreeSurrogate();
}
Интерфейс ISurrogate предоставляет COM механизм запроса суррогатного процесса для регистрации объектов класса с последующим его остановом. Суррогатный механизм существует в первую очередь для поддержки удаленной активации старых внутрипроцессных серверов. В общем случае суррогаты могли бы использоваться только в тех случаях, когда внутрипроцесные серверы не могут быть перестроены во внепроцессные.
Если, наконец, не существует ни одного из этих ключей реестра или именованных величин, то SCM будет искать элемент RemoteServerName под ключом AppID, соответствующим классу:
[HKCR\AppID\{27EE6A4D-DF65-11d0-8CSF-0080C7392SBA}] RemoteServerName="www.apes.com"
При наличии этой величины активационный запрос будет переадресован SCM указанной хост-машины. Отметим, что даже если клиент указал в начальном запросе на активацию только флаг CLSCTX_LOCAL_SERVER, то запрос будет переадресован только в случае, если не зарегистрировано ни одного локального серверного процесса.
Еще один дополнительный фактор, способный изменить адресацию активационных запросов, относится только к запросам CoGetInstanceFromFile (включая вызовы BindToObject файлового моникера). По умолчанию, если имя файла, использованное для наименования постоянного объекта, относится к файлу из удаленной файловой системы, то COM будет использовать вышеописанный алгоритм для определения того, где активировать объект. Если, однако, AppID класса имеет именованную величину ActivateAtStorage и эта величина равна "Y" или "y", то COM направит активационный запрос к той машине, на которой располагается файл, при условии, что вызывающий объект не передавал явное хост-имя через структуру COSERVERINFO. Этот способ гарантирует, что во всей сети будет существовать только один экземпляр.
Исходная версия COM не занималась проблемой защиты. Это можно рассматривать как упущение, так как многие примитивы NT без удаленного доступа (nonremotable) (например, процессы, потоки) могут быть защищены, несмотря на то, что ими нельзя управлять дистанционно. Версия Windows NT 4.0 вынудила добавить к COM обеспечение безопасности, так как стало возможным осуществлять доступ к серверным процессам практически от любой машины в сети. К счастью, поскольку COM использует в качестве средства сообщения RPC, то защита COM просто применяет существующую инфраструктуру защиты RPC.
Защита COM может быть разделена на три категории: аутентификация (authentication), контроль доступа (access control) и управление маркерами (token management). Аутентификация заключается в обеспечении подлинности сообщения, а именно: отправитель действительно тот, за кого он себя выдает, а данное сообщение действительно пришло от отправителя. Контроль за доступом проверяет, кто имеет допуск к объектам сервера и кто имеет право запуска серверного процесса. Управление маркерами осуществляет контроль за тем, чьи полномочия использованы при запуске серверных процессов и при выполнении самого вызова метода. В COM предусмотрены до некоторой степени разумные установки по умолчанию по всем трем аспектам защиты, что делает теоретически возможным писать приложения COM, не учитывая вопросов безопасности. Эти установки по умолчанию основаны на принципе наименьших сюрпризов; то есть если программист не делает ничего явного по защите, то маловероятно, что этим будут внесены какие-либо «дыры» в систему безопасности NT. В то же время построение даже простейшего рассредоточенного приложения на базе COM требует, чтобы каждому аспекту обеспечения безопасности было уделено определенное внимание.
Большинство аспектов защиты COM может быть сконфигурировано путем помещения в реестр нужной информации. Программа DCOMCNFG.ЕХЕ позволяет администраторам настраивать большинство установок (но не все), относящихся к защите COM. Для большинства этих установок (но не для всех) программист приложения может предпочесть употребление явных API-функций вместо установок в реестре. В целом большинство приложений используют комбинацию установок DCOMCNFG.EXE с явными API-функциями. Первый вариант проще для отладки системными администраторами, в то время как второй предполагает большую гибкость без риска неправильного обращения с DCOMCNFG.EXE.
Защита COM использует основные средства RPC для аутентификации и заимствования прав (impersonation). Напомним, что RPC использует загружаемые транспортные модули для того, чтобы после их загрузки в систему были добавлены новые сетевые протоколы. Транспортные модули именуются при помощи последовательностей протоколов (protocol sequences) (например, «ncadg_ip_udp»), которые преобразуются в реестре в специальную транспортную библиотеку DLL. Это позволяет третьим участникам устанавливать поддержку новых транспортных протоколов без модифицирования библиотеки COM. Подобным же образом RPC поддерживает загружаемые модули защиты, позволяя добавлять в систему новые протоколы защиты. Модули защиты именуются при помощи целых чисел, которые преобразуются в реестре в специальные DLL модулей защиты. Эти DLL должны соответствовать требованиям SSPI (Security Support Provider Interface – интерфейс провайдера поддержки безопасности), который является производным от Internet Draft Standard GSSAPI.
В системных заголовочных файлах определено несколько констант для известных модулей защиты. Ниже приведен текущий список известных на момент написания этого текста модулей защиты:
enum {
RPC_C_AUTHN_NONE = 0, // no authentication package
// модуля аутентификации нет
RPC_C_AUTHN_DCE_PRIVATE = 1, // DCE private key (not used)
// закрытый ключ DCE (не используется)
RPC_C_AUTHN_DCE_PUBLIC = 2, // DCE public key (not used)
// открытый ключ DCE (не используется)
RPC_C_AUTHN_DEC_PUBLIC = 4, // Digital Equip, (not used)
// цифровое оборудование (не используется)
RPC_C_AUTHN_WINNT = 10, // NT Lan Manager
// администратор локальной сети NT
RPC_C_AUTHN_GSS_KERBEROS,
RPC_C_AUTHN_MQ = 100, // MS Message Queue package
// модуль MS Message Queue (очереди сообщений Microsoft)
RPC_C_AUTHN_DEFAULT = 0xFFFFFFFFL
};
RPC_C_AUTHN_WINNT показывает, что должен использоваться протокол аутентификации Администратора локальной сети (NT LAN (local area network) Manager – NTLM). RPC_C_AUTHN_GSS_KERBEROS показывает, что будет использован протокол аутентификации Kerberos. Под Windows NT 4.0 поддерживается только протокол NTLM, если не установлена SSP третьего участника. Windows NT 5.0 будет выпущена с поддержкой как минимум NTLM и Kerberos. По вопросам получения других модулей защиты обращайтесь к соответствующей документации.
Каждый интерфейсный заместитель может быть сконфигурирован независимо, что дает возможность использовать различные модули защиты. Если интерфейсный заместитель сконфигурирован для использования протокола защиты, в клиентский процесс загружается соответствующая SSP DLL. Для того чтобы запрос на соединение с защитой был принят, серверный процесс должен зарегистрировать и загрузить соответствующую библиотеку SSP DLL до получения от клиента первого вызова ORPC. Если соединение сконфигурировано для использования модуля защиты, то соответствующая SSP DLL работает в связке с динамическим уровнем иерархии RPC и имеет возможность видеть каждый пакет, передаваемый или получаемый в рамках конкретного соединения. Библиотеки SSP DLL могут посылать в каждом пакете дополнительную информацию, специфическую для защиты, а также модифицировать маршалированное состояние параметра в целях шифрования. DCE RPC (и COM) предусматривают шесть уровней аутентификационной защиты, которые варьируются от отсутствия защиты до полного шифрования состояния всех параметров:
enum {
RPC_C_AUTHN_LEVEL_DEFAULT, // use default level for pkg
// используем для модуля уровень, принятый по умолчанию
RPC_C_AUTHN_LEVEL_NONE, // по authentication
// без аутентификации
RPC_C_AUTHN_LEVEL_CONNECT, // only authenticate credentials
// только аутентификация мандата
RPC_C_AUTHN_LEVEL_CALL, // protect message headers
// защищаем заголовки сообщений
RPC_C_AUTHN_LEVEL_PKT, // protect packet headers
// защищаем заголовки пакетов
RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, // protect parameter state
// защищаем состояние параметров
RPC_C_AUTHN_LEVEL_PKT_PRIVACY, // encrypt parameter state
// зашифровываем состояние параметров
};
Каждый последующий уровень аутентификации включает в себя функциональные возможности предыдущего уровня. Уровень RPC_C_AUTHN_LEVEL_NONE показывает, что не будет проведено никакой аутентификации. Уровень RPC_C_AUTHN_LEVEL_CONNECT показывает, что при первом вызове метода полномочия клиента должны быть аутентифицированы на сервере. Если у клиента нет необходимых полномочий, то вызов ORPC будет прерван с ошибкой E_ACCESSDENIED. Как именно проверяется достоверность этих полномочий, зависит от того, какая SSP используется. Под NTML серверный процесс выдает запрос пароля (challenge) клиентскому процессу. Этот запрос представляет собой просто непредсказуемое большое случайное число. Клиент использует закодированную версию пароля вызывающего объекта для шифрования этого запроса, который затем пересылается обратно серверу в качестве отклика (response). Затем сервер шифрует исходный запрос пароля с помощью того, что он считает закодированным паролем, и сравнивает результат с тем откликом, который он получил от клиента. Если отклик клиента совпадает с зашифрованным запросом сервера, то «личность» клиента считается установленной. NTLMSSP последовательно располагает пары квитирования (установления связи) запрос-отклик в отправных пакетах, которые посылаются исполняемой программой RPC для синхронизации порядковых номеров. Поэтому между клиентом и сервером не производится никакого дополнительного сетевого обмена данными. В зависимости от типа учетной записи (доменная, локальная) дополнительный обмен данными с контроллером домена для поддержки опосредованной аутентификации (pass-through authentication) может производиться или не производиться.
При использовании уровня аутентификации RPC_AUTHN_LEVEL_CONNECT никакого дополнительного обмена информацией, касающейся защиты, после проверки начальных полномочий не осуществляется. Это означает, что программы-злоумышленники могут перехватывать сообщения в сети и воспроизводить RPC-запросы путем простого изменения порядковых номеров DCE (среды распределенных вычислений) в заголовках пакетов. Для дополнительной защиты от воспроизведения вызова следовало бы использовать уровень аутентификации RPC_C_AUTHN_LEVEL_CALL. Он информирует библиотеки SSP DLL о необходимости защиты RPC-заголовка первого пакета каждого запроса или отклика RPC путем привязывания к передаваемому пакету однонаправленного хэш-ключа (на базе случайных чисел). Поскольку запрос или отклик RPC может быть помещен частями в более чем один сетевой пакет, то RPC API поддерживает также уровень аутентификации RPC_C_AUTHN_LEVEL_PKT. Этот уровень защищает от воспроизведения на уровне сетевых пакетов, что является большей защитой, чем уровень RPC_C_AUTHN_LEVEL_CALL, поскольку RPC-сообщение может занимать два пакета или более.
До уровня аутентификации RPC_C_AUTHN_LEVEL_PKT включительно SSP DLL в большей или меньшей мере игнорирует фактическую полезную нагрузку RPC-пакетов и защищает только целостность RPC-заголовков. Для того чтобы убедиться, что состояние маршалированного параметра не изменено вражеским агентом в сети, в RPC предусмотрен уровень аутентификации RPC_C_AUTHN_LEVEL_PKT_INTEGRITY. Этот уровень предписывает SSP DLL вычислять контрольную сумму состояния маршалированного параметра и проверять, что содержимое пакета не было изменено в процессе передачи. Поскольку при этом уровне аутентификации каждый передаваемый байт должен обрабатываться с помощью SSP DLL, она проходит значительно медленнее, чем при уровне RPC_C_AUTHN_LEVEL_PKT, и его следует использовать только в ситуациях, требующих особой защиты.
До уровня аутентификации RPC_C_AUTHN_LEVEL_PKT_INTEGRITY включительно фактическое содержание RPC-пакетов пересылается как открытый текст (например, незашифрованный). Для обеспечения невидимости состояния маршалированного параметра для вражеских агентов сети в RPC предусмотрен уровень аутентификации RPC_C_AUTHN_LEVEL_PKT_PRIVACY. Данный уровень аутентификации предписывает SSP DLL зашифровывать состояние маршалированного параметра до передачи. Подобно всем прочим уровням аутентификации уровень RPC_C_AUTHN_LEVEL_PKT_PRIVACY включает в себя защиту всех уровней ниже себя. Как и в случае RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, каждый передаваемый байт должен обрабатываться SSP DLL, поэтому во избежание излишних издержек этот уровень аутентификации следует использовать только в особых с точки зрения безопасности ситуациях.
Наиболее важной API-функцией в службе безопасности COM является CoInitializeSecurity. Каждый процесс, использующий COM, вызывает CoInitializeSecurity ровно один раз, явно или неявно. Функция CoInitializeSecurity вводит автоматические установки по защите. Эти установки применяются ко всем импортируемым и экспортируемым ссылкам на объекты, за исключением явно переопределенных с использованием дополнительных вызовов API-функций. Чтобы использовать один или нескольких модулей защиты, CoInitializeSecurity конфигурирует используемый исполняемый слой RPC, а также устанавливает уровень аутентификации, принимаемый по умолчанию для процесса. Кроме того, CoInitializeSecurity позволяет вызывающей программе указать, каким пользователям разрешено делать ORPC-запросы на объекты, экспортируемые из текущего процесса. CoInitia1izeSecurity имеет довольно большое число параметров:
HRESULT CoInitializeSecurity(
[in] PSECURITY_DESCRIPTOR pSecDesc, // access control
// контроль за доступом
[in] LONG cAuthSvc, // # of sec pkgs (-1 == use defaults)
// количество модулей защиты (-1 == используем по умолчанию)
[in] SOLE_AUTHENTICATION_SERVICE *rgsAuthSvc, // SSP array
// массив SSP
[in] void *pReserved1, // reserved MBZ
// зарезервировано, должен быть О
[in] DWORD dwAuthnLevel, // auto, AUTHN_LEVEL
// аутентификация AUTHN_LEVEL
[in] DWORD dwImpLevel, // auto. IMP_LEVEL
// аутентификация IMP_LEVEL
[in] void *pReserved2, // reserved MBZ
// зарезервировано, должен быть О
[in] DWORD dwCapabilities, // misc flags
// различные флаги
[in] void *pReserved3 // reserved MBZ
// зарезервировано, должен быть О
);
Некоторые из этих параметров применяются только в тех случаях, когда процесс выступает как экспортер/сервер. Другие – только если процесс действует как импортер/клиент. Остальные применяются в обоих случаях.
Первый параметр функции CoInitializeSecurity, pSecDesc, применим только в случае, когда процесс выступает как экспортер. Этот параметр используется для контроля того, каким принципалам – пользователям или процессам, имеющим учетную запись (principals) – разрешен доступ к объектам, экспортируемым из данного процесса. В деталях этот параметр будет обсужден позже в данной главе. Второй и третий параметры функции CoInitializeSecurity, соответственно cAuthSvc и rgsAuthSvc, используются при работе процесса в качестве экспортера для регистрации одного или нескольких модулей защиты с помощью библиотеки COM. Эти два параметра ссылаются на массив описаний модулей защиты:
typedef struct tagSOLE_AUTHENTICATION_SERVICE {
DWORD dwAuthnSvc; // which authentication package?
// какой модуль защиты?
DWORD dwAuthzSvc; // which authorization service?
// какая служба авторизации?
OLECHAR *pPrincipalName; // server principal name?
// имя серверного принципала?
HRESULT hr; // result of registration
// результат регистрации
} SOLE_AUTHENTICATION_SERVICE;
В Windows NT 4.0 единственной установленной службой аутентификации является RPC_C_AUTHN_WINNT (NTLM). При использовании аутентификации NTLM служба авторизации (authorization service – сервис контроля доступа, определяющий права клиента) должна быть указана как RPC_C_AUTHZ_NONE, а имя серверного принципала не используется и должно быть нулевым[1]. Для тех процессов, которые просто хотят использовать пакет (пакеты) защиты по умолчанию на отдельной машине, следует использовать значения: cAuthSvc, равное -1, и rgsAuthSvc, равное нулю.
Пятый параметр функции CoInitializeSecurity, dwAuthnLevel, применим как к экспортируемым, так и к импортируемым объектным ссылкам. Величина, заданная для этого параметра, устанавливает предельно низкий уровень аутентификации для объектных ссылок, экспортируемых из этого процесса. Это означает, что поступающие ORPC-запросы должны иметь по крайней мере такой уровень аутентификации; в противном случае этот вызов будет отклонен. Эта величина определяет также минимальный уровень аутентификации, используемый новыми интерфейсными заместителями, которые возвращаются API-функциями или методами COM. Создавая новый интерфейсный заместитель во время демаршалинга, COM рассматривает число, обозначающее нижний уровень аутентификации, заданный экспортером, как часть разрешения OXID. Затем COM устанавливает уровень аутентификации нового заместителя равным или нижнему уровню экспортера, или нижнему уровню текущего процесса – в зависимости от того, какой из них выше. Если процесс, импортирующий объектную ссылку, имеет уровень аутентификации ниже, чем экспортирующий процесс, то для установки уровня аутентификации используется нижний уровень экспортера. Такой способ гарантирует, что любые ORPC-запросы, посылаемые интерфейсным заместителем, пройдут через нижний уровень экспортера. Далее в этой главе будет рассмотрено, как с целью более детального контроля можно явным образом изменить уровень аутентификации для отдельного интерфейсного заместителя[2].
Шестой параметр функции CoInitializeSecurity, dwImpLevel применяется для импортируемых объектных ссылок. Величина, определенная для этого параметра, устанавливает уровень заимствования прав (impersonation level), используемый для всех объектных ссылок, которые возвращаются функцией CoUnmarshalInterface. Уровень заимствования прав позволяет одному процессу заимствовать атрибуты защиты у другого процесса и отражает степень доверия, которое клиент испытывает к серверу. Этот параметр должен принимать одно из следующих четырех значений, характеризующих уровень заимствования прав:
enum {
// hide credentials of caller from object
// скрываем от объекта полномочия вызывающей программы
RPC_C_IMP_LEVEL_ANONYMOUS = 1,
// allow object to query credentials of caller
// разрешаем объекту запрашивать полномочия вызывающей программы
RPC_C_IMP_LEVEL_IDENTIFY = 2,
// allow use of caller's credentials up to one-hop away
// разрешаем использовать полномочия вызывающей
// программы не далее одной сетевой передачи
RPC_C_IMP_LEVEL_IMPERSONATE = 3,
// allow use of caller's credentials across multiple hops
// разрешаем использовать полномочия вызывающей
// программы в течение нескольких сетевых передач
RPC_C_IMP_LEVEL_DELEGATE = 4
};
Уровень RPC_C_IMP_LEVEL_ANONYMOUS не позволяет реализации объекта обнаружить идентификатор безопасности вызывающей программы[3]. Уровень доверия RPC_C_IMP_LEVEL_IDENTIFY указывает, что реализация объекта может программно определить идентификатор защиты вызывающей программы. Уровень доверия RPC_C_IMP_LEVEL_IMPERSONATE указывает, что сервер может не только определить идентификатор защиты вызывающей программы, но также выполнить операции на уровне операционной системы с использованием полномочий вызывающей программы. На этом уровне доверия объекты могут использовать полномочия вызывающей программы, но имеют доступ только к локальным ресурсам[4]. В противоположность этому, уровень доверия RPC_C_IMP_LEVEL_DELEGATE разрешает серверу доступ как к локальным, так и к удаленным ресурсам с использованием полномочий вызывающей программы. Этот уровень доверия не поддерживается протоколом аутентификации NTLM, но поддерживается протоколом аутентификации Kerberos.
Восьмой параметр функции CoInitializeSecurity, dwCapabilities применим к импортируемым и к экспортируемым объектным ссылкам. Этот параметр является битовой маской, которая может состоять из нуля или более следующих битов:
typedef enum tagEOLE_AUTHENTICATION_CAPABILITIES {
EOAC_NONE = 0х0,
EOAC_MUTUAL_AUTH = 0х1,
// These are only valid for CoInitializeSecurity
// только эти допустимы для CoInitializeSecurity
EOAC_SECURE_REFS = 0х2,
EOAC_ACCESS_CONTROL = 0х4,
EOAC_APPID = 0х8
} EOLE_AUTHENTICATION_CAPABILITIES;
Взаимная аутентификация (EOAC_MUTUAL_AUTH) под NTLM не поддерживается. Она используется для проверки того, что сервер исполняется как ожидаемый принципал. Ссылки защиты (EOAC_MUTUAL_AUTH) указывают, что распределенные вызовы подсчета ссылок COM будут аутентифицироваться для гарантии того, что никакие вражеские агенты не могут испортить счетчик ссылок, используемый OR и администраторами заглушек для управления жизненным циклом. EOAC_ACCESS_CONTROL и EOAC_APPID используются для управления семантикой первого параметра функции CoInitializeSecurity и будут обсуждаться далее в этой главе.
Как было установлено ранее в этом разделе, CoInitializeSecurity вызывается один раз на процесс, явно или неявно. Приложения, желающие вызвать CoInitializeSecurity явно, должны делать это после первого вызова CoInitializeEx, но перед «первым интересным вызовом COM» (first interesting COM call). Фраза «первый интересный вызов COM» относится к любой API-функции, которой может понадобиться OXID. Сюда относятся CoMarshalInterface и CoUnmarshalInterface, а также любые API-вызовы, неявно вызывающие эти функции. Поскольку вызовы CoRegisterClassObject связывают объекты класса с апартаментами, то CoInitializeSecurity должна быть вызвана до регистрации объектов класса. API-функции активации (например, CoCreateInstanceEx) являются любопытным исключением. Активация API-функций для определенных внутренних классов, которые являются частью COM API (например, глобальная интерфейсная таблица, COM-объект контроля доступа по умолчанию) может быть произведена до вызова CoInitializeSecurity. Тем не менее, функция CoInitializeSecurity должна быть вызвана раньше, чем любые активационные вызовы, которые фактически консультируются с реестром и загружают другие DLL или контактируют с другими серверами. Если приложение не вызывает функцию CoInitializeSecurity явно, то COM вызовет ее неявно в качестве первого интересного вызова COM.
Когда COM вызывает функцию CoInitializeSecurity неявно, она читает значения большинства параметров из реестра. Некоторые из этих параметров содержатся в реестровом ключе, общем для всей машины, в то время как остальные записаны под специфическим AppID данного приложения. Чтобы извлечь AppID приложения, COM ищет имя файла процесса приложения под ключом реестра
HKEY_CLASSES_ROOT\AppID
Если COM находит здесь имя файла, она извлекает AppID из именованной величины AppID:
[HKCR\AppID\ServerOfTheApes.exe]
AppID="{27EE6A4D-DF65-11d0-8C5F-0080C73925BA}"
Если никакого соответствия не существует, то COM считает, что приложение не имеет в реестре специфических установок по защите.
Неявные вызовы CoInitializeSecurity находят первый параметр, pSecDesc, путем поиска сериализованного (приведенного в последовательную форму) дескриптора защиты NT SECURITY_DESCRIPTOR в следующей именованной величине:
[HKCR\AppID\{27EE6A4D-DF65-11d0-8C5F-0080C73925BA}]
AccessPermission=
Если эта именованная величина не найдена, то COM ищет общий для всей машины элемент:
[HKEY_LOCAL_MACHINE\Software\Microsoft\OLE]
DefaultAccessPermission=
Оба этих элемента реестра могут быть легко изменены при помощи DCOMCNFG.ЕХЕ. Если не найден ни один из этих элементов реестра, то COM создаст дескриптор защиты (security descriptor), предоставляющий доступ принципалу только вызывающей программы и встроенной учетной записи SYSTEM. COM использует этот дескриптор защиты, чтобы при помощи Win32 API-функции AccessCheck предоставить или запретить доступ к объектам, экспортированным из данного процесса.
Неявные вызовы CoInitializeSecurity используют для второго и третьего параметров (cAuthSvc и rgsAuthSvc) значения -1 и нуль соответственно, указывая тем самым, что должны использоваться модули защиты, принятые по умолчанию. Неявные вызовы CoInitializeSecurity находят значения для пятого и шестого параметров (dwAuthnLevel и dwImpLevel) в следующем элементе реестра всей машины:
[HKEY_LOCAL_MACHINE\Software\Microsoft\OLE]
LegacyAuthenticationLevel = 0x5
LegacyImpersonationLevel = 0x3
RPC_C_AUTHN_LEVEL_PKT_INTEGRITY и RPC_C_AUTHN_LEVEL_IMPERSONATE принимают числовые значения 5 и 3 соответственно. Если эти именованные величины отсутствуют, то используются значения RPC_C_AUTHN_LEVEL_CONNECT и RPC_C_IMP_LEVEL_IDENTIFY. Из флагов, используемых в восьмом параметре функций CoInitializeSecurity, dwCapabilities, в настоящее время читается из элемента реестра всей машины только флаг EOAC_SECURE_REFS:
[HKEY_LOCAL_MACHINE\Software\Microsoft\OLE]
LegacySecureRefs = "Y"
Если эта именованная величина в наличии и содержит "Y" или "y", то COM будет использовать флаг EOAC_SECURE_REFS; в противном случае используется флаг EOAC_NONE. Каждая из этих традиционных установок аутентификации может быть легко изменена при помощи DCOMCNFG.ЕХЕ.
Установки, сделанные при помощи CoInitializeSecurity , называются автоматическими установками защиты, поскольку они автоматически применяются ко всем маршалированным объектным ссылкам. Часто бывает, что небольшому числу объектных ссылок необходимо использовать установки защиты, которые отличаются от установок по умолчанию для всего процесса. Наиболее часто встречающийся сценарий таков: для повышения производительности используется довольно низкий уровень аутентификации, но необходимо зашифровать один определенный интерфейс. Вместо того чтобы принудительно использовать шифрование во всем процессе, предпочтительно применить его к тем объектным ссылкам, для которых это необходимо.
Чтобы позволить разработчикам игнорировать автоматические установки защиты на базе интерфейсных заместителей, администратор заместителей выставляет интерфейс IClientSecurity:
[local, object, uuid(0000013D-0000-0000-C000-000000000046)]
interface IClientSecurity : IUnknown {
// get security settings for interface proxy pProxy
// получаем установки защиты для интерфейсного заместителя
pProxy 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 *pCapabilities );
// change security settings for interface proxy pProxy
// изменяем установки защиты для интерфейсного заместителя
pProxy 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 Capabilities );
// duplicate an interface proxy
// дублируем интерфейсный заместитель
HRESULT CopyProxy([in] IUnknown *pProxy, [out] IUnknown **ppCopy );
}
Второй, третий и четвертый параметры методов SetBlanket и QueryBlanket соответствуют трем членам структуры данных SOLE_AUTHENTICATION_SERVICE. Под Windows NT 4.0 единственными допустимыми величинами являются соответственно RPC_C_AUTHN_WINNT, RPC_C_AUTHN_NONE и нуль.
Как показано на рис. 6.1, каждый отдельный интерфейсный заместитель имеет свои собственные установки защиты. Метод IClientSecurity::SetBlanket позволяет вызывающей программе изменять эти установки для каждого интерфейсного заместителя по отдельности.
Метод IClientSecurity::QueryBlanket позволяет вызывающей программе прочитать эти установки для отдельного интерфейсного заместителя. В качестве параметров, о которых вызывающая программа не беспокоится, могут быть переданы нулевые указатели. Метод IClientSecurity::СоруРгоху позволяет вызывающей программе копировать интерфейсный заместитель. Это дает возможность делать изменения в копии интерфейса, которая не будет возвращаться при последующих запросах QueryInterface об администраторе заместителей. В идеале установки защиты следовало бы делать только в скопированных интерфейсных заместителях, чтобы изолировать измененный заместитель от нормальной реализации администратора заместителей в QueryInterface ; это также позволило бы нескольким потокам независимо друг от друга изменять полные установки защиты между вызовами метода.
Все параметры методов IClientSecurity::SetBlanket и IClientSecurity::QueryBlanket соответствуют параметрам CoInitializeSecurity с одним значительным исключением. Седьмой параметр (pAuthInfo ) указывает на набор полномочий клиента. Точная форма этих полномочий специфична для каждого модуля безопасности. Для модуля безопасности NTLM этот параметр может указывать на структуру COAUTHIDENTITY:
typedef struct _COAUTHIDENTITY {
OLECHAR *User;
// user account name
// имя учетной записи пользователя
ULONG UserLength;
// wcslen(User)
// длина имени пользователя
OLECHAR *Domain;
// Domain/Machine name
// имя домена/машины
ULONG DomainLength;
// wcslen(Domain)
// длина имени домена
OLECHAR *Password;
// cleartext password
// пароль открытым текстом
ULONG PasswordLength;
// wcslen(Password)
// длина пароля
ULONG Flags;
// must be SEC_WINNT_AUTH_IDENTITY_UNICODE
// должно быть SEC_WINNT_AUTH_IDENTITY_UNICODE
} COAUTHIDENTITY;
Эта структура позволяет клиентам делать вызовы методов COM как любым принципалам защиты, при условии, что они знают открытые тексты паролей для желаемой учетной записи[1]. Если вместо указателя на явную структуру COAUTHIDENTITY передается нулевой указатель, то каждый внешний вызов будет делаться с использованием полномочий вызывающего процесса[2].
Чаще всего метод IClientSecurity::SetBlanket применяется для повышения уровня аутентификации отдельного заместителя. Следующий код демонстрирует эту технологию:
HRESULT Encrypt(IApe *pApe) {
IClientSecurity *pcs = 0;
// ask proxy manager for IClientSecurity interface
// запрашиваем интерфейс IClientSecurity у администратора заместителей
HRESULT hr = pApe->QueryInterface(IID_IClientSecurity, (void**)&pcs);
if (SUCCEEDED(hr)) {
hr = pcs->SetBlanket(pApe, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, 0, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IDENTIFY, 0, EOAC_NONE);
pcs->Release();
}
return hr;
}
В идеале вызывающая программа передаст этой функции скопированный интерфейсный заместитель. В противном случае с целью проведения операции копирования эту функцию можно было бы изменить следующим образом:
HRESULT DupeAndEncrypt(IApe *pApe, IApe * &rpSecretApe) {
rpSecretApe = 0;
IClientSecurity *pcs = 0;
// ask proxy manager for IClientSecurity interface
// запрашиваем интерфейс IClientSecurity у администратора заместителей
HRESULT hr = pApe->QueryInterface(IID_IClientSecurity, (void**)&pcs);
if (SUCCEEDED(hr)) {
hr = pcs->CopyProxy(pApe, (IUnknown**)&rpSecretApe);
if (SUCCEEDED(hr))
hr = pcs->SetBlanket (rpSecretApe, RPC_AUUTHN_WINNT, RPC_C_AUTHZ_NONE, 0, RPC_С_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IDENTIFY, 0, EOAC_NONE);
pcs->Release();
}
return hr;
}
Для удобства в COM API предусмотрены оберточные функции вокруг каждого из трех методов IClientSecurity , которые изнутри вызывают QueryInterface для нахождения соответствующего интерфейса IClientSecurity и затем вызывают нужный метод:
// get security settings for interface proxy pProxy
// получаем установки защиты для интерфейсного заместителя
pProxy HRESULT CoQueryProxyBlanket([in] IUnknown *pProxy, [out] DWORD *pAuthnSvc, [out] DWORD *pAuthzSvc, [out] OLECHAR **pServerPrincName, [out] DWORD *pAuthnLevel, [out] DWORD *pImpLevel, [out] void **pAuthInfo, [out] DWORD *Capabilities);
// change security settings for interface proxy pProxy
// изменяем установки защиты для интерфейсного заместителя
pProxy HRESULT CoSetProxyBlanket([in] IUnknown *pProxy, [in] DWORD AuthnSvc, [in] DWORD AuthzSvc, [in] OLECHAR *pServerPrincName, [in] DWORD AuthnLevel, [in] DWORD ImpLevel, [in] void *pAuthInfo, [in] DWORD Capabilities);
// duplicate an interface proxy
// копируем интерфейсный заместитель
HRESULT CoCopyProxy([in] IUnknown *pProxy, [out] IUnknown **ppCopy);
Следующий код представляет собой модифицированную версию предыдущей функции, где используются эти удобные процедуры:
HRESULT DupeAndEncrypt(IApe *pApe, IАре *ArpSecretApe) {
rpSecretApe = 0; HRESULT hr = СоСоруProxy(pApe, (IUnknown**)&rpSecretApe);
if (SUCCEEDED(hr))
hr = CoSetProxyBlanket(rpSecretApe, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, 0, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IDENTIFY, 0, EOAC_NONE);
return hr;
}
Первая версия несколько эффективнее, так как для нахождения интерфейса IClientSecurity в ней использован только один вызов QueryInterface. Для последней версии требуется меньше кода, поэтому и вероятность ошибок в ней меньше.
Важно отметить, что методы IClientSecurity могут применяться только в тех интерфейсах, которые используют интерфейсные заместители. Это означает, что те интерфейсы, которые реализованы локально администратором заместителей (например, IMultiQI, IClientSecurity), не могут использоваться с методами IClientSecurity. Интерфейс IUnknown – это особый случай. IUnknown фактически является локальным интерфейсом, реализованным администратором заместителей. Однако администратору заместителей часто требуется связаться с апартаментом сервера для запроса новых интерфейсов и для освобождения ресурсов, хранящихся в соответствующем администраторе заглушек. Эта связь осуществляется через закрытый интерфейс IRemUnknown, который реализуется библиотекой COM отдельно внутри каждого апартамента. Разработчики могут контролировать полную защиту, использованную для этих вызовов IRemUnknown путем передачи реализации IUnknown администратора заместителей в IClientSecurity::SetBlanket (подобно интерфейсному заместителю, администратор заместителей использует общие для процесса автоматические установки защиты в случае, если функция SetBlanket не вызывается)[3]. Поскольку все интерфейсные заместители агрегированы администратором заместителей, то это, в сущности, означает, что вызовы IClientSecurity::SetBlanket на какой-либо определенный интерфейсный заместитель не влияют на функции QueryInterface, AddRef и Release. Скорее, на них влияют установки, примененные к реализации IUnknown администратором заместителей. Для того чтобы получить указатель на реализацию IUnknown администратором заместителей, можно просто запросить с помощью QueryInterface интерфейсный заместитель для IID_IUnknown. Следующий фрагмент кода демонстрирует эту технологию, отключая защиту как для интерфейсного заместителя, так и для его администратора заместителей:
void TurnOffAllSecurity(IApe *pApe) {
IUnknown *pUnkProxyManager = 0;
// get a pointer to the proxy manager
// получаем указатель на администратор заместителей
HRESULT hr = pApe->QueryInterface(IID_IUnknown, (void**)&pUnkProxyManager);
assert(SUCCEEDED(hr));
// set blanket for proxy manager
// устанавливаем защиту для администратора заместителей
hr = CoSetProxyBlanket(pUnkProxyManager, RPC_C_AUTHN_NONE, RPC_C_AUTHZ_NONE, О, RPC_C_AUTHN_LEVEL_NONE, RPC_C_IMP_LEVEL_ANONYMOUS, 0, EOAC_NONE);
assert(SUCCEEDED(hr));
// set blanket for interface proxy
// устанавливаем защиту для интерфейсного заместителя
hr = CoSetProxyBlanket(pApe, RPC_C_AUTHN_NONE, RPC_C_AUTHZ_NONE, 0, RPC_C_AUTHN_LEVEL_NONE, RPC_C_IMP_LEVEL_ANONYMOUS, 0, EOAC_NONE);
assert(SUCCEEDED(hr));
// release temporary pointer to proxy manager
// освобождаем временный указатель на администратор заместителей
pUnkProxyManager->Release();
}
Хотя представляется возможным установить и запросить защиту для администратора заместителей, невозможно скопировать администратор заместителей с помощью IClientSecurity::CopyProxy, так как это нарушило бы правила идентификации COM.
Когда ORPC-запрос направляется интерфейсной заглушке, COM создает объект контекста вызова (call context object), представляющий различные аспекты вызова, в том числе установки защиты того интерфейсного заместителя, который отправил этот запрос. COM связывает этот контекстный объект с потоком, который будет выполнять вызов метода. Библиотека COM выставляет API-функцию CoGetCallContext, позволяющую реализациям метода получить контекст для текущего вызова метода:
HRESULT CoGetCallContext ([in] REFIID riid, [out, iid_is(riid)] void **ppv);
В Windows NT 4.0 единственным интерфейсом, доступным для контекстного объекта вызова, является интерфейс IServerSecurity:
[local, object, uuid(0000013E-0000-0000-C000-000000000046)]
interface IServerSecurity : IUnknown {
// get caller's security settings
// получаем установки защиты вызывающей программы
HRESULT QueryBlanket( [out] DWORD *pAuthnSvc,
// authentication pkg
// модуль аутентификации
[out] DWORD *pAuthzSvc,
// authorization pkg
// модуль авторизации
[out] OLECHAR **pServerName,
// server principal
// серверный принципал
[out] DWORD *pAuthnLevel,
// authentication level
// уровень аутентификации
[out] DWORD *pImpLevel,
// impersonation level
// уровень заимствования прав
[out] void *pPrivs,
// client principal
// клиентский принципал
[out] DWORD *pCaps
// EOAC flags
// флаги EOAC
);
// start running with credentials of caller
// начинаем выполнение с полномочиями вызывающей программы
HRESULT ImpersonateClent(void);
// stop running with credentials of caller
// заканчиваем выполнение с полномочиями вызывающей программы
HRESULT RevertToSelf(void);
// test for Impersonation
// тест для заимствования прав BOOL
IsImpersonating(void);
}
IServerSecurity::QueryBlanket возвращает установки полной защиты, фактически использованные для текущего ORPC-вызова (которые могут несколько отличаться от клиентских установок благодаря специфическому для SSP повышению уровней). Как было в случае с IClientSecurity::QueryBlanket, функции IServerSecurity::QueryBlanket также разрешается передавать нуль вместо неиспользуемых параметров. Ниже приведен пример реализации метода, которая гарантирует, что вызывающая программа обеспечила возможность шифрования перед обработкой вызова:
STDMETHODIMP Gorilla::SwingFromTree(/*(in]*/ long nTreeID) {
// get current call context
// получаем контекст текущего вызова IServerSecurity *pss = 0;
HRESULT hr = CoGetCallContext(IID_IServerSecurity, (void**)&pss);
DWORD dwAuthnLevel;
if (SUCCEEDED(hr)) {
// get authentication level of current call
// получаем уровень аутентификации текущего вызова
hr = pss->QueryBlanket(0, 0, 0, &dwAuthnLevel, 0, 0, 0);
pss->Release(); }
// verify proper authentication level
// проверяем правильность уровня аутентификации
if (FAILED(hr) || dwAuthnLevel != RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
hr = APE_E_NOPUBLICTREE;
else hr = this->ActuallySwingFromTree(nTreeID);
return hr;
}
Как было в случае с IClientSecurity, каждый метод IServerSecurity доступен в качестве удобной API-функции. Приводимая ниже реализация метода использует удобную подпрограмму вместо явного вызова интерфейса IServerSecurity
STDMETHODIMP Gorilla::SwingFromTree(/*[in]*/ long nTreeID) {
DWORD dwAuthnLevel;
// get authentication level of current call
// получаем уровень аутентификации текущего вызова
HRESULT hr = CoQueryClientBlanket(0, 0, 0, &dwAuthnLevel, 0, 0, 0);
// verify proper authentication level
// проверяем правильность уровня аутентификации
if (FAILED(hr) || dwAuthnLevel != RPC_C_AUTHN_LEVEL_РКТ_PRIVACY)
hr = АРЕ_Е_NOPUBLICTREE;
else hr = this->ActuallySwingFromTree(nTreeID);
return hr;
}
И снова мы видим, что последняя версия требует меньше кода и поэтому вероятность ошибок в ней меньше.
Метод IServerSecurity::QueryBlanket также позволяет разработчику объекта находить идентификатор защиты вызывающей программы через параметр pPrivs. Как и в случае с полномочиями, передаваемыми в IClientSecurity::SetBlanket , точный формат этого идентификатора является специфическим для конкретного модуля защиты. Для NTLM этот формат является просто строкой вида Authority\AccountName
Следующая реализация метода отыскивает идентификатор защиты вызывающей программы с помощью API-функции CoQueryClientBlanket:
STDMETHODIMP Gorilla::EatBanana() {
OLECHAR *pwszClientPrincipal = 0;
// get security identifier of caller
// получаем идентификатор защиты вызывающей программы
HRESULT hr = CoQueryClientBlanket(0, 0, 0, 0, 0, (void**)&pwszClientPrincipal, 0);
// log user name
// регистрируем имя пользователя
if (SUCCEEDED(hr)) {
this->LogCallerIDToFile(pwszClientPrincipal);
hr = this->ActuallyEatBanana();
}
return hr;
}
При вызове CoQueryClientBlanket для успешного возвращения идентификатора защиты вызывающей программы последняя должна определить:
По крайней мере RPC_C_IMP_LEVEL_IDENTIFY как автоматический (или явный) уровень заимствования прав;
По крайней мере RPC_C_AUTHN_LEVEL_CONNECT как автоматический (или явный) уровень аутентификации.
Если вызывающая программа явно изменила вызывающий принципал в установках полной защиты заместителя с помощью функции COAUTHIDENTITY, то вместо него будет возвращено имя явно заданного принципала.
Точно так же, как можно полностью контролировать установки защиты, использующиеся при вызове метода с помощью интерфейса IClientSecurity , представляется полезным контролировать установки защиты, использованные при вызове на активацию. К сожалению, активационные вызовы являются глобальными API-функциями, не имеющими соответствующего администратора заместителей, откуда можно было бы получить интерфейс IClientSecurity. Для того чтобы позволить вызывающим программам задавать установки защиты для активационных вызовов, каждый активационный вызов принимает структуру СОSERVERINFO:
typedef struct _COSERVERINFO {
DWORD dwReserved1;
LPWSTR pwszName; COAUTHINFO * pAuthInfo;
DWORD * dwReserved2;
} COSERVERINFO;
В одной из предыдущих глав было отмечено, что элемент данных pwszName позволяет вызывающей программе осуществлять явный контроль того, какая хост-машина будет обслуживать активационный запрос. Третий элемент данных, pAuthInfo, указывает на структуру данных, которая позволяет вызывающей программе контролировать установки защиты, использованные при осуществлении активационного вызова. Этот параметр является указателем на структуру COAUTHINFO, определенную следующим образом:
typedef struct _COAUTHINFO {
DWORD dwAuthnSvc;
DWORD dwAuthzSvc;
LPWSTR pwszServerPrincName;
DWORD dwAuthnLevel;
DWORD dwImpersonationLevel;
COAUTHIDENTITY * pAuthIdentityData;
DWORD dwCapabilities;
} COAUTHINFO;
Эти элементы данных соответствуют параметрам IClientSecurity::SetВlanket, однако используются только во время активационного вызова и не влияют на результирующий интерфейсный заместитель[4].
Следующий фрагмент кода осуществляет активационный вызов, используя структуру COAUTHINFO, чтобы заставить SCM использовать при активационном вызове шифрование (RPC_C_AUTHN_LEVEL_PKT_PRIVACY):
void CreateSecretChimp(IApe *&rpApe) {
rpApe = 0;
// create a COAUTHINFO that specifies privacy
// создаем COAUTHINFO, которая определяет секретность
COAUTHINFO cai = { RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, 0, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IDENTIFY, 0, 0 };
// issue an activation call using the COAUTHINFO
// осуществляем активационный вызов с использованием COAUTHINFO
COSERVERINFO csi = { 0, 0, &cai, 0 };
IApeClass *pac = 0;
hr = CoGetClassObject(CLSID_Chimp, CLSCTX_ALL, &csi, IID_IApeClass, (void**)&pac);
assert(SUCCEEDED(hr));
// the activation call occurred with encryption,
// but рас is using automatic security settings
// активационный вызов произошел с шифрованием,
// но пакет использует автоматические установки защиты
hr = pac->CreateApe(&rpApe);
pac->Release();
return hr;
}
Важно отметить, что, поскольку структура COAUTHINFO оказывает воздействие только на сам активационный вызов, результирующий интерфейсный заместитель IApeClass будет использовать автоматические установки защиты, установленные более ранним вызовом CoInitializeSecurity. Это означает, что вызов метода IApeClass::CreateApe будет использовать автоматические установки защиты, а не те, которые определены структурой COAUTHINFO . Для того чтобы гарантировать применение шифрования во время создания или обработки нового Chimp, необходимо модифицировать функцию так, чтобы она ставила полную защиту на заместители обоих интерфейсов – IApeClass и IАре:
// encrypt calls on IApeClass reference
// зашифровываем вызовы на ссылку на IApeClass CoSetProxyBlanket(pac, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, 0, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_ANONYMOUS, 0, EOAC_NONE);
// issue call to create object
// осуществляем вызов для создания объекта
pac->CreateApe(&rpApe);
// encrypt calls on IApe reference
// зашифровываем вызовы на ссылку на IApe
CoSetProxyBlanket(rpApe, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, 0, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_ANONYMOUS, 0, EOAC_NONE);
Использование явного вызова COAUTHIDENTITY во время активации может позволить вызывающей программе создавать объекты в процессах, которые в противном случае были бы недоступны принципалу вызывающего процесса. Однако в этом случае вызывающая программа должна гарантировать, что администратор заместителей использует эти же самые полномочия при освобождении интерфейсного указателя, иначе будет утечка ресурсов со стороны сервера. Как уже упоминалось ранее в этой главе, полная защита администратора заместителей контролируется отдельно путем вызова метода IClientSecurity::SetBlanket на реализацию IUnknown администратором заместителей.
Как уже упоминалось ранее в этой главе, каждый процесс COM может защитить сам себя от несанкционированного доступа. COM рассматривает контроль доступа на двух уровнях: права запуска (launch permissions) и права доступа (access permissions). Права запуска используются, чтобы определить, какие пользователи могут запускать серверные процессы при осуществлении активационных вызовов к SCM. Права доступа определяют, какие пользователи могут обращаться к объектам процесса после того, как сервер уже запущен. Оба типа контроля доступа можно сконфигурировать при помощи DCOMCNFG.EXE, но только права доступа могут быть заданы программно на этапе выполнения (поскольку после того, как сервер запущен, уже слишком поздно отказывать пользователю в правах запуска). Вместо этого право запуска предоставляется диспетчеру управления сервнсами SCM во время активации.
Когда SCM решает, что должен быть запущен новый серверный процесс, он пытается получить дескриптор защиты NT SECURITY_DESCRIPTOR, описывающий, каким пользователям разрешено запускать серверный процесс. В первую очередь SCM проверяет AppID класса для явной установки прав запуска. Эта установка приходит в форме сериализованного дескриптора защиты NT, который хранится в именованной величине LaunchPermission AppID:
[HKCR\AppID\{27EE6A4D-DF65-1d0-8C5F-0080C73925BA}]
LaunchPermission=
Если эта именованная величина отсутствует, SCM пытается прочитать общие для всей машины права запуска из такой именованной величины:
[HKEY_LOCAL_MACHINE\Software\Microsoft\OLE]
DefaultLaunchPermission=
Обе эти установки могут быть модифицированы с помощью DCOMCNFG.EXE. Если не найден ни один из этих ключей реестра, то COM запретит запуск кому бы то ни было. Если же SECURITY_DESCRIPTOR найден, SCM проверяет идентификатор защиты активизирующей вызывающей программы (формально называемой активизатором – actiuator) по списку разграничительного контроля доступа DACL (Discretionary Access Control List), имеющемуся в дескрипторе, чтобы определить, имеет ли активизатор полномочия на запуск сервера. Если активизатор не имеет необходимых полномочий, то следует отказ на активационный вызов с HRESULT E_ACCESSDENIED, и никаких процессов не запускается. В случае успешной проверки SCM запускает серверный процесс и продолжает выполнение активационного запроса.
Права запуска определяют только, какие пользователи могут или не могут начинать серверные процессы во время активации. Эта проверка всегда выполняется SCM на основе информации, записанной в реестре. Права доступа определяют, какие пользователи могут действительно связываться с объектами серверного процесса. Эта проверка осуществляется библиотекой COM при каждом запросе на установку соединения, приходящем от клиента. Для контроля установок прав доступа к процессу разработчики могут использовать API-функцию CoIntializeSecurity.
Напомним, что процессы, не вызывающие явно функцию CoInitializeSecurity, автоматически используют список контроля доступа, записанный под ключом реестра AppID приложения:
[HKCR\AppIO\{27EE6A4D-DF65-11d0-8C5F-0080C73925BA}]
AccessPermission=
Ранее объяснялось, что если этот элемент реестра отсутствует, то COM ищет установку по умолчанию для всей машины, а если она также отсутствует, то создается новый список контроля доступа, который включает только принципала серверных процессов и встроенной учетной записи SYSTEM.
Приложения, явно вызывающие CoInitializeSecurity, могут вручную контролировать, каким вызывающим программам разрешен доступ к объектам, экспортируемым данным процессом. По умолчанию первый параметр CoIntializeSecurity принимает указатель на SECURITY_DESCRIPTOR NT. Если вызывающая программа передает в качестве этого параметра нулевой указатель, то COM не будет осуществлять никакого контроля входящих вызовов. Это разрешает вызовы от любого аутентифицированного принципала защиты. Если и клиент, и сервер укажут RPC_C_AUTHN_LEVEL_NONE, то COM разрешит вызовы от кого угодно, независимо от его аутентификации. Если же в вызывающей программе имеется легальный указатель на дескриптор защиты, то COM с помощью DACL этого дескриптора защиты определит, каким вызывающим программам разрешен доступ к объектам процесса. Заголовки SDK определяют так называемый флаг прав (COM_RIGHTS_EXECUTE), который используется при создании DACL для явного разрешения или запрета пользователям на связь с объектами процесса.
Хотя и допускается использовать API-функции Win32 для создания SECURITY_DESCRIPTOR с целью передачи его в CoInitializeSecurity, этот способ контроля доступа к объектам процесса не является предпочтительным, в основном по причине темной природы API-функций защиты Win32. Для упрощения программирования в COM контроля доступа в реализации COM для Windows NT 4.0 Service Pack 2 разработчикам разрешено указывать тот объект COM, который будет использоваться для выполнения проверки доступа при установке новых соединений. Этот объект регистрируется библиотекой COM во время выполнения CoInitializeSecurity и должен реализовать интерфейс IAccessControl:
[object, uuid(EEDD23EO-8410-11CE-A1C3-08002B2B8D8F)]
interface IAccessControl : IUnknown {
// add access allowed rights for a list of users
// добавляем разрешенные права доступа для списка пользователей
HRESULT GrantAccessRights([in] PACTRL_ACCESSW pAccessList);
// explicitly set the access rights for a list of users
// явно устанавливаем права доступа для списка пользователей
HRESULT SetAccessRights([in] PACTRL_ACCESSW pAccessList
// users+rights
// пользователи + Права
);
// set the owner/group IDs of the descriptor
// устанавливаем идентификаторы владельца/группы для дескриптора
HRESULT Set0wner(
[in] PTRUSTEEW pOwner, // owner ID
// ID владельца
[in] PTRUSTEEW pGroup // group ID
// ID группы
);
// remove access rights for a list of users
// удаляем права доступа для списка пользователей
HRESULT RevokeAccessRights(
[in] LPWSTR lpProperty, // not used
// не используется
[in] ULONG cTrustees, // how many users
// сколько имеется пользователей
[in, size_is(cTrustees)] TRUSTEEW prgTrustees[] // users
// пользователи
);
// get list of users and their rights
// получаем список пользователей и их прав
HRESULT GetAllAccessRights(
[in] LPWSTR lpProperty, // not used
// не используется
[out] PACTRL_ACCESSW *ppAccessList, // users+rights
// пользователи + права
[out] PTRUSTEEW *ppOwner, // owner ID
// ID владельца
[out] PTRUSTEEW *ppGroup // group ID
// ID группы
);
// called by COM to allow/deny access to an object
// вызывается COM для разрешения/запрета доступа к объекту
HRESULT IsAccessAllowed(
[in] PTRUSTEEW pTrustee, // caller's ID
// ID вызывающей программы
[in] LPWSTR lpProperty, // not used
// не используется
[in] ACCESS_RIGHTS Rights, // COM_RIGHTS_EXECUTE
[out] BOOL *pbAllowed // yes/no!
// да/нет!
);
}
Этот интерфейс предназначен для того, чтобы разработчики могли создавать объекты контроля доступа на основе статических таблиц данных, преобразующих имена принципалов в права доступа. Интерфейс основывается на новом Windows NT 4.0 API защиты на базе опекуна (trustee), то есть пользователя, обладающего правами доступа к объекту. Основным типом данных, используемым этим API, является TRUSTEE:
typedef struct _TRUSTEE_W {
struct _TRUSTEE_W *pMultipleTrustee;
MULTIPLE_TRUSTEE_OPERATION MultipleTrusteeOperation;
TRUSTEE_FORM TrusteeForm;
TRUSTEE_TYPE TrusteeType;
switch_is(TrusteeForm)]
union {
[case(TRUSTEE_IS_NAME)]
LPWSTR ptstrName;
[case(TRUSTEE_IS_SID)]
SID *pSid;
};
} TRUSTEE_W, *PTRUSTEE_W, TRUSTEEW, *PTRUSTEEW;
Этот тип данных используется для описания принципала защиты. Первые два параметра, pMultipleTrustee и MultipleTrusteeOperation, позволяют вызывающей программе отличать настоящие регистрационные имена (logins – логины) от попыток заимствования прав. Пятый параметр, ptstrName/pSid, содержит либо идентификатор защиты NT (security identifier – SID), либо текстовое имя учетной записи, подлежащее идентификации. При этом третий параметр, TrusteeForm, указывает, какой именно член объединения (union member) используется. Четвертый параметр, TrusteeType, указывает, является ли данный принципал учетной записью пользователя или группы.
Для связывания опекуна с полномочиями, которые ему даны или в которых ему отказано, в Win32 API предусмотрен тип данных ACTRL_ACCESS_ENTRY:
typedef struct _ACTRL_ACCESS_ENTRYW {
TRUSTEE_W Trustee; // who?
// кто?
ULONG fAccessFlags; // allowed/denied?
// разрешено/запрещено?
ACCESSRIGHTS Access;// which rights?
// какие права?
ACCESSRIGHTS ProvSpecificAccess; // not used by COM
// в COM не используется
INHERIT_FLAGS Inheritance; // not used by COM
// в COM не используется
LPWSTR lpInheritProperty; // not used by COM
// в COM не используется
} ACTRL_ACCESS_ENTRYW, *PACTRL_ACCESS_ENTRYW;
а также тип данных для создания списков элементов для опекунов/полномочий:
typedef struct _ACTRL_ACCESS_ENTRY_LISTW {
ULONG cEntries;
[size_is(cEntries)] ACTRL_ACCESS_ENTRYW *pAccessList;
} ACTRL_ACCESS_ENTRY_LISTW, *PACTRL_ACCESS_ENTRY_LISTW;
И наконец, в Win32 предусмотрено еще два дополнительных типа данных, которые позволяют связывать элементы списков доступа с именованными признаками.
typedef struct _ACTRL_PROPERTY_ENTRYW {
LPWSTR lpProperty; // not used by COM
// не используется в COM
ACTRL_ACCESS_ENTRY_LISW *pAccessEntryList;
ULONG fListFlags; // not used by COM
// не используется в COM
} ACTRL_PROPERTY_ENTRYW, *PACTRL_PROPERTY_ENTRYW;
typedef struct _ACTRL_ALISTW {
ULONG cEntries;
[size_is(cEntries)]
ACTRL_PROPERTY_ENTRYW *pPropertyAccessList;
} ACTRL_ACCESSW, *PACTRL_ACCESSW;
Хотя в настоящее время COM не использует возможности контроля по каждому признаку, заключенному в этих двух типах данных, тип данных ACTRL_ACCESSW все же используется в интерфейсе IAccessControl для представления списков контроля доступа. Дело в том, что этот интерфейс широко используется также в службе директорий Windows NT 5.0, где требуется контроль доступа по каждому признаку.
В COM предусмотрена реализация интерфейса IAccessControl (CLSID_DCOMAccessControl), которую вызывающие программы могут заполнять явными именами учетных записей и правами доступа, используя типы данных контроля доступа NT 4.0[1]. Следующий фрагмент кода использует эту реализацию для создания объекта контроля доступа, разрешающего доступ для встроенной учетной записи SYSTEM и для пользователей в группе Sales\Managers, но запрещающего доступ для отдельного пользователя Sales\Bob:
HRESULT CreateAccessControl(IAccessControl * &rpac)
{
rpac = 0;
// create default access control object
// создаем объект контроля доступа по умолчанию
HRESULT hr = CoCreateInstance(CLSID_DCOMAccessControl,
0, CLSCTX_ALL, IID_IaccessControl,
(void**)&rpac);
if (SUCCEEDED(hr)) {
// build list of users/rights using NT4 security data types
// создаем списов пользователей/прав, используя типы данных защиты из NT4
ACTRL_ACCESS_ENTRYW rgaae[] = {
{ { 0, NO_MULTIPLE_TRUSTEE, TRUSTEE_IS_NAME,
TRUSTEE_IS_USER, L"Sales\\Bob" },
ACTRL_ACCESS_DENIED, COM_RIGHTS_EXECUTE, 0,
NO_INHERITANCE, 0 },
{ { 0, NO_MULTIPLE_TRUSTEE, TRUSTEE_IS_NAME,
TRUSTEE_IS_GROUP, L"Sales\\Managers" },
ACTRL_ACCESS_ALLOWED, COM_RIGHTS_EXECUTE, 0,
NO_INHERITANCE, 0 },
{ { 0, NO_MULTIPLE_TRUSTEE, TRUSTEE_IS_NAME,
TRUSTEE_IS_USER, L"NT AUTHORITY\\SYSTEM" },
ACTRL_ACCESS_ALLOWED, COM_RIGHTS_EXECUTE, 0,
NO_INHERITANCE, 0 }
};
ACTRL_ACCESS_ENTRY_LISTW aael =
{ sizeof(rgaae)/sizeof(*rgaae), rgaae };
ACTRL_PROPERTY_ENTRYW ape = { 0, &aael, 0 };
ACTRL_ACCESSW aa = { 1, &ape };
// present list of users+rights to Access Control object
// представляем список пользователей + прав объекту контроля доступа
hr = rpac->SetAccessRights(&aa);
}
return hr;
}
Имея эту функцию, приложение может связать вновь созданный объект контроля доступа с его процессом следующим образом:
IAccessControl *pac = 0;
HRESULT hr = CreateAccessControl(pac);
assert(SUCCEEDED(hr));
hr = CoInitializeSecurity(pac, -1, 0, 0,
RPC_C_AUTHN_LEVEL_PKT, RPC_C_IMP_LEVEL_IDENTIFY, 0,
EOAC_ACCESS_CONTROL,
// use IAccessControl
// используем IAccessControl
0);
assert(SUCCEEDED(hr));
pac->Release();
// COM holds reference until last CoUninitialize
// COM сохраняет ссылку до последнего CoUninitialize
Флаг EOAC_ACCESS_CONTROL показывает, что первый параметр в функции СоInitializeSecurity является указателем на интерфейс IAccessControl, а не указателем на SECURITY_DESCRIPTOR NT. При каждом поступающем запросе на связь COM будет использовать метод этого объекта IsAccessAllowed для определения того, разрешен или запрещен доступ к объектам процесса. Отметим, что хотя этот код должен исполняться до первого интересного вызова COM, вызов CoCreateInstance для получения реализации по умолчанию IAccessControl является допустимым, так как COM не рассматривает его как интересный.
Если список авторизованных пользователей не может быть известен во время запуска процесса, то можно зарегистрировать специальную (custom) реализацию IAccessControl, которая выполняет определенного рода проверку доступа во время выполнения в своей реализации метода IsAccessAllowed. Поскольку сама COM использует только метод IsAccessAllowed, то такая специальная реализация могла бы безошибочно возвращать E_NOTIMPL для всех других методов IAccessControl. Ниже приведена простая реализация IAccessControl, позволяющая получить доступ к объектам процесса только пользователям с символом "x" в именах своих учетных записей:
class XOnly : public IAccessControl {
// Unknown methods
// методы IUnknown
STDMETHODIMP QueryInterface(REFIID riid, void **ppv) {
if (riid == IID_IAccessControl || riid == IID_IUnknown)
*ppv = static_cast
else
return (*ppv = 0), E_NOINTERFACE;
((IUnknown*)*ppv)->AddRef();
return S_OK;
}
STDMETHODIMP_(ULONG) AddRef(void) { return 2; }
STDMETHODIMP_(ULONG) Release(void) { return 1; }
// IAccessControl methods
// методы IAccessControl
STDMETHODIMP GrantAccessRights(ACTRL_ACCESSW *)
{ return E_NOTIMPL; }
STDMETHODIMP SetAccessRights(ACTRL_ACCESSW *)
{ return E_NOTIMPL; }
STDMETHODIMP SetOwner(PTRUSTEEW, PTRUSTEEW)
{ return E_NOTIMPL; }
STDMETHODIMP RevokeAccessRights(LPWSTR, ULONG, TRUSTEEW[])
{ return E_NOTIMPL; }
STDMETHODIMP GetAllAccessRights(LPWSTR, PACTRL_ACCESSW_ALLOCATE_ALL_NODES *,
PTRUSTEEW *, PTRUSTEEW *)
{ return E_NOTIMPL; }
// this is the only IAccessControl method called by COM
// это единственный метод IAccessControl, вызванный COM
STDMETHODIMP IsAccessAllowed(
PTRUSTEEW pTrustee,
LPWSTR lpProperty,
ACCESS_RIGHTS AccessRights,
BOOL *pbIsAllowed)
{
// verify that trustee contains a string
// удостоверяемся, что опекун содержит строку
if (pTrustee == 0 || pTrustee->TrusteeForm != TRUSTEE_IS_NAME)
return E_UNEXPECTED;
// look for X or x and grant/deny based on presence
// ищем "X" или "x" и в зависимости от его наличия
// предоставляем или запрещаем
*pbIsAllowed = wcsstr(pTrustee->ptstrName, L"x") != 0 ||
wcsstr(pTrustee->ptstrName, L"X") != 0;
return S_OK;
}
}
Если экземпляр вышеприведенного класса C++ зарегистрирован c CoInitializeSecurity:
XOnly xo;
// declare an instance of the C++ class
// объявляем экземпляр класса C++
hr = CoInitializeSecurity(static_cast
–1, 0, 0, RPC_C_AUTHN_LEVEL_PKT,
RPC_C_IMP_LEVEL_IDENTIFY, 0,
EOAC_ACCESS_CONTROL,
// use IAccessControl
// используем IAccessControl
0);
assert(SUCCEEDED(hr));
то от пользователей, не имеющих "x" в именах своих учетных записей, никакие поступающие вызовы не будут приняты. Поскольку имя опекуна содержит в качестве префикса имя домена, этот простой тест также предоставит доступ учетным записям пользователей, принадлежащих к доменам, содержащим "x" в своих именах. Хотя этот тест доступа вряд ли будет слишком полезен, он демонстрирует технологию использования специального объекта IAccessControl с CoInitializeSecurity.
Под Windows NT каждый процесс имеет маркер доступа (access token), представляющий полномочия принципала защиты. Этот маркер доступа создается во время инициализации процесса и содержит различные виды информации о пользователе, в том числе его идентификатор защиты NT (SID), список групп, к которым принадлежит пользователь, а также список привилегий, которыми он обладает (например, может ли пользователь прекращать работу системы, может ли он менять значение системных часов). Когда процесс пытается получить доступ к ресурсам ядра безопасности (например, к файлам, ключам реестра, семафорам), контрольный монитор защиты NT (SRM – Security Reference Monitor) использует маркер вызывающей программы в целях аудита (отслеживания действий пользователей путем записи в журнал безопасности выбранных типов событий безопасности) и контроля доступа.
Когда в процесс поступает сообщение об ORPC-запросе, COM организует выполнение вызова соответствующего метода или в RPC-потоке (в случае объектов, расположенных в МТА), или в потоке, созданном пользователем (в случае объектов, расположенных в STA). В любом случае метод выполняется с использованием маркера доступа, соответствующего данному процессу. В целом этого достаточно, так как это позволяет разработчикам объекта прогнозировать, какие привилегии и права будут иметь их объекты, независимо от того, какой пользователь осуществляет запрос. В то же время иногда бывает полезно, чтобы метод выполнялся с использованием прав доступа клиента, вызывающего метод; чтобы можно было либо ограничить, либо усилить обычные права и привилегии объекта. Для поддержки такого стиля программирования в Windows NT допускается присвоение маркеров защиты отдельным потокам. Если поток имеет свой собственный маркер, контрольный монитор защиты не использует маркер процесса. Вместо него для выполнения аудита и контроля доступа используется маркер, присвоенный потоку. Хотя есть возможность программно создавать маркеры и присваивать их потокам, в COM предусмотрен гораздо более прямой механизм создания маркера на основе ORPC-запроса, обслуживаемого текущим потоком. Этот механизм раскрывается разработчикам объекта посредством контекстного объекта вызова, то есть вспомогательного объекта, который содержит информацию об операционном окружении серверного объекта.
Напоминаем, что контекстный объект вызова сопоставляется с потоком, когда ORPC-запрос направляется на интерфейсную заглушку. Разработчики объекта получают доступ к контексту вызова через API-функцию CoGetCallContext. Контекстный объект вызова реализует интерфейс IServerSecurity:
[local, object, uuid(0000013E-0000-0000-C000-000000000046)]
interface IServerSecurity : IUnknown {
// get caller's security settings
// получаем установки защиты вызывающей программы HRESULT
QueryBlanket(
[out] DWORD *pAuthnSvc, // authentication pkg
// модуль аутентификации
[out] DWORD *pAuthzSvc, // authorization pkg
// модуль авторизации
[out] OLECHAR **pServerName, // server principal
// серверный принципал
[out] DWORD *pAuthnLevel, // authentication level
// уровень аутентификации
[out] DWORD *pImpLevel, // impersonation level
// уровень заимствования прав
[out] void **pPrivs, // client principal
// клиентский принципал
[out] DWORD *pCaps // EOAC flags
// флаги EOAC
);
// start running with credentials of caller
// начинаем выполнение с полномочиями вызывающей программы
HRESULT ImpersonateClient(void);
// stop running with credentials of caller
// заканчиваем выполнение с полномочиями вызывающей программы
HRESULT RevertToSelf(void);
// test for impersonation
// проверка заимствования прав
BOOL IsImpersonating(void);
}
В одном из предыдущих разделов этой главы уже рассматривался метод QueryBlanket. Остальные три метода используются для управления маркерами потока во время выполнения метода. Метод ImpersonateClient создает маркер доступа, основанный на полномочиях клиента, и присваивает этот маркер текущему потоку. Как только возвращается IServerSecurity::ImpersonateClient, все попытки доступа к ресурсам операционной системы будут разрешаться или запрещаться в соответствии с полномочиями клиента, а не объекта. Метод RevertToSelf заставляет текущий процесс вернуться к использованию маркера доступа, принадлежащего процессу. Если текущий вызов метода заканчивает работу во время режима заимствования прав, то COM неявно вернет поток к использованию маркера процесса. И наконец, метод IServerSecurity::IsImpersonating показывает, что использует текущий поток: полномочия клиента или маркер процесса объекта. Подобно методу QueryBlanket, два метода IServerSecurity также имеют удобные оболочки, которые вызывают CoGetCallContext изнутри и затем вызывают соответствующий метод:
HRESULT CoImpersonateClient(void);
HRESULT CoRevertToSelf(void);
В общем случае, если будет использоваться более одного метода IServerSecurity, то эффективнее было бы вызвать CoGetCallContext один раз, а для вызова каждого метода использовать результирующий интерфейс IServerSecurity.
Следующий код демонстрирует использование контекстного объекта вызова для выполнения части кода метода с полномочиями клиента:
STDMETHODIMP MyClass::ReadWrite(DWORD dwNew, DWORD *pdw0ld)
{
// execute using server's token to let anyone read the value
// выполняем с использованием маркера сервера, чтобы
// все могли прочитать данное значение
ULONG cb;
HANDLE hfile = CreateFile(«C:\\file1.bin», GENERIC_READ,
0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hfile == INVALID_HANDLE_VALUE)
return MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, GetLastError());
ReadFile(hfile, pdwOld, sizeof(DWORD), &cb, 0);
CloseHandle(hfile);
// get call context object
// получаем контекстный объект вызова
IServerSecurlty *pss = 0;
HRESULT hr = CoGetCallContext(IID_IServerSecurity, (void**)&pss);
if (FAILED(hr)) return hr;
// set thread token to use caller's credentials
// устанавливаем маркер потока для использования
// полномочий вызывающей программы
hr = pss->ImpersonateClient();
assert(SUCCEEDED(hr));
// execute using client's token to let only users that can
// write to the file change the value
// выполняем с использованием маркера клиента, чтобы
// изменять это значение могли только те пользователи,
// которые имеют право записывать в файл
hfile = CreateFile(«C:\\file2.bin»,
GENERIC_READ | GENERIC_WRITE, 0, 0,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hfile == INVALID_HANDLE_VALUE)
hr = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_WIN32, GetLastError());
else {
WriteFile(hfile, &dwNew, sizeof(DWORD), &cb, 0);
CloseHandle(hfile);
}
// restore thread to use process-level token
// восстанавливаем режим использования потоком маркера процесса
pss->RevertToSelf();
// release call context
// освобождаем контекст вызова
pss->Release();
return hr;
}
Отметим, что первый вызов CreateFile выполняется с использованием полномочий процесса объекта, в то время как второй вызов – с полномочиями клиента. Если клиент имеет права доступа для чтения/записи в соответствующий файл, то второй вызов метода CreateFile может быть успешным, даже если обычно процесс объекта не имеет доступа к этому файлу.
Важно, что хотя методы IServerSecurity::ImpersonateClient всегда достигают цели, исключая катастрофический сбой, клиент объекта контролирует уровень заимствования прав, допускаемый результирующим маркером. Каждый интерфейсный заместитель имеет свой уровень заимствования прав, который должен быть равным одной из четырех констант (RPC_C_IMP_LEVEL_ANONYMOUS, RPC_C_IMP_LEVEL_IDENTIFY, RPC_C_IMP_LEVEL_IMPERSONATE или RPC_C_IMP_LEVEL_DELEGATE). Во время демаршалинга COM устанавливает этот уровень равным величине, определенной в клиентском вызове CoInitializeSecurity; однако данная установка может быть изменена вручную с помощью IClientSecurity::SetBlanket. Когда объект вызывает IServerSecurity::ImpersonateClient, новый маркер будет ограничен уровнем, заданном в интерфейсном заместителе, который использовался в данном вызове. Это означает, что если клиент задал только уровень RPC_C_IMP_LEVEL_IDENTIFY, то объект не может получить доступ к ресурсам ядра во время выполнения с полномочиями клиента. Объект, однако, может применить API-функции Win32 OpenThreadToken или GetTokenInformation для чтения информации о клиенте (например, ID защиты, групповое членство) из маркера режима анонимного воплощения (impersonation token). Важно отметить, что пока клиент не задал уровень RPC_C_IMP_LEVEL_DELEGATE, объект не может получить доступ ни к одному из удаленных ресурсов защиты, используя полномочия клиента. В их число входят открытие файлов в удаленной файловой системе, а также выполнение аутентифицированных COM-вызовов к удаленным объектам. К сожалению, протокол аутентификации NTLM не поддерживает уровень RPC_C_IMP_LEVEL_DELEGATE, так что под Windows NT 4.0 делегирование невозможно.
Во время предыдущего обсуждения акцент делался на том, что в нормальном режиме методы объекта выполняются с использованием маркера доступа процесса объекта. Однако не обсуждался вопрос о том, как проконтролировать, какой принципал защиты должен использоваться для создания начального маркера серверного процесса. Когда SCM запускает серверный процесс, то он присваивает новому серверному процессу маркер, основанный на конфигурации именованной величины RunAs из AppID. Если же в AppID нет величины RunAs, то считается, что сервер неправильно сконфигурирован для работы в режиме распределенного доступа. Для того чтобы этот тип серверного процесса не внедрял указанные «дыры» в защите в систему, SCM запускает такие процессы с использованием того принципала защиты, который произвел запрос на активацию. Такой тип активации часто называют активацией «как активизатор» («As Activator»), так как серверный процесс выполняет тот же принципал защиты, что и запускающий пользователь. Активация типа «как активизатор» предназначена для поддержки удаленной активации старых серверов и содержит несколько ловушек. Во-первых, чтобы придерживаться семантики типа «как активизатор», COM запустит отдельный серверный процесс для каждой активационной учетной записи пользователя, независимо от того, используется ли REGCLS_MULTIPLEUSE в CoRegisterClassObject. Это вступает в серьезный конфликт с принципом расширяемости и вдобавок делает невозможным сохранение всех экземпляров класса в одном и том же процессе. Во-вторых, каждый серверный процесс запускается с маркером, ограниченным уровнем RPC_C_IMP_LEVEL_IMPERSONATE, из чего следует, что серверные процессы не имеют доступа ни к каким удаленным ресурсам или объектам[1].
В идеале серверные процессы конфигурируются для запуска как отдельные принципалы защиты. Управлять этим можно, помещая именованную величину RunAs в имя учетной записи в AppID:
[HKCR\AppID\{27EE6A4D-DF65-11d0-8C5F-0080C73925BA}]
RunAs="DomainX\UserY"
Если эта именованная величина присутствует, SCM будет использовать указанное имя учетной записи для создания нового регистрационного маркера (login token) и присвоит этот маркер серверному процессу. Для правильной работы этой схемы требуются два условия. Во-первых, соответствующий пароль должен быть записан в определенном месте реестра в качестве ключа локальных средств защиты (LSA – Local Security Authority). Во-вторых, указанная учетная запись пользователя должна иметь полномочия «Вход в систему как пакетное задание» («Logon as a batch job»). При установке значения RunAs утилита DCOMCNFG.EXE обеспечивает выполнение обоих этих условий[2].
Для предотвращения спуфинга (spoofing, получение доступа путем обмана) классов злонамеренными программами CoRegisterClassObject проверяет, зарегистрирован ли AppID данного класса. Если AppID имеет установку RunAs, то COM гарантирует, что принципал вызывающей программы совпадает с именем принципала, записанным в реестре. Если же вызывающая программа не имеет указанной учетной записи RunAs для AppID класса, то вызов метода CoRegisterСlassObject будет отклонен и возвратится известный HRESULT CO_E_WRONG_SERVER_IDENTITY. Поскольку конфигурационные установки COM записаны в защищенной части реестра, только привилегированные пользователи могут изменять список соответствия классов и пользователей.
Важно отметить, что когда в AppID имеется явная учетная запись пользователя RunAs, то SCM всегда будет запускать серверный процесс в его собственной отдельной window-станции (window station)[3]. Это означает, что серверный процесс не может с легкостью ни создавать окна, видимые для интерактивного пользователя на данной машине, ни принимать информацию с клавиатуры, от мыши или из буфера (clipboard). Вообще говоря, такая защита полезна, поскольку не дает простым (naive) серверам COM влиять на деятельность пользователя, работающего на машине[4]. К сожалению, иногда серверному процессу бывает необходимо связаться с авторизовавшимся (logged on) в данный момент пользователем. Одним из способов достижения этого является использование для управления window-станциями и рабочими столами (desktop) явных API-функций COM, что дает потоку возможность временно выполняться на интерактивном рабочем столе. При выполнении на интерактивном рабочем столе любые окна, которые создает поток, будут видимы интерактивному пользователю, и, кроме того, поток может получать аппаратные сообщения (hardware messages) от клавиатуры и мыши. Если же все, что нужно, – это получить от пользователя ответ типа да/нет, то на этот случай в API-функции Win32 MessageBox имеется флаг MB_SERVICE_NOTIFICATION, при выставлении которого, без какого-либо добавочного кода, на интерактивном рабочем столе появится окно сообщения.
Если требуется расширенное взаимодействие с интерактивным пользователем, то использование Win32 API window-станции может стать весьма громоздким. Лучшим подходом могло бы стать выделение компонентов пользовательского интерфейса во второй внепроцессный сервер, который сможет работать на window-станции, отличной от той, на который запущена основная иерархия объектов. Чтобы заставить серверный процесс, содержащий компоненты пользовательского интерфейса, работать при интерактивной пользовательской window-станции, COM распознает характерное значение RunAs «Interactive User» («Интерактивный пользователь»):
[HKCR\AppID\{27EE6A4D-DF65-11d0-8C5F-0080C73925BA}]
RunAs="Interactive User"
При использовании этого значения COM запускает новый серверный процесс в window-станции, соответствующей подсоединенному в текущий момент пользователю. Для запроса полномочий для нового серверного процесса COM при создании этого нового серверного процесса просто копирует маркер текущего интерактивного сеанса. Это означает, что в реестр не требуется записывать никаких паролей. К сожалению, и этот режим активации не обходится без ловушек. Во-первых, если активационный запрос поступает в момент, когда на хост-машине не зарегистрировано ни одного пользователя, то активационный запрос даст сбой с результатом E_ACCESSDENIED. Кроме того, если интерактивный пользователь выйдет из сети в тот момент, когда у серверного процесса еще есть подключенные клиенты, то серверный процесс будет преждевременно прерван, что приведет к грубому отсоединению всех существующих в тот момент заместителей. И наконец, часто невозможно предсказать, какой пользователь будет подсоединен во время активации, что усложняет обеспечение достаточных прав и привилегий доступа ко всем необходимым ресурсам для данного объекта. Эти ограничения сводят применимость такого режима активации к простым компонентам пользовательского интерфейса[5].
Одна интересная разновидность управления маркером и window-станцией серверного процесса относится к службам NT. Напомним, что наличие именованной величины LocalService заставляет SCM использовать для запуска серверного процесса NT Service Control Manager вместо CreateProcess или CreateProcessAsUser. При запуске серверных процессов как сервисов NT COM не контролирует, с каким принципалом запускается этот процесс просто потому, что это жестко запрограммировано в конфигурации соответствующей запущенной службы NT. В этом случае COM игнорирует именованную величину RunAs, чтобы убедиться, что случайные процессы не могут имитировать вызовы CoRegisterClassObject. Наличие именованной величины LocalService требует, чтобы вызывающая программа выполнялась как сервис NT. Если сам этот сервис сконфигурирован на запуск как встроенная учетная запись SYSTEM, то серверный процесс либо запустит интерактивную window– станцию, либо будет запущена заранее определенная window-станция, совместно используемая всеми сервисами NT в качестве SYSTEM (это зависит от того, как именно сконфигурирован сервис NT). Если вместо этого сервис NT сконфигурирован для выполнения как отдельная учетная запись пользователя, то NT Service Control Manager будет всегда запускать сервис NT под новой window– станцией, специфической для данного серверного процесса.
Одно общее соображение в пользу реализации сервера COM как сервиса NT заключается в том, что только сервисы NT способны выполняться со встроенной учетной записью SYSTEM. Эта учетная запись обыкновенно имеет больший доступ к таким локальным ресурсам, как файлы и ключи реестра. Кроме того, эта учетная запись часто является единственной, которая может выступать как часть доверительной компьютерной базы (trusted computing base) и использовать низкоуровневые службы защиты, доступ к которым был бы опасен из обычных пользовательских учетных записей. К сожалению, хотя учетная запись SYSTEM воистину всемогуща в локальной системе, она полностью бессильна для доступа к защищенным удаленным ресурсам, в том числе к удаленным файловым системам и к удаленным объектам COM. Это обстоятельство делает учетную запись SYSTEM отчасти менее полезной для построения распределенных систем, чем можно было бы ожидать. Вне зависимости от того, используется ли сервер как сервис NT или в качестве традиционного процесса Win32, принято создавать отдельную учетную запись пользователя для каждого приложения COM, которое имеет полные полномочия для доступа в сеть.
В данной главе рассматривались вопросы, относящиеся к выделению классов в отдельные серверные процессы. COM поддерживает запуск серверных процессов на основе запросов на активацию. Эти серверные процессы должны саморегистрироваться с помощью библиотеки COM, используя CoRegisterClassObject для того, чтобы обеспечить доступ к объектам своего класса со стороны внешних клиентов. Архитектура системы безопасности COM тесно связана с собственной моделью безопасности операционной системы и основывается на трех различных понятиях. Целостность и аутентичность сообщений ORPC, которыми обмениваются клиент и объект, обеспечивается аутентификацией. Контроль доступа выявляет, какие принципалы защиты могут иметь доступ к объектам, экспортированным из данного процесса. Управление маркерами отслеживает, какие полномочия используются для запуска серверных процессов и выполнения методов объекта.