Перехоплення системних функцій операційної системи - прийом, відомий давно. Зазвичай перехоплюється деяка системна функція з метою моніторингу або зміни її поведінки. За часів 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: