Статья WinHook – легальный шпионаж

Порой логика инженеров Microsoft не выдерживает никакой критики. В священных войнах с хакерами они сами вручают им мандат на использование уязвимых функций, после чего отлавливают их-же с претензиями на взлом. Из большого числа подобных API на передний план буйком выплывает SetWindowsHookEx(), при помощи которой одним кликом мыши можно внедрить "творческую" свою DLL сразу во все активные процессы системы. В итоге, даже обычный юзверь может перехватить любые действия пользователя, вплоть до логирования клавиатурного ввода. И что особенно важно, ничего криминального в этом нет, поскольку мелкософт сама предлагает нам эту функцию, оформив ей прописку в библиотеке user32.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, которая позволит собирать буквально все типы оконных сообщений, а внутри процедуры-фильтра потом можно будет отсеивать не нужные. Позиционно, этот тип хуков занимает место в системном "трубопроводе" аппаратных сообщений, как показано на рисунке ниже:


HookSheme.png


Значит три нижних блока – это процедуры "обработки сообщений" пользовательских процессов. Когда потоки этих процессов вызывают функцию 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 (учитывая верхний/нижний регистр и раскладку клавиатуры), и нам остаётся лишь сбрасывать этот чар в лог:

HookType.png


Не смотря на такой зоопарк, все 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:


TotalHook.png


Как программировать библиотеки на ассемблере я рассматривал в одной из прошлых статей
"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.


KeyLog.png



4. Методы обнаружения хуков

Теперь поговорим о более интересных вещах, и развернувшись на 180 градусов проследуем в обратном направлении. Глава повествует о том, как прямо из пользовательского уровня собрать информацию об установленных в системе хуках; сколько их, и кем они были внедрены в цепочку. Возможно-ли решить данную задачу, обладая только правами юзера? Оказывается да, ..а если мы сидим на системах Win7+, то сделать это ещё проще. На первый взгляд сей факт противоречит системе безопасности Win, однако инженерами всё продумано и без драйвера они предоставляют нам доступ исключительно на чтение. Но обо всё по порядку..


NT.png


Начиная с 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 лишь для того, чтобы исключить потенциальные проблемы с медленной прорисовкой окон. Рисунок ниже отображает эти изменения в визуальной форме:


NewNT.png


Обратите внимание, что хоть увесистая часть сервиса 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. В конечном счёте, общем схема блуждания по тёмным переулкам системы будет выглядеть как на рисунке ниже (значимые поля в структурах выделены красным):


FindHook.png


Теперь остаётся найти валидную дельту, и завернуть всё/это хозяйство в законченный код.
Выше упоминалось, что дельта прописана в структуре 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'

SystemHook.png



5. Постскриптум

В данной статье планировалось охватить хотя-бы половину информации о технологии WinHook, но на практике это оказалось непосильной задачей с выхлопом ~30%. Поэтому расценивать её следует как попытку просто заинтересовать читателя, в меру не демократичными (с точки зрения системы безопасности Win) возможностями хуков. А развивать её дальше или нет, зависит от интересов конкретного индивидуума. По-большому счёту, в этом ключе интерес представляет только вполне легальный метод инжекта своих DLL в адресное пространство чужих процессов. Остальное так.. для самообразования.

В скрепке можно найти исполняемые файлы приведённых выше исходников, а так-же инклуд с описанием критических структур, для поиска и сбора информации об установленных в системе хуков. Всем удачи, пока!
 

Вложения

  • WinHook.zip
    5,1 КБ · Просмотры: 192

Full-R

Заблокирован
06.09.2020
72
39
Как насчет Windows 10 pro? Тут Hyper-V с App Guard + изоляция памяти и вот такие настройки:

1.png
 

Marylin

Mod.Assembler
Red Team
05.06.2019
291
1 267
Как насчет Windows 10 pro? Тут Hyper-V с App Guard + изоляция памяти и вот такие настройки:

Хуки и на 10-ке работают, а все эти механизмы не оказывают на них влияния. Например, при обычных обстоятельствах инжект своей DLL включает три этапа - выделение памяти в чужом процессе, копирование туда своей библиотеки, и передача каким-либо образом на неё управления. Причём делать это придётся самостоятельно, пытаясь обойти гуарды.

В случае-же с хуками, ОС сама осуществляет инжект и передачу управления функции callback, и ей видней, как это нужно сделать правильно. Так-что ASLR и CFG нервно курят в сторонке. Туда-же отправляется и DEP (Data Execute Protect), т.к. в библиотеке есть легальные секции-кода/данных, и исполнение этих данных изначально не предусмотрено алгоритмом. К сожалению проверить на Win-10 сейчас не могу (нет в наличии системы), может-быть позже..
 

Full-R

Заблокирован
06.09.2020
72
39
Хуки и на 10-ке работают, а все эти механизмы не оказывают на них влияния. Например, при обычных обстоятельствах инжект своей DLL включает три этапа - выделение памяти в чужом процессе, копирование туда своей библиотеки, и передача каким-либо образом на неё управления. Причём делать это придётся самостоятельно, пытаясь обойти гуарды.

В случае-же с хуками, ОС сама осуществляет инжект и передачу управления функции callback, и ей видней, как это нужно сделать правильно. Так-что ASLR и CFG нервно курят в сторонке. Туда-же отправляется и DEP (Data Execute Protect), т.к. в библиотеке есть легальные секции-кода/данных, и исполнение этих данных изначально не предусмотрено алгоритмом. К сожалению проверить на Win-10 сейчас не могу (нет в наличии системы), может-быть позже..

Проверьте пожалуйста. Мне кажется большинство этого уже не работает.
 
  • Нравится
Реакции: Mari

pp11

Green Team
16.09.2018
200
81
Статья супер, правда иногда ассемблерный код читать трудновато)

Бинари ваши на 10 не работают, скорее всего из-за того, что они под 7.
Как освобожусь, то напишу код на плюсах и проверю работает ли это на 10. (это к комментариям выше)
 
  • Нравится
Реакции: Marylin

Marylin

Mod.Assembler
Red Team
05.06.2019
291
1 267
Бинари ваши на 10 не работают,
Система не загружает 32-битные библиотеки в процессы х64, а в моём примере HookDll именно 32-битная. Видимо от сюда и проблемы с Win10. На сайте Манхунтера есть пример х32 локального хука (без длл), и загружаясь из-под WOW он прекрассно отрабатывает на Win10. Это говорит о том, что хуки на десятке всё-же работают.
 

Вложения

  • KeybHook.zip
    3 КБ · Просмотры: 157
Последнее редактирование:
Мы в соцсетях: