Хуки и DLL

Хуки
и
DLL


Dr. Joseph M. Newcomer


Перевод: Алексей Остапенко



Существует большая неразбериха по поводу установки и использования глобальных хуков.




ПРИМЕЧАНИЕ


Возможно стоит упомянуть, что Камбалы (прим. переводчика: Flounder - псевдоним автора) в целом не одобряют использование крючков (hooks), но такие крючки (хуки) кажутся допустимыми.



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


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


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


Концепция разделенных адресных пространств трудна для понимания. Позвольте мне продемонстрировать ее на картинке.



Здесь мы имеем три процесса. Ваш Процесс показан слева (Your Process). У DLL есть сегменты кода (Code), данных (Data) и разделяемый сегмент (Shared), как его создать мы обсудим позже. Теперь, если перехватывающая DLL вызывается для перехвата события в Процессе A (Process A), она отображается в адресное пространство Процесса A, как указано. Код является разделяемым, поэтому адреса в Процессе A ссылаются на те же страницы памяти, что и адреса в Вашем Процессе. По совпадению страницы памяти оказались отображенными в Процесс A по тем же самым виртуальным адресам, т.е. адресам, которые видит Процесс A. Процесс A также получает свою собственную копию сегмента данных, поэтому все что видит Процесс A в секции "Data", полностью принадлежит ему и не может повлиять на любой другой процесс (или быть измененным любым другим процессом!). Однако, фокус который заставляет все это работать заключается в разделяемом сегменте данных, показанном здесь красным цветом. Страницы, адресуемые Вашим Процессом в точности те же страницы памяти, что и адресуемые в Процессе A. Заметим, что по совпадению эти страницы оказались в адресном пространстве Процесса A в точности на тех же виртуальных адресах, что и в Вашем Процессе. Если бы вы сидели за отладкой Вашего Процесса и Процесса A одновременно (что вы можете делать, запустив две копии VC++!) и смотрели бы по адресу &something, находившемуся в разделяемом сегменте данных, и смотрели бы по нему в Вашем Процессе и затем по тому же адресу &something в Процессе A, вы бы увидели в точности одни и те же данные и даже по тому же самому адресу. Если бы вы использовали отладчик для изменения или отслеживали изменения программой значения something, то вы могли бы перейти к другому процессу, исследовать его и увидеть, что новое значение появилось также и здесь.


Но вот облом: одинаковый адрес - это совпадение. Это совпадение абсолютно и однозначно не гарантируется. Посмотрите на Процесс B. Когда событие перехвачено в Процессе B, в него отображается DLL. Но адреса, занимаемые ею в Вашем Процессе и Процессе A, не доступны в адресном пространстве Процесса B. Поэтому происходит перемещение кода на другой адрес в Процессе B. Код в порядке; ему действительно безразлично по какому адресу он исполняется. Адреса данных подправлены так, чтобы ссылаться на новое положение данных, и даже разделяемые данные отображены в другое множество адресов, таким образом к ним обращаются по-другому. Если бы вы использовали отладчик с Процессом B и посмотрели бы на адрес &something в разделяемой области, вы бы обнаружили, что адрес something был бы другим, но содержимое something было бы тем же самым; выполнение изменения содержимого в Вашем Процессе или в Процессе A немедленно сделало бы это изменение видимым в Процессе B, хотя Процесс B и видит его по другому адресу. Это то же самое место физической памяти. Виртуальная память - это отображение между адресами, видимыми вами, как программистом, и физическими страницами памяти, которые в действительности содержит ваш компьютер.


Хотя я и назвал одинаковое расположение совпадением, "совпадение" частично умышленно; Windows пытается отображать библиотеки в те же самые виртуальные области, что и у других экземпляров одной и той же библиотеки, всякий раз, когда это возможно. Она пытается. Ей может не удаться это сделать.




ПРИМЕЧАНИЕ


Если вы знаете немного больше (достаточно, чтобы это представляло опасность), вы можете сказать: "Ага! Я могу переместить (rebase) мою DLL так, что она загружается по не конфликтующему адресу, и я смогу проигнорировать эту особенность". Это отличный пример того, как малые знания могут представлять серьезную опасность. Вы не можете гарантировать что такая DLL будет работать с любым возможным исполняемым модулем, который может быть когда-либо запущен на вашей машине! Поскольку это DLL глобального хука, она может быть вызвана из Word, Excel, Visio, VC++ и шести тысяч приложений, о которых вы никогда не слышали, но можете когда-либо запустить или может запустить ваш клиент. Поэтому забудьте об этом. Не пытайтесь перемещать. В конце концов, вы проиграете. Обычно, в самое неподходящее время с самым важным вашим клиентом (например, с обозревателем вашего продукта из журнала или с вашим очень богатым заказчиком, который уже обеспокоен другими ошибками, которые у вас могут быть...). Считайте, что разделяемый сегмент данных "перемещаем". Если вы не понимаете этот параграф, значит вы знаете недостаточно много, чтобы это представляло опасность. И вы можете спокойно его проигнорировать.



У перемещения есть и другие последствия. Если в DLL вы сохранили указатель на callback-функцию в контексте Вашего Процесса, то для DLL бессмысленно вызывать ее в Процессе A или Процессе B. Этот адрес приведет к передаче управления в указываемую им область, что нормально, но эта передача произойдет в адресное пространство Процесса A или Процесса B, что совершенно бесполезно, не говоря уже о том, что почти наверняка фатально.


Это также означает, что вы не можете использовать в своей DLL ничего из MFC. Она не может быть ни MFC DLL, ни MFC Extension DLL. Почему? Потому, что она будет вызывать функции MFC. А где они? Ну, они в вашем адресном пространстве. А не в адресном пространстве Процесса A, написанного на Visual Basic, или Процесса B, написанного на Java. Таким образом, вы должны написать DLL на чистом C, и я бы рекомендовал совсем не использовать библиотеку времени исполнения C (CRT). Вы должны использовать только API. Используйте lstrcpy вместо strcpy или tcscpy, lstrcmp вместо strcmp или tcscmp, и т.д.


Существует множество решений для организации взаимодействия вашей DLL и ее управляющего сервера. Одно из решений заключается в использовании ::PostMessage или ::SendMessage (заметим, что здесь я ссылаюсь на вызовы чистого API, а не вызовы MFC!). Там, где возможно использовать вызов ::PostMessage, лучше используйте его, а не ::SendMessage, т.к. иначе вы можете получить опасные тупиковые ситуации. Если Ваш Процесс в итоге останавливается, все остальные процессы в системе остановятся, т.к. все заблокированы на вызове ::SendMessage, который никогда не возвратится, и вы просто вывели всю систему из строя с возможностью серьезной потери данных в важных для пользователя приложениях. Это Совершенно Однозначно Не Хорошая Ситуация.


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


Вы не можете возвратить указатель из вызовов ::SendMessage и ::PostMessage (мы забудем про возможность передавать обратно относительные указатели в разделяемую область памяти; это также выходит за рамки этой статьи). Это из-за того, что любой указатель, который вы можете создать, будет ссылаться либо на адрес в DLL (перемещенной в перехваченный процесс), либо на адрес в перехваченном процессе (Процессе A или Процессе B), и, следовательно, он будет абсолютно бесполезен в Вашем Процессе. Вы можете возвращать лишь адресно-независимую информацию в WPARAM или LPARAM.


Я сильно рекомендую использовать для таких целей Зарегистрированные Оконные Сообщения (смотрите мой обзор по Управлению Сообщениями ). Вы можете использовать макрос ON_REGISTERED_MESSAGE в MESSAGE_MAP окна, которому вы отсылаете сообщение.


Основным требованием теперь является получение HWND этого окна. К счастью, это несложно.


Первое, что вы должны сделать - это создать разделяемый сегмент данных. Это делается при помощи объявления #pragma data_seg. Выберите какое-либо хорошее мнемоническое имя для сегмента данных (оно должно быть не длиннее 8 символов). Просто чтобы подчеркнуть произвольность имени, я использовал здесь свое собственное имя. Во время преподавания я обнаружил, что если я использую имена вида .SHARE или .SHR, или .SHRDATA, то студенты полагают, что имя имеет значение. А оно не имеет значения.




#pragma data_seg(".JOE")


>

HANDLE hWnd = NULL;


#pragma dta_seg()


#pragma comment(linker, "/section:.JOE,rws")



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




ПРИМЕЧАНИЕ


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



Директива #pragma comment вызывает добавление указанного ключа к командной строке компоновщика на этапе связывания. Вы могли бы использовать Project | Settings в VC++ и изменить командную строку компоновщика, однако трудно помнить про необходимость такого действия, когда вы перемещаете код с места на место (и обычная ошибка - забыть выбрать All Configurations при изменении установок и, таким образом, успешно отлаживать, но получить сбой в конфигурации Release). Итак, я обнаружил, что лучше всего помещать команду непосредственно в исходном файле. Заметим, что используемый текст должен соответствовать синтаксису командного ключа компоновщика. Это означает, что вы не должны включать в указанный текст пробелы, иначе компоновщик не обработает его должным образом.


Обычно вы предоставляете некоторый механизм для установки дескриптора окна. Например,




void SetWindow(HWND w)


{


hWnd = w;


}



хотя эта операция, как я покажу далее, часто совмещена с собственно установкой хука.


Пример: Мышиный Хук


заголовочный файл (myhook.h)


Здесь должны быть объявлены функции setMyHook и clearMyHook, но это требование разъяснено в моем очерке The Ultimate DLL Header File.




#define UWM_MOUSEHOOK_MSG


_T("UMW_MOUSEHOOK-"


"{B30856F0-D3DD-11d4-A00B-006067718D04}")



исходный файл (myhook.cpp)




#include "stdafx.h"


#include "myhook.h"


#pragma data_seg(".JOE")


HWND hWndServer = NULL;


#pragma data_seg()


#pragma comment("linker, /section:.JOE,rws")


HINSTANCE hInstance;


UINT HWM_MOUSEHOOK;


HHOOK hook;


// опережающееобъявление


static LRESULT CALLBACK msghook(int nCode, WPARAM wParam, LPARAM lParam);


/****************************************************************


* DllMain


* Вход:


* HINSTANCE hInst: Дескрипторэкземпляра DLL


* DWORD Reason: причинавызова


* LPVOID reserved: зарезервировано


* Выход: BOOL


* TRUE при успешном завершении


* FALSE при наличии ошибок (не возвращается никогда)


* Действие:


* инициализация DLL.


****************************************************************/


BOOL DllMain(HINSTANCE hInst, DWORD Reason, LPVOID reserved)


{


switch(Reason)


{ /* причина */


//**********************************************


// PROCESS_ATTACH


//**********************************************


case DLL_PROCESS_ATTACH:


// Сохраним дескриптор экземпляра, т.к. он понадобится нам позднее для установки хука


hInstance = hInst;


// Данный код инициализирует сообщение уведомления хука


UWM_MOUSEHOOK = RegisterWindowMessage(UWM_MOUSEHOOK_MSG);


return TRUE;


//**********************************************


// PROCESS_DETACH


//**********************************************


case DLL_PROCESS_DETACH:


// Если сервер не снял хук, снимем его, т.к. мы выгружаемся


if(hWndServer != NULL)


clearMyHook(hWndServer);


return TRUE;


} /* причина */


}


/****************************************************************


* setMyHook


* Вход:


* HWND hWnd: Окно, чей хук предстоит поставить


* Выход: BOOL


* TRUE если хук успешно поставлен


* FALSE если произошла ошибка, например, если хук


* уже был установлен


* Действие:


* Устанавливает хук для указанного окна


* Сначала устанавливает хук перехватывающий сообщения (WH_GETMESSAGE)


* Если установка прошла успешно, hWnd устанавливается в качестве


* окнасервера.


****************************************************************/


__declspec(dllexport) BOOL WINAPI setMyHook(HWND hWnd)


{


if(hWndServer != NULL)


return FALSE;


hook = SetWindowsHookEx(


WH_GETMESSAGE,


(HOOKPROC)msghook,


hInstance,


0);


if(hook != NULL)


{ /* удача */


hWndServer = hWnd;


return TRUE;


} /* удача */


return FALSE;


} // SetMyHook


/****************************************************************


* clearMyHook


* Вход:


* HWND hWnd: Окно, чей хук должен быть снят


* Выход: BOOL


* TRUE если хук успешно снят


* FALSE если вы передали неверный параметр


* Действие:


* Снимает установленный хук.


****************************************************************/


__declspec(dllexport) BOOL clearMyHook(HWND hWnd)


{


if(hWnd != hWndServer)


return FALSE;


BOOL unhooked = UnhookWindowsHookEx(hook);


if(unhooked)


hWndServer = NULL;


return unhooked;


}


/****************************************************************


* msghook


* Вход:


* int nCode: Значениекода


* WPARAM wParam: параметр


* LPARAM lParam: параметр


* Выход: LRESULT


*


* Действие:


* Если сообщение является сообщением о перемещении мыши, отправляет его


* окну сервера с координатами мыши


* Замечания:


* Функция должна быть CALLBACK-функцией, или она не будет работать!


****************************************************************/


static LRESULT CALLBACK msghook(int nCode, WPARAM wParam, LPARAM lParam)


{


// If the value of nCode is < 0, just pass it on and return 0


// this is required by the specification of hook handlers


// Если значение nCode < 0, просто передаем его дальше и возвращаем 0


// этого требует спецификация обработчиков хуков


if(nCode < 0)


{ /* передаем дальше */


CallNextHookEx(hook, nCode,


wParam, lParam);


return 0;


} /* передаем дальше */


// Прочитайте документацию, чтобы выяснить смысл параметров WPARAM и LPARAM


// Для хука WH_MESSAGE, LPARAM определяется как указатель на структуру MSG,


// таким образом следующий код делает эту структуру доступной


LPMSG msg = (LPMSG)lParam;


// Если это сообщение о перемещении мыши, либо в клиентской (client), либо


// в не клиентской (non-client) области, мы хотим уведомить родителя о его


// возникновении. Заметим, что вместо SendMessage используется PostMessage


if(msg->message == WM_MOUSEMOVE ||


msg->message == WM_NCMOUSEMOVE)


PostMessage(hWndServer,


UWM_MOUSEMOVE,


0, 0);


// Передаем сообщение следующему хуку


return CallNextHookEx(hook, nCode,


wParam, lParam);


} // msghook



Приложение сервера


В заголовочном файле добавьте следующее в секцию protected класса:




afx_msg LRESULT OnMyMouseMove(WPARAM,LPARAM);

В фале приложения добавьте это где-нибудь в начале файла:




UINT UWM_MOUSEMOVE = ::RegisterWindowMessage(UWM_MOUSEMOVE_MSG);

Добавьте следующее в MESSAGE_MAP вне специальных комментариев //{AFX_MSG:




ON_REGISTERED_MESSAGE(UWM_MOUSEMOVE, OnMyMouseMove)

В файл приложения добавьте следующую функцию:




LRESULT CMyClass::OnMyMouseMove(WPARAM, LPARAM)


{


// ...тут что-то делаем


return 0;


}




Я написал небольшой пример для демонстрации, но поскольку я утомился создавать функцию глобального хука в n+1 раз, я сделал ему отличный пользовательский интерфейс. Кот смотрит из окна и следит за мышью. Но будьте осторожны! Подойдите достаточно близко к коту, и он схватит мышь!


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


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

Сохранить в соц. сетях:
Обсуждение:
comments powered by Disqus

Название реферата: Хуки и DLL

Слов:2550
Символов:21230
Размер:41.46 Кб.