Перехват (программирование)

08.03.2021

Перехват (англ. hooking) — технология, позволяющая изменить стандартное поведение тех или иных компонентов информационной системы.

Назначение технологии перехвата

Очень часто в системном программировании возникает задача изменения стандартного поведения системных функций. Например довольно интересным применением данной технологии является переопределение оконной процедуры у GUI приложений Windows (сабклассинг). Это нужно, если программист хочет организовать собственную обработку какого-либо оконного сообщения и только потом передать стандартной оконной процедуре. После сабклассинга цикл обработки сообщений будет выглядеть так:

  • ДО САБКЛАССИНГА:

Сообщение Windows->Окно (оконная процедура)

  • ПОСЛЕ:

Сообщение Windows->Наша оконная процедура->Окно (оконная процедура)

Например, в Уроках Iczelion’а описан пример того, как сабклассинг может использоваться для организации контроля ввода в элементы управления. Технологии перехвата нужны не только в этом случае, но и, например, для предварительной обработки результатов системных функций поиска файлов FindFirst и FindNext, EnumProcess, которая перечисляет процессы в Windows и т. д. Причем в этих целях такие технологии применяют как антивирусные средства, так и различного рода вирусы, руткиты и прочие виды вредоносного программного обеспечения.

Очень часто перехват бывает важен для организации отладки программ и является одной из основных технологий, применяемых в отладчиках. В данном случае эта технология позволяет одной программе контролировать выполнение другой. Для этих целей предусмотрен системный вызов ptrace, который позволяет подключаться к процессам, отслеживать значения регистров у контекста отлаживаемого процесса и в том числе контролировать другие системные вызовы. Он является основой для реализации такой возможности отладчиков как точки останова. Данный системный вызов хорошо документирован и присутствует во всех главных *Nix системах: Linux, FreeBSD, Solaris. Чаще всего используется совместно с системным вызовом fork, который и вызывает ptrace, указывая в параметрах вызова, что запускаемый процесс — дочерний. Microsoft Windows также предоставляет для похожих целей т. н. DebugAPI.

Виды перехвата системных функций

Основными методами перехвата являются:

  • Подмена адреса настоящей функции (модификация IAT таблиц, модификация SSDT/IDT таблиц)
  • Непосредственное изменение функции (сплайсинг, перехват в режиме ядра с модификацией тела функции)
  • Непосредственная подмена всего компонента приложения/системы (например библиотеки с целевой функцией)

Методы можно также разделить по критерию режима выполнения:

  • Пользовательские (ring3) методы: модификация IAT таблиц, сплайсинг. Их особенность в том, что невозможно что-либо изменить в поведении ядра операционной системы и его расширений.
  • Режима ядра: модификация SSDT/IDT таблиц, перехват в режиме ядра с модификацией тела функции. Позволяет модифицировать структуры данных и код любой части операционной системы и приложений.

Сплайсинг

Сплайсинг (от англ. splice — «сращивать или склеивать концы чего-либо») — метод перехвата API функций путём изменения кода целевой функции. Обычно изменяются первые 5 байт функции. Вместо них вставляется переход на функцию, которую определяет программист. Чтобы обеспечить корректность выполнения операции, приложение, которое перехватывает функцию, обязано дать возможность выполниться коду, который был изменён в результате сплайсинга. Для этого приложение сохраняет заменяемый участок памяти у себя, а после отработки функции перехвата восстанавливает изменённый участок функции и дает полностью выполниться настоящей функции.

Hot-patch point

Все функции стандартных dll Windows поддерживают hot-patch point. При использовании этой технологии перед началом функции располагаются пять неиспользуемых однобайтовых операций nop, сама же функция начинается с двубайтовой инструкции mov edi, edi. Места, занимаемого пятью nop, достаточно, чтобы разместить команду перехода на функцию-перехватчик. Два байта, занимаемых mov edi, edi, предоставляют достаточно места для команды перехода на код, размещенный на месте пяти nop. При этом, так как инструкция mov edi, edi не выполняет никаких осмысленных действий, её затирание никак не влияет на работоспособность исходной функции. Таким образом программист освобождается от необходимости где-либо сохранять исходное значение изменённого им кода.

Сферы применения сплайсинга и методы обнаружения

Он применяется:

  • В ПО, которому необходимо осуществлять функции мониторинга системы
  • Механизмом хуков в Windows
  • Различного рода вредоносными программами. Это основная технология сокрытия для руткитов пользовательского уровня

Основной метод обнаружения факта сплайсинга — это сравнение машинного кода функции, проверяемой на сплайсинг, и кода системной функции, полученного в заведомо чистой системе. Также в обнаружении сплайсинга функции может помочь контроль адресов перехода.

Сравнение с другими технологиями

  • Изменение IAT таблиц процесса. Данная технология не позволяет изменить поведение самой системной функции, а лишь дает возможность «обмануть» выбранное приложение, заставив его использовать вашу функцию. IAT таблица — таблица адресов функций, импортируемых процессом. Технология носит лишь локальный характер, хотя может быть применена сразу к группе приложений. Может быть довольно быстро обнаружена из-за необходимости загрузки DLL в адресное пространство целевого процесса. Сплайсинг же не требует DLL и внедрения в чужой процесс, обладает возможностью глобального захвата функции. У сплайсинга есть ещё одно преимущество: не все системные функции импортируются процессом через IAT. Например, функция может быть загружена вызовом GetProcAddress. Использование же непосредственной модификации кода функции снимает подобное ограничение.
  • Перехват в режиме ядра. Позволяет перехватывать любые функции, в том числе и экспортируемые ядром. Наиболее труден для обнаружения в случае успеха, так как позволяет фальсифицировать любые данные, предоставляемые операционной системой. Требует написания специального компонента для взаимодействия с ядром драйвера. Может привести к BSOD при неправильном программировании в режиме ядра. Может быть обнаружен на фазе загрузки драйвера в ядро или при проверке активных драйверов, а также при проверке ядра на изменения. Более трудный в программировании метод, чем сплайсинг, но более гибкий, так как позволяет перехватить функции самого ядра, а не только WinAPI функции, которые служат лишь посредником между ядром и программой, которая что-либо запрашивает у операционной системы.
  • Замена самой библиотеки с функцией. Весьма радикальное решение проблемы, обладающее рядом существенных недостатков:
  • Требует замены файла на диске, что может быть запрещено и пресечено самой системой. Например, замену системных файлов Windows не позволит выполнить защита файлов Windows (WFP), хотя её можно и отключить. Такое действие может быть также обнаружено при статическом анализе системы ревизорами.
  • Требуется полная эмуляция всех возможностей заменяемой DLL или иного компонента, что весьма трудоемко даже в случае открытости и осложняется необходимостью дизассемблирования в случае закрытости целевой программы.
  • Все это показывает, что это весьма нерациональный способ решения проблемы изменения поведения программы в случае возможности применения первых двух подходов или сплайсинга.

    Перехват в режиме ядра

    Он основан на модификации структур данных ядра и функций. Главными мишенями воздействия являются таблицы

    • IDT Таблица диспетчеризации прерываний. Довольно важным для перехвата является прерывание, обрабатывающее обращение к таблице служб SSDT (0x2E).
    • SSDT (System Service Dispatch Table) Таблица диспетчеризации системных сервисов. Обращаясь к ней, система по номеру запрошенного сервиса может получить адрес соответствующего сервиса ядра и вызвать его. А таблица SSPT содержит общий размер параметров, передаваемых системному сервису.
    • psActiveprocess Структура ядра, хранящая список процессов в системе.
    • IRP Таблица драйвера, которая хранит указатели на функции обработки IRP-пакетов.
    • EPROCESS Структура ядра, хранящая большое количество информации о процессе, включая, например, PID (идентификатор процесса).

    Такого рода руткиты называются DKOM-руткитами, то есть руткитами, основанными на непосредственной модификации объектов ядра. В руткитах для систем Windows Server 2003 и XP эта технология была модернизирована, так как в этих ОС появилась защита от записи некоторых областей памяти ядра. Windows Vista и 7 получили дополнительную защиту ядра PatchGuard, однако все эти технологии были преодолены руткитописателями. В то же время перехват системных функций в режиме ядра — основа проактивных систем защиты и гипервизоров.

    Иные формы перехвата

    Можно выделить и другие формы перехвата:

    • Перехват сетевых соединений и пакетов.
    • Перехват паролей. Например, при помощи шпионажа за клавиатурным вводом при помощи кейлоггера.
    • Перехват обращений браузера к сайтам при помощи HTTP Proxy или расширений браузера. Позволяет проанализировать и/или подменить данные, которыми обмениваются браузер и сервер.

    Здесь описана лишь часть применений данной технологии.

    Примеры программ, использующих перехват

    • Гипервизор XEN
    • Утилиты Марка Руссиновича Filemon, Regmon, Process Explorer
    • Руткит Rustock.C

    Примеры кода

    Перехват клавиатурных событий Microsoft Windows на С# с использованием Microsoft .NET Framework using System; using System.Collections; using System.Diagnostics; using System.Runtime.InteropServices; namespace Hooks { public class KeyHook { #region Member variables protected static int hook; protected static LowLevelKeyboardDelegate dele; protected static readonly object Lock = new object(); protected static bool isRegistered = false; #endregion #region Dll Imports [DllImport("user32")] private static extern Int32 SetWindowsHookEx(Int32 idHook, LowLevelKeyboardDelegate lpfn, Int32 hmod, Int32 dwThreadId); [DllImport("user32")] private static extern Int32 CallNextHookEx(Int32 hHook, Int32 nCode, Int32 wParam, KBDLLHOOKSTRUCT lParam); [DllImport("user32")] private static extern Int32 UnhookWindowsHookEx(Int32 hHook); #endregion #region Type Definitions & Constants protected delegate Int32 LowLevelKeyboardDelegate(Int32 nCode, Int32 wParam, ref KBDLLHOOKSTRUCT lParam); private const Int32 HC_ACTION = 0; private const Int32 WM_KEYDOWN = 0x0100; private const Int32 WM_KEYUP = 0x0101; private const Int32 WH_KEYBOARD_LL = 13; #endregion [StructLayout(LayoutKind.Sequential)] public struct KBDLLHOOKSTRUCT { public int vkCode; public int scanCode; public int flags; public int time; public int dwExtraInfo; } static private Int32 LowLevelKeyboardHandler(Int32 nCode, Int32 wParam, ref KBDLLHOOKSTRUCT lParam) { if (nCode == HC_ACTION) { if (wParam == WM_KEYDOWN) System.Console.Out.WriteLine("Key Down: " + lParam.vkCode); else if (wParam == WM_KEYUP) System.Console.Out.WriteLine("Key Up: " + lParam.vkCode); } return CallNextHookEx(0, nCode, wParam, lParam); } public static bool RegisterHook() { lock(Lock) { if(isRegistered) return true; dele = new LowLevelKeyboardDelegate(LowLevelKeyboardHandler); hook = SetWindowsHookEx( WH_KEYBOARD_LL, dele, Marshal.GetHINSTANCE( System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0] ).ToInt32(),0 ); if(hook != 0) return isRegistered = true; else { dele= null; return false; } } } public static bool UnregisterHook() { lock(Lock) { return isRegistered = (UnhookWindowsHookEx(hook) != 0); } } } } Netfilter hook

    Этот пример показывает, как используются хуки для контроля сетевого трафика в ядре Linux при помощи Netfilter.

    #include <linux/module.h> #include <linux/kernel.h> #include <linux/skbuff.h> #include <linux/ip.h> #include <linux/tcp.h> #include <linux/in.h> #include <linux/netfilter.h> #include <linux/netfilter_ipv4.h> /* Port we want to drop packets on */ static const uint16_t port = 25; /* This is the hook function itself */ static unsigned int hook_func(unsigned int hooknum, struct sk_buff **pskb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { struct iphdr *iph = ip_hdr(*pskb); struct tcphdr *tcph, tcpbuf; if (iph->protocol != IPPROTO_TCP) return NF_ACCEPT; tcph = skb_header_pointer(*pskb, ip_hdrlen(*pskb), sizeof(*tcph), &tcpbuf); if (tcph == NULL) return NF_ACCEPT; return (tcph->dest == port) ? NF_DROP : NF_ACCEPT; } /* Used to register our hook function */ static struct nf_hook_ops nfho = { .hook = hook_func, .hooknum = NF_IP_PRE_ROUTING, .pf = NFPROTO_IPV4, .priority = NF_IP_PRI_FIRST, }; static __init int my_init(void) { return nf_register_hook(&nfho); } static __exit void my_exit(void) { nf_unregister_hook(&nfho); } module_init(my_init); module_exit(my_exit);