Хуки в Windows. Часть вторая. Работа с окнамиИсточник: pblog Грузин
В первой статье про хуки речь шла об основах механизмов хуков и о клавиатурных хуках. Во второй статье про хуки в Windows я расскажу про слежение событий создания, активации, уничтожения окон. Также много внимания будет уделено методам межпроцессорного взаимодействия с использованием разделяемой памяти (мэпинга) и синхронизации потоков с использованием мьютексов. Также будет написана программа на Delphi для осуществления мониторинга окон. Итак, начнём сначала. Для создания хука для мониторинга событий окон надо указать тип хука WH_CBT в первом параметре функции SetWindowsHookEx. Хук типа WH_CBT позволяет отслеживать следующие события окон: создание, уничтожение, активация, установку фокуса, минимизация, максимизация и прочее. LRESULT CALLBACK CBTProc( int nCode, // код события WPARAM wParam, // depends on hook code LPARAM lParam // depends on hook code ); Назначение параметров wParam и lParam полностью зависит от типа события. Обработчик хука всегда вызывается до осуществления события. Если обработчик хука не вызовет следующий обработчик хука (функция CallNextHookEx), то перехватываемое действие не произойдёт, таким образом можно блокировать некоторые действия. Но тем не менее, отменять события в хуках такого типа не рекомендуется, так как это будет очень неожиданно для приложения. Представьте себе ситуацию, когда программа хочет уничтожить окно, а у него не получается, или же хочет создать окно, но не получается, намного корректнее было бы уничтожить окно после его создания (к примеру, через 10 мс). Далее приведены наиболее часто используемые типы событий. typedef struct tagCBTACTIVATESTRUCT { // cas BOOL fMouse; HWND hWndActive; } CBTACTIVATESTRUCT; Если событие произошло вследствие клика мыши, то поле fMouse будет равно TRUE. Поле hWndActive содержит хендл окна, активного в данный момент. typedef struct tagCBT_CREATEWND { // cbtcw LPCREATESTRUCT lpcs; HWND hwndInsertAfter; } CBT_CREATEWND; Поле hwndInsertAfter содержит хендл окна, которое по Z координате находится сразу же за вновь создаваемым. Изменив этот хендл можно изменить Z координату вновь создаваемого окна. Поле lpcs указывает на структуру CREATESTRUCT, она имеет следующий формат: typedef struct tagCREATESTRUCT { // cs LPVOID lpCreateParams; HINSTANCE hInstance; HMENU hMenu; HWND hwndParent; int cy; int cx; int y; int x; LONG style; LPCTSTR lpszName; LPCTSTR lpszClass; DWORD dwExStyle; } CREATESTRUCT; Я думаю здесь всё понятно. · HCBT_CLICKSKIPPED - Фильтр вызывается при удалении сообщения мыши из системной очереди сообщений, при условии, что дополнительно определен фильтр WH_MOUSE. · HCBT_KEYSKIPPED - Фильтр вызывается при удалении клавиатурного сообщения из системной очереди сообщений, при условии, что дополнительно определен фильтр WH_KEYBOARD. · HCBT_MINMAX - минимизация/максимизация окна · HCBT_MOVESIZE - окно будет перемещено либо будет изменён размер окна · HCBT_QS - Система извлекла сообщение WM_QUEUESYNC из системной очереди сообщений · HCBT_SETFOCUS - окно получило фокус ввода. · HCBT_SYSCOMMAND - будет обработана системная команда. Итак, хук WH_CBT мы изучили. Теперь надо подумать как вести лог. В примере прошлой статьи у нас было окно-сервер, которое принимало специальные сообщения и заносило их в лог. Как ни хвали этот метод, он всё равно имеет ограничения и не всегда приемлем. На этот раз мы используем другой более гибкий и универсальный метод. Для ведения лога мы будем использовать технику файлового мэпинга. С помощью этой техники можно создать кусок виртуальной памяти, который будет доступен нескольким процессам. HANDLE CreateFileMapping( HANDLE hFile, LPSECURITY_ATTRIBUTES lpFileMappingAttributes, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpName ); Первый параметр это хендл файла, который будет использован как файл подкачки для этой области памяти. Если хендл файла равен значению INVALID_HANDLE_VALUE, то выделенная область памяти при необходимости будет сбрасываться в файл подкачки (как и любая другая область памяти). Второй параметр это атрибуты защиты. Третий параметр задаёт параметры доступа к выделенной памяти: PAGE_READONLY - только чтение, файл в этом случае должен быть открыт как минимум с флагом GENERIC_READ; PAGE_READWRITE - чтение и запись, файл должен быть открыт как минимум с флагами GENERIC_READ и GENERIC_WRITE; PAGE_WRITECOPY - тоже самое, что и с предыдущим флагом, но все выделенные страницы помечаются как копируемые при записи. В этом случае изменения в выделенной памяти не будут отражаться на искомом файле, и в случае необходимости область памяти будет сбрасываться в файл подкачки. В общем, не будем слишком сильно заморачиваться этим флагом, лучше всего использовать флаг PAGE_READWRITE. Третий и четвёртый параметры задают максимальный размер создаваемого объекта, соответственно старшую и младшую часть. Последний параметр задаёт имя создаваемого объекта, через которое смогут обратиться к нему другие процессы. HANDLE OpenFileMapping( DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName ); Первый параметр задаёт тип доступа к объекту, может принимать следующие значения: FILE_MAP_WRITE - чтение и запись, объект должен быть создан с атрибутом PAGE_READWRITE; FILE_MAP_READ - только чтение, объект должен быть создан к минимум с атрибутом PAGE_READONLY; FILE_MAP_ALL_ACCESS- тоже самое, что и FILE_MAP_WRITE; FILE_MAP_COPY - копирование при записи, объект должен быть создан с атрибутом PAGE_WRITECOPY. Второй параметр это флаг наследования. Третий параметр задаёт имя отрываемого файл-мэпинг объекта. LPVOID MapViewOfFile( HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, DWORD dwNumberOfBytesToMap ); Первый параметр это хендл файл-мэпинг объекта. Второй параметр задаёт атрибуты доступа, требования полностью идентичны требованиям первого параметра для функции OpenFileMapping. Третий и четвёртый параметры задают начальное смещение в файле, с которого начнётся проецирование на память, соответственно старшая и младшая часть смещения. Последний параметр задаёт количество байт для проецирования на память. Функция в случае успеха возвращает указатель на выделенную память. HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName ); Первый параметр этой функции задаёт параметры защиты объекта. Второй параметр задаёт начальное состояние мьютекса, если оно равно TRUE (-1) то начальное созданный мьютекс сразу же захватывается создающим потоком, иначе начальное состояние создаваемого мьютекса свободное. Третий параметр задаёт имя мьютекса, чтобы к созданному мьютексу могли обратиться другие процессы. HANDLE OpenMutex( DWORD dwDesiredAccess, // access flag BOOL bInheritHandle, // inherit flag LPCTSTR lpName // pointer to mutex-object name ); Первый параметр задаёт флаги доступа к мьютексу, второй параметр задаёт флаг наследования, третий имя мьютекса. Первый параметр может принимать следующие значения: MUTEX_ALL_ACCESS - полный доступ, SYNCHRONIZE - только для синхронизации. (впрочем, так и непонятно чем они друг от друга отличаются) WaitForSingleObject(MutexHandle,INFINITE); //код работающий с общими данными ReleaseMutex(MutexHandle); Итак, все знания необходимые для написания монитора окон мы получили, настало время написать программу для мониторинга окон. Сначала приведу код DLL который устанавливает и снимает хук: procedure SetKeyHook; stdcall; export; begin if HookHandle=0 then begin HookHandle:=SetWindowsHookEx(WH_CBT, @CBTHook, hInstance, 0); FileMappingHandle :=OpenFileMapping(FILE_MAP_WRITE, false, FileMappingName); SharedBuffer :=MapViewOfFile(FileMappingHandle, FILE_MAP_WRITE, 0,0, MaxBufferSize); SyncMutexHandle :=OpenMutex(SYNCHRONIZE,False,MutexName); end; end; procedure DelKeyHook; stdcall; export; begin if HookHandle 0 then begin UnhookWindowsHookEx(HookHandle); HookHandle:=0; UnmapViewOfFile(SharedBuffer); CloseHandle(FileMappingHandle); CloseHandle(SyncMutexHandle); FileMappingHandle:=0; end; end; Проблем с этим кодом быть не должно: при установке хука мы открываем нужные нам объекты и проецируем в нашу память общий буфер. Далее приведён код функции фильтра. function CBTHook(CODE, WParam, LParam: DWORD): DWORD; stdcall; var ServerWnd: THandle; CurrentOffsetInBuffer:DWORD; CurrentPointer:pointer; NewStr:string; WindowName:array[0..MAX_PATH-1] of char; begin Result:=CallNextHookEx(HookHandle, CODE, WParam, LParam); case CODE of HCBT_ACTIVATE: begin GetWindowText(WParam,@WindowName,MAX_PATH); if WindowName='' then exit; NewStr:='Window activated at '+GetTime; NewStr:=NewStr+'. Window name '+WindowName+#13#10; end; HCBT_CREATEWND: begin if PCBTCreateWnd(LParam)^.lpcs^.hwndParent0 then exit; NewStr:='Window created at ' +GetTime; if PCBTCreateWnd(LParam)^.lpcs^.lpszNamenil then NewStr:=NewStr +'. Window name '+ PCBTCreateWnd(LParam)^.lpcs^.lpszName +#13#10 else NewStr:=NewStr+#13#10; end; HCBT_DESTROYWND: begin GetWindowText(WParam, @WindowName,MAX_PATH); if WindowName='' then exit; NewStr:='Window destoyed at '+GetTime; NewStr:=NewStr+'. Window name '+ WindowName+#13#10; end; end; WaitForSingleObject(SyncMutexHandle,INFINITE); CurrentOffsetInBuffer:=DWORD(SharedBuffer^); CurrentPointer :=pointer(DWORD(SharedBuffer) + CurrentOffsetInBuffer); CopyMemory(CurrentPointer,PChar(NewStr),length(NewStr)); DWORD(SharedBuffer^):=CurrentOffsetInBuffer+length(NewStr); ReleaseMutex(SyncMutexHandle); end; В начале мы сразу же вызываем следующий обработчик в цепочке обработчиков. Потом обрабатываем данные в зависимости от типа события. В событии HCBT_CREATEWND мы поучаем имя окна из структуры PCBTCreateWnd на которую указывает параметр lParam, в остальных двух случаях мы получаем имя окна, используя её хендл который находится в параметре wParam. В событии HCBT_CREATEWND мы получаем имя окна только в том случае если оно главное, т.е. не имеет родителя, в остальных двух случаях мы производим обработку только в случае, если имя окна не является пустой строкой. После того как мы получили строку нам необходимо её добавить в буфер. Добавление производится между вызовами функций WaitForSingleObject и ReleaseMutex чтобы обновление мог производить только один поток одновременно. procedure RunHook; begin FileMappingHandle :=CreateFileMapping(INVALID_HANDLE_VALUE, 0, PAGE_READWRITE, 0, MaxBufferSize,FileMappingName); SharedBuffer :=MapViewOfFile(FileMappingHandle, FILE_MAP_WRITE, 0, 0, MaxBufferSize); ZeroMemory(SharedBuffer,MaxBufferSize); DWORD(SharedBuffer^):=4; SyncMutexHandle:=CreateMutex(0,false,MutexName); SetKeyHook; end; procedure StopHook; begin DelKeyHook; DumpBuffer; UnmapViewOfFile(SharedBuffer); CloseHandle(FileMappingHandle); CloseHandle(SyncMutexHandle); end; procedure DumpBuffer; var FH:THandle; _WR:DWORD; _Buff:pointer; begin _Buff:=pointer(DWORD(SharedBuffer)+4); FH :=CreateFile(LogFileName,GENERIC_WRITE or GENERIC_READ, FILE_SHARE_READ, 0, OPEN_ALWAYS,0,0); SetFilePointer(FH,0,0,FILE_END); WriteFile(FH, _Buff^, lstrlen(_Buff),_WR,0); CloseHandle(FH); ZeroMemory(SharedBuffer, MaxBufferSize); end; Я думаю, ничего сложного в этом коде нет. Функция DumpBuffer скидывает содержимое буфера в файл. При создании объекта файлового мэпинга мы не указываем никакого файла. Сразу возникает вопрос: почему? Смысл в том, что размера выделяемого буфера может не хватить и придётся его время от времени сбрасывать в файл, а если выделять сразу большой буфер, то хук станет слишком ресурсоёмким. Хотя в данном примере не реализован сброс буфера в файл при нехватке места в буфере, об этом нельзя забывать и это надо будет обязательно реализовать в своих программах. |