Сообщений 0    Оценка 115        Оценить  
Система Orphus

Создание инструментария для Windows-хуков (C и C++)

(любителям постановки хуков посвящается…)

Автор: Игорь Вартанов
The RSDN Group
Опубликовано: 08.10.2002
Исправлено: 13.03.2005
Версия текста: 1.0


Вместо предисловия
Предпосылки
Инструменты для C
Инструменты для C++
Послесловие
«Мой рок-н-ролл – это не цель, и даже не средство…»
Би-2 и Чичерина

Исходный код
Демонстрационный проект

Вместо предисловия

На самом деле я не люблю ставить хуки. Более того, мне не пришлось ни разу применить их на практике, разумеется, за исключением программ, имеющих образовательный, обучающий либо исследовательский характер. Если быть еще более откровенным, то мне самому не понятно, почему я пошел по пути написания подобного инструментария – ведь постановка хуков, в сущности, не сложнее какой бы то ни было другой темы в пределах Win32 API.

Насколько эта тема актуальна и/или интересна для других – судить трудно, особенно это касается части C++ (насколько я могу вспомнить, на вопрос «как засунуть хук в класс» я наткнулся всего лишь один раз, и было это в форуме на www.codeguru.com). Но если данный материал послужит кому-нибудь хотя бы отправной точкой для построения собственных инструментов – что же, уже хорошо…

Предпосылки

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

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

Посмотрим, как можно уложить описанные нами выше правила в шаблоны и макросы.

Инструменты для C

Макросы, конечно же макросы – что еще кроме них может нам помочь спрятать детали кода, оставив видимым лишь смысловое содержание. Итак, начнем с простого – поставим…

Локальный хук

В полном соответствии с вышеприведенными правилами наш первый макрос DECLARE_LOCAL_HOOK() объявляет глобальную переменную для хэндла хука и хук-процедуру.

#define DECLARE_LOCAL_HOOK( hook_message, HookProc )             \
    static HHOOK __g_hhk_##hook_message = NULL;                  \
    LRESULT CALLBACK __HookProc_##hook_message(                  \
                         int code,                               \
                         WPARAM wParam,                          \
                         LPARAM lParam  )                        \
        {                                                        \
            LRESULT res = 0;                                     \
            if( 0 > code )                                       \
                return CallNextHookEx( __g_hhk_##hook_message,   \
                                       code, wParam, lParam );   \
            res = HookProc( code, wParam, lParam );              \
            if( !res )                                           \
                return CallNextHookEx( __g_hhk_##hook_message,   \
                                       code, wParam, lParam );   \
            return res;                                          \
        }

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

// прототип пользовательского обработчика хука
LRESULT (*)( int, WPARAM, LPARAM );
// либо
LRESULT (WINAPI *)( int, WPARAM, LPARAM );

Функция обязана в случае отказа от обработки пришедшего сообщения вернуть нуль (либо FALSE), а в противоположном случае – ненулевое значение (например TRUE).

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

Непосредственно постановка хука осуществляется макросом SET_LOCAL_HOOK()

#define SET_LOCAL_HOOK( hook_message )     \
    { __g_hhk_##hook_message =             \
                SetWindowsHookEx(          \
                hook_message,              \
                __HookProc_##hook_message, \
                GetModuleHandle(NULL),     \
                GetCurrentThreadId() );    \
    }

Снятие хука производится макросом UNHOOK_LOCAL_HOOK().

#define UNHOOK_LOCAL_HOOK( hook_message )   \
        { if( __g_hhk_##hook_message )      \
              UnhookWindowsHookEx( __g_hhk_##hook_message ); \
        }

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

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

#define EXTERNAL_HOOK_HANDLE( hook_message ) \

external HHOOK __g_hhk_##hook_message

Проверку хук-хэндла на неравенство нулю можно выполнить макросом IS_VALID_HHOOK()

#define IS_VALID_HHOOK( hook_message ) \
    ( NULL != __g_hhk_##hook_message ) \

Пример типичного применения приведенных макросов будет выглядеть следующим образом:

// typical.c

// «рабочая лошадка» - пользовательский обработчик системных сообщений
int MessageHookProc( int code, WPARAM wParam, LPARAM lParam )
{
	. . .
	return FALSE;
}

DECLARE_LOCAL_HOOK( WH_MSGFILTER, MessageHookProc ) // объявляем хук

BOOL CALLBACK DlgProc( HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam )
{
	switch( msg )
    {
    case WM_COMMAND:
        switch( LOWORD( wParam ) )
        {
        case IDC_SET_HOOK:
            if( BN_CLICKED == HIWORD( wParam ) )            
            {
                SET_LOCAL_HOOK( WH_MSGFILTER )    // ставим хук
                _ASSERT( IS_VALID_HHOOK( WH_MSGFILTER ) );
            }
            break;
        case IDCANCEL:
            if( BN_CLICKED == HIWORD( wParam ) )            
                UNHOOK_LOCAL_HOOK( WH_MSGFILTER ) // снимаем хук
            EndDialog( hDlg, 0 );
            break;
        }
        break;
    . . .
    }
    return 0;
}

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

Глобальный хук

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

Макрос серверного кода выглядит следующим образом:

#define DECLARE_GLOBAL_HOOK( hook_message, HookProc )            \
    EXTERN_C __declspec(dllexport)                               \
    __declspec( allocate("HOOKDAT") )                            \
    HHOOK __g_hhk_##hook_message = NULL;                         \
    EXTERN_C __declspec(dllexport)                               \
    LRESULT CALLBACK __HookProc_##hook_message(                  \
                         int code,                               \
                         WPARAM wParam,                          \
                         LPARAM lParam )                         \
        {                                                        \
            LRESULT res = 0;                                     \
            if( 0 > code )                                       \
                return CallNextHookEx( __g_hhk_##hook_message,   \
                                       code, wParam, lParam );   \
            res = HookProc( code, wParam, lParam );              \
            if( !res )                                           \
                return CallNextHookEx( __g_hhk_##hook_message,   \
                                       code, wParam, lParam );   \
            return res;                                          \
        }

От DECLARE_LOCAL_HOOK() наш макрос отличается лишь наличием директив экспорта для хук-хэндла и хук-процедуры.

Обратите внимание, хук-хэндл объявлен принадлежащим секции “HOOKDAT”. Для того, чтобы эта секция была создана линкером, необходимо объявить директиву

#define __GLOBAL_HOOK

до включения нашего хэдера в исходный код. Без этого объявления при сборке dll будет выдана ошибка:

error C2341: 'HOOKDAT' : segment must be defined using #pragma data_seg or code_seg prior to use

Рассмотрим теперь макросы, предназначенные для клиентской части приложения.

Выделение памяти под указатель на хук-хэндл и хэндл инстанса dll, содержащей хук-процедуру, выполняет DECLARE_HOOK_DLL():

#define DECLARE_HOOK_DLL( hook_message )                         \
    static HINSTANCE __HookLib_##hook_message = NULL;            \
    static HHOOK*    __g_phhk_##hook_message  = NULL;            \
    HOOKPROC __HookProc_##hook_message = NULL;

Назначение указателя на хук-хэндл объясняется ниже.

Загрузку библиотеки, содержащей хук-процедуру, и постановку хука осуществляет макрос LOAD_HOOK_DLL():

#define LOAD_HOOK_DLL( hook_message, libname )                   \
    {                                                            \
        __HookLib_##hook_message = LoadLibrary( libname );       \
        _ASSERTE(__HookLib_##hook_message);                      \
        if( __HookLib_##hook_message )                           \
        {                                                        \
            __HookProc_##hook_message = (HOOKPROC)               \
                    GetProcAddress( __HookLib_##hook_message,    \
                    stringer(___HookProc_##hook_message@12) );   \
            _ASSERTE( __HookProc_##hook_message );               \
            __g_phhk_##hook_message = (HHOOK*)                   \
                    GetProcAddress( __HookLib_##hook_message,    \
                             stringer(__g_hhk_##hook_message) ); \
            _ASSERTE(__g_phhk_##hook_message);                   \
            *__g_phhk_##hook_message =                           \
                            SetWindowsHookEx( hook_message,      \
                                      __HookProc_##hook_message, \
                                      __HookLib_##hook_message,  \
                                      NULL );                    \
            _ASSERTE(*__g_phhk_##hook_message);                  \
        }                                                        \
    }

Снятие хука и выгрузка библиотеки производятся макросом UNLOAD_HOOK_DLL():

#define UNLOAD_HOOK_DLL( hook_message )                          \
    {                                                            \
        if(*__g_phhk_##hook_message)                             \
            UnhookWindowsHookEx(*__g_phhk_##hook_message);       \
        if(__HookLib_##hook_message)                             \
            FreeLibrary( __HookLib_##hook_message );             \
    }

Проверку хук-хэндла на неравенство нулю можно выполнить макросом IS_VALID_DLL_HHOOK()

#define IS_VALID_DLL_HHOOK( hook_message ) \
    ( _CrtIsValidPointer( __g_phhk_##hook_message, sizeof(HHOOK*), TRUE ) \
      && *__g_phhk_##hook_message ) \

Как можно видеть, память для хранения хук-хэндла выделяется в dll, а указатель на нее экспортируется. Это делается для того, чтобы дать возможность клиентскому коду получить доступ к этой памяти для записи в нее значения хук-хэндла при постановке хука (см. макрос LOAD_HOOK_DLL()). Доступность хэндла из хук-процедуры опять-таки обеспечивается размещением его в глобальной памяти модуля dll.

Значение экспортированного указателя на хук-хэндл получается обычным явным динамическим связыванием посредством GetProcAddress(), и сохраняется в указателе на хук-хэндл, объявленном макросом DECLARE_HOOK_DLL().

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

/////////////////////////////////////////////////////////////
// hooklib.c
// серверная часть

#include "stdafx.h"
#define __GLOBAL_HOOK
#include "HookHlpr.h"

// пользовательский обработчик системных сообщений
int SysMsgHookProc( int code, WPARAM wParam, LPARAM lParam )
{
	. . .
	return FALSE;
}

// объявление и экспорт хук-хэндла и хук-процедуры
DECLARE_GLOBAL_HOOK ( WH_SYSMSGFILTER, SysMsgHookProc )


/////////////////////////////////////////////////////////////
// hooker.c
// клиентская часть

#include "HookHlpr.h"

DECLARE_HOOK_DLL( WH_SYSMSGFILTER )

BOOL CALLBACK DlgProc( HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam )
{
	switch( msg )
    {
    case WM_COMMAND:
        switch( LOWORD( wParam ) )
        {
        case IDC_SET_HOOK:
            if( BN_CLICKED == HIWORD( wParam ) )            
            {
                LOAD_HOOK_DLL( WH_SYSMSGFILTER, “hooklib.dll” )    // ставим хук
                _ASSERT( IS_VALID_DLL_HHOOK( WH_SYSMSGFILTER ) );
            }
            break;
        case IDCANCEL:
            if( BN_CLICKED == HIWORD( wParam ) )            
                UNLOAD_HOOK_DLL( WH_SYSMSGFILTER )                // снимаем хук
            EndDialog( hDlg, 0 );
            break;
        }
        break;
    . . .
    }
    return 0;
}

Ограничения методики

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

Инструменты для C++

Макросы для C, описанные выше, вполне допустимо применять и в коде C++. Но давайте помнить об ограничениях… Если Вы считаете (по какой бы то ни было причине) применение макросов в C++-коде нежелательным или, более того, невозможным, то Вам на помощь придет шаблонный класс CHookBaseT<>.

// CHookBaseT class
// Тела методов класса не приводятся по причине значительного объема кода
// Полный код см. в HookHlpr.h

template <class T, class TData = CHookBaseData> 
class CHookBaseT
{
private:
    CHookThunk m_thunk;
    int        m_nCode;

protected:  
    TData* m_pData;
    
private:
    static LRESULT WINAPI _HookProc( T* pThis, WPARAM wParam, LPARAM lParam );

public:
    CHookBaseT() : m_pData(NULL){}
    CHookBaseT( TData* pData ) : m_pData(pData){}
    void Init();
    void SetHookHandle( HHOOK hHook );
    void SetHookType( int nHookType );
    BOOL SetHook( HINSTANCE hInst, DWORD dwThreadId );
    void Unhook();
    BOOL IsValid() const;
    inline HHOOK GetHookHandle() const;
    inline int GetHookType() const;
    inline int GetCode() const;
    void AttachData( TData* pData );
};

Экземпляр класса содержит переходник m_thunk, осуществляющий во время исполнения передачу указателя на экземпляр (this) в статический метод _HookProc(). Этот метод, в свою очередь, реализует базовую логику хук-процедуры, описанную нами выше. Для выполнения полезной нагрузки статический метод вызывает метод HookProc(), реализуемый пользовательским классом, наследующим данному шаблонному классу. Поведение метода должно быть аналогично описывавшемуся выше: в случае отказа от обработки сообщения возвращается нуль, в противном случае – ненулевое значение.

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

Назначение большинства методов, надеюсь, понятно из их имен. В методе Init() выполняется инициализация - заполнение полей переходника перед постановкой хука, IsValid() позволяет выяснить, успешно завершилась установка хука после SetHook() или нет, AttachData() позволяет на ходу присоединить память, содержащую параметры хука.

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

Локальный хук

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

ПРИМЕЧАНИЕ

Разумеется, этот метод необязателен, и экземпляр данных может быть самостоятельным объектом. В этом случае необходимо создать его либо в конструкторе класса хука, либо передать указатель на него в конструктор через один из параметров последнего, как это сделано в примере для глобального хука.

// MsgFilterHook.h
#include "HookHlpr.h"

class CData : public CHookBaseData
{
public:
    HWND m_hWnd;
};

class CMsgFilterHook : public CData,
                       public CHookBaseT<CMsgFilterHook, CData>
{
public:
    CMsgFilterHook()
    {
        Init();
        m_pData     = static_cast< CData* > (this);
        m_hHook     = NULL;
        m_iHookType = WH_MSGFILTER;
    }

    void SetHwnd( HWND hWnd )
    {
        m_pData->m_hWnd = hWnd;
    }
    
    LRESULT HookProc( int code, WPARAM wParam, LPARAM lParam )
    {
        switch(code)
        {
        case MSGF_DIALOGBOX:
        . . .
        }
        return FALSE;
    }
};

// Hooker.cpp
#include "stdafx.h"
#include "MsgFilterHook.h"

CMsgFilterHook mf; // реализует пользовательский обработчик системных сообщений

BOOL CALLBACK DlgProc( HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam )
{
	switch( msg )
    {
    case WM_COMMAND:
        switch( LOWORD( wParam ) )
        {
        case IDC_SET_HOOK:
            if( BN_CLICKED == HIWORD( wParam ) )            
            {
                mf.SetHwnd( hDlg );
                mf.SetHook( NULL, GetCurrentThreadId() );   // ставим хук
                _ASSERT( mf.IsValid() );
            }
            break;
        case IDCANCEL:
            if( BN_CLICKED == HIWORD( wParam ) )            
                mf.Unhook();                                // снимаем хук
            EndDialog( hDlg, 0 );
            break;
        }
        break;
    . . .
    }
    return 0;
}

Все предельно просто – создаем необходимый тип данных для нашего хука (в примере это класс CData), в пользовательском классе хука (CMsgFilterHook) реализуем метод HookProc(), выполняющий некую полезную работу, а также прочие необходимые вспомогательные методы. Далее ставим хук, снимаем хук… Уверен, что и постановка глобального хука не должна вызвать никаких затруднений. Итак…

Глобальный хук

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

Кроме того, нелишне напомнить, что для того, чтобы разделяемая секция в dll была объявлена, необходимо до включения в код нашего хэдера hookhlpr.h объявить глобальный хук:

#define __GLOBAL_HOOK

Теперь настала пора взглянуть на код типичной dll (в нашем примере она реализует хук типа WH_SYSMSGFILTER).

// HookLib.cpp
#include "stdafx.h"

#define __GLOBAL_HOOK
#include "SysMsgFilter.h"  // включает hookhlpr.h


// класс ChookSysMsgFilter реализует 
// пользовательский обработчик системных сообщений
// и интерфейс постановщика хуков IHookDriver

ALLOCATE_GLOBAL_HOOK_OBJ( CHookSysMsgFilter, mf, CData, g_data ) 
                                         = CData(WH_SYSMSGFILTER);

extern "C" __declspec(dllexport) 
IHookDriver* GetHookObject()
{
    return static_cast<IHookDriver*> (&mf);
}

Как видим, в данном примере управление объектом хука производится нами по второму варианту – класс реализует методы интерфейса IHookDriver, посредством которого мы управляем процессом установки и снятия хука и его параметрами. Клиентский код выглядит следующим образом:

// ApiMenu.cpp
#include "stdafx.h"
#include "HookLib\SysMsgFilter.h"

BOOL CALLBACK EditDlgProc( HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam )
{
    static HINSTANCE    hLib  = NULL;
    static IHookDriver* pHook = NULL;

    switch( msg )
    {
    case WM_COMMAND:
        switch(wParam)
        {
        case IDOK:
            {
                hLib = LoadLibrary( _T("HookLib.dll") );
                if( hLib )
                {
                    IHookDriver* (*pfnGetHookObject)();
                    *(FARPROC*)&pfnGetHookObject = GetProcAddress( hLib, "GetHookObject" );
                    if( pfnGetHookObject )
                    {
                        // получаем указатель на интерфейс управления хуком
                        pHook = pfnGetHookObject();
                        if( pHook )
                        {
                            // устанавливаем параметры хука
                            pHook->SetHwnd( (long)GetDlgItem(hDlg, IDC_LOG) );
                            // выполняем постановку хука
                            pHook->Set( (long)hLib, 0 );
                        }
                    }
                    if(pHook) 
                        EnableWindow( GetDlgItem( hDlg, IDOK ), FALSE );
                }
            }
            break;
        case IDCANCEL:
            {
                if(hLib)
                {
                    // снимаем хук
                    if( pHook )
                        pHook->Free();
                    FreeLibrary( hLib );
                }
            }
            EndDialog( hDlg, 0 );
            break;
        }
        break;
    }
    return 0;
}

За подробностями реализации пользовательского класса хука CHookSysMsgFilter предлагаю Вам обратиться к демонстрационному проекту EditMenu.zip.

Послесловие

Итак, за время, потраченное на чтение этой статьи, мы неоднократно ставили и снимали хуки, что, с одной стороны, должно нас убедить, что рассмотренные инструменты несколько облегчают задачу правильной постановки хуков. Но, с другой стороны, львиная доля успеха будет зависеть от того, насколько корректно будет написан код пользовательской функции (или метода класса), выполняющей полезную работу. Например, в процессе написания демонстрационного кода к статье я неоднократно убедился в том, что код хука, отлично работающего в Windows 2000/XP, может совершенно не работать в Windows 98. И в этом вопросе уже никакие инструменты Вам не помогут, Вы остаетесь один на один с Windows. Желаю Вам удачи!..

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


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 0    Оценка 115        Оценить