API Spying h2>
Сергій холодиль p>
Я відкриваю властивості рослин і трав .. p>
Борис Гребєнщиков p>
Словосполученням «API Spying» називається детектор
викликами функцій API деяким додатком. Тобто, кожен факт виклику цим
додатком вибраних функцій якимось чином фіксується, наприклад,
додається запис в лог. p>
ПРИМІТКА p>
Для ясності назвемо «деяке
додаток »досліджуваним додатком, а« вибрані функції »- відстежує
функціями. p>
Навіщо це потрібно
h2>
API Spying може використовуватися на одному з етапів
дослідження програми, логіку роботи якої ви поки що не до кінця розумієте.
Хоча ця технологія і не дозволяє отримати детальну інформацію, вона може
значно звузити область подальших етапів дослідження, сконцентрувавши
вашу увагу на ті виклики, які відбуваються в ключові моменти роботи
програми. p>
На перший погляд може здатися, що завдання краще
вирішується за допомогою перехоплення API, тому що він дає можливість не тільки
відстежити дзвінок, а й вивчити/змінити параметри та повертається значення, або
навіть повністю переписати функцію. p>
Дійсно, перехоплення API - чудова і часто
згадувана техніка (на даний момент на RSDN цій темі присвячені три статті),
що дозволяє досить глибоко вивчити досліджуваний додаток, але це і набагато
більш трудомістка рішення. Навіть якщо реалізації функцій будуть майже порожніми
(тільки запис в лог і виклик оригінальній функції), ваш код буде приблизно
таким: p>
typedef int (__stdcall * Function1_type) (int i); p>
Function_type _Function1; p>
// Обгортка, логірующая виклики p>
int __stdcall MyFunction1 (int i) p>
( p>
printf ( "MyFunction1n "); p>
return _Function (i);// Виклик оригінальній функції p>
) p>
... p>
// Перехоплення всіх функцій p>
void HookThemAll () p>
( p>
... p>
// Перехоплення функції _Function1, що експортується
some.dll p>
HookIt ( "some.dll",
"_Function1 @ 4", MyFunction1, & _Function1); p>
... p>
) p>
ПРИМІТКА p>
Це приблизний код,
що використовується під час перехоплення через таблицю імпорту; інші варіанти перехоплення в
даному випадку не мають суттєвих переваг. p>
Тобто, для кожної функції доведеться: p>
визначити тип; p>
визначити змінну; p>
написати обгортку; p>
додати рядок у HookThemAll. p>
Це, звичайно, досить прості операції ... Але
уявіть, що таким чином вам потрібно перехопити кілька сотень функцій. А
якщо не у всіх функцій відомі прототипи? А якщо деякі dll завантажується
динамічно, і ви поки навіть не знаєте, які їх функції використовуються
додатком? А якщо після того, як ви всі успішно перехопити і перегляньте
отримані логи, стане зрозуміло, що для детального розуміння роботи
програми треба було перехопити всього дві функції і вивчити їх параметри:)?
p>
Коли всі ці питання постали переді мною, я зайнявся
API Spying-му. P>
API Spying не виключає перехоплення API, але ці методики
використовуються знаходяться на різних стадіях аналізу програми. Спочатку за допомогою
API Spying-а визначається декілька найбільш цікавих функцій, потім, якщо
необхідно, ці функції перехоплюються і вивчаються «у більш тісному контакті». p>
Постановка завдання
p>
У самому загальному вигляді завдання виглядає так: p>
Необхідно отримувати інформацію про факти виклику
вибраних функцій досліджуваних додатком. p>
Для отримання статистики не обов'язково наперед знати
назви функцій, які будуть викликатися додатком. Тим більше, не потрібно знати
їх прототипи. p>
Збір статистики для будь-якого (в тому числі заздалегідь
невідомого) кількості функцій з будь-яких (у тому числі, із завантаження
динамічно) модулів не має становити труднощів. p>
Працездатність досліджуваного програми не повинна
порушуватися. p>
Формулювання ТЗ
p>
Логічно розвинемо вимоги, висловлені в
постановці завдання: p>
Ми розглядаємо тільки програмні реалізації. Це
означає, що статистика збирається програмно, і цим займається наш код. p>
Оскільки при кожному виклику відслідковуються функцій
управління має передаватися нашому коду, все відслідковують функції потрібно
перехопити (тим чи іншим способом; детальний розгляд способів перехоплення
API виходить за рамки статті). Нашу функцію, якої в результаті перехоплення
передаватиметься управління, назвемо функцією-шпигуном (термінологія моя,
жалкую, якщо не правий). p>
Щоб вести статистику викликів і не порушувати роботу
додатки, функція-шпигун повинна визначити, яку саме відстежуємо функцію
збиралося викликати досліджуваний додаток. Єдиний спосіб реалізувати це
- Зіставити кожної відслідковується функції свою функцію-шпигуна, «знає», як
мінімум, адреса оригіналу. p>
По можливості, присутність функції-шпигуна не повинно
впливати на виконання відслідковується функції. Це виходить не завжди, розумні
винятки описані нижче, в розділі «Чому додаток може перестати
працювати ». p>
Так як кількість відслідковуються функцій може бути
велике або заздалегідь не відомо, функції-шпигуни повинні генеруватися
автоматично в процесі виконання. p>
Кілька додаткових побажань: p>
Автоматично генерувати складні функції-шпигуни
непросто. :) Їх навіть писати на асемблері замало ... Добре б підрахунок
статистики взяв на себе хтось інший. p>
ПРИМІТКА p>
Ви класно знаєте асемблер, і
вважаєте, що це пари дрібниць? Можливо, ви не врахували, що код функцій буде
розташований в довільному місці адресного простору і що (забігаючи вперед;
але ви-то це всі повинні розуміти) функції не можуть модифіковані стек і
регістри. Якщо і це для вас не проблема, то, по-перше, прийміть моє
щире захоплення (без жартів!), по-друге, прочитайте наступний пункт. :) p>
Автоматична генерація має на увазі виділення
пам'яті для коду функцій, а, оскільки їх може бути багато, бажано щоб
функції були короткими. Тому, знову ж таки, добре б підрахунок статистики взяв на
себе хтось інший. p>
І кілька обмежень: p>
Ця реалізація підтримує тільки Intel
x86-сумісні процесори. p>
Для роботи функції-шпигуна від ОС потрібно тільки одне:
вона повинна дозволяти виконувати динамично згенерований код. Ця умова
дотримується у всіх версіях Windows і, швидше за все, в переважній більшості
інших ОС загального користування. Але, оскільки нам потрібен ще й спосіб
перехоплення, обмежимося лінійкою Windows NT/2000/XP. Використовуючи інші способи
перехоплення, можна реалізувати API Spying для інших ОС. p>
Невідомо, як на виконання згенерованого кода
будуть реагувати антивіруси. Можливо, вони будуть недостатньо толерантні. :) P>
РАДА p>
Про подібні обмеження краще не
забувати і в реальних проектах, тому що інакше виконати ТЗ буде практично
неможливо. p>
Чому додаток може перестати працювати
p>
Проблема полягає в тому, що (на жаль!) статистика
збирається не магічно, її збирає наш код, впроваджений в досліджуваний
додаток. У цього простого факту є три неприємних слідства: p>
При додаванні збору статистики зміниться швидкість
роботи функцій. Звичайно це ні на що не впливає, але якщо в середині критичного до
швидкості виконання коду ми несподівано (для програми та його автора) почнемо запис
у файл, може вийти погано. Наприклад, FPS впаде раз на десять:) Але FPS --
це не страшно, страшно, якщо ви досліджуєте багатопотокове додаток c
некоректно написаної синхронізацією, і зміна часу виконання потоків
призведе до дедлокам, падінням або просто незрозумілої поведінки. p>
Крім процесорного часу, наш код використовує і
інші ресурси: пам'ять, стек, (можливо) вікна, об'єкти ядра (файли, події, і
тощо) та інші. У певному фантастичному випадку це може стати останньою
краплею, що приводить до вичерпання доступних ресурсів потоком, процесом, або навіть
системою. p>
Якщо автор програми вирішив подбати про захист
свого дітища, він цілком в змозі засікти наші маніпуляції, образитися (він же
не знає, що ми нічого поганого не хотіли) і зробити якусь бяку.
Наприклад, відмовиться працювати, або стане працювати неправильно, або
відформатуйте випадково вибраний диск ... p>
Всі ці слідства в тій чи іншій мірі властиві
будь-якої програмної реалізації API Spying-а, і в жодній з цих ситуацій я не
можу порадити вам нічого хорошого. Можна лише спробувати зменшити
ступінь впливу і уникнути настільки згубних наслідків. p>
Передпроектні дослідження: функції в Intel x86
p>
Як ви вже, напевно, зрозуміли, нам належить
динамічна генерація коду функцій-шпигунів. Хоча нічого особливо складного в цьому
не буде (вони дійсно дуже прості), невелике теоретичне введення
допоможе вам зрозуміти (а мені - пояснити), як повинна бути написана функція-шпигун,
щоб виклик відслідковується функції завершився без перешкод. p>
Виклик
p>
З точки зору процесора виклик функції виконує
інструкція call, що має декілька різних форм: p>
call xxxxxxh p>
call xxxxh: xxxxxxh p>
call eax p>
call
[eax] p>
... p>
Вона зберігає в стеку адресу, за якою потрібно
передати управління після закінчення функції, і передає керування на початок
функції. p>
Передача параметрів
p>
Процесор Intel x86 нічого не знає про параметри
викликаються функцій, тому механізм передачі параметрів може бути
довільним, головне щоб викликає і викликається код домовилися про нього
заздалегідь. Місць, де можна зберегти параметри, не так вже й багато: або в
регістрах, або в стеку, або частину там, а частина там. p>
ПРИМІТКА p>
Звичайно, можна передавати параметри по
посиланням або значенням, в прямому порядку або в зворотному, але це для нас не
важливо, важливо тільки те, де передана інформація (параметри або їх адреси)
знаходиться. p>
Передача параметрів через регістри використовується в
основному в двох випадках: p>
компілятором для оптимізації. p>
Асемблер-програмістом з ліні або в гонитві за
продуктивністю. Щоб дістати параметри із стека, треба написати декілька
додаткових команд, а в регістрах вони відразу під рукою. p>
У більшості інших випадків параметри передаються
через стек. При цьому виклик функції виглядає приблизно так: p>
push ...
; Параметр p>
push ...
; Ще один параметр p>
push ...
; І останній параметр p>
call xxxxxh; Виклик p>
А стек до моменту початку виконання функції - так: p>
p>
Малюнок 1. Стан стека на початку виконання
функції. p>
Повернення з функції
p>
Повернення управління виробляє інструкція ret, що має
чотири різні форми: p>
ret p>
ret xxxh p>
retf p>
retf xxxh p>
ПРИМІТКА p>
Модифікація retf призначена для
повернення з функції, яку викликали з іншого сегмента ( «далеким викликом»).
Нижче вона не згадується, тому що, по-перше, в Windows ви її навряд чи
зустрінете, по-друге, з точки зору реалізації API Spying-а, вона практично
не відрізняється від ret. p>
Завдання, що виконується ret *: p>
Видалити з стека адреса повернення. p>
(опціонально) Вилучити з стека вказану кількість
байт. p>
Надіслати управління за адресою повернення. p>
При цьому всі версії ret * припускають, що адреса
повернення знаходиться на вершині стека, а байти, які треба видалити (якщо треба)
- Відразу за ним. P>
Оскільки, як і при виклику, процесор нічого не знає
про параметри, видаляти їх з стека при поверненні чи ні - особиста справа функції і
викликає її коду. Поширені обидва варіанти: згідно формату виклику
функцій __cdecl за очищення стека відповідає викликає код, а згідно формату
__stdcall цим займається сама функція. p>
ПРИМІТКА p>
Майже всі стандартні API Windows
дотримуються __stdcall, і більшість функцій, що експортуються з dll
інших виробників, також слідують цього формату. p>
що повертається значення
p>
Як і у випадку з параметрами, про що повертаються
значення процесор теж нічого не знає, і те, як саме і що саме ви
будете повертати, його не стосується. Зазвичай повертається значення передається
через регістр eax або через пару eax: edx. p>
Стан регістрів до і після виклику
p>
І це питання залишається повністю на совісті
програміста (у випадку мови високого рівня - програміста, який писав
компілятор). Якщо вірити статті «Arguments Passing and Naming Conventions» в
MSDN, для всіх стандартних форматів виконання функцій компілятор гарантує
збереження регістрів ESI, EDI, EBX і EBP. Це означає, що викликає код: p>
Може розраховувати на те, що ці регістри не
поміняються. p>
Не повинен розраховувати на регістри EAX, ECX, EDX,
EFLAGS (з ним трохи складніше, очевидно, частина прапорів все-таки має залишитися
незмінною, просто MSDN про це не згадує), а також на регістри MMX, FPU,
XMM. p>
ПРИМІТКА p>
А як же інші регістри? Сегментні,
керуючі, GDTR, LDTR, ....? З ними просто: якщо функція змінює якийсь із
цих регістрів, то, або це документований побічний ефект (наприклад,
очікуваний результат) її виклику, або від функції дуже, дуже погано
пожартував ... p>
Проектування
p>
Система в цілому складається з чотирьох частин: p>
Функція-шпигун. p>
Механізм встановлення шпигунів. p>
Функція збору статистики. p>
Механізм збору і відображення статистики. p>
Функція-шпигун
p>
Завдання p>
Завдання роботи функції-шпигуна: p>
Викликати функцію збору статистики, якимось чином
повідомивши їй, яка відстежуємо функція викликається. p>
Викликати відстежуємо функцію. p>
Обмеження p>
Обмеження пов'язані з тим, що відстежувати функція
повинна працювати без змін. Для цього перед її викликом: p>
Необхідно привести стек в той же стан, який
було до початку роботи функції-шпигуна. Це означає, що, по-перше, не можна
зберегти в стеку яке-небудь значення для використання після повернення з
відслідковується функції, по-друге, не можна використовувати для виклику інструкцію
call, так як вона додасть в стек адреса повернення (на цю тему див. нижче, в
розділі «Отримання управління після повернення з відслідковується функції»). p>
Оскільки в принципі параметри можуть передаватися і в
регістрах, бажано привести регістри в той же стан, який був до
початку роботи функції-шпигуна, або хоча б в максимально близьке. p>
Код, який треба згенерувати p>
Так як код функції-шпигуна може розташовуватися в
пам'яті за довільним адресою, при виклику з неї функцій необхідно або
використовувати абсолютну адресацію, або при генерації обчислювати їх адреси для
кожної нової функції-шпигуна. p>
Обидва підходи однаково просто реалізуються, але через
особливості системи команд Intel x86 ближній виклик/передача управління з
абсолютному адресою буде виглядати приблизно так: p>
; Виклик p>
mov eax, <абсолютний адреса функції
> p>
call eax p>
; Передача управління p>
mov eax, <абсолютний адреса
функції> p>
jmp eax p>
Тобто, як не старайся, а значення одного регістра
(в даному прикладі регістра eax, але на його місці міг бути кожен) зберегти не
вдається. p>
Тому вибрана версія з відносною адресацією: p>
pusha; зберігаємо регістри і прапори. p>
pushf; Це, звичайно, параноя ... p>
push
<номер>; передаємо в параметрі
номер відслідковується функції p>
call
<відносний адреса функція збору статистик> p>
p>
popf;
відновлюємо прапори p>
popa;
і регістри p>
p>
jmp
<відносний адреса відслідковується функції> p>
Оскільки ця функція-шпигун закінчується
безпосереднім викликом відслідковується функції, вона може спільно працювати
тільки з методами перехоплення, що не змінюють код перехоплюваних функції. Це: p>
перехоплення через таблицю імпорту; p>
перехоплення через таблицю експорту; p>
перехоплення GetProcAddress і підміна адреси запитуваної
функції. p>
Якщо ви використовуєте інший метод перехоплення (наприклад,
заміну декількох початкових байтів на команду jmp), вам доведеться трохи
змінити мій код. p>
Отримання управління після повернення з відслідковується
функції p>
Якщо з якихось причин вам дуже потрібно отримати
повертається значення відслідковується функції, або ви хочете виміряти час її
виконання, або ще щось, недоступне моєму розумінню, ви все-таки можете
написати функцію-шпигун так, щоб вона використовувала call для дзвінка
відслідковується функції і отримувала управління після її завершення. p>
Для цього потрібно: p>
Видалити з стека стара адреса повернення. p>
ПРИМІТКА p>
А якщо функція викликана далеким викликом,
то (сюрприз!) адреса повернення буде займати 6 байт. Гірше того, нова адреса
теж повинен бути шестібайтним, так як відстежуємо функція дуже на це
розраховує. ?? ряд ви зустрінетесь з такою ситуацією в Windows, але про
інші ОС я нічого сказати не можу. p>
Десь зберегти його на час дзвінка відслідковується
функції. p>
Викликати функцію. p>
Отримати/виміряти/.. те, що ви хотіли. p>
Повернути управління за старою адресою. p>
Ключовим питанням цього алгоритму є: «де ж
це десь, в якому можна зберегти адреса повернення? »Стек міняти не можна,
тому він відпадає. Зберігати в регістрах теж не можна: ті регістри, які
можуть змінитися після виклику функції, може змінити відстежуємо функція, і
дані пропадуть, а ті регістри, які не повинні змінюватися після виклику, не можна
змінювати нам, тому що відновити їх ми не зуміємо - ніде зберегти їх старі
значення:) p>
Залишається тільки зберігання в глобальній області пам'яті.
Так як додаток може бути багато-, доступ до пам'яті потрібно
синхронізувати, і окремо зберігати дані для кожного потоку. Так як
можлива рекурсія, необхідно зберігати не одну адресу повернення, а стек адрес ...
І, не дивлячись на всі ці заходи, що буде, якщо в відслідковується
функції відбудеться виняток і почнеться розгортання стека? Правильно, буде
дуже погано ... p>
Загалом, це шлях для людей, міцних духом і готових до
випробувань. Далі в статті він не розглядається. P>
Механізм встановлення шпигунів
p>
Алгоритм установки однієї функції-шпигуна: p>
Генерується функція-шпигуна, що при генерації
встановлюється її номер, адреса відслідковується функції та адресу функції збору
статистики. p>
Перехоплюється відстежуємо функція, тепер замість
неї додатком повинна викликатися функція-шпигун. p>
Где-то зберігається інформація, про те, що перехоплена
функція з таким-то ім'ям і їй сопоставлен такий-то номер. Ця інформація буде
використана при виконанні функції збору статистики. p>
Очевидно, що цей алгоритм ніяк не залежить від
прототипу/формату виклику/.. відслідковується функції, і може бути без змін
застосований для будь-якої кількості функцій. Тим не менше, розглянемо два випадки. P>
Відстеження викликів функцій динамічно завантажуваних
dll p>
Це найпростіше. Оскільки адреси таких функцій
додаток отримує через GetProcAddress, достатньо просто перехопити
GetProcAddress і проводити описану вище процедуру для всіх запитуваних
функцій. p>
Відстеження всіх викликів p>
Загальна ідея: пройтися по таблицях імпорту завантажених
модулів і, не особливо замислюючись, перехопити всі згадані там функції. Крім
того, потрібно подбати про GetProcAddress (див. попередній пункт) і про ще не
завантажених модулях: їх таблиці імпорту теж необхідно обробити. Щоб не
пропустити появу нових модулів, можна, наприклад, перехопити всі версії
LoadLibrary [Ex] A/W. p>
Просто, правда? Просто, але, на жаль, у такому вигляді
працювати, швидше за все, не буде. p>
ПОПЕРЕДЖЕННЯ p>
Цей варіант я так і не реалізував
(не було потреби), тому про його неминучих маленьких особливості майже нічого не
знаю. Мої спроби поміркувати представлені нижче, але практики за ними не
стоїть, і гарантувати відсутність проблем я не можу. Шкодую. P>
Проблема цього підходу полягає в майже
гарантованому виникненні нескінченної рекурсії. Наприклад, нехай collectStatistic
записує дані у файл за допомогою функції WriteFile. Якщо ця функція
виявилася перехоплена і у вашому модулі, то спроба запису призведе до виклику
вашої функції-шпигуна, яка викличе collectStatistic і т.д. поки не скінчиться
місце в стек. p>
Гаразд, ви зрозуміли свою помилку і більше не міняєте
таблицю імпорту свого модуля. Але справа в тому, що для реалізації WriteFile
kernel32.dll викликає функцію NtWriteFile з ntdll.dll. А, оскільки таблицю
імпорту kernel32.dll ви змінили, знову викликається функція-шпигун, яка
викликає colleclStatistic і все починається заново. p>
Звідси висновок: при проведенні перехоплення необхідно
пропустити модулі, які ви самі прямо або побічно використовуєте. Ідеально
було б змінювати таблиці імпорту тільки в «нестандартних» модулях, так як,
швидше за все, саме це вам і потрібно: навряд чи вас цікавить, які функції
ntdll.dll викликаються під час виклику WriteFile, зазвичай достатньо просто знати,
що додаток викликало WriteFile. Визначати нестандартні модулі можна різними
способами, мене прийшли в голову такі: p>
За каталогу, в якому лежить файл. p>
За датою створення файлу (системні файли зазвичай мають
цілком певні дати створення). p>
За фіксованим списку імен. p>
Крім того, завжди є радикальне рішення: написати
графічний інтерфейс і покласти це завдання на користувача. :) p>
Функція збору статистики
p>
Відповідно до того, як вона використовується
функціями-шпигунами, функція збору статистики повинна мати наступні
характеристики: p>
Бере один четирехбайтний параметр, який передається
через стек. p>
Не повертає значення (в усякому разі, воно
ігнорується). p>
Сама очищає стек. p>
Очевидно, як-то збирає якусь статистику. Як
саме і яку, поки не важливо. p>
На C + + це реалізується приблизно так: p>
void
__stdcall collectStatistic (unsigned long n) p>
( p>
//
Будь-що, наприклад таке p>
functions [n]. count ++; p>
printf (( "called
% s (% d) n ", functions [n]. name.c_str (), functions [n]. count); p>
) p>
У цьому прикладі статистична інформація складається з
імені функції та кількості викликів, все це зберігається в масиві functions,
відображенням статистики займається сама досліджуваний додаток. p>
Механізм збору і відображення статистики
p>
Що збирати p>
Потенційно, функція збирання статистики може для
кожного дзвінка зберігати наступні параметри: p>
Ім'я функції. p>
Назва модуля. p>
Назва модуля, з якого стався виклик. p>
Ідентифікатор поточного потока.Время дзвінка. p>
Дамп стека. p>
Стан регістрів процесора p>
і так далі. p>
Загалом, рівень деталізації може бути дуже різним і
залежить від завдання. p>
Політика відображення p>
Два принципово різних підходи: p>
Дані доступні в реальному часі (за допомогою
якого-небудь GUI). p>
Дані доступні після завершення досліджуваного
додатки (у файлі на диску). p>
Обидва підходи мають свої плюси і мінуси: з точки зору
отримання даних, очевидно, що першим має всі можливості другу
(якщо вже дані відображаються, паралельно зберігати їх в лог не проблема), а, з
точки зору впливу на досліджуваний додаток, друге може вийти набагато
м'якше, і в якійсь ситуації це може виявитися критичним. Крім того, другий
підхід може виявитися значно простіше в реалізації. p>
ПРИМІТКА p>
Наприклад, якщо дані можна протягом
усього часу виконання зберігати в пам'яті, а запис на диск зробити тільки в
самому кінці (в DllMain). Або, трохи більше інтелектуально, спробувати
записувати/передавати дані тільки в ті моменти, коли досліджуваний
додаток само звертається до диску. p>
Але, оскільки перший підхід набагато ефектніше
(real-time, on-line, і навіть мультимедіа, якщо постаратися, - всі ці слова
можна обгрунтовано вжити в прес-релізі:)), далі розглядається в
основному він. p>
Де зберігати і як відображати статистику p>
Є три варіанти реалізації «збору і відображення»: p>
Дані зберігаються і відображаються dll, впровадженої в
досліджуваний додаток. p>
Дані зберігаються dll, впровадженої в досліджуваний
додаток, для відображення вона пересилає їх зовнішньому додатку. p>
І зберіганням, і відображенням займається зовнішнє
додаток, dll просто пересилає йому дані по мірі надходження. p>
Найбільш цікавий останній варіант (розглядаємо
відображення в реальному часі), так як за рахунок виносу частини логіки у зовнішню
додаток dll виходить відносно простий, в результаті чого знижується
ризик випадково зіпсувати що-небудь у досліджуваному додатку, спрощується налагодження
і підвищується надійність системи в цілому. p>
Реалізація
p>
Обмежимося простим випадком: p>
відслідковуються тільки виклики функцій, адреси яких
досліджуваний додаток отримує через GetProcAddress. p>
Зберігаємо лише імена функцій і модулів. p>
відображає дані в реальному часі. Як GUI
виступає консоль. :) P>
Дані зберігаються і відображаються в зовнішньому додатку. p>
Генерація функції-шпигуна
p>
Основну роботу з створення виконують наступні
нескладні класи: p>
// Клас, що дозволяє
працювати з відносними адресами. p>
// Дозволяє копіювати
відносні адреси, зберігаючи їх коректними. p>
struct relative_address p>
( p>
relative_address (): value (0)
() p>
// Коректно копіює відносний адресу. p>
relative_address (const
relative_address & a) p>
( p>
// Копіювання зі зміщенням на відстань
між покажчиками. p>
value = (unsigned long) a.value p>
+ (unsigned
long) & a.value p>
- (unsigned long) &value; p>
) p>
// Коректно присвоює відносний
адресу. p>
relative_address & operator = (const
relative_address & a) p>
( p>
if (this! = & a) p>
( p>
// Копіювання зі зміщенням на відстань
між покажчиками. p>
value = (unsigned long) a.value p>
+ (unsigned
long) & a.value p>
- (unsigned
long) &value; p>
) p>
return * this; p>
) p>
// Встановлює відносний адреса
відповідним вказаною абсолютного. p>
void set_absolute (void * a) p>
( p>
// Відносний адреса відраховується від
початку наступної інструкції. p>
// Оскільки в тих інструкціях, в які
входить відносний адреса, p>
// він знаходиться в кінці, початок наступної
інструкції - це кінець адреси. p>
value = (unsigned long) a - (unsigned
long) & value - sizeof (value); p>
) p>
unsigned long value; p>
); p>
// Клас, що спрощує роботу
з однобайтной командою. p>
template p>
struct one_byte_command p>
( p>
one_byte_command (): code (c) () p>
unsigned char code; p>
); p>
// Клас, що спрощує роботу
з командою з однобайтним кодом p>
// і 4-байтним операндом. p>
template p>
struct one_byte_value_command p>
( p>
one_byte_value_command ():
code (c) () p>
unsigned char code; p>
unsigned long value; p>
); p>
// Клас, що спрощує роботу
з командою з однобайтним кодом p>
// і відносним адресою p>
template p>
struct one_byte_rel_address_command p>
( p>
one_byte_rel_address_command ()
: Code (c) () p>
unsigned char code; p>
relative_address address; p>
); p>
З їх допомогою можна визначити класи для команд
процесора, а з них вже зібрати функцію. Наприклад, так: p>
// Команда pusha p>
typedef one_byte_command <0x60> pusha; p>
// Команда pushf p>
typedef one_byte_command <0x9C> pushf; p>
// Команда push xxx p>
typedef one_byte_value_command <0x68> push_value; p>
// Команда popa p>
typedef one_byte_command <0x61> popa; p>
// Команда popf p>
typedef one_byte_command <0x9D> popf; p>
// Команда call xxx p>
typedef one_byte_rel_address_command <0xE8> call_address; p>
// Команда jmp xxx p>
typedef one_byte_rel_address_command <0xE9> jmp_address; p>
// p>
// Функція-шпигун, зібрана
з цих команд p>
struct spy_function p>
( p>
pusha c1; p>
pushf c2; p>
push_value number; p>
call_address statistic; p>
p>
popf c5; p>
popa c6; p>
p>
jmp_address func; p>
); p>
ПРИМІТКА p>
Природно, щоб це
працювало, необхідно при оголошенні класів встановити вирівнювання даних по
кордоні одного байта. У Visual C + + це робиться так: p>
# pragma pack (1, push) p>
...// тут всі оголошення p>
# pragma pack (pop) p>
Як користуватися вийшла в підсумку класом
spy_function, продемонстровано нижче. p>
myGetProcAddress
p>
Не містить в собі нічого складного. Працює за
алгоритму установки однієї функції-шпигуна, як збереження інформації про
перехопленою функції повідомляє зовнішньому додатку ім'я функції і отримує в
відповідь відповідний цій функції номер. p>
void * __stdcall myGetProcAddress (HMODULE hLib, const char * name) p>
( p>
// Викликаємо справжню GetProcAddress,
отримуємо адресу функції p>
void * address = _GetProcAddress (hLib, name); p>
if (address == 0) p>
( p>
// Не доля p>
return NULL; p>
) p>
char full_name [MAX_PATH * 2]; p>
GetModuleFileNameA (hLib,
full_name, sizeof (full_name)/sizeof (full_name [0 ])); p>
strcat (full_name, ""); p>
if
(reinterpret_cast (name)> 0x0000ffff) p>
( p>
// Копіюємо ім'я p>
strcat (full_name, name); p>
) p>
else p>
( p>
// А деякі функції експортуються за
ордіналам ... p>
char ordinal [10]; p>
strcat (full_name, "by
ordinal: "); p>
strcat (full_name,
itoa (reinterpret_cast (name), ordinal, 16 )); p>
) p>
COPYDATASTRUCT cd = (0); p>
// 1
потрібно, щоб врахувати в довжині завершальний NULL-символ. p>
cd.cbData = strlen (full_name) + 1; p>
cd.lpData = full_name; p>
p>
// посилаємо рядок p>
int number =
SendMessage (g_hSecretWindow, WM_COPYDATA, 0, p>
reinterpret_cast (& cd )); p>
p>
// Генеруємо функцію-шпигуна p>
try p>
( p>
// Див «Чим же все це закінчиться?» p>
void * spyMem = HeapAlloc (GetProcessHeap (), 0,
sizeof (spy_function )); p>
spy_function * spy =
new (spyMem) spy_function; p>
// Встановлюємо її параметри. p>
spy-> number.value = number; p>
spy-> statistic.address.set_absolute (collectStatistic); p>
spy-> func.address.set_absolute (address); p>
// Повертаємо покажчик на функцію-шпигун. p>
return spy; p>
) p>
catch (...) p>
( p>
// Не доля p>
PostMessage (g_hSecretWindow,
WM_CANNOTHOOK, number, 0); p>
// Повертаємо покажчик на функцію p>
return address; p>
) p>
) p>
collectStatistic
p>
Оскільки даних мало і надсилати їх нескладно, функція
collectStatistic вийшла просто чудова: p>
void __stdcall collectStatistic (unsigned long n) p>
( p>
// Посилаємо номер викликається функції p>
PostMessage (g_hSecretWindow,
WM_CALLED, n, 0); p>
) p>
Зберігання і відображення
p>
І тим і іншим займається зовнішню програму.
Реалізовано всі вкрай нехитро: p>
// Структура, що зберігає
статистику для однієї функції p>
struct func_descrition p>
( p>
std:: string name;// Ім'я функції p>
int count;// Кількість
викликів p>
); p>
// Вектор, який зберігає всю
статистику взагалі p>
std:: vector functions; p>
# define WM_CALLED (WM_USER + 1) p>
# define WM_CANNOTHOOK (WM_USER + 2) p>
// Процедура вікна, якому
впроваджена dll посилає дані p>
LRESULT CALLBACK WndProc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM
lParam) p>
( p>
switch (uMsg) p>
( p>
// Викликана GetProcAddress p>
case WM_COPYDATA: p>
( p>
// Отримуємо вказівник на передану структуру p>
COPYDATASTRUCT * pcd =
reinterpret_cast (lParam); p>
// Отримуємо ім'я p>
char * str =
(char *) pcd-> lpData; p>
printf ( "New function:
% sn ", str); p>
// Нова функція p>
func_descrition f; p>
f.count = 0; p>
f.name = str; p>
// Додаємо її в вектор p>
functions.push_back (f); p>
) p>
// Повертаємо номер p>
return (functions.size () - 1); p>
// Викликана перехоплена функція p>
case WM_CALLED: p>
// Збільшуємо кількість викликів p>
functions [wParam]. count ++; p>
printf ( "Called% sn",
functions [wParam]. name.c_str ()); p>
return 0; p>
// Не вдалося встановити перехоплювач на
функцію p>
case WM_CANNOTHOOK: p>
// Повідомляємо користувача p>
printf ( "Can not hook
% sn ", functions [wParam]. name.c_str ()); p>
return 0; p>
) p>
return DefWindowProc (hwnd,
uMsg, wParam, lParam); p>
) p>
ПРИМІТКА p>
Для простоти цей код не
перевіряє ім'я функції на унікальність, тому в functions може виявитися
декілька записів для однієї і тієї ж функції. p>
Впровадження в додаток і перехоплення GetProcAddress
p>
Так як ця стаття не присвячена ні перехоплення, ні
запровадження (на ці теми є багато інших хороших статей), для реалізації
вибрані прості, але радикальні засоби. Впровадження зроблено через
CreateRemoteThread, а перехоплення GetProcAddress - заміною її перших п'яти байт на
команду jmp. p>
Для передачі впровадженої dll описувача вікна, якому
вона повинна надсилати повідомлення (g_hSecretWindow у прикладі), використана техніка
зі статті «HOWTO: Виклик функції в іншому процесі». p>
Чим же все це закінчиться?
p>
Буде завершення процесу. Як відомо, під час
завершення процесу все dll вивантажуються, і вся виділена пам'ять звільняється.
При цьому можуть відбутися наступні неприємності: p>
Наша dll буде вивантажено завчасно. p>
Раніше часу буде звільнена пам'ять, в якій
розташовані згенеровані функції. p>
В обох випадках досліджуваний програма просить Access
Violation, після чого говорити про те, що його робота не порушена, буде
досить складно. p>
Невигружаемая dll p>
Оскільки в нашої dll лічильник посилань завжди більше 0
(LoadLibrary була викликана, а FreeLibrary немає), вона вивантажується однією з
останніх, але в деяких випадках цього може виявитися недостатньо.
Радикальним рішенням проблеми є «ручна» завантаження dll, описана в
статті Максима М. Гумерова «Завантажувач PE-файлів». Це досить трудомісткий, але
зате практично гарантований варіант. Іншим можливим рішенням (для
NT/2000/...) може бути видалення dll зі списку завантажених модулів в PEB, але як
це зробити і чи буде це працювати, я поки не знаю ... p>
Остання ідея, що прийшла мені в голову: p>
чесно завантажити dll в процес, дозволити завантажувачу
виконати свою роботу p>
скопіювати що вийшов образ p>
вивантажити dll p>
записати в те саме місце адресного простору образ
dll. p>
молитися. p>
Це одна з са?? их «брудних хаков», які я
коли-небудь провертала:) Іноді воно працює, іноді - ні. І навіть якщо все
на перший погляд працює, я не беруся сказати, які будуть побічні ефекти. p>
Підводячи підсумок: якщо завдання і має хороше рішення, його
опис виходить далеко за рамки цієї статті. Тому наша dll буде
розвантажуватися, хоча іноді це і може привести до проблем. p>
Неосвобождаемая пам'ять p>
З пам'яттю простіше: щоб її точно ніхто не звільнив,
досить відмовитися від стандартного оператора new, і використовувати замість нього
placement new, виділяючи пам'ять як-небудь інакше. p>
ПРИМІТКА p>
Під час тестів виявилося, що в
Windows XP, при виділенні пам'яті звичайним new і статичної лінковке CRT,
деякі (не всі і не завжди, але цілком відтворено) блоки пам'яті з
функціями-шпигунами виявляються звільнені. При використанні CRT в dll цієї
проблеми не було, з чим це пов'язано, я не знаю. p>
Результат
p>
Yes! Воно працює!! :) p>
ПОПЕРЕДЖЕННЯ p>
Нормального тестування не проводилося,
крім того, у мене під рукою не виявилося Windows NT 4. Але на Windows 2000,
XP і 2003 Server перевірив, на перший погляд все добре ... І навіть XP SP2 НЕ
страшний:) p>
Для успішного старту треба покласти spyloader.exe і
apispy.dll в один каталог, після чого запустити spyloader, передавши йому в
командному рядку шлях до exe-файлу досліджуваного програми. p>
Тільки приготуйтеся до того, що GetProcAddress --
досить популярна функція, і отримати сотню функцій-шпигунів (тобто викликів
GetProcAddress) при дослідженні notepad.exe - не питання, досить спробувати
відкрити який-небудь файл. А вже якщо ви запустите довідку і трохи по ній
походіть ... У мене вийшло 530 функцій-шпигунів за дві хвилини:) Тому, якщо
ви дійсно будете реалізовувати щось подібне, то краще фіксувати не
все підряд, а фільтрувати виклики хоча б на ім'я модуля. p>
Список літератури h2>
Тихомиров В.А. «Перехоплення
API-функций в Windows NT/2000/XP ». p>
Ігор Філімонов «Методи перехоплення API-викликів в Win32»
p>
Intel Corporation
«IA-32 Intel Architecture Software Developer's Manual», частини 2A і 2B p>
Максим М. Гумер «Завантажувач PE-файлів» p>
Сергій холодиль «HOWTO: Виклик функції в іншому
процесі » p>
Для підготовки даної роботи були використані
матеріали з сайту http://www.rsdn.ru/
p>