Методи перехоплення api-викликів в win32

Перехоплення системних функцій операційної системи - прийом, відомий давно. Зазвичай перехоплюється деяка системна функція з метою моніторингу або зміни її поведінки. За часів DOS програмісти перехоплювали програмні переривання (int 21h, int 16h, int 10h). З приходом Win16 знадобилися кошти для перехоплення API-функцій. І, нарешті, з появою Win32 засоби перехоплення ще раз еволюціонували, підстроївшись під нову систему. Операційні системи сімейства Windows ніколи не містили вбудованих коштів, спеціально призначених для перехоплення системних функцій. І зрозуміло чому - все-таки це трохи хакерський прийом. Тому перехоплення зазвичай здійснюється «підручними засобами», і для його реалізації потрібно чітко уявляти багато глибинні аспекти пристрої та функціонування операційної системи.

Особливості організації пам'яті в Windows

Так як перехоплення практично завжди пов'язаний з модифікацією пам'яті (або коду перехоплюваних функції, або таблиць імпорту / експорту), то для його здійснення необхідно враховувати особливості архітектури пам'яті WinNT і Win9X.

  • Молодші два гігабайти (00400000-7FFFFFFF) - код і дані користувача режиму (в діапазоні 00000000-003FFFFF розташовані розділи для виявлення нульових покажчиків і для сумісності з програмами DOS і Win16);
  • Третій гігабайт - для загальних файлів, що проектуються в пам'ять (MMF), і системних DLL.
  • Четвертий гігабайт - для коду та даних режиму ядра (тут розташовується ядро ​​операційної системи і драйвери).

Всі ці відмінності істотно впливають на способи реалізації перехоплення функцій, розташованих в системних DLL.

Перехоплення можна розділити на два типи: локальні (перехоплення в межах одного процесу) і глобальні (в масштабах всієї системи).

локальний перехоплення

Локальний перехоплення з використанням розділу імпорту

При реалізації даного методу слід враховувати, що виклики з DllMain бібліотеки, в якій знаходиться перехоплюється функція, перехопити не вдасться. Це пов'язано з тим, що перехоплення може бути здійснений тільки після закінчення виконання LoadLibrary, а до цього часу DllMain вже буде викликана. Звичайно, можна написати свій варіант LoadLibrary (приклади завантаження DLL «вручну» існують) і здійснювати перехоплення між завантаженням DLL і викликом DllMain, але це сильно ускладнює завдання.

Основною перевагою даного методу є те, що він однаково реалізується як в Win9X, так і в WinNT.

У Windows NT функції Module32First і Module32Next не реалізовані, і для перерахування модулів процесу замість них доведеться скористатися функціями з PSAPI.dll.

Локальний перехоплення за допомогою зміни перехоплюваних функції (тільки WinNT)

Існує безліч прикладів реалізації цього методу. Я розгляну метод, пропонований Microsoft - Detours library.

Detours - це перша офіційна бібліотека, призначена для перехоплення функцій (не тільки системних, але і будь-яких інших). До основних понять Detours відносяться:

  • цільова функція (target function) - функція, перехоплення якої здійснюється;
  • функція-перехоплювач (detour function) - функція, що заміщає перехоплюваних;
  • функція-трамплін (trampoline function) - функція, що складається з заголовка цільової функції і команди переходу до решти коду цільової функції.

Trampoline в перекладі з англійської - «батут», однак словосполучення «функція-трамплін» більш точно передає логіку її роботи.

Таким чином, якщо цільова функція має наступний заголовок:

Макрос DETOUR_TRAMPOLINE і функція DetourFunction включають в себе вбудований табличний дизассемблер, який визначає, яка кількість байт з заголовка цільової функції має бути скопійовано в функцію-трамплін (не менше 5 байт (розмір команди jmp), що складають ціле число команд процесора). Якщо цільова функція займає менше 5 байт, то перехоплення закінчується невдачею.

На момент установки / зняття перехоплення потрібно зупиняти всі інші потоки процесу, в якому відбувається перехоплення (або упевнитися, що вони не можуть викликати перехоплюваних функцію).

Основним недоліком даного способу є його вкрай мала швидкодія (обробка виключення в Windows займає досить тривалий час). Крім того, наявність обробника винятків в перехоплювати процесі призведе до того, що даний метод працювати не буде. Також даний спосіб не буде працювати під відладчиком.

Глобальний перехоплення

Глобальний перехоплення може бути реалізований різними способами. Перший спосіб - застосування локального перехоплення до всіх програм в системі (запущеним в момент перехоплення або пізніше). Другий спосіб - «злом системи» - має на увазі підміну коду перехоплюваних функції безпосередньо в DLL-файлі або його образі в пам'яті.

Глобальний перехоплення методом тотального локального перехоплення

Даний метод заснований на наступному: якщо можна перехопити функцію з поточного процесу, то потрібно виконати код перехоплення у всіх процесах в системі. Існує кілька методів змусити чужий процес виконати код перехоплення. Найпростіший - внести цей код в DllMain деякої бібліотеки, а потім впровадити її в чужій процес. Методів впровадження DLL також існує кілька (див. Джеффрі Ріхтер). Найпростіший, що працює і в Win9X, і в WinNT - впровадження DLL за допомогою пасток. Реалізується він так: в системі встановлюється пастка (за допомогою функції SetWindowsHookEx) типу WH_GETMESSAGE (ця пастка служить для перехоплення Windows-повідомлень). У цьому випадку модуль, в якому знаходиться пастка, автоматично підключається до потоку, вказаною в останньому аргумент SetWindowsHookEx (якщо вказано 0, то здійснюється підключення до всіх потоків в системі). Однак підключення відбувається не відразу, а перед тим, як в чергу повідомлень потоку буде надіслано яке-небудь повідомлення. Тому перехоплення здійснюється не відразу після запуску програми, а перед обробкою процесом першого повідомлення. Так що всі виклики перехоплюваних функції до обробки процесом першого повідомлення перехоплюватися не будуть. А в додатках без черги повідомлень (наприклад, консольних) цей спосіб впровадження взагалі не працює.

Я написав приклад, який реалізує глобальний перехоплення функції GetDriveTypeA з використанням впровадження DLL за допомогою пасток і перехоплення з використанням секції імпорту.

Функція GetDriveTypeA з бібліотеки kernel32.dll використовується програмами Windows для визначення типу диска (локальний, CD-ROM, мережевий, віртуальний і т. Д.). Вона має наступний прототип:

lpRootPathName - шлях до диска (А: \, В: \ і т.д.)

GetDriveTypeA повертає одне з наступних значень:

Перехоплення цієї функції дозволяє «обманювати» програми Windows, перевизначаючи значення, що повертається цією функцією, для будь-якого диска.

Програма DriveType2 складається з двох модулів: DriveType2.exe і DT2lib.dll.

DriveType2.exe реалізує інтерфейс, а вся робота виконується в DT2lib.dll.

Проект DT2lib складається з трьох основних файлів:

APIHook.cpp - цей файл написаний Джеффрі Ріхтером (за винятком деяких виправлень, зроблених мною. Про них я розповім нижче). У цьому файлі описано клас CAPIHook, який реалізує перехоплення заданої API-функції у всіх модулях з поточною діяльністю. Тут же автоматично перехоплюються функції LoadLibraryA, LoadLibraryW, LoadLibraryExA, LoadLibraryExW і GetProcAddress.

Toolhelp.h - цей файл також написаний Джеффрі Ріхтером. У ньому описаний клас CToolhelp, який реалізує звернення до системних toolhelp-функцій. В даному випадку він використовується класом CAPIHook для перерахування всіх модулів, підключених до процесу.

DT2Lib.cpp - в цьому файлі я реалізував перехоплення функції GetDriveTypeA з використанням класу CAPIHook, а також установку пастки типу WH_GETMESSAGE, що забезпечує підключення даного модуля (DT2lib.dll) до всіх потоків в системі.

Як же відбувається перехоплення?

Відразу ж після запуску DriveType2.exe викликається функція DT2_HookAllApps з DT2lib.dll, яка встановлює пастку.

Після підключення DLL до потоку відбувається ініціалізація всіх змінних (кожен процес, до якого підключається DLL, має копії всіх глобальних і статичних змінних, описаних в ній). З глобальних змінних у нас є 6 примірників класу CAPIHook (для GetDriveTypeA в DT2Lib.cpp і для LoadLibraryA, LoadLibraryW, LoadLibraryExA, LoadLibraryExW і GetProcAddress - в APIHook.cpp). Таким чином, при підключенні DLL відбувається шестиразовий виклик конструктора класу CAPIHook, що перехоплює перераховані вище функції в поточному (тобто в тому, до якого тільки що відбулося підключення) процесі.

При завершенні процесу впроваджена DLL відключається. При цьому відбувається виклик деструктора CAPIHook для всіх екземплярів класу.

Ця функція тепер буде викликатися кожен раз, коли з даного модуля буде відбуватися звернення до GetDriveTypeA.

Функція Hook_GetDriveTypeA спочатку викликає оригінальну GetDriveTypeA. Потім, якщо повертається значення більше DRIVE_NO_ROOT_DIR (тобто функції був переданий коректний аргумент, і вона виконалася без помилок), то перевіряється, перевизначений чи диск, тип якого запитується. Інформація про значеннях перехоплюваних функцій в даному випадку зберігається в реалізованому мною масиві BYTE Drives [26], що дозволяє реалізувати перехоплення 26 дисків, від A: до Z. У цьому масиві зберігаються значення, що повертаються функцією GetDriveTypeA для кожного з дисків. Отже, якщо значення елемента масиву, відповідного аргументу GetDriveTypeA одно 0xFF, то значення повертається без змін, в іншому випадку повертається значення з масиву. Запис значень в цей масив реалізується в DriveType2.cpp.

Якщо ви хочете, щоб ця програма повноцінно працювала в WinNT, слід також перехопити функцію GetDriveTypeW.

У цього методу є ще один істотний недолік: деякі комерційні програми (наприклад, популярний файловий менеджер Total Commander, упакований ASPack) використовують різні системи захисту (ASProtect, VBox і т. Д.), Шифрувальні таблицю імпорту захищається програми. З такими програмами цей метод не працює.

Глобальний перехоплення може бути реалізований і за допомогою Detours (тільки в WinNT). А так як методів впровадження DLL відомо кілька, то різних варіантів реалізації глобального перехоплення можна запропонувати досить багато.

Глобальний перехоплення методом підміни коду в DLL

Даний метод можна реалізувати двома способами: безпосередній редагуванням коду DLL, в якій розташована цільова функція, або підміною цієї DLL іншого, що експортує той же набір функцій. Другий спосіб відомий під назвою «Підміна з використанням обгорток (wrappers)».

Перший спосіб дозволяє реалізовувати тільки порівняно невеликі за розміром функції-перехоплювачі, так як код необхідно впроваджувати в вільні ділянки DLL - в основному в міжсекційне простір. Інший недолік - код необхідно писати на асемблері. Загальна ідеологія роботи цього методу та ж, що і в Detours. У код цільової функції впроваджується команда jmp до функції-перехоплювачі. Байти, скопійовані «з-під» jmp'а, переміщаються в перехоплювач (так як перехоплювач все одно пишеться на асемблері, в цьому випадку його простіше відразу поєднати з функцією-трампліном). Ось приклад реалізації цього методу.

В каталозі DriveType0 знаходиться файл kernel32.dll, в якому я зробив наступні виправлення (за допомогою hiew32.exe):

Таким чином, коли світлодіод ScrollLock не горить, функція GetDriveTypeA працює як зазвичай, а якщо горить - то для всіх Windows-додатків всі локальні диски (у мене це С: \ і D: \) перетворюються в CD-ROMи.

Щоб все це запрацювало, необхідно замінити файл C: \ Windows \ System \ kernel32.dll на файл DriveType0 \ kernel32.dll. Зробити це можна, тільки завантаживши комп'ютер в режимі MS-DOS, так як kernel32.dll - одна з системних бібліотек Windows. Даний приклад реалізований для Windows 98. Оскільки системні бібліотеки змінюються в залежності від версії Windows (і навіть від номера билда), то в інших операційних системах цей приклад працювати не буде (його потрібно реалізовувати для кожної версії kernel32.dll заново).

Цей спосіб перехоплення - один з найпотужніших. Однак в комерційних продуктах його використовувати не вдасться, так як він, очевидно, порушує практично будь-яке ліцензійну угоду.

Інший спосіб реалізації цього методу - використання обгорток (wrappers). Суть його в створенні власної DLL з тим же набором функцій, що експортуються, що і оригінальна. Як приклад можу навести такий варіант реалізації вищенаведеного прикладу:

При цьому функція-перехоплювач може викликати оригінальну функцію з kernel31.dll.

Основним недоліком даного способу є те, що він не годиться для DLL, що експортують змінні.

Глобальний перехоплення методом підміни коду DLL в пам'яті (тільки Win9X)

Приклад DriveType1 складається з двох частин - драйвера DTDrv.sys і установочного скрипта DTDrv.inf, а також програми DriveType.exe.

DriveType.exe компілюється з одного модуля DriveType.cpp, в якому реалізовані користувальницький інтерфейс і інтерфейс з драйвером. Інтерфейс з драйвером реалізується через функції CreateFile (відкриття драйвера), DeviceIoControl (операції введення-виведення) і CloseHandle (закриття драйвера). Реалізовано чотири команди, які викликаються через DeviceIoControl - перехоплення функції GetDriveTypeA, зняття перехоплення, установка повертається функцією перехоплення для кожного з дисків A. Z. читання поточного стану перехоплення.

Основні змінні, використовувані DriveType.cpp:

Схожі статті