BugTraq.Ru
Русский BugTraq
https://bugtraq.ru/library/programming/hookguard.html

Система обнаружения программ-шпионов типа keylogger методом перехвата системных сервисов
Марк Ермолов
Опубликовано: dl, 09.11.06 21:34

В моей предыдущей статье я описал принцип, который лежит в основе практически всех современных keylogger-ов, функционирующих в Windows системах. В этой статье я попытаюсь раскрыть механизм обнаружения программ такого типа, основанный на перехвате системных сервисов (system service) и анализа функции ловушки (hook procedure).

Для успешного понимания материала желательно иметь представление об архитектуре Windows NT, в частности владеть такими понятиями, как: ядро, исполнительная система, подсистема окружения и системные сервисы, а также некоторые знания архитектуры процессоров IA-32. В процессе изложения я при необходимости буду рассматривать некоторые системные механизмы более подробно.

Моя статья будет состоять из двух частей. В первой я рассмотрю механизм перехвата системных сервисов (в частности, перехват вызовов API-функций подсистемы Windows). Во второй части предложу алгоритм, для выявления программ-шпионов среди обычных программ, использующих те же функции.

Перехват системных сервисов

По определению (взятому из [1]) системный сервис (system service) - это функция, экспортируемая исполнительной системой Windows и доступная для вызова из пользовательского режима.

Вы когда-нибудь задумывались, где находится код, который при вызове API функции CreateFile выполняет настоящие (низкоуровневые) операции по созданию/открытию файла (обращение к драйверу жесткого диска для чтения/записи секторов, к драйверу файловой системы для модификации оглавления каталога, а так же поверку запрашиваемых прав доступа к файлу и т.д.). В kernel32.dll (экспортирует CreateFile)? Я тоже так когда-то думал...

Я постараюсь ответить на приведенный выше вопрос и начну это с краткого обзора архитектуры операционных систем семейства Windows NT - NT, 2000, XP, Server 2003, Vista (далее, под словом Windows я буду понимать именно эти системы).

Windows состоит из следующих основных компонент: ядра, исполнительной системы, ntdll.dll, подсистем окружения (subsystems). На более высоком уровне находятся пользовательские приложения и службы, использующие одну из подсистем окружения для выполнения своих задач.

Наиболее распространенной подсистемой является Windows (называвшаяся Win32, до появления 64-х разрядных версий операционных систем Windows. Отсюда и термин Win32 API...). Имеется также POSIX и OS/2 (которая, начиная с Windows 2000, была удалена!). Подсистема Windows предоставляет интерфейс прикладных программ (API), который хорошо документирован в Platform SDK и состоит из экспортируемых функций библиотек kernell32.dll, user32.dll, gdi32.dll и advapi32.dll. Таким образом, CreateFile - это API функция подсистемы Windows из kernel32.dll.

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

Для того чтобы организовать перехват API функций, нужно знать, где они реализованы и как они реализованы.

Процессоры архитектуры IA-32 имеют несколько уровней привилегий (далее CPL, current privilege level) выполнения машинного кода. То есть, если код выполняется с высокими привилегиями (CPL=0), он может сделать то, чего не сможет сделать код, выполняемый с низкими привилегиями (CPL=3). Например, выполнить инструкцию x86:

in al, 60h ; Прочитать байт из порта ввода-вывода 60h (регистр данных клавиатуры)
можно только в привилегированном (CPL=0) режиме процессора (при IO privilege level, IOPL=0). Если код, выполняемый на CPL=3, попытается выполнить данную команду, то процессор прервет его исполнение и сгенерирует исключение #GP (General Protection Error, IDT vector = 0Dh).

Режим процессора CPL=3 называется пользовательским режимом, CPL=0 - режимом ядра. CPL=1 и CPL=2 Windows не использует.

Очевидно, что для успешного выполнения API функции, часть ее функциональности обязана выполняться в режиме ядра. Можно сказать - практически вся. Так как же переключить процессор в режим ядра? Может для этого нужно что-то занести в определенный регистр или выполнить определенную инструкцию? Эх, если бы все было так просто ... ;-)). Архитектура IA-32 определяет интерфейс взаимодействия между пользовательским режимом и режимом ядра. Его смысл в том, что код пользовательского режима может (с помощью определенных инструкций) запросить некоторую функциональность режима ядра. То есть, вызвать функцию, которая будет выполняться в режиме ядра. Этой функцией и являются системные сервисы. Набор системных сервисов четко определен (операционной системой на этапе загрузки) и его нельзя дополнить из пользовательского режима (однако, его можно пополнить в режиме ядра, но об этом позже...). То есть, системные сервисы - это API режима ядра операционной системы. Большего того, что они позволяют, код пользовательского режима сделать никак не может. Причем, поскольку сервисы - это функции с набором параметров, код пользовательского режима сообщает только то, что нужно сделать и передает набор необходимых параметров, а уже сервис выполняет всю нужную работу, осуществляя контроль прав доступа и многое другое. В случае отсутствия прав, сервис возвращает знаменитый STATUS_ACCESS_DENIED и пользовательский код 'остается ни с чем'. Можно сказать, что системные сервисы - это основа современных, защищенных операционных систем. Концепция системных сервисов поддерживается аппаратно в последних версиях IA-32-совместимых процессоров (Pentium 2 и выше) и Windows использует эту аппаратную реализацию. Весомая часть системных сервисов Windows реализованы в файле ntoskrnl.exe как часть исполнительной системы. Системные сервисы поддержки окон и графики реализованы в win32k.sys.

Теперь можно ответить на вопрос, где находится исполнительный код для CreateFile: в системном сервисе NtCreateFile, расположенном в ntoskrnl.exe (попробуйте его дисассемблировать ;-) ).

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

На уровне ядра Windows существует такое понятие как таблица системных сервисов (System Service Table) . Эта таблица состоит из 4-х байтных элементов (можно сказать, что это массив элементов типа DWORD), каждый из которых определяет точку входа (содержит адрес функции) в соответствующий системный сервис. Так-так, то есть, если мы изменим один из ее элементов и запишем туда адрес своей функции, то при вызове соответствующего системного сервиса управление будет получать наша функция, а не системный сервис в ntoskrnl.exe. То, что нужно! Но, возникает вопрос, как получить доступ к таблице сервисов и как определить, какой элемент нужно изменить, чтобы перехватить конкретный системный сервис.

Рассмотрим следующий рисунок, демонстрирующий вызов API функции SetWindowsHookEx:

Первое, на что я хочу обратить внимание, это то, что существует не одна, а несколько таблиц системных сервисов. Всего их может быть 4. Две из них используются Windows, а две другие доступны для использования системным программным обеспеченьем (ядро предоставляет интерфейс для добавления этих двух user-defined таблиц - функцию KeAddSystemServiceTable). Каждая из таблиц описывается с помощью дескриптора (Service Table Descriptor), который указывает: адрес в памяти начала таблицы, ее размер (число системных сервисов) и некоторые другие атрибуты:

Дескриптор таблицы системных сервисов описывается структурой ядра _KSERVICE_TABLE_DESCRIPTOR, однако вы не найдете описания этой структуры в ntoskrnl.pdb. Скорее всего, ребята из Microsoft удалили ее преднамеренно, чтобы при отладке ядра нельзя было посмотреть ее описание с помощью команды dt отладчика WinDbg/kd.

Привожу вам ее описание прямо из исходников (private\ntos\inc\ke.h):

typedef struct _KSERVICE_TABLE_DESCRIPTOR {
    PULONG_PTR Base;
    PULONG Count;
    ULONG Limit;
#if defined(_IA64_)
    LONG TableBaseGpOffset;
#endif
    PUCHAR Number;
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;

Здесь поле Base содержит виртуальный адрес таблицы сервисов, Count -адрес таблицы счетчиков вызовов системных сервисов, Limit - число системных сервисов и Number - адрес таблицы аргументов. В контексте нашего рассуждения нам интересны только Base и Limit, поэтому другие поля я описывать не буду. Если кому интересно, можете посмотреть код диспетчера системных сервисов (KiFastCallEntry + KiSystemService в ntoskrnl.exe).

Далее, для того чтобы описать четыре сервисные таблицы, нужны четыре дескриптора. Привожу определение этих дескрипторов (private\ntos\ke\kernldat.c):

#define NUMBER_SERVICE_TABLES 4
...

//
// KeServiceDescriptorTable - This is a table of descriptors for system
//      service providers. Each entry in the table describes the base
//      address of the dispatch table and the number of services provided.
//

KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable[NUMBER_SERVICE_TABLES];
KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTableShadow[NUMBER_SERVICE_TABLES];

Что мы видим, оказывается дескрипторов не 4, а 8. Что за дела, зачем еще 4. Дело в том, что программисты из Microsoft, точнее David N. Cutler, для чего-то сделали именно так. Как я уже говорил, Windows использует 2-е сервисные таблицы: одну для сервисов, экспортируемых через kernel32.dll и одну для user32.dll + gdi32.dll (точнее сказать, для сервисов, вызываемых API функциями этих dll). Так вот, эти массивы дескрипторов дублируют друг-друга, за тем исключением, что дескриптора для USER+GDI (индекс равен 1) нет в , а он имеется только в KeServiceDescriptorTableShadow.

Очевидно, что поскольку таблиц несколько, а диспетчер один и тот же, нам нужно как-то идентифицировать таблицу. Идентификатором таблицы являются биты 12 и 13 числа, которое передается в регистре eax, перед выполнением инструкции sysenter. Таблица с сервисами kernel32.dll имеет индекс 0, с сервисами USER + GID - индекс 1.

В нашем примере для SetWindowsHookEx, в регистр eax передается 1225h. То есть, адрес системного сервиса NtUserSetWindowsHookEx находится в таблице с индексом 1. Остальные биты этого числа указывают индекс сервиса внутри соответствующей таблицы (225h).

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

  1. Получает индекс таблицы из битов 12 и 13 числа, переданного в регистре eax. Этот индекс начинается с 0, то есть 0 - первая таблица, 1- вторая и т.д.
  2. Получает индекс системного сервиса из остальных битов регистра eax
  3. Находит дескриптор для соответствующей таблицы, индексируя глобальную переменную KeServiceDescriptorTable (в случае, если индекс таблицы равен 1, то индексируется KeServiceDescriptorTableShadow).
  4. Умножая индекс сервиса на 4 и складывая с полем Base из дескриптора, получает адрес, по которому записан 4-х байтный адрес точки входа соответствующего сервиса.

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

Небольшое примечание: хотя для каждой таблицы, кроме USER+GDI, имеются два дескриптора, каждый из них указывает на одну и ту же таблицу. Дескриптор для USER+GDI, в массиве KeServiceDescriptorTable, нулевой, то есть содержит нули во всех полях.

Как же получить доступ к KeServiceDescriptorTable или KeServiceDescriptorTableShadow? Есть два пути: можно с помощью отладчика ядра (kd/WinDbg), применив команду "x nt!KeServiceDescriptorTable* ", получить адреса этих переменных, для вашей версии Windows:

У меня это выглядит так:

lkd> x nt!KeServiceDescriptorTable*
80559440 nt!KeServiceDescriptorTableShadow = <no type information>
80559480 nt!KeServiceDescriptorTable = <no type information>

Имея адрес, можно его жестко закодировать в программе (что, возможно, привяжет вас к конкретной версии Windows).

Есть другой путь для доступа к массиву дескрипторов: файл ntoskrnl.exe экспортирует символ KeServiceDescriptorTable, который есть не что иное, как наш массив дескрипторов. Имея его, нетрудно вычислить и KeServiceDescriptorTableShadow, отняв из его адреса 40h.

Вот и все с перехватом сервисов...

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

При реализации этой задачи я встретил много проблем... Основная проблема была в том, что таблица системных сервисов USER+GID, где и находится NtUserSetWindowsHookEx, располагается в адресном пространстве сеанса (session address space). Кто программировал kernel mode, наверно знает, что это такое. Поэтому, все действия по модификации этой таблицы нужно было производить в arbitrary thread context (не в system thread context). Эту задачу я решил так: поймал момент запуска процесса winlogon.exe (первый процесс, запускаемый в адресном пространстве сеанса) и выполнил APC (Asynchronous Procedure Call) режима ядра (кстати, тоже недокументированная функциональность) в его главном потоке. Все получилось...

Анализ функции ловушки

Как я уже говорил в предыдущей статье, любой keylogger пользовательского режима функционирующий в Windows, обязан вызвать функцию Windows API - SetWindowsHookEx экспортируемую библиотекой user32.dll. Конечно, если кто осмелится написать keylogger режима ядра, который перехватывает прерывание от клавиатуры (IRQ 1) и напрямую обращается к ее портам ввода-вывода (60h, 64h), то обнаружить его по предложенной мною схеме не удастся. Однако до сих пор я такой keylogger не встречал...

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

Начнем с описания системного сервиса NtUserSetWindowsHookEx, обслуживающего функцию SetWindowsHookEx.

Точка входа для этого сервиса находится по смещению 225h * 4, в таблице системных сервисов с индексом 1. Данную таблицу (да и собственно и ее сервисы) предоставляет драйвер режима ядра win32k.sys, который располагается в адресном пространстве сеанса. Этот драйвер отвечает за поддержку окон и графики подсистемы Windows (он как бы является ее частью, работающей в режиме ядра).

Этот системный сервис имеет следующий прототип:

HHOOK __stdcall NtUserSetWindowsHookEx(IN HANDLE hmod, IN PUNICODE_STRING 
   pstrLib OPTIONAL, IN DWORD idThread, IN int nFilterType, IN PROC 
   pfnFilterProc, IN DWORD dwFlags);

Почти все параметры этого сервиса соответствуют параметрам функции SetWindowsHookEx. Я не буду здесь описывать их подробно. Отмечу только, что в параметре idThread передается ID потока, на который нужно установить ловушку. В случае если устанавливается глобальная ловушка, которая будет срабатывать на сообщения, посылаемые всем потокам (то есть, ловушка на все процессы), параметр idThread равен 0. Параметр nFilterType содержит тип устанавливаемой ловушки (нам интересен WH_KEYBOARD и WH_KEYBOARD_LL). В параметре pfnFilterProc, передается адрес (в пользовательском адресном пространстве процесса, вызывающего этот сервис) функции, которая должна вызываться, в нашем случае, при нажатии на любую клавишу...

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

Далее, для определения того, является ли вызывающий процесс keylogger-ом, я предлагаю следующий алгоритм:

  1. Установить фильтр-драйвера на устройства, через которые возможно осуществлять запись информации на какие-либо носители (например, устройства, принадлежащие к Device Setup Class - DiskDrive, {4d36e967-e325-11ce-bfc1-08002be10318}).
  2. При вызове системного сервиса NtUserSetWindowsHookEx, получить адрес функции ловушки, которая передается в pfnFilterProc.
  3. Вызывать искусственно переданную функцию ловушки с неким определенным набором параметров, имитируя нажатие каких-то определенных клавиш N-ое число раз.
  4. Анализировать число байт, обрабатываемых I/O пакетами типа IRP_MJ_WRITE, посылаемых в моменты исскуственого вызова функции фильтра на устройства, для которых установлены FiDO (Filter Device Object).
  5. Анализировать содержимое буферов запросов IRP_MJ_WRITE, при вызовах функции фильтра.
  6. Если число записываемых байт в моменты вызова функции фильтра пропорционально N, либо в буферах записи имеются ASCII символы или скан-коды имитируемых клавиш, то существует вероятность того, что анализируемый процесс является keylogger-ом.
  7. Данной последовательностью я лишь хотел объяснить основную идею анализа - исскуственно вызывать функцию фильтра и анализировать IRP_MJ_WRITE в эти моменты. Сам алгоритм в реальности может быть намного изощреннее и сложнее, учитывать возможности передачи информации по сети, буферизации, шифрования и т.д. Данная идея основана на простом предположение: keylogger, рано или поздно, должен записать информацию о нажатых клавишах на некий постоянный носитель (либо послать по сети), причем размер записываемой информации должен быть пропорционален количеству нажатых клавиш.

    На этом, пожалуй, все...

    К статье прилагается проект hookguard, в котором реализован перехват сервиса SetWindowsHookEx. В данном драйвере я не стал реализовывать какой-либо анализ процесса на принадлежность к keylogger-ам. В нем просто, при перехвате вызова, на экране показывается сообщение, спрашивающее, можно ли установить глобальную ловушку на клавиатуру или нет. В случае если пользователь отвергает эту попытку, процесс, пытавшийся установить ловушку, получает access denied. Получилось забавно: выглядит так, как будто keylogger сам спрашивает пользователя, можно ли установить ему ловушку или нет... Кстати, кому интересно, может попытаться разобраться в механизме вызова функции user32.dll!MessageBox непосредственно из режима ядра (с понижением привилегий до CPL=3 и возвратом в kernel mode через IA-32 call-gate).

    Проект разрабатывался и тестировался в Windows XP SP2. Его работоспособность в других версиях Windows гарантировать не буду...

    P. S.

    Если кто внимательно следит за новостями BugTraq, то должен был обратить внимание на недавнюю шумиху по поводу новой технологии от Microsoft - Kernel Patch Protection, которая, как я понимаю, уже внедрена в новые обновления 64-х разрядных версий Windows (см: Kernel Patch Protection: больше вреда, чем пользы?, McAfee присоединилась к Symantec в атаках на Microsoft, McAfee и Symantec не удовлетворены действиями Microsoft). Так вот, в контексте наших рассуждений, очень уместно сообщить, что эта новая технология Kernel Patch Protection как-раз и защищает от модификаций описанные мною таблицы системных сервисов. При этом для таблицы системных сервисов рассчитывается контрольная сумма по некому определенному алгоритму, которая, как я понимаю, проверяется, при каждом вызове системного сервиса. Если одна из таблиц модифицированна (что собственно мы и делали) - система уходит в BSOD.

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

    Список использованной литературы

    1. Марк Руссинович, Дэвид Соломон. Внутреннее устройство Microsoft Windows: Windows Server 2003, Windows XP и Windows 2000.
    2. Microsoft Windows 2000 kernel sources


    обсудить  |  все отзывы (3)

    [42355; 54; 7.72]




      Copyright © 2001-2024 Dmitry Leonov Design: Vadim Derkach