Порой логика инженеров Microsoft не выдерживает никакой критики. В священных войнах с хакерами они сами вручают им мандат на использование уязвимых функций, после чего отлавливают их-же с претензиями на взлом. Из большого числа подобных API на передний план буйком выплывает SetWindowsHookEx(), при помощи которой одним кликом мыши можно внедрить "творческую" свою DLL сразу во все активные процессы системы. В итоге, даже обычный юзверь может перехватить любые действия пользователя, вплоть до логирования клавиатурного ввода. И что особенно важно, ничего криминального в этом нет, поскольку мелкософт сама предлагает нам эту функцию, оформив ей прописку в библиотеке user32.dll. Чем они руководствовались остаётся для нас загадкой, но функция есть, а значит имеет смысл рассмотреть подробно её реализацию.
Оглавление:
1. Основной посыл
Фундамент программной модели ОС держится на оконных сообщениях "Window Message". Всего в системе зарегистрировано порядка 1000 их типов с префиксом WM_***, а внутри оконной процедуры программист может фильтровать и обрабатывать предписанные конкретным алгоритмом, только часть из них. При нажатии пимпы на клавиатуре или какого-нибудь буттона в окне, управляемый драйвером Win32k.sys диспетчер форточек Dwm.exe (Desktop Window Manager) адресует нашему приложению соответствующую мессагу. Далее, движимый системным механизмом, её код (вместе с уточняющими аргументами wParam и lParam) поступает в очередь-сообщений текущего потока "Virtual Input Queue" (VIQ) , и только от туда – в процедуру окна, которая должна обработать его приблизительно по такому сценарию:
Однако часто в программировании возникает задача переопределения функциональности существующей оконной процедуры – это т.н. "субклассинг". В родственном ему варианте, системный надзор позволяет на законных основаниях организовать перехват и обработку любого сообщения, после чего возвратить его по конвейеру стандартной процедуре окна. Махинации подобного рода известны под общим названием "Hook" (ловушка) или "Grab". Последнее звучит грубо, зато по-честному, ведь появляется возможность реально грабить данные с приложения, которое в фокусе на текущий момент. Особый трепет вызывают ситуации, когда подвергшееся атаке окно лежит не в нашей компетенции, а например принадлежит интернет-браузеру, куда ничего не подозревающий юзер беспечно вводит пароль.
Судя по-всему, продумАны из Microsoft слишком поздно поняли, что "натворили делов" предоставив пользователям поддержку хуков. Они с огромной радостью отказались-бы от них сейчас, но поезд ушёл, т.к. к хукам привязан целый класс API из либы user32.dll типа: RegisterHotKey() для регистрации горячих клавиш, RegisterShellHookWindow() и ещё вагон подобных функций, где фигурирует слово хук. Более того, хуки использует и легальный софт сторонних разработчиков таких-как "PuntoSwitcher", который включив свой радар парсит клавиатурный ввод и на автомате меняет раскладку клавы RU/ENG. Теперь отказ от хуков повлечёт за собой судебные разбирательства с клиентами, чего в своём шатком положении мелкософт позволить себе не может.
Таким образом, начиная ещё с Win-98 хуки были, есть и будут, предоставляя в наше распоряжение весьма богатый арсенал. В сети предостаточно информации на данную тему, но в этом и проблема, поскольку где-то она разнится, а где-то на порядок устарела (особенно описание структур) и не понятно, кому именно доверять. По этой причине, предлагаю выбрать иной путь и вооружившись авторитетным отладчиком WinDbg, попрактиковаться на реальном его выхлопе – тогда проколов уж точно не будет.
Тема хуков достаточно обширна, а потому в данном обзоре мы рассмотрим лишь универсальную ловушку WH_GETMESSAGE, которая позволит собирать буквально все типы оконных сообщений, а внутри процедуры-фильтра потом можно будет отсеивать не нужные. Позиционно, этот тип хуков занимает место в системном "трубопроводе" аппаратных сообщений, как показано на рисунке ниже:
Значит три нижних блока – это процедуры "обработки сообщений" пользовательских процессов. Когда потоки этих процессов вызывают функцию GetMessage(), система снимает с очереди VIQ очередное сообщение (у каждого процесса очередь своя) и доставляет её приложению. Но как видим, установленный нами хук WH_GETMESSAGE внедряется между двух собеседников и перехватывает весь их диалог. Важно отметить, что кроме нас, ставить хуки могут и другие приложения, поэтому в системе всегда присутствует цепочка их них, и соответственно после обработки очередного события нужно передавать мессагу как эстафетную палочку дальше, посредством функции СallNextHookEx(). Если мы последние, кто устанавливал общесистемную ловушку, то становимся первым в этой цепочке, по принципу кольцевого буфера LIFO – LastIn / FirstOut (последним пришёл, первым выйдешь).
Особого внимания заслуживают хуки нижнего уровня(LL), или Low-Level-Hook. Ловушки данного типа относятся к аппаратным и их всего две. Поскольку они взаимодействуют непосредственно с драйверами ядра, то процедуры их обработки оказывают влияние на всю систему. От сюда следует, что фильтры(LL) вполне могут находиться прямо внутри нашего приложения и область их действия по любому будет глобальна, в то время как ловушку WH_GETMESSAGE нужно оформить в отдельной библиотеке DLL, чтобы система одним выстрелом дробью смогла внедрить её сразу во-все процессы ОС.
Однако хуки(LL) плохо себя зарекомендовали и по возможности их следует избегать. Во-первых это жуткие тормоза и падение производительности, а во-вторых ограниченное в 0,5-сек время обработки события Callback-фильтром (порог хранится в реестре). Если мы не уложимся в заданный период времени, то обитающий в Win32k.sys диспетчер окон просто отберёт у нас сообщение и передаст его следующему хуку в цепочке, даже никак не оповестив нас об этом. По сути, это вынужденные меры, т.к. незапланированный мёртвый цикл внутри фильтра(LL) завесит не только нашу программу, но и всю операционную систему – спасти её сможет только ребут, с полной потерей не сохранённых данных.
2. Функция SetWindowsHookEx()
Для работы с хуками в составе библиотеки user32.dll имеются три функции: SetWindowsHookEx(), CallNextHookEx() и UnhookWindowsHookEx(). Их имена говорят сами за себя – установка, передача сообщения следующему в цепочке, и анулирование. Общая информация на эту тему зарыта на сайте MSDN по
Хуки можно устанавливать или только на один какой-нибудь поток (например свой, и тогда его именуют локальным), или-же сразу на все рабочие процессы в системе. В последнем случае хук считается глобальным, и фильтрующая его Callback-процедура должна обязательно находиться в отдельной DLL, которая и будет внедряться в пространство каждого процесса. Организовать глобальные хуки без инжекта нашей DLL в чужие треды просто нельзя, т.к. у каждого приложения своё виртуальное пространство, и при обычных обстоятельствах они в упор не видят друг-друга. Только учтите, что получив такие полномочия, не нужно на радостях сразу "рвать баян" – всё должно быть в пределах этической нормы, ато можно на несколько лет занять свободные апартаменты, и не кому будет кормить кота.
Значит функция SetWindowsHookEx() может устанавливать как локальные, так и глобальные хуки – вот её прототип для обоих случаев. Она принимает на вход 4-аргумента, а на выходе возвращает нуль при ошибке, иначе дескриптор хука для его отмены, или передачи в цепочку:
Смысл в том, что мы определяем свою хук-процедуру, и при помощи данной функции передаём её адрес системе. Теперь, если кто-нибудь в глобальной ОС попытается вызвать из библиотеки user32.dll функцию GetMessage(), то сняв с очереди запрашиваемое сообщение, системный диспетчер передаст полный его контекст сначала нашей процедуре, а после трепанации, мы должны будем возвратить мессагу обратно диспетчеру CallNextHookEx(). Дальнейшие действия ОС нам не интересны, поскольку это уже диалог системы с вызывающим. Такой алгоритм позволяет не только "заглядывая через плечо читать чужие письма" (В.Высоцкий), но и модифицировать их в рамках своей совести.
Рассматриваемый нами хук типа WH_GETMESSAGE не один в своём роде – их там целая секта, с адептами которой можно ознамиться или
Не смотря на такой зоопарк, все Callback-процедуры их фильтров имеют одинаковый прототип, и на входе принимают по три аргумента – всегда имеется
Отметим, что из этой троицы, исключением является только аргумент
Ниже представлен возможный вариант процедуры-фильтра рассматриваемого нами хука WH_GETMESSAGE. Как было сказано выше, в аргументе
3. Практика
Думаю для теории написано более-чем предостаточно букаф, и теперь займёмся практикой.
В своей демке, я установлю глобальный хук и буду фильтровать в нём нажатие клавиш, по-ходу сбрасывая их в форточку MessageBox(). Как трансформировать сей код в реальный кейлоггер додумайте сами. Только авансом скажу, что начиная с Win-7 задача эта носит нетривиальный характер и мыслить здесь придётся шире, т.к. системный протекторат запретил нам внутри процедуры-фильтра использовать стандартные методы записи в файл.
Десантировав развед-отряд на вражескую территорию я обнаружил, что ни одна из функций типа WriteFile(), _lwrite(), fprintf() не справляется со-своими обязанностями, и приходится искать обходные пути. Видимо достала мелкософт проблема вездесущих кейлоггеров, и они как-обычно решили рубануть с плеча.
Но ведь никто не запрещает нам сначала копить пользовательский ввод в выделенном для всех процессов общем буфере (см. CreateFileMapping()), и только потом из основного приложения сливать его содержимое в лог-файл. Ну или создать расшаренную (shareable) секцию в DLL, тогда объявленные в ней данные будут доступны всем экземплярам этой библиотеки. Блуждание именно в таких коридорах кода позволяют программисту накапливать собственный скилл, в то время-как тупая копипаста наоборот атрофирует мозг. Больше практикуйтесь, насилуйте отладчик и всё получится.
3.1. Задачи библиотеки DLL
Выше упоминалось, что процедура-фильтра глобального хука должна находиться в DLL – это обязательное условие для ловушки типа WH_GETMESSAGE. После того как мы загрузим данную библиотеку в своё пространство и установим хук, система осуществит её инжект буквально во-все процессы, ..только не сразу, а в момент срабатывания нашего условия WH_CHAR. То-есть наша либа загрузится в чужое пространство при первом нажатии клавиши в его окне. Вот как это выглядит в отладчике OllyDbg:
Как программировать библиотеки на ассемблере я рассматривал в одной из прошлых статей "DLL инструкция к метле", поэтому повторяться не буду. Достаточно будет сказать, что либа должна иметь свою точку-входа в виде документированной процедуры DllMain(), внутри которой можно провести её инициализацию по флагу DLL_PROCESS_ATTACH (факт загрузки dll в память процесса). С форматом самого Callback-фильтра хука мы уже знакомы, а я добавил в него лишь алго получения имени активного окна. Это можно организовать всего двумя функциями – GetActiveWindow() и GetWindowText(). Вот пример:
Обратите внимание на вспомогательную процедуру Setup(). Чтобы не загружать библиотеку динамически через LoadLibrary(), я просто вызываю функцию из неё, и она на автомате грузиться в пространство основного файла EXE (статический вызов). Но раз-уж имеем обращение к ней, то по-ходу можно передать в аргументе и дескриптор хука, который создаёт родительский процесс. Кстати скомпилированную библиотеку нужно будет обязательно назвать "hookDll.dll", поскольку в секции-экспорта либы и секции-импорта основного приложения прописано именно такое имя.
3.2. Код приложения-шпиона
Здесь всё просто и не требует особых пояснений. Во-первых мы статически подгружаем нашу либу потянув за её процедуру Setup(), после чего ставим хук типа WH_GETMESSAGE и передаём его дескриптор в DLL – вот и весь алго на данный момент..
Однако имеется нюанс, который преподнесли нам операционные системы начиная с Win-7 и выше. В сети спорят о том, что мол хуки могут обрабатывать только приложения с графическим интерфейсом GUI, но никак не консольный софт. Аргументируется это тем, что система не посылает консоли сообщения типа WM_CHAR, а только WM_KEYDOWN/UP (нажатие и отпускание клавиш). Это-же касается и некоторых других мессаг, которые в принципе не могут существовать в консольных приложениях, на подобии кнопок Button, заголовка меню и т.п. Нужно сказать, что это утверждение не актуально для современных систем, т.к. консоль сейчас в корне преобразилась и её обслуживает специальный процесс Conhost.exe (см.диспетчер задач). К деталям этой темы мы вернёмся в следующей главе, а пока посмотрим на код приложения хуков:
На скрине ниже видно, что даже такой незамысловатый код прекрасно внедрил свою DLL во-всевозможные процессы (см.заголовки форточек), и из структуры MSG можно вытягивать ascii-коды нажатых клавиш. Более того можно сделать вывод, что технология ASLR (Advanced Space Layout Randomization) на моей Win7 вполне себе бодрствует, поскольку мы задавали базовый адрес в исходнике DLL
4. Методы обнаружения хуков
Теперь поговорим о более интересных вещах, и развернувшись на 180 градусов проследуем в обратном направлении. Глава повествует о том, как прямо из пользовательского уровня собрать информацию об установленных в системе хуках; сколько их, и кем они были внедрены в цепочку. Возможно-ли решить данную задачу, обладая только правами юзера? Оказывается да, ..а если мы сидим на системах Win7+, то сделать это ещё проще. На первый взгляд сей факт противоречит системе безопасности Win, однако инженерами всё продумано и без драйвера они предоставляют нам доступ исключительно на чтение. Но обо всё по порядку..
Начиная с NT-4.0 в графической подсистеме Win32 произошли координальные изменения. Чтобы логическая нить не увела нас далеко от темы, мы не будем обсуждать здесь все архитектурные новшества, а сделаем акцент только на драйвере Win32k.sys (часть подсистемы Win32 в kernel) и системном сервисе CSRSS.EXE (Client/Server Runtime SubSystem). В данном случае под клиентом подразумевается юзер, а под сервером – ядро операционной системы. В нёдрах именно этих двух файлов можно будет найти дескрипторы всех GUI-объектов, в число которых входят и ловушки Hook. Далее мы потянем за найденный дескриптор и выудим всю информацию о требуемом объекте.
Будем считать, что план нарисовали – теперь о его реализации..
Драйвер Win32k.sys является компонентом ядра и в наше время отвечает за большое кол-во функционала ОС, например буфер обмена, управление окнами, двумерную графику GDI в этих окнах (типа кнопки, полосы прокрутки) и многое другое. В свою очередь процесс csrss.exe находится в пространстве пользователя, являясь неотъемлемой частью ядра. На этот процесс возложены задачи по созданию процессов/потоков (но не регистрацию их), поддержку консоли, и графического окна завершения работы Win.
Но так было не всегда. Например в Win-NT 3 процесс csrss.exe представлял собой достаточно творческую единицу. Именно в нём находился диспетчер окон и интерфейс GDI (Graphic Device Interface), которые перекочевали в ядро только с приходом NT-4 лишь для того, чтобы исключить потенциальные проблемы с медленной прорисовкой окон. Рисунок ниже отображает эти изменения в визуальной форме:
Обратите внимание, что хоть увесистая часть сервиса csrss.exe и была перемещена в пространство Kernel, инженеры всё-же оставили её копию для юзерских нужд. В талмудах по системному программированию эту копию назвали SharedInfo. Система раздаёт экземпляры данной таблицы буквально всем пользовательским приложениям, в том числе и нашему. Она оформлена в виде одноимённой структуры, а её адресом в юзерском пространстве является точка-входа в функцию gSharedInfo() из библиотеки user32.dll (только для систем Win7+). Если-же система XP и ниже, то адрес этой шары можно получить обратившись напрямую к системному сервису csrss.exe функцией CsrClientConnectToServer() из Ntdll.dll.
Допустим адрес получили.. двигаемся дальше..
Теперь нужно найти описание этой структуры, для чего воспользуемся ядерным отладчиком WinDbg. Команда из всего одной буквы(х) позволяет искать в нём структуры ядра по соответствующей маске, которую нужно поместить между двумя ёжиками(*) – вот что возвращает отладчик на моей 32-битной Win7:
В этой структуре нам интересны всего четыре поля:
Теперь посмотрим на описание очередной структуры HANDLEENTRY, которая продолжает цепочку поиска установленных хуков. Оседлаем всё тот-же отладчик и запросим эту структуру по имени:
Так, решив весьма занятную комбинаторику, мы получим описатель каждого хука в системе, и остаётся выводить на консоль данные из очередной структуры _HOOK. В конечном счёте, общем схема блуждания по тёмным переулкам системы будет выглядеть как на рисунке ниже (значимые поля в структурах выделены красным):
Теперь остаётся найти валидную дельту, и завернуть всё/это хозяйство в законченный код.
Выше упоминалось, что дельта прописана в структуре TEB каждого потока, а значит мы без труда её сможем от туда утянуть. На системах х32 она лежит во-вложенной в ТЕВ структуре CLIENTINFO по смещению
Ну и теперь собственно исходный код приложения, которое в цикле пропарсит все найденные структуры _HOOK, в юзерской копии SHAREDINFO. Этот код будет работать только на 32-битных системах выше Win-7, поскольку содержит в своём теле отсутствующие в ХР функции Win32-API. C небольшими правками его можно заточить и под х64, подправив прототипы функций и увеличив вдвое размеры полей всех структур. Требуемый инклуд "EnumHook.inc" лежит в скрепке:
5. Постскриптум
В данной статье планировалось охватить хотя-бы половину информации о технологии WinHook, но на практике это оказалось непосильной задачей с выхлопом ~30%. Поэтому расценивать её следует как попытку просто заинтересовать читателя, в меру не демократичными (с точки зрения системы безопасности Win) возможностями хуков. А развивать её дальше или нет, зависит от интересов конкретного индивидуума. По-большому счёту, в этом ключе интерес представляет только вполне легальный метод инжекта своих DLL в адресное пространство чужих процессов. Остальное так.. для самообразования.
В скрепке можно найти исполняемые файлы приведённых выше исходников, а так-же инклуд с описанием критических структур, для поиска и сбора информации об установленных в системе хуков. Всем удачи, пока!
Оглавление:
1. Основной посыл;
2. Функция SetWindowsHookEx();
3. Практика;
• задачи библиотеки DLL;
• код приложения-шпиона;
4. Алгоритм обнаружения хуков в системе;
5. Постскриптум.
--------------------------------------------------1. Основной посыл
Фундамент программной модели ОС держится на оконных сообщениях "Window Message". Всего в системе зарегистрировано порядка 1000 их типов с префиксом WM_***, а внутри оконной процедуры программист может фильтровать и обрабатывать предписанные конкретным алгоритмом, только часть из них. При нажатии пимпы на клавиатуре или какого-нибудь буттона в окне, управляемый драйвером Win32k.sys диспетчер форточек Dwm.exe (Desktop Window Manager) адресует нашему приложению соответствующую мессагу. Далее, движимый системным механизмом, её код (вместе с уточняющими аргументами wParam и lParam) поступает в очередь-сообщений текущего потока "Virtual Input Queue" (VIQ) , и только от туда – в процедуру окна, которая должна обработать его приблизительно по такому сценарию:
C-подобный:
;// Типичная процедура фильтрации оконных сообщений
;//------------------------------------------------
proc DialogProc hndl,msg,wParam,lParam
cmp [msg],WM_INITDIALOG ;// проверить код поступившего сообщения
je @wminit ;// если равно: уйти на обработчик, на выходе из которого вернуть EAX=1.
cmp [msg],WM_COMMAND ;// фильтр кода на другие сообщения
je @wmcmd ;// ^^^^
cmp [msg],WM_KEYDOWN ;// ^^^^
je @wmkeydown ;// ^^^^
cmp [msg],WM_CLOSE ;// ^^^^
je @wmclose ;// ^^^^
xor eax,eax ;// иначе: возвратить диспетчеру EAX=0,
ret ;// что отсеит не нужные сообщения.
endp
Однако часто в программировании возникает задача переопределения функциональности существующей оконной процедуры – это т.н. "субклассинг". В родственном ему варианте, системный надзор позволяет на законных основаниях организовать перехват и обработку любого сообщения, после чего возвратить его по конвейеру стандартной процедуре окна. Махинации подобного рода известны под общим названием "Hook" (ловушка) или "Grab". Последнее звучит грубо, зато по-честному, ведь появляется возможность реально грабить данные с приложения, которое в фокусе на текущий момент. Особый трепет вызывают ситуации, когда подвергшееся атаке окно лежит не в нашей компетенции, а например принадлежит интернет-браузеру, куда ничего не подозревающий юзер беспечно вводит пароль.
Судя по-всему, продумАны из Microsoft слишком поздно поняли, что "натворили делов" предоставив пользователям поддержку хуков. Они с огромной радостью отказались-бы от них сейчас, но поезд ушёл, т.к. к хукам привязан целый класс API из либы user32.dll типа: RegisterHotKey() для регистрации горячих клавиш, RegisterShellHookWindow() и ещё вагон подобных функций, где фигурирует слово хук. Более того, хуки использует и легальный софт сторонних разработчиков таких-как "PuntoSwitcher", который включив свой радар парсит клавиатурный ввод и на автомате меняет раскладку клавы RU/ENG. Теперь отказ от хуков повлечёт за собой судебные разбирательства с клиентами, чего в своём шатком положении мелкософт позволить себе не может.
Таким образом, начиная ещё с Win-98 хуки были, есть и будут, предоставляя в наше распоряжение весьма богатый арсенал. В сети предостаточно информации на данную тему, но в этом и проблема, поскольку где-то она разнится, а где-то на порядок устарела (особенно описание структур) и не понятно, кому именно доверять. По этой причине, предлагаю выбрать иной путь и вооружившись авторитетным отладчиком WinDbg, попрактиковаться на реальном его выхлопе – тогда проколов уж точно не будет.
Тема хуков достаточно обширна, а потому в данном обзоре мы рассмотрим лишь универсальную ловушку WH_GETMESSAGE, которая позволит собирать буквально все типы оконных сообщений, а внутри процедуры-фильтра потом можно будет отсеивать не нужные. Позиционно, этот тип хуков занимает место в системном "трубопроводе" аппаратных сообщений, как показано на рисунке ниже:
Значит три нижних блока – это процедуры "обработки сообщений" пользовательских процессов. Когда потоки этих процессов вызывают функцию GetMessage(), система снимает с очереди VIQ очередное сообщение (у каждого процесса очередь своя) и доставляет её приложению. Но как видим, установленный нами хук WH_GETMESSAGE внедряется между двух собеседников и перехватывает весь их диалог. Важно отметить, что кроме нас, ставить хуки могут и другие приложения, поэтому в системе всегда присутствует цепочка их них, и соответственно после обработки очередного события нужно передавать мессагу как эстафетную палочку дальше, посредством функции СallNextHookEx(). Если мы последние, кто устанавливал общесистемную ловушку, то становимся первым в этой цепочке, по принципу кольцевого буфера LIFO – LastIn / FirstOut (последним пришёл, первым выйдешь).
Особого внимания заслуживают хуки нижнего уровня(LL), или Low-Level-Hook. Ловушки данного типа относятся к аппаратным и их всего две. Поскольку они взаимодействуют непосредственно с драйверами ядра, то процедуры их обработки оказывают влияние на всю систему. От сюда следует, что фильтры(LL) вполне могут находиться прямо внутри нашего приложения и область их действия по любому будет глобальна, в то время как ловушку WH_GETMESSAGE нужно оформить в отдельной библиотеке DLL, чтобы система одним выстрелом дробью смогла внедрить её сразу во-все процессы ОС.
Однако хуки(LL) плохо себя зарекомендовали и по возможности их следует избегать. Во-первых это жуткие тормоза и падение производительности, а во-вторых ограниченное в 0,5-сек время обработки события Callback-фильтром (порог хранится в реестре). Если мы не уложимся в заданный период времени, то обитающий в Win32k.sys диспетчер окон просто отберёт у нас сообщение и передаст его следующему хуку в цепочке, даже никак не оповестив нас об этом. По сути, это вынужденные меры, т.к. незапланированный мёртвый цикл внутри фильтра(LL) завесит не только нашу программу, но и всю операционную систему – спасти её сможет только ребут, с полной потерей не сохранённых данных.
2. Функция SetWindowsHookEx()
Для работы с хуками в составе библиотеки user32.dll имеются три функции: SetWindowsHookEx(), CallNextHookEx() и UnhookWindowsHookEx(). Их имена говорят сами за себя – установка, передача сообщения следующему в цепочке, и анулирование. Общая информация на эту тему зарыта на сайте MSDN по
Ссылка скрыта от гостей
.Хуки можно устанавливать или только на один какой-нибудь поток (например свой, и тогда его именуют локальным), или-же сразу на все рабочие процессы в системе. В последнем случае хук считается глобальным, и фильтрующая его Callback-процедура должна обязательно находиться в отдельной DLL, которая и будет внедряться в пространство каждого процесса. Организовать глобальные хуки без инжекта нашей DLL в чужие треды просто нельзя, т.к. у каждого приложения своё виртуальное пространство, и при обычных обстоятельствах они в упор не видят друг-друга. Только учтите, что получив такие полномочия, не нужно на радостях сразу "рвать баян" – всё должно быть в пределах этической нормы, ато можно на несколько лет занять свободные апартаменты, и не кому будет кормить кота.
Значит функция SetWindowsHookEx() может устанавливать как локальные, так и глобальные хуки – вот её прототип для обоих случаев. Она принимает на вход 4-аргумента, а на выходе возвращает нуль при ошибке, иначе дескриптор хука для его отмены, или передачи в цепочку:
C-подобный:
;//************************************************************
;// Установка локального хука в своём потоке
;//************************************************************
invoke SetWindowsHookEx, WH_GETMESSAGE,\ ;// тип ловушки
msgHookProc,\ ;// указатель на процедуру-фильтра
0,\ ;// нуль = локальный хук!
invoke GetCurrentThreadId ;// идентификатор нашего (или чужого) потока
mov [hookHndl],eax ;// запомнить дескриптор хука в переменной.
;//************************************************************
;// Установка глобального хука на всю систему
;//************************************************************
invoke GetModuleHandle,<'hook.dll',0> ;// адрес нашей DLL в памяти
push eax ;// запомнить его
invoke GetProcAddress,eax,<'msgHookProc',0> ;// адрес процедуры-фильтра внутри DLL
pop ebx ;// EBX = адрес библиотеки, EAX = адрес процедуры в ней
invoke SetWindowsHookEx, WH_GETMESSAGE,\ ;// тип ловушки
eax,\ ;// линк на процедуру-фильтра в DLL
ebx,\ ;// адрес DLL
0 ;// нуль = глобальный хук!
mov [hookHndl],eax ;// запомнить его дескриптор в переменной.
Смысл в том, что мы определяем свою хук-процедуру, и при помощи данной функции передаём её адрес системе. Теперь, если кто-нибудь в глобальной ОС попытается вызвать из библиотеки user32.dll функцию GetMessage(), то сняв с очереди запрашиваемое сообщение, системный диспетчер передаст полный его контекст сначала нашей процедуре, а после трепанации, мы должны будем возвратить мессагу обратно диспетчеру CallNextHookEx(). Дальнейшие действия ОС нам не интересны, поскольку это уже диалог системы с вызывающим. Такой алгоритм позволяет не только "заглядывая через плечо читать чужие письма" (В.Высоцкий), но и модифицировать их в рамках своей совести.
Рассматриваемый нами хук типа WH_GETMESSAGE не один в своём роде – их там целая секта, с адептами которой можно ознамиться или
Ссылка скрыта от гостей
, или-же в таблице ниже. В данном треде я не спроста выбрал для примера именно этот хук. Во-первых он универсальный и позволяет фильтровать буквально все оконные сообщения, а во-вторых идеально подходит для программирования кейлоггеров, т.к. в отличии от того-же WH_KEYBOARD возвращает уже готовый ascii-код нажатой клавиши в виде сообщения WM_CHAR (учитывая верхний/нижний регистр и раскладку клавиатуры), и нам остаётся лишь сбрасывать этот чар в лог:Не смотря на такой зоопарк, все Callback-процедуры их фильтров имеют одинаковый прототип, и на входе принимают по три аргумента – всегда имеется
iCode
, wParam
и lParam
. Однако содержимое этих аргументов в корне отличаются для каждого типа ловушек. К примеру если мы ставим всё тот-же хук WH_KEYBOARD, то в аргументе wParam
получим ascii-код нажатой клавиши (причём без учёта раскладки/регистра, и чтобы привести его в привычный нам вид, нужно дополнительно звать TranslateMessage()), а в аргументе lParam
будет лежать скан-код + кол-во повторов. Но если мы решим поставить хук WH_GETMESSAGE, то в аргументе wParam
диспетчер передаёт нам флаг удаления (или нет) мессаги из системной очереди сообщений, а в lParam
– указатель на структуру _MSG с полным описанием отправленного сообщения.Отметим, что из этой троицы, исключением является только аргумент
iCode
. Во-всех случаях он несёт в себе одинаковую инфо-нагрузку, и представляет собой число со-знаком. Если в этом аргументе лежит отрицательное число больше 0x80000000
, то мы не имеем права обрабатывать данное сообщение, и должны сразу отфутболить его обратно по CallNextHookEx(). Если-же в "Багдаде всё спокойно", ОС может завернуть в iCode
или константу HC_ACTION=0
(обработка разрешена), или-же HC_NOREMOVE=3
(это копия мессаги, а оригинал остался в очереди VIQ). Других числовых значений в аргументе iCode документацией не предусмотрено.Ниже представлен возможный вариант процедуры-фильтра рассматриваемого нами хука WH_GETMESSAGE. Как было сказано выше, в аргументе
lParam
получим линк на структуру _MSG с исчерпывающей информацией о принятом сообщении. Из этой структуры нужно будет взять поле "message" с типом сообщения, и проверить его на WM_CHAR (готовая к употреблению печатная клавиша Charset):
C-подобный:
.data
WM_CHAR = 0102h ;// константа с типом сообщения
hookHndl dd 0 ;// переменная под дескриптор хука
struct MSG ;//<---- Cтруктура с описанием сообщения
hwnd dd 0 ;// дескриптор окна, которому послана мессага
message dd 0 ;// тип сообщения: WM_KEYDOWN/UP, WM_CHAR и т.д. (всего 1000 штук)
wParam dd 0 ;// ASCII-код нажатой клавиши
lParam dd 0 ;// скан-код + кол-во повторов
time dd 0 ;// время прибытия сообщения
pt dd 0,0 ;// координаты X/Y курсора мыши на момент мессаги
ends
;//************************
.code
proc msgHookProc iCode, wParam, lParam
push eax
mov eax,[iCode] ;// взять в EAX системный код
or eax,eax ;// проверить его флагами на отрицательное число
js @next ;// пропустить, если флаг SF=1 (sign-flag)
;//<-------- значит обработка разрешена
mov esi,[lParam] ;// взять в ESI указатель на структуру MSG
mov eax,[esi+MSG.message] ;// EAX = "тип сообщения" из этой структуры
cmp eax,WM_CHAR ;// проверить его на печатную клавишу
jne @next ;// прокол.. что угодно, только не WM_CHAR
;//===== Иначе: здесь обрабатываем нажатую клавишу на своё усмотрение,
;//===== после чего отправляем сообщение по цепочке дальше..
@next: pop eax
invoke CallNextHookEx,[hookHndl],[iCode],[wParam],[lParam]
ret
endp
3. Практика
Думаю для теории написано более-чем предостаточно букаф, и теперь займёмся практикой.
В своей демке, я установлю глобальный хук и буду фильтровать в нём нажатие клавиш, по-ходу сбрасывая их в форточку MessageBox(). Как трансформировать сей код в реальный кейлоггер додумайте сами. Только авансом скажу, что начиная с Win-7 задача эта носит нетривиальный характер и мыслить здесь придётся шире, т.к. системный протекторат запретил нам внутри процедуры-фильтра использовать стандартные методы записи в файл.
Десантировав развед-отряд на вражескую территорию я обнаружил, что ни одна из функций типа WriteFile(), _lwrite(), fprintf() не справляется со-своими обязанностями, и приходится искать обходные пути. Видимо достала мелкософт проблема вездесущих кейлоггеров, и они как-обычно решили рубануть с плеча.
Но ведь никто не запрещает нам сначала копить пользовательский ввод в выделенном для всех процессов общем буфере (см. CreateFileMapping()), и только потом из основного приложения сливать его содержимое в лог-файл. Ну или создать расшаренную (shareable) секцию в DLL, тогда объявленные в ней данные будут доступны всем экземплярам этой библиотеки. Блуждание именно в таких коридорах кода позволяют программисту накапливать собственный скилл, в то время-как тупая копипаста наоборот атрофирует мозг. Больше практикуйтесь, насилуйте отладчик и всё получится.
3.1. Задачи библиотеки DLL
Выше упоминалось, что процедура-фильтра глобального хука должна находиться в DLL – это обязательное условие для ловушки типа WH_GETMESSAGE. После того как мы загрузим данную библиотеку в своё пространство и установим хук, система осуществит её инжект буквально во-все процессы, ..только не сразу, а в момент срабатывания нашего условия WH_CHAR. То-есть наша либа загрузится в чужое пространство при первом нажатии клавиши в его окне. Вот как это выглядит в отладчике OllyDbg:
Как программировать библиотеки на ассемблере я рассматривал в одной из прошлых статей "DLL инструкция к метле", поэтому повторяться не буду. Достаточно будет сказать, что либа должна иметь свою точку-входа в виде документированной процедуры DllMain(), внутри которой можно провести её инициализацию по флагу DLL_PROCESS_ATTACH (факт загрузки dll в память процесса). С форматом самого Callback-фильтра хука мы уже знакомы, а я добавил в него лишь алго получения имени активного окна. Это можно организовать всего двумя функциями – GetActiveWindow() и GetWindowText(). Вот пример:
C-подобный:
format pe gui dll at 00100000h ;// cобираем DLL с дефолтной базой 0x100000
include 'win32ax.inc' ;// ..всё-равно ASLR изменит эту базу.
entry DllEntry
;//----------
.data
winText db 128 dup(0) ;// буфер под имя окна
hookHndl dd 0 ;// под дескриптор хука для CallNextHookEx()
buff db 0 ;// буфер под спецификаторы wsprintf()
;//----------
.code
start:
proc DllEntry hinstDll,fdwReason,lpReserve ;// точка-входа в библиотеку
cmp [fdwReason],DLL_PROCESS_ATTACH ;// проверить на первую загрузку образа
jnz @f ;// пропустить, если мы уже в памяти EXE
cinvoke printf,<10,' DLL loaded OK!',\ ;// иначе: даём о себе знать!
10,' Base address: 0x%08X',0>,[hinstDll]
@@: mov eax,1 ;// инициализация ОК! можно продолжать..
ret
endp
;//===== Callback-процедура фильтра ловушки ==========================
proc msgHookProc iCode,wParam,lParam
push eax
cmp [iCode],0 ;// проверить системный код
jne @next ;// пропустить, если не нуль
mov esi,[lParam] ;// иначе: указатель на структуру MSG
mov eax,[esi+MSG.message] ;// получить тип-сообщения
cmp eax,WM_CHAR ;// проверить его на WM_CHAR
jne @next ;// если нет..
;// иначе: сбросить в буфер ASCII-код клавиши
cinvoke wsprintf,buff,<10,'MSG.wParam = WM_CHAR = 0x%02X = "%c"',0>,\
[esi+MSG.wParam],[esi+MSG.wParam]
invoke GetActiveWindow ;// узнать заголовок окна,
invoke GetWindowText,eax,winText,128 ;// ..в котором осуществлялся ввод.
invoke MessageBox,0,buff,winText,0 ;// сбоксить мессагу в форточку!
@next: pop eax
invoke CallNextHookEx,[hookHndl],[iCode],[wParam],[lParam]
ret
endp
;//===== Вспомогательная процедура для передачи дескриптора хука,
;//===== и статической загрузки DLL в память EXE.
proc Setup arg1
mov eax,[arg1]
mov [hookHndl],eax
ret
endp
;//=== Секция-импорта системных Dll ===========
section '.idata' import data readable
library msvcrt,'msvcrt.dll',user32,'user32.dll'
import msvcrt, printf,'printf'
include 'api\user32.inc'
;//=== Секция-экспорта своих функций ==========
section '.edata' export data readable
export 'hookDLL.dll',msgHookProc,'msgHookProc',Setup,'Setup' ;// перечисляем их..
;//=== Секция-релоков (поправок) ==============
section '.reloc' fixups data readable discardable
Обратите внимание на вспомогательную процедуру Setup(). Чтобы не загружать библиотеку динамически через LoadLibrary(), я просто вызываю функцию из неё, и она на автомате грузиться в пространство основного файла EXE (статический вызов). Но раз-уж имеем обращение к ней, то по-ходу можно передать в аргументе и дескриптор хука, который создаёт родительский процесс. Кстати скомпилированную библиотеку нужно будет обязательно назвать "hookDll.dll", поскольку в секции-экспорта либы и секции-импорта основного приложения прописано именно такое имя.
3.2. Код приложения-шпиона
Здесь всё просто и не требует особых пояснений. Во-первых мы статически подгружаем нашу либу потянув за её процедуру Setup(), после чего ставим хук типа WH_GETMESSAGE и передаём его дескриптор в DLL – вот и весь алго на данный момент..
Однако имеется нюанс, который преподнесли нам операционные системы начиная с Win-7 и выше. В сети спорят о том, что мол хуки могут обрабатывать только приложения с графическим интерфейсом GUI, но никак не консольный софт. Аргументируется это тем, что система не посылает консоли сообщения типа WM_CHAR, а только WM_KEYDOWN/UP (нажатие и отпускание клавиш). Это-же касается и некоторых других мессаг, которые в принципе не могут существовать в консольных приложениях, на подобии кнопок Button, заголовка меню и т.п. Нужно сказать, что это утверждение не актуально для современных систем, т.к. консоль сейчас в корне преобразилась и её обслуживает специальный процесс Conhost.exe (см.диспетчер задач). К деталям этой темы мы вернёмся в следующей главе, а пока посмотрим на код приложения хуков:
C-подобный:
format pe console
include 'win32ax.inc'
entry start
;//----------
.data
hHndl dd 0
buff dd 0
;//----------
.code
start: invoke SetConsoleTitle,<'***KeyLogger v0.1***',0>
;//===== Загружаем в своё пространство hookDll.dll (пока без дескриптора)
invoke Setup,0
;//===== Ставим глобальный хук с фильтром внутри DLL
invoke GetModuleHandle,<'hookDll.dll',0>
push eax
invoke GetProcAddress,eax,<'msgHookProc',0>
pop ebx
invoke SetWindowsHookEx,WH_GETMESSAGE,eax,ebx,0
mov [hHndl],eax
;//===== Передаём дескриптор ловушки в DLL и выводим его на консоль
cinvoke printf,<10,' Hook handle.: 0x%08X',10,10,0>,eax
invoke Setup,[hHndl]
;//===== Глобальный хук будет активен,
;//===== пока мы не нажмём в этой консоли клавишу "Enter"
@exit: cinvoke gets,buff
invoke UnhookWindowsHookEx,[hHndl] ;// снимаем ловушку с цепочки
cinvoke exit,0
;//----------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',hookDll,'hookDll.dll',kernel32,'kernel32.dll',user32,'user32.dll'
import msvcrt, printf,'printf',gets,'gets',exit,'exit'
import hookDll,msgHookProc,'msgHookProc',Setup,'Setup'
include 'api\kernel32.inc'
include 'api\user32.inc'
На скрине ниже видно, что даже такой незамысловатый код прекрасно внедрил свою DLL во-всевозможные процессы (см.заголовки форточек), и из структуры MSG можно вытягивать ascii-коды нажатых клавиш. Более того можно сделать вывод, что технология ASLR (Advanced Space Layout Randomization) на моей Win7 вполне себе бодрствует, поскольку мы задавали базовый адрес в исходнике DLL
= 0x100000
, а в итоге получили совсем другую 0x6DC80000
.4. Методы обнаружения хуков
Теперь поговорим о более интересных вещах, и развернувшись на 180 градусов проследуем в обратном направлении. Глава повествует о том, как прямо из пользовательского уровня собрать информацию об установленных в системе хуках; сколько их, и кем они были внедрены в цепочку. Возможно-ли решить данную задачу, обладая только правами юзера? Оказывается да, ..а если мы сидим на системах Win7+, то сделать это ещё проще. На первый взгляд сей факт противоречит системе безопасности Win, однако инженерами всё продумано и без драйвера они предоставляют нам доступ исключительно на чтение. Но обо всё по порядку..
Начиная с NT-4.0 в графической подсистеме Win32 произошли координальные изменения. Чтобы логическая нить не увела нас далеко от темы, мы не будем обсуждать здесь все архитектурные новшества, а сделаем акцент только на драйвере Win32k.sys (часть подсистемы Win32 в kernel) и системном сервисе CSRSS.EXE (Client/Server Runtime SubSystem). В данном случае под клиентом подразумевается юзер, а под сервером – ядро операционной системы. В нёдрах именно этих двух файлов можно будет найти дескрипторы всех GUI-объектов, в число которых входят и ловушки Hook. Далее мы потянем за найденный дескриптор и выудим всю информацию о требуемом объекте.
Будем считать, что план нарисовали – теперь о его реализации..
Драйвер Win32k.sys является компонентом ядра и в наше время отвечает за большое кол-во функционала ОС, например буфер обмена, управление окнами, двумерную графику GDI в этих окнах (типа кнопки, полосы прокрутки) и многое другое. В свою очередь процесс csrss.exe находится в пространстве пользователя, являясь неотъемлемой частью ядра. На этот процесс возложены задачи по созданию процессов/потоков (но не регистрацию их), поддержку консоли, и графического окна завершения работы Win.
Но так было не всегда. Например в Win-NT 3 процесс csrss.exe представлял собой достаточно творческую единицу. Именно в нём находился диспетчер окон и интерфейс GDI (Graphic Device Interface), которые перекочевали в ядро только с приходом NT-4 лишь для того, чтобы исключить потенциальные проблемы с медленной прорисовкой окон. Рисунок ниже отображает эти изменения в визуальной форме:
Обратите внимание, что хоть увесистая часть сервиса csrss.exe и была перемещена в пространство Kernel, инженеры всё-же оставили её копию для юзерских нужд. В талмудах по системному программированию эту копию назвали SharedInfo. Система раздаёт экземпляры данной таблицы буквально всем пользовательским приложениям, в том числе и нашему. Она оформлена в виде одноимённой структуры, а её адресом в юзерском пространстве является точка-входа в функцию gSharedInfo() из библиотеки user32.dll (только для систем Win7+). Если-же система XP и ниже, то адрес этой шары можно получить обратившись напрямую к системному сервису csrss.exe функцией CsrClientConnectToServer() из Ntdll.dll.
C-подобный:
;//===== Поиск адреса SharedInfo на системах Win7+
;//==================================================
invoke LoadLibrary,<'user32.dll',0> ;// загрузили user32.dll
invoke GetProcAddress,eax,<'gSharedInfo',0> ;// ищем в ней точку-входа в функцию
mov [shareOffs],eax ;// EAX = адрес структуры SHAREDINFO
;//===== Поиск адреса SharedInfo на системах WinXP
;//==================================================
.data
objDir db '\\Windows',0
sz dd sizeof.USERCONNECT
buff dd 0
uc USERCONNECT
;//----------
.code
start: mov dword[uc.ulVersion],0x50000000
mov dword[uc.siClient],0
invoke CsrClientConnectToServer, objDir, 3, uc, sz, buff ;// в "uc.siClient" лежит SHAREDINFO
Допустим адрес получили.. двигаемся дальше..
Теперь нужно найти описание этой структуры, для чего воспользуемся ядерным отладчиком WinDbg. Команда из всего одной буквы(х) позволяет искать в нём структуры ядра по соответствующей маске, которую нужно поместить между двумя ёжиками(*) – вот что возвращает отладчик на моей 32-битной Win7:
Код:
lkd> x win32k!*sharedinfo*
9178f500 win32k!gSharedInfo = <no type information>
lkd> dt win32k!tagSharedInfo 9178f500
+0x000 psi : 0xff5e0578 tagSERVERINFO
+0x004 aheList : 0xff520000 _HANDLEENTRY
+0x008 HeEntrySize : 0xc
+0x00c pDispInfo : 0xff5e1728 tagDISPLAYINFO
+0x010 ulSharedDelta : 0
+0x014 awmControl : [31] _WNDMSG
+0x10c DefWindowMsgs : _WNDMSG
+0x114 DefWindowSpecMsgs : _WNDMSG
В этой структуре нам интересны всего четыре поля:
• psi – указатель на связанную структуру
SERVERINFO
;
• aheList – указатель на массив структур
HANDLEENTRY
, каждая из которых описывает один дескриптор. Это ключевая для нас структура, поскольку именно в ней зашита детальная информация об объекте окна;
• HeEntrySize – размер одной структуры
HANDLEENTRY
;
• ulSharedDelta – по задумке инженеров, это поле зарезервировано для значения, которое определяет разницу между адресом оригинальной таблицы в ядре, и её копией в пространстве юзера. В литературе этот адрес называют "дельта-смещением". Однако на практике, дельта из этого поля почему-то указывает в космос, и приходится считывать её валидное значение из структуры TEB текущего потока – Thread Information Block.
Теперь посмотрим на описание очередной структуры HANDLEENTRY, которая продолжает цепочку поиска установленных хуков. Оседлаем всё тот-же отладчик и запросим эту структуру по имени:
Код:
lkd> dt _handleentry
win32k!_HANDLEENTRY
+0x000 pHead : Ptr32 _HEAD
+0x004 pOwner : Ptr32 Void
+0x008 bType : UChar
+0x009 bFlags : UChar
+0x00a wUniq : Uint2B
• pHead – указатель на структуру с описанием GDI/USER-объекта. Поскольку мы рассматриваем сейчас хуки, то
pHead
будет указывать на структуру _HOOK с полным паспортом обнаруженной ловушки. Данная структура последняя, к которой мы сможем обратиться в пользовательском пространстве, поскольку далее все дорожки ведут уже в ядро, без отображения их в юзер-space. Вообще-то адрес в pHead
тоже ядерный (выше 0х80000000), но именно для этих целей нам нужна была "дельта", чтобы получить доступ к спроецированным в юзер, структурам ядра. То-есть мы берём значение из этого поля, и отняв от него дельту, получаем адрес ксерокопии ядерной структуры _HOOK.
• pOwner – указывает на структуру родительского объекта, которой может быть как
THREADINFO
, так и PROCESSINFO
. К сожалению у нас нет доступа к этим структурам т.к. они расположены в пространстве ядра. Иначе, в ней можно было-бы найти имя установившего хук процесса.
• bType – здесь лежит тип дескриптора, и содержит константу
TYPE_HOOK=0x05
в случае, если попавшаяся HANDLEENTRY описывает хук. Эта константа понадобится нам для того, чтобы оставив только дескрипторы хуков, отсеивать все остальные. Например на моей машине, в массиве на который указывает aheList
, насчитывается аж 3415-структур HANDLEENTRY с различными типами дескрипторов, и каждая из этих структур описывает свой объект окна. Нажав на пимпу ниже можно ознакомиться с полным листом.
Код:
TYPE_FREE = 0x00 ;//
TYPE_WINDOW = 0x01 ;//
TYPE_MENU = 0x02 ;//
TYPE_CURSOR = 0x03 ;//
TYPE_SETWINDOWPOS = 0x04 ;//
TYPE_HOOK = 0x05 ;// <--- наш клиент
TYPE_CLIPDATA = 0x06 ;//
TYPE_CALLPROC = 0x07 ;//
TYPE_ACCELTABLE = 0x08 ;//
TYPE_DDEACCESS = 0x09 ;//
TYPE_DDECONV = 0x0a ;//
TYPE_DDEXACT = 0x0b ;//
TYPE_MONITOR = 0x0c ;//
TYPE_KBDLAYOUT = 0x0d ;//
TYPE_KBDFILE = 0x0e ;//
TYPE_WINEVENTHOOK = 0x0f ;//
TYPE_TIMER = 0x10 ;//
TYPE_INPUTCONTEXT = 0x11 ;//
TYPE_CTYPES = 0x12 ;//
TYPE_GENERIC = 0xff ;//
Так, решив весьма занятную комбинаторику, мы получим описатель каждого хука в системе, и остаётся выводить на консоль данные из очередной структуры _HOOK. В конечном счёте, общем схема блуждания по тёмным переулкам системы будет выглядеть как на рисунке ниже (значимые поля в структурах выделены красным):
Теперь остаётся найти валидную дельту, и завернуть всё/это хозяйство в законченный код.
Выше упоминалось, что дельта прописана в структуре TEB каждого потока, а значит мы без труда её сможем от туда утянуть. На системах х32 она лежит во-вложенной в ТЕВ структуре CLIENTINFO по смещению
0x06CC
от начала, а если система 64-разрядная, то смещение равно 0х0800
. Это достаточно объёмная структура размером 62 дворда, а необходимая нам дельта хранится в седьмом на х32, или пятом на х64 дворде – поле называется "ulClientDelta".
Код:
struct CLIENTINFO ;//<----------;// (TEB.Win32ClientInfo = макс.62-дворда)
CI_flags dd 0 ;// [0] +00
cSpins dd 0 ;// [1] +04
dwExpWinVer dd 0 ;// [2] +08
dwCompatFlags dd 0 ;// [3] +12
dwCompatFlags2 dd 0 ;// [4] +16
dwTIFlags dd 0 ;// [5] +20
pDeskInfo dd 0 ;// [6] +24
ulClientDelta dd 0 ;// [7] +28 = дельта в кернел для х32, для х64 = [5]
phkCurrent dd 0 ;// [8] +32
fsHooks dd 0 ;// [9] +36
CallbackWnd dd 0 ;// [10] +40 = CALLBACKWND
dwHookCurrent dd 0 ;// [11] +44
cInDDEMLCallback dd 0 ;// [12] +48
pClientThreadInfo dd 0 ;// [13] +52 = CLIENTTHREADINFO
dwHookData dd 0 ;// [14] +56
dwKeyCache dd 0 ;// [15] +60
afKeyState dq 0 ;// [16] +64
dwAsyncKeyCache dd 0 ;// [17] +68
afAsyncKeyState dq 0 ;// [18] +72
afAsyncKeyStateDown dq 0 ;// [19] +76
hKL dd 0 ;// [20] +80
CodePage dw 0 ;// [21] +84
achDbcsCF dw 0 ;// [22] +88
msgDbcsCB dd 0 ;// [23] +92 = tagMSG
lpdwRegClasses dd 0 ;// [24] +96
ends
Ну и теперь собственно исходный код приложения, которое в цикле пропарсит все найденные структуры _HOOK, в юзерской копии SHAREDINFO. Этот код будет работать только на 32-битных системах выше Win-7, поскольку содержит в своём теле отсутствующие в ХР функции Win32-API. C небольшими правками его можно заточить и под х64, подправив прототипы функций и увеличив вдвое размеры полей всех структур. Требуемый инклуд "EnumHook.inc" лежит в скрепке:
C-подобный:
format pe console
include 'win32ax.inc'
include 'equates\EnumHook.inc'
entry start
;//----------
.data
hookCodeTable dd wh00,wh01,wh02,wh03,wh04,wh05,wh06,wh07
dd wh08,wh09,wh10,wh11,wh12,wh13,wh14
wh00 db '00 = WH_JOURNALRECORD',0
wh01 db '01 = WH_JOURNALPLAYBACK',0
wh02 db '02 = WH_KEYBOARD',0
wh03 db '03 = WH_GETMESSAGE',0
wh04 db '04 = WH_CALLWNDPROC',0
wh05 db '05 = WH_CBT',0
wh06 db '06 = WH_SYSMSGFILTER',0
wh07 db '07 = WH_MOUSE',0
wh08 db '08 = WH_HARDWARE',0
wh09 db '09 = WH_DEBUG',0
wh10 db '10 = WH_SHELL',0
wh11 db '11 = WH_FOREGROUNDIDLE',0
wh12 db '12 = WH_CALLWNDPROCRET',0
wh13 db '13 = WH_KEYBOARD_LL',0
wh14 db '14 = WH_MOUSE_LL',0
wh15 db '-1 = WH_MSGFILTER',0
lHook db 'Local',0
gHook db 'Global',0
counter dd 0
shareOffs dd 0
stdOut dd 0
delta dd 0
buff dd 0
;//----------
.code
start: invoke SetConsoleTitle,<'***Enum Hook v0.1***',0>
invoke GetStdHandle,STD_OUTPUT_HANDLE
mov [stdOut],eax
;//===== Динамически грузим user32.dll в своё пространство,
;//===== и получаем адрес главной структуры SHAREDINFO
invoke LoadLibrary,<'user32.dll',0>
invoke GetProcAddress,eax,<'gSharedInfo',0>
mov [shareOffs],eax
;//===== Выводим из этой структуры основные данные
mov ebx,[eax+SHAREDINFO.psi]
mov ecx,[eax+SHAREDINFO.aheList]
mov edx,[ebx+SERVERINFO.HandleEntries]
cinvoke printf,<10,' SHARED info = 0x%08X',\
10,' SERVER info = 0x%08X',\
10,' HANDLE entry = 0x%08X',\
10,' Total entries = %u',\
10,' *******************************',\
10,' Found hooks = ',0>,eax,ebx,ecx,edx
;//===== Находим дельту в пространство юзера
invoke NtCurrentTeb
xchg esi,eax
lea eax,[esi+TEB.Win32ClientInfo]
mov eax,[eax+CLIENTINFO.ulClientDelta]
mov [delta],eax
cinvoke printf,<10,' Kernel delta = 0x%08X',\
10,' Hook information...',10,0>,eax
;//===== ОСНОВНОЙ ЦИКЛ ПОИСКА ДЕСКРИПТОРОВ ЛОВУШЕК
mov esi,[shareOffs]
mov ebx,[esi+SHAREDINFO.psi]
mov ecx,[ebx+SERVERINFO.HandleEntries]
mov esi,[esi+SHAREDINFO.aheList]
@findHook:
push esi ecx
movzx eax,[esi+HANDLEENTRY.bType] ;// берём тип дескриптора
cmp eax,TYPE_HOOK ;// проверим его на 5
jne @fuck ;// прокол..
inc [counter] ;// иначе: счётчик найденных +1
;// Основная информация из заголовка
mov ebx,[esi+HANDLEENTRY.pOwner]
mov ecx,[esi+HANDLEENTRY.pHead] ;// указатель на структуру HOOK ядра
sub ecx,[delta] ;// ..преобразуем его в юзер.
push ecx
cinvoke printf,<10,' HandleType..........: %d',\
10,' KPROCESS (pOwner)..: 0x%08X',\
10,' HookStruct (pHead)..: 0x%08X',0>,eax,ebx,ecx
;// Локальный или глобальный нашли хук ?
pop esi
cmp [esi+HOOK.dHooked],0
mov ebx,lHook
jne @f
mov ebx,gHook
@@: cinvoke printf,<10,' Hook mode........: %s',0>,ebx
;// Паспорт найденного хука
mov ebx,[esi+HOOK.dHookType] ;// тип хука: WH_GETMESSAGE или иной
cmp ebx,-1
jne @f
mov ebx,wh15
jmp @prn
@@: shl ebx,2 ;// получаем смещение в таблице по номеру
add ebx,hookCodeTable
mov ebx,[ebx]
@prn: cinvoke printf,<10,' Next hook........: 0x%08X',\
10,' Hook type........: %s',\
10,' Callback func....: 0x%08X',\
10,' Flags............: 0x%08X',\
10,' DLL handle link..: 0x%08X',\
10,' Hooked process...: 0x%08X',10,0>,\
[esi+HOOK.pNextHook],ebx,[esi+HOOK.pFuncAddress],\
[esi+HOOK.dFlags],[esi+HOOK.dModuleHandle],[esi+HOOK.dHooked]
;// Хвост основного цикла
@fuck: pop ecx esi ;// ECX = счётчик структур в массиве
add esi,sizeof.HANDLEENTRY ;// прыгаем к следующей HANDLEENTRY в массиве
dec ecx ;// счётчик -1
jnz @findHook ;// повторить цикл, пока ECX не станет нуль.
;//===== Покажем общее число найденных хуков!
invoke SetConsoleCursorPosition,[stdOut],0x60011
cinvoke printf,<'%d',0>,[counter]
@exit: cinvoke gets,buff
cinvoke exit,0
;//---------------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',ntdll,'ntdll.dll',\
kernel32,'kernel32.dll',user32,'user32.dll'
import msvcrt, printf,'printf',gets,'gets',exit,'exit'
import ntdll, NtCurrentTeb,'NtCurrentTeb'
include 'api\kernel32.inc'
include 'api\user32.inc'
5. Постскриптум
В данной статье планировалось охватить хотя-бы половину информации о технологии WinHook, но на практике это оказалось непосильной задачей с выхлопом ~30%. Поэтому расценивать её следует как попытку просто заинтересовать читателя, в меру не демократичными (с точки зрения системы безопасности Win) возможностями хуков. А развивать её дальше или нет, зависит от интересов конкретного индивидуума. По-большому счёту, в этом ключе интерес представляет только вполне легальный метод инжекта своих DLL в адресное пространство чужих процессов. Остальное так.. для самообразования.
В скрепке можно найти исполняемые файлы приведённых выше исходников, а так-же инклуд с описанием критических структур, для поиска и сбора информации об установленных в системе хуков. Всем удачи, пока!