Алексей Остапенко
ВведениеВ этой статье рассматривается технология, позволяющая перехватывать вызовы методов интерфейса IUnknown COM-объекта. Кроме исследовательских целей, эта технология может иметь и практическое применение. Она позволяет осуществлять такие полезные действия, как почти прозрачная подмена контекста пользователя, “под которым” производятся вызовы методов удаленного объекта, “агрегирование” удаленных объектов, агрегирование объектов, не поддерживающих агрегацию и т.п. С исследовательской точки зрения перехват вызовов IUnknown позволяет заглянуть во внутренности взаимодействия приложения и используемых им COM-объектов. Например, отлаживая приведенный в статье пример, я обнаружил, что вызов функции CreateObject скриптового рантайма приводит к запросу четырех (!) интерфейсов вместо одного у создаваемого объекта. :)
Немного теорииИнтерфейс IUnknown является основополагающим элементом COM. Он имеет 3 метода, управляющих доступом к другим интерфейсам объекта:
QueryInterface.
AddRef.
Release.
Перехватив вызовы методов IUnknown, можно управлять набором интерфейсов, предоставляемых объектом “наружу” (например, можно спрятать некоторые из них или добавить свои интерфейсы, сделав вид, что они тоже предоставляются объектом), а также управлять некоторыми параметрами вызова методов интерфейсов (например, proxy blanket’ом). Любой другой интерфейс, наследуемый от IUnknown, соответственно, наследует и эти три метода.
Работа с любым интерфейсом осуществляется через указатель на этот интерфейс. Физически указатель на интерфейс – это указатель на переменную, которая, в свою очередь, указывает на таблицу указателей на методы этого интерфейса (VTBL, см. рисунок 1).
Рисунок 1.
Несколько интерфейсов могут ссылаться как на одну и ту же VTBL, так и на разные – для клиента это не имеет значения. Кроме того, физически разные экземпляры COM-класса имеют разные указатели на интерфейсы (так как из них при вызове выводится this, см. ниже), но могут иметь (а объекты, реализованные с помощью ATL - имеют) одни и те же VTBL.
Интерфейс может использоваться клиентом напрямую, если COM-объект создавался внутри процесса (in-proc) клиента и в том же апартаменте (apartment), из которого происходит вызов. В противном случае (внепроцессный (out-of-proc) объект или другой апартамент) клиент вместо реального интерфейса будет использовать его прокси. Однако в обоих случаях указатель на интерфейс ссылается на указатель на VTBL, содержащую указатели на методы. Таким образом, в обоих случаях VTBL интерфейса (или его прокси) напрямую доступна в процессе клиента для чтения, и может быть сделана доступной для записи.
ПРИМЕЧАНИЕ Компилятор C++ может разместить VTBL в константном сегменте данных, который может быть загружен в страницу памяти с атрибутами защиты “только для чтения” (PAGE_READONLY) (за это замечание отдельное спасибо Николаю Меркину и Алексею Ширшову). В этом случае просто так писать в VTBL не получится. Но, поскольку VTBL находится в адресном пространстве процесса, можно поменять атрибуты защиты страницы на “для чтения и записи” (PAGE_READWRITE) с помощью функции VirtualProtect. |
Кроме этих кратких сведений об устройстве указателей на интерфейс нам понадобится понимание того, как именно происходит вызов и передача параметров в метод интерфейса, реализованного как метод C++ класса, унаследованного от интерфейса:
Клиент вызывает метод QueryInterface:
pUnknown->QueryInterface(IID_IDispatch, reinterpret_cast<void**>(&pDispatch)); |
Этот вызов транслируется примерно в такой:
((VTBL*)pUnknown)->vtbl->QueryInterface(pUnknown, IID_IDispatch, reinterpret_cast<void**>(&pDispatch)); |
т.е. указатель на интерфейс передается первым параметром для метода. Формат вызова фактически соответствует формату вызова нестатического метода класса в конвенции _stdcall.
В начале метода указатель на интерфейс заменяется указателем на this (на самом деле через VTBL вызывается не сам метод, а сгенерированный компилятором переходник, который корректирует указатель на интерфейс и передает управление собственно методу).
Принцип перехвата
Для перехвата IUnknown потребуется подменить указатели на методы QueryInterface, AddRef и Release в VTBL всех интерфейсов COM-объекта указателями на нашу реализацию этих методов. Другими словами, необходимо установить хук на вызовы этих методов. Методы-перехватчики не могут быть нестатическими методами класса, так как передаваемый в метод указатель на интерфейс никак не связан с this объекта-перехватчика. Кроме того, по полученному указателю на интерфейс нужно уметь определять указатели на оригинальные методы QueryInterface, AddRef и Release перехваченного интерфейса. Эту задачу можно легко решить с помощью статического экземпляра контейнера std::map (или hash_map), позволяющего по указателю на VTBL получить структуру (или класс), содержащую указатели на оригинальные реализации QueryInterface и т.п.
Наша реализация QueryInterface, помимо делегирования вызовов оригинальному QueryInterface, должна также осуществлять перехват запрашиваемых интерфейсов (если они не были перехвачены ранее) и добавлять соответствующие пары в map. AddRef и Release должны заниматься дополнительным подсчетом ссылок, чтобы отследить момент, когда необходимо снять с интерфейса ранее установленный хук.
Реализация перехвата
Большая часть действий по перехвату возложена на вспомогательный класс HookEntry. Изначальный перехват интерфейса осуществляется в статическом методе HookInteface:
Метод HookInterface
RPC_AUTH_IDENTITY_HANDLE HookEntry::HookInterface(IUnknown* pItf, const CredentialsHolder::CredentialsPtr& pCredentials) { { void* tmp = *reinterpret_cast<void**>(pItf); CComCritSecLock<CComAutoCriticalSection> lock(m_csHooks); HookMap::iterator it = m_hooks.lower_bound(tmp); if (it == m_hooks.end() || (*it).first != tmp) //а нет ли уже такого хука? { // ставим новый хук // по хорошему, тут бы стоило еще проверять, не вернул ли new 0 HookPtr pNewHook(new HookEntry()); // if (pNewHook->Hook(pItf)) { m_hooks.insert(it, HookMap::value_type(tmp, pNewHook)); //предотвращаем преждевременную выгрузку dll if (++m_totalRefCount == 1) _Module.Lock(); } else return NULL; } else (*it).second->AddRef(); //хук уже есть. просто добавляем ссылку } //lock.Unlock(); //добавляем или достаем credentials CComCritSecLock<CComAutoCriticalSection> lock2(m_csCredentials); CredentialsMap::iterator itc = m_credentials.lower_bound(pItf); //такого элемента еще нет – придется добавить if (itc == m_credentials.end() || (*itc).first != pItf) { m_credentials.insert(itc, CredentialsMap::value_type(pItf, pCredentials)); return pCredentials->GetCredentials(); } else return (*itc).second->GetCredentials(); } |
В дальнейшем этот же метод вызывается из перехватчика QueryInterface:
Метод QueryInterfaceHook
//хук QueryInterface STDMETHODIMP HookEntry::QueryInterfaceHook(void* pItf, REFIID iid, void** ppvObject) { //добываем хелпер-объект HookEntry* pHook = HookFromItf(reinterpret_cast<IUnknown*>(pItf)); if (pHook == NULL) //хук уже кто-то снял :( return ((IUnknown*)pItf)->QueryInterface(iid, ppvObject); //IClientSecurity должен проходить мимо if (::InlineIsEqualGUID(iid, IID_IClientSecurity)) return pHook->m_oldQI(pItf, iid, ppvObject); //собственно QueryInterface CComPtr<IUnknown> spUnknown; HRESULT hr = pHook->m_oldQI(pItf, iid, (void**)&spUnknown.p); if (FAILED(hr)) return hr; //добываем пары «логин-пароль» CredentialsHolder::CredentialsPtr* pCredentials = CredentialsFromItf(reinterpret_cast<IUnknown*>(pItf)); if (pCredentials != NULL) { // устанавливаем хук на интерфейс RPC_AUTH_IDENTITY_HANDLE ident = HookInterface(spUnknown.p, *pCredentials); if (ident) { //и устанавливаем на интерфейс proxy blanket hr = ::CoSetProxyBlanket(spUnknown.p, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, ident, EOAC_NONE); if (FAILED(hr) && hr != E_NOINTERFACE) return hr; } } *ppvObject = reinterpret_cast<void*>(spUnknown.Detach()); return S_OK; } |
Подсчет ссылок для подправленных VTBL реализован непосредственно в классе HookEntry.
ПРЕДУПРЕЖДЕНИЕ Первый вызов HookInterface должен быть произведен сразу после получения первого указателя на любой интерфейс COM-объекта. В противном случае в дальнейшем может возникнуть ситуация, когда хук будет снят, но объект все еще будет жить. |
Полезная нагрузка
В качестве полезной нагрузки рассматриваемый пример осуществляет подмену контекста пользователя. Таким образом, пример раcсчитан на работу с удаленными COM-объектами (CLSCTX_REMOTE_SERVER). Контекст пользователя подменяется при помощи вызова CoSetProxyBlanket с указанием нового RPC_AUTH_IDENTITY_HANDLE.
ПРИМЕЧАНИЕ Довольно интересный момент - в MSDN практически отсутствует информация о том, как правильно создавать RPC_AUTH_IDENTITY_HANDLE. Написано, что при определенных условиях он является просто указателем на структуру SEC_WINNT_AUTH_IDENTITY(_EX). Однако эксперименты показали, что простое создание такой структуры и подсовывание указателя на нее в CoSetProxyBlanket не приводит ни к чему, кроме ошибки (в то же время с CoCreateInstanseEx такой фокус проходит). Опытным путем было установлено, что правильную структуру можно создать вызовом DsMakePasswordCredentials. Увы, этот API специфичен для ОС Win2k и далее, и пример не будет работать под NT 4. |
В итоге, для “борьбы” с RPC_AUTH_IDENTITY_HANDLE был создан отдельный хелпер-класс CredentialsHolder. Для хранения соответствия credentials конкретным интерфейсам используется еще один контейнер std::map. Стоит отдельно заметить, что в контейнере хранятся не экземпляры классов CredentialsHolder, а “умные” (smart) указатели shared_ptr, реализующие подсчет ссылок. Это сделано для предотвращения необходимости копирования экземпляров CredentialsHolder, для которых операция копирования не только накладна, но и попросту не очевидна.
Использование
В файле с проектом содержится демонстрационный скрипт test.js, показывающий, как можно подменять пользовательский контекст при создании и использовании удаленного объекта. Метод CreateObject позволяет инстанциировать удаленный объект и вернуть указатель на его главный интерфейс IDispatch. Метод HookObject позволяет перехватить ранее полученный интерфейс IDispatch.
ПРИМЕЧАНИЕ В теории HookObject может давать не совсем те результаты, на которые хочется расчитывать. Это связано с тем, что в данном случае невозможно точно отследить момент освобождения интерфейса, т.к. на него (а возможно, что и на другие интерфейсы объекта) уже имеются ссылки, не учтенные до перехвата. Однако, при практическом использовании данного механизма внутри ASP-скриптов, преждевременного снятия хуков не наблюдалось. |
С помощью описанного механизма перехвата можно делать и другие забавные трюки. Например, агрегировать объекты, не поддерживающие агрегацию. В самом деле, раз мы имеем полный контроль над вызовами IUnknown, ничто не мешает полностью заменить его реализацию так, что она будет в точности эмулировать поведение IUnknown агрегированного объекта.
Список литературыMSDN
Введение в COM
Защита в DCOM/C
Похожие работы
... ATL COM-сервера в окне “Output” появятся сведения об указателях на интерфейс, для которых счетчик ссылок не достиг значения 0, т.е. об утечках COM объектов. “Магия” ATL работает благодаря перехвату вызовов методов COM-интерфейсов, в частности, AddRef, Release и QueryInterface. Когда клиент запрашивает интерфейс у объекта с помощью QueryInterface, класс CComObject делегирует вызов базовому классу ...
... по соответствующему полю). В окне Конструктора таблиц созданные связи отображаются визуально, их легко изменить, установить новые, удалить (клавиша Del). 1 Многозвенные информационные системы. Модель распределённого приложения БД называется многозвенной и её наиболее простой вариант – трёхзвенное распределённое приложение. Тремя частями такого приложения являются: ...
... 6.0. – Microsoft Press, 1998. – 260 c. ISBN 1-57231-961-5 ТУЛЬСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ На правах рукописи Карпов Андрей Николаевич ЗАЩИТА ИНФОРМАЦИИ В СИСТЕМАХ ДИСТАНЦИОННОГО ОБУЧЕНИЯ С МОНОПОЛЬНЫМ ДОСТУПОМ Направление 553000 - Системный анализ и управление Программная подготовка 553005 – Системный анализ данных и моделей принятия решений АВТОРЕФЕРАТ диссертации на соискание степени ...
... разработку ПО. Большинство пространств имен FCL предоставляет типы, которые можно задействовать в любых видах приложений [1]. 4. Новые возможности платформы .NETFramework 4.0 В 2010 году компанией Microsoft была выпущена платформа NET Framework 4.0. Эта платформа содержит ряд усовершенствований и нововведений. Список некоторых из них представлен ниже: - Среда DLR. Среда DLR представляет ...
0 комментариев