ПЕРЕЛІК ДИСЦИПЛІН:
  • Адміністративне право
  • Арбітражний процес
  • Архітектура
  • Астрологія
  • Астрономія
  • Банківська справа
  • Безпека життєдіяльності
  • Біографії
  • Біологія
  • Біологія і хімія
  • Ботаніка та сільське гос-во
  • Бухгалтерський облік і аудит
  • Валютні відносини
  • Ветеринарія
  • Військова кафедра
  • Географія
  • Геодезія
  • Геологія
  • Етика
  • Держава і право
  • Цивільне право і процес
  • Діловодство
  • Гроші та кредит
  • Природничі науки
  • Журналістика
  • Екологія
  • Видавнича справа та поліграфія
  • Інвестиції
  • Іноземна мова
  • Інформатика
  • Інформатика, програмування
  • Юрист по наследству
  • Історичні особистості
  • Історія
  • Історія техніки
  • Кибернетика
  • Комунікації і зв'язок
  • Комп'ютерні науки
  • Косметологія
  • Короткий зміст творів
  • Криміналістика
  • Кримінологія
  • Криптология
  • Кулінарія
  • Культура і мистецтво
  • Культурологія
  • Російська література
  • Література і російська мова
  • Логіка
  • Логістика
  • Маркетинг
  • Математика
  • Медицина, здоров'я
  • Медичні науки
  • Міжнародне публічне право
  • Міжнародне приватне право
  • Міжнародні відносини
  • Менеджмент
  • Металургія
  • Москвоведение
  • Мовознавство
  • Музика
  • Муніципальне право
  • Податки, оподаткування
  •  
    Бесплатные рефераты
     

     

     

     

     

     

         
     
    API Spying
         

     

    Інформатика, програмування

    API Spying

    Сергій холодиль

    Я відкриваю властивості рослин і трав ..

    Борис Гребєнщиков

    Словосполученням «API Spying» називається детектор викликами функцій API деяким додатком. Тобто, кожен факт виклику цим додатком вибраних функцій якимось чином фіксується, наприклад, додається запис в лог.        

    ПРИМІТКА   

    Для ясності назвемо «деяке   додаток »досліджуваним додатком, а« вибрані функції »- відстежує   функціями.     

    Навіщо це потрібно

    API Spying може використовуватися на одному з етапів дослідження програми, логіку роботи якої ви поки що не до кінця розумієте. Хоча ця технологія і не дозволяє отримати детальну інформацію, вона може значно звузити область подальших етапів дослідження, сконцентрувавши вашу увагу на ті виклики, які відбуваються в ключові моменти роботи програми.

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

    Дійсно, перехоплення API - чудова і часто згадувана техніка (на даний момент на RSDN цій темі присвячені три статті), що дозволяє досить глибоко вивчити досліджуваний додаток, але це і набагато більш трудомістка рішення. Навіть якщо реалізації функцій будуть майже порожніми (тільки запис в лог і виклик оригінальній функції), ваш код буде приблизно таким:        

    typedef int (__stdcall * Function1_type) (int i);   

    Function_type _Function1;      

    // Обгортка, логірующая виклики   

    int __stdcall MyFunction1 (int i)   

    (   

    printf ( "MyFunction1n ");   

    return _Function (i);// Виклик оригінальній функції   

    )      

    ...      

    // Перехоплення всіх функцій   

    void HookThemAll ()   

    (   

    ...   

    // Перехоплення функції _Function1, що експортується   some.dll   

    HookIt ( "some.dll",   "_Function1 @ 4", MyFunction1, & _Function1);   

    ...   

    )             

    ПРИМІТКА   

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

    Тобто, для кожної функції доведеться:

    визначити тип;

    визначити змінну;

    написати обгортку;

    додати рядок у HookThemAll.

    Це, звичайно, досить прості операції ... Але уявіть, що таким чином вам потрібно перехопити кілька сотень функцій. А якщо не у всіх функцій відомі прототипи? А якщо деякі dll завантажується динамічно, і ви поки навіть не знаєте, які їх функції використовуються додатком? А якщо після того, як ви всі успішно перехопити і перегляньте отримані логи, стане зрозуміло, що для детального розуміння роботи програми треба було перехопити всього дві функції і вивчити їх параметри:)?

    Коли всі ці питання постали переді мною, я зайнявся API Spying-му.

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

    Постановка завдання

    У самому загальному вигляді завдання виглядає так:

    Необхідно отримувати інформацію про факти виклику вибраних функцій досліджуваних додатком.

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

    Збір статистики для будь-якого (в тому числі заздалегідь невідомого) кількості функцій з будь-яких (у тому числі, із завантаження динамічно) модулів не має становити труднощів.

    Працездатність досліджуваного програми не повинна порушуватися.

    Формулювання ТЗ

    Логічно розвинемо вимоги, висловлені в постановці завдання:

    Ми розглядаємо тільки програмні реалізації. Це означає, що статистика збирається програмно, і цим займається наш код.

    Оскільки при кожному виклику відслідковуються функцій управління має передаватися нашому коду, все відслідковують функції потрібно перехопити (тим чи іншим способом; детальний розгляд способів перехоплення API виходить за рамки статті). Нашу функцію, якої в результаті перехоплення передаватиметься управління, назвемо функцією-шпигуном (термінологія моя, жалкую, якщо не правий).

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

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

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

    Кілька додаткових побажань:

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

    ПРИМІТКА   

    Ви класно знаєте асемблер, і   вважаєте, що це пари дрібниць? Можливо, ви не врахували, що код функцій буде   розташований в довільному місці адресного простору і що (забігаючи вперед;   але ви-то це всі повинні розуміти) функції не можуть модифіковані стек і   регістри. Якщо і це для вас не проблема, то, по-перше, прийміть моє   щире захоплення (без жартів!), по-друге, прочитайте наступний пункт. :)     

    Автоматична генерація має на увазі виділення пам'яті для коду функцій, а, оскільки їх може бути багато, бажано щоб функції були короткими. Тому, знову ж таки, добре б підрахунок статистики взяв на себе хтось інший.

    І кілька обмежень:

    Ця реалізація підтримує тільки Intel x86-сумісні процесори.

    Для роботи функції-шпигуна від ОС потрібно тільки одне: вона повинна дозволяти виконувати динамично згенерований код. Ця умова дотримується у всіх версіях Windows і, швидше за все, в переважній більшості інших ОС загального користування. Але, оскільки нам потрібен ще й спосіб перехоплення, обмежимося лінійкою Windows NT/2000/XP. Використовуючи інші способи перехоплення, можна реалізувати API Spying для інших ОС.

    Невідомо, як на виконання згенерованого кода будуть реагувати антивіруси. Можливо, вони будуть недостатньо толерантні. :)        

    РАДА   

    Про подібні обмеження краще не   забувати і в реальних проектах, тому що інакше виконати ТЗ буде практично   неможливо.     

    Чому додаток може перестати працювати

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

    При додаванні збору статистики зміниться швидкість роботи функцій. Звичайно це ні на що не впливає, але якщо в середині критичного до швидкості виконання коду ми несподівано (для програми та його автора) почнемо запис у файл, може вийти погано. Наприклад, FPS впаде раз на десять:) Але FPS -- це не страшно, страшно, якщо ви досліджуєте багатопотокове додаток c некоректно написаної синхронізацією, і зміна часу виконання потоків призведе до дедлокам, падінням або просто незрозумілої поведінки.

    Крім процесорного часу, наш код використовує і інші ресурси: пам'ять, стек, (можливо) вікна, об'єкти ядра (файли, події, і тощо) та інші. У певному фантастичному випадку це може стати останньою краплею, що приводить до вичерпання доступних ресурсів потоком, процесом, або навіть системою.

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

    Всі ці слідства в тій чи іншій мірі властиві будь-якої програмної реалізації API Spying-а, і в жодній з цих ситуацій я не можу порадити вам нічого хорошого. Можна лише спробувати зменшити ступінь впливу і уникнути настільки згубних наслідків.

    Передпроектні дослідження: функції в Intel x86

    Як ви вже, напевно, зрозуміли, нам належить динамічна генерація коду функцій-шпигунів. Хоча нічого особливо складного в цьому не буде (вони дійсно дуже прості), невелике теоретичне введення допоможе вам зрозуміти (а мені - пояснити), як повинна бути написана функція-шпигун, щоб виклик відслідковується функції завершився без перешкод.

    Виклик

    З точки зору процесора виклик функції виконує інструкція call, що має декілька різних форм:        

    call xxxxxxh   

    call xxxxh: xxxxxxh   

    call eax   

    call   [eax]   

    ...     

    Вона зберігає в стеку адресу, за якою потрібно передати управління після закінчення функції, і передає керування на початок функції.

    Передача параметрів

    Процесор Intel x86 нічого не знає про параметри викликаються функцій, тому механізм передачі параметрів може бути довільним, головне щоб викликає і викликається код домовилися про нього заздалегідь. Місць, де можна зберегти параметри, не так вже й багато: або в регістрах, або в стеку, або частину там, а частина там.        

    ПРИМІТКА   

    Звичайно, можна передавати параметри по   посиланням або значенням, в прямому порядку або в зворотному, але це для нас не   важливо, важливо тільки те, де передана інформація (параметри або їх адреси)   знаходиться.     

    Передача параметрів через регістри використовується в основному в двох випадках:

    компілятором для оптимізації.

    Асемблер-програмістом з ліні або в гонитві за продуктивністю. Щоб дістати параметри із стека, треба написати декілька додаткових команд, а в регістрах вони відразу під рукою.

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

    push ...   ; Параметр   

    push ...   ; Ще один параметр   

    push ...   ; І останній параметр   

    call xxxxxh; Виклик     

    А стек до моменту початку виконання функції - так:

    Малюнок 1. Стан стека на початку виконання функції.

    Повернення з функції

    Повернення управління виробляє інструкція ret, що має чотири різні форми:        

    ret   

    ret xxxh   

    retf   

    retf xxxh             

    ПРИМІТКА   

    Модифікація retf призначена для   повернення з функції, яку викликали з іншого сегмента ( «далеким викликом»).   Нижче вона не згадується, тому що, по-перше, в Windows ви її навряд чи   зустрінете, по-друге, з точки зору реалізації API Spying-а, вона практично   не відрізняється від ret.       

    Завдання, що виконується ret *:

    Видалити з стека адреса повернення.

    (опціонально) Вилучити з стека вказану кількість байт.

    Надіслати управління за адресою повернення.

    При цьому всі версії ret * припускають, що адреса повернення знаходиться на вершині стека, а байти, які треба видалити (якщо треба) - Відразу за ним.

    Оскільки, як і при виклику, процесор нічого не знає про параметри, видаляти їх з стека при поверненні чи ні - особиста справа функції і викликає її коду. Поширені обидва варіанти: згідно формату виклику функцій __cdecl за очищення стека відповідає викликає код, а згідно формату __stdcall цим займається сама функція.        

    ПРИМІТКА   

    Майже всі стандартні API Windows   дотримуються __stdcall, і більшість функцій, що експортуються з dll   інших виробників, також слідують цього формату.     

    що повертається значення

    Як і у випадку з параметрами, про що повертаються значення процесор теж нічого не знає, і те, як саме і що саме ви будете повертати, його не стосується. Зазвичай повертається значення передається через регістр eax або через пару eax: edx.

    Стан регістрів до і після виклику

    І це питання залишається повністю на совісті програміста (у випадку мови високого рівня - програміста, який писав компілятор). Якщо вірити статті «Arguments Passing and Naming Conventions» в MSDN, для всіх стандартних форматів виконання функцій компілятор гарантує збереження регістрів ESI, EDI, EBX і EBP. Це означає, що викликає код:

    Може розраховувати на те, що ці регістри не поміняються.

    Не повинен розраховувати на регістри EAX, ECX, EDX, EFLAGS (з ним трохи складніше, очевидно, частина прапорів все-таки має залишитися незмінною, просто MSDN про це не згадує), а також на регістри MMX, FPU, XMM.        

    ПРИМІТКА   

    А як же інші регістри? Сегментні,   керуючі, GDTR, LDTR, ....? З ними просто: якщо функція змінює якийсь із   цих регістрів, то, або це документований побічний ефект (наприклад,   очікуваний результат) її виклику, або від функції дуже, дуже погано   пожартував ...     

    Проектування

    Система в цілому складається з чотирьох частин:

    Функція-шпигун.

    Механізм встановлення шпигунів.

    Функція збору статистики.

    Механізм збору і відображення статистики.

    Функція-шпигун

    Завдання

    Завдання роботи функції-шпигуна:

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

    Викликати відстежуємо функцію.

    Обмеження

    Обмеження пов'язані з тим, що відстежувати функція повинна працювати без змін. Для цього перед її викликом:

    Необхідно привести стек в той же стан, який було до початку роботи функції-шпигуна. Це означає, що, по-перше, не можна зберегти в стеку яке-небудь значення для використання після повернення з відслідковується функції, по-друге, не можна використовувати для виклику інструкцію call, так як вона додасть в стек адреса повернення (на цю тему див. нижче, в розділі «Отримання управління після повернення з відслідковується функції»).

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

    Код, який треба згенерувати

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

    Обидва підходи однаково просто реалізуються, але через особливості системи команд Intel x86 ближній виклик/передача управління з абсолютному адресою буде виглядати приблизно так:        

    ; Виклик   

    mov eax, <абсолютний адреса функції   >   

    call eax      

    ; Передача управління   

    mov eax, <абсолютний адреса   функції>   

    jmp eax     

    Тобто, як не старайся, а значення одного регістра (в даному прикладі регістра eax, але на його місці міг бути кожен) зберегти не вдається.

    Тому вибрана версія з відносною адресацією:        

    pusha; зберігаємо регістри і прапори.   

    pushf; Це, звичайно, параноя ...      

    push   <номер>; передаємо в параметрі   номер відслідковується функції   

    call   <відносний адреса функція збору статистик>   

      

    popf;   відновлюємо прапори   

    popa;   і регістри   

      

    jmp   <відносний адреса відслідковується функції>     

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

    перехоплення через таблицю імпорту;

    перехоплення через таблицю експорту;

    перехоплення GetProcAddress і підміна адреси запитуваної функції.

    Якщо ви використовуєте інший метод перехоплення (наприклад, заміну декількох початкових байтів на команду jmp), вам доведеться трохи змінити мій код.

    Отримання управління після повернення з відслідковується функції

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

    Для цього потрібно:

    Видалити з стека стара адреса повернення.        

    ПРИМІТКА   

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

    Десь зберегти його на час дзвінка відслідковується функції.

    Викликати функцію.

    Отримати/виміряти/.. те, що ви хотіли.

    Повернути управління за старою адресою.

    Ключовим питанням цього алгоритму є: «де ж це десь, в якому можна зберегти адреса повернення? »Стек міняти не можна, тому він відпадає. Зберігати в регістрах теж не можна: ті регістри, які можуть змінитися після виклику функції, може змінити відстежуємо функція, і дані пропадуть, а ті регістри, які не повинні змінюватися після виклику, не можна змінювати нам, тому що відновити їх ми не зуміємо - ніде зберегти їх старі значення:)

    Залишається тільки зберігання в глобальній області пам'яті. Так як додаток може бути багато-, доступ до пам'яті потрібно синхронізувати, і окремо зберігати дані для кожного потоку. Так як можлива рекурсія, необхідно зберігати не одну адресу повернення, а стек адрес ... І, не дивлячись на всі ці заходи, що буде, якщо в відслідковується функції відбудеться виняток і почнеться розгортання стека? Правильно, буде дуже погано ...

    Загалом, це шлях для людей, міцних духом і готових до випробувань. Далі в статті він не розглядається.

    Механізм встановлення шпигунів

    Алгоритм установки однієї функції-шпигуна:

    Генерується функція-шпигуна, що при генерації встановлюється її номер, адреса відслідковується функції та адресу функції збору статистики.

    Перехоплюється відстежуємо функція, тепер замість неї додатком повинна викликатися функція-шпигун.

    Где-то зберігається інформація, про те, що перехоплена функція з таким-то ім'ям і їй сопоставлен такий-то номер. Ця інформація буде використана при виконанні функції збору статистики.

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

    Відстеження викликів функцій динамічно завантажуваних dll

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

    Відстеження всіх викликів

    Загальна ідея: пройтися по таблицях імпорту завантажених модулів і, не особливо замислюючись, перехопити всі згадані там функції. Крім того, потрібно подбати про GetProcAddress (див. попередній пункт) і про ще не завантажених модулях: їх таблиці імпорту теж необхідно обробити. Щоб не пропустити появу нових модулів, можна, наприклад, перехопити всі версії LoadLibrary [Ex] A/W.

    Просто, правда? Просто, але, на жаль, у такому вигляді працювати, швидше за все, не буде.        

    ПОПЕРЕДЖЕННЯ   

    Цей варіант я так і не реалізував   (не було потреби), тому про його неминучих маленьких особливості майже нічого не   знаю. Мої спроби поміркувати представлені нижче, але практики за ними не   стоїть, і гарантувати відсутність проблем я не можу. Шкодую.     

    Проблема цього підходу полягає в майже гарантованому виникненні нескінченної рекурсії. Наприклад, нехай collectStatistic записує дані у файл за допомогою функції WriteFile. Якщо ця функція виявилася перехоплена і у вашому модулі, то спроба запису призведе до виклику вашої функції-шпигуна, яка викличе collectStatistic і т.д. поки не скінчиться місце в стек.

    Гаразд, ви зрозуміли свою помилку і більше не міняєте таблицю імпорту свого модуля. Але справа в тому, що для реалізації WriteFile kernel32.dll викликає функцію NtWriteFile з ntdll.dll. А, оскільки таблицю імпорту kernel32.dll ви змінили, знову викликається функція-шпигун, яка викликає colleclStatistic і все починається заново.

    Звідси висновок: при проведенні перехоплення необхідно пропустити модулі, які ви самі прямо або побічно використовуєте. Ідеально було б змінювати таблиці імпорту тільки в «нестандартних» модулях, так як, швидше за все, саме це вам і потрібно: навряд чи вас цікавить, які функції ntdll.dll викликаються під час виклику WriteFile, зазвичай достатньо просто знати, що додаток викликало WriteFile. Визначати нестандартні модулі можна різними способами, мене прийшли в голову такі:

    За каталогу, в якому лежить файл.

    За датою створення файлу (системні файли зазвичай мають цілком певні дати створення).

    За фіксованим списку імен.

    Крім того, завжди є радикальне рішення: написати графічний інтерфейс і покласти це завдання на користувача. :)

    Функція збору статистики

    Відповідно до того, як вона використовується функціями-шпигунами, функція збору статистики повинна мати наступні характеристики:

    Бере один четирехбайтний параметр, який передається через стек.

    Не повертає значення (в усякому разі, воно ігнорується).

    Сама очищає стек.

    Очевидно, як-то збирає якусь статистику. Як саме і яку, поки не важливо.

    На C + + це реалізується приблизно так:        

    void   __stdcall collectStatistic (unsigned long n)   

    (   

    //   Будь-що, наприклад таке   

    functions [n]. count ++;   

    printf (( "called   % s (% d) n ", functions [n]. name.c_str (), functions [n]. count);   

    )     

    У цьому прикладі статистична інформація складається з імені функції та кількості викликів, все це зберігається в масиві functions, відображенням статистики займається сама досліджуваний додаток.

    Механізм збору і відображення статистики

    Що збирати

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

    Ім'я функції.

    Назва модуля.

    Назва модуля, з якого стався виклик.

    Ідентифікатор поточного потока.Время дзвінка.

    Дамп стека.

    Стан регістрів процесора

    і так далі.

    Загалом, рівень деталізації може бути дуже різним і залежить від завдання.

    Політика відображення

    Два принципово різних підходи:

    Дані доступні в реальному часі (за допомогою якого-небудь GUI).

    Дані доступні після завершення досліджуваного додатки (у файлі на диску).

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

    ПРИМІТКА   

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

    Але, оскільки перший підхід набагато ефектніше (real-time, on-line, і навіть мультимедіа, якщо постаратися, - всі ці слова можна обгрунтовано вжити в прес-релізі:)), далі розглядається в основному він.

    Де зберігати і як відображати статистику

    Є три варіанти реалізації «збору і відображення»:

    Дані зберігаються і відображаються dll, впровадженої в досліджуваний додаток.

    Дані зберігаються dll, впровадженої в досліджуваний додаток, для відображення вона пересилає їх зовнішньому додатку.

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

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

    Реалізація

    Обмежимося простим випадком:

    відслідковуються тільки виклики функцій, адреси яких досліджуваний додаток отримує через GetProcAddress.

    Зберігаємо лише імена функцій і модулів.

    відображає дані в реальному часі. Як GUI виступає консоль. :)

    Дані зберігаються і відображаються в зовнішньому додатку.

    Генерація функції-шпигуна

    Основну роботу з створення виконують наступні нескладні класи:        

    // Клас, що дозволяє   працювати з відносними адресами.   

    // Дозволяє копіювати   відносні адреси, зберігаючи їх коректними.   

    struct relative_address   

    (   

    relative_address (): value (0)   ()      

    // Коректно копіює відносний адресу.   

    relative_address (const   relative_address & a)   

    (   

    // Копіювання зі зміщенням на відстань   між покажчиками.   

    value = (unsigned long) a.value   

    + (unsigned   long) & a.value   

    - (unsigned long) &value;   

    )      

    // Коректно присвоює відносний   адресу.   

    relative_address & operator = (const   relative_address & a)   

    (   

    if (this! = & a)   

    (   

    // Копіювання зі зміщенням на відстань   між покажчиками.   

    value = (unsigned long) a.value   

    + (unsigned   long) & a.value   

    - (unsigned   long) &value;   

    )      

    return * this;   

    )      

    // Встановлює відносний адреса   відповідним вказаною абсолютного.   

    void set_absolute (void * a)   

    (   

    // Відносний адреса відраховується від   початку наступної інструкції.   

    // Оскільки в тих інструкціях, в які   входить відносний адреса,   

    // він знаходиться в кінці, початок наступної   інструкції - це кінець адреси.   

    value = (unsigned long) a - (unsigned   long) & value - sizeof (value);   

    )      

    unsigned long value;   

    );      

    // Клас, що спрощує роботу   з однобайтной командою.   

    template   

    struct one_byte_command   

    (   

    one_byte_command (): code (c) ()   

    unsigned char code;   

    );      

    // Клас, що спрощує роботу   з командою з однобайтним кодом   

    // і 4-байтним операндом.   

    template   

    struct one_byte_value_command   

    (   

    one_byte_value_command ():   code (c) ()   

    unsigned char code;   

    unsigned long value;   

    );      

    // Клас, що спрощує роботу   з командою з однобайтним кодом   

    // і відносним адресою   

    template   

    struct one_byte_rel_address_command   

    (   

    one_byte_rel_address_command ()   : Code (c) ()   

    unsigned char code;   

    relative_address address;   

    );     

    З їх допомогою можна визначити класи для команд процесора, а з них вже зібрати функцію. Наприклад, так:        

    // Команда pusha   

    typedef one_byte_command <0x60> pusha;   

    // Команда pushf   

    typedef one_byte_command <0x9C> pushf;   

    // Команда push xxx   

    typedef one_byte_value_command <0x68> push_value;   

    // Команда popa   

    typedef one_byte_command <0x61> popa;   

    // Команда popf   

    typedef one_byte_command <0x9D> popf;   

    // Команда call xxx   

    typedef one_byte_rel_address_command <0xE8> call_address;   

    // Команда jmp xxx   

    typedef one_byte_rel_address_command <0xE9> jmp_address;      

    //   

    // Функція-шпигун, зібрана   з цих команд   

    struct spy_function   

    (   

    pusha c1;   

    pushf c2;      

    push_value number;   

    call_address statistic;   

      

    popf c5;   

    popa c6;   

      

    jmp_address func;   

    );             

    ПРИМІТКА   

    Природно, щоб це   працювало, необхідно при оголошенні класів встановити вирівнювання даних по   кордоні одного байта. У Visual C + + це робиться так:   

    # pragma pack (1, push)   

    ...// тут всі оголошення   

    # pragma pack (pop)       

    Як користуватися вийшла в підсумку класом spy_function, продемонстровано нижче.

    myGetProcAddress

    Не містить в собі нічого складного. Працює за алгоритму установки однієї функції-шпигуна, як збереження інформації про перехопленою функції повідомляє зовнішньому додатку ім'я функції і отримує в відповідь відповідний цій функції номер.        

    void * __stdcall myGetProcAddress (HMODULE hLib, const char * name)   

    (   

    // Викликаємо справжню GetProcAddress,   отримуємо адресу функції   

    void * address = _GetProcAddress (hLib, name);      

    if (address == 0)   

    (   

    // Не доля   

    return NULL;   

    )      

    char full_name [MAX_PATH * 2];   

    GetModuleFileNameA (hLib,   full_name, sizeof (full_name)/sizeof (full_name [0 ]));      

    strcat (full_name, "");      

    if   (reinterpret_cast (name)> 0x0000ffff)   

    (   

    // Копіюємо ім'я   

    strcat (full_name, name);   

    )   

    else   

    (   

    // А деякі функції експортуються за   ордіналам ...   

    char ordinal [10];      

    strcat (full_name, "by   ordinal: ");   

    strcat (full_name,   itoa (reinterpret_cast (name), ordinal, 16 ));   

    )      

    COPYDATASTRUCT cd = (0);      

    // 1   потрібно, щоб врахувати в довжині завершальний NULL-символ.   

    cd.cbData = strlen (full_name) + 1;   

    cd.lpData = full_name;   

      

    // посилаємо рядок   

    int number =   SendMessage (g_hSecretWindow, WM_COPYDATA, 0,   

    reinterpret_cast (& cd ));   

      

    // Генеруємо функцію-шпигуна   

    try   

    (   

    // Див «Чим же все це закінчиться?»   

    void * spyMem = HeapAlloc (GetProcessHeap (), 0,   sizeof (spy_function ));   

    spy_function * spy =   new (spyMem) spy_function;      

    // Встановлюємо її параметри.   

    spy-> number.value = number;   

    spy-> statistic.address.set_absolute (collectStatistic);   

    spy-> func.address.set_absolute (address);      

    // Повертаємо покажчик на функцію-шпигун.   

    return spy;   

    )   

    catch (...)   

    (   

    // Не доля   

    PostMessage (g_hSecretWindow,   WM_CANNOTHOOK, number, 0);      

    // Повертаємо покажчик на функцію   

    return address;   

    )   

    )     

    collectStatistic

    Оскільки даних мало і надсилати їх нескладно, функція collectStatistic вийшла просто чудова:        

    void __stdcall collectStatistic (unsigned long n)   

    (   

    // Посилаємо номер викликається функції   

    PostMessage (g_hSecretWindow,   WM_CALLED, n, 0);   

    )     

    Зберігання і відображення

    І тим і іншим займається зовнішню програму. Реалізовано всі вкрай нехитро:        

    // Структура, що зберігає   статистику для однієї функції   

    struct func_descrition   

    (   

    std:: string name;// Ім'я функції   

    int count;// Кількість   викликів   

    );      

    // Вектор, який зберігає всю   статистику взагалі   

    std:: vector functions;      

    # define WM_CALLED (WM_USER + 1)   

    # define WM_CANNOTHOOK (WM_USER + 2)      

    // Процедура вікна, якому   впроваджена dll посилає дані   

    LRESULT CALLBACK WndProc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM   lParam)   

    (   

    switch (uMsg)   

    (   

    // Викликана GetProcAddress   

    case WM_COPYDATA:   

    (   

    // Отримуємо вказівник на передану структуру   

    COPYDATASTRUCT * pcd =   reinterpret_cast (lParam);      

    // Отримуємо ім'я   

    char * str =   (char *) pcd-> lpData;   

    printf ( "New function:   % sn ", str);      

    // Нова функція   

    func_descrition f;      

    f.count = 0;   

    f.name = str;      

    // Додаємо її в вектор   

    functions.push_back (f);   

    )      

    // Повертаємо номер   

    return (functions.size () - 1);      

    // Викликана перехоплена функція   

    case WM_CALLED:   

    // Збільшуємо кількість викликів   

    functions [wParam]. count ++;   

    printf ( "Called% sn",   functions [wParam]. name.c_str ());   

    return 0;      

    // Не вдалося встановити перехоплювач на   функцію   

    case WM_CANNOTHOOK:   

    // Повідомляємо користувача   

    printf ( "Can not hook   % sn ", functions [wParam]. name.c_str ());   

    return 0;   

    )      

    return DefWindowProc (hwnd,   uMsg, wParam, lParam);   

    )             

    ПРИМІТКА   

    Для простоти цей код не   перевіряє ім'я функції на унікальність, тому в functions може виявитися   декілька записів для однієї і тієї ж функції.       

    Впровадження в додаток і перехоплення GetProcAddress

    Так як ця стаття не присвячена ні перехоплення, ні запровадження (на ці теми є багато інших хороших статей), для реалізації вибрані прості, але радикальні засоби. Впровадження зроблено через CreateRemoteThread, а перехоплення GetProcAddress - заміною її перших п'яти байт на команду jmp.

    Для передачі впровадженої dll описувача вікна, якому вона повинна надсилати повідомлення (g_hSecretWindow у прикладі), використана техніка зі статті «HOWTO: Виклик функції в іншому процесі».

    Чим же все це закінчиться?

    Буде завершення процесу. Як відомо, під час завершення процесу все dll вивантажуються, і вся виділена пам'ять звільняється. При цьому можуть відбутися наступні неприємності:

    Наша dll буде вивантажено завчасно.

    Раніше часу буде звільнена пам'ять, в якій розташовані згенеровані функції.

    В обох випадках досліджуваний програма просить Access Violation, після чого говорити про те, що його робота не порушена, буде досить складно.

    Невигружаемая dll

    Оскільки в нашої dll лічильник посилань завжди більше 0 (LoadLibrary була викликана, а FreeLibrary немає), вона вивантажується однією з останніх, але в деяких випадках цього може виявитися недостатньо. Радикальним рішенням проблеми є «ручна» завантаження dll, описана в статті Максима М. Гумерова «Завантажувач PE-файлів». Це досить трудомісткий, але зате практично гарантований варіант. Іншим можливим рішенням (для NT/2000/...) може бути видалення dll зі списку завантажених модулів в PEB, але як це зробити і чи буде це працювати, я поки не знаю ...

    Остання ідея, що прийшла мені в голову:

    чесно завантажити dll в процес, дозволити завантажувачу виконати свою роботу

    скопіювати що вийшов образ

    вивантажити dll

    записати в те саме місце адресного простору образ dll.

    молитися.

    Це одна з са?? их «брудних хаков», які я коли-небудь провертала:) Іноді воно працює, іноді - ні. І навіть якщо все на перший погляд працює, я не беруся сказати, які будуть побічні ефекти.

    Підводячи підсумок: якщо завдання і має хороше рішення, його опис виходить далеко за рамки цієї статті. Тому наша dll буде розвантажуватися, хоча іноді це і може привести до проблем.

    Неосвобождаемая пам'ять

    З пам'яттю простіше: щоб її точно ніхто не звільнив, досить відмовитися від стандартного оператора new, і використовувати замість нього placement new, виділяючи пам'ять як-небудь інакше.        

    ПРИМІТКА   

    Під час тестів виявилося, що в   Windows XP, при виділенні пам'яті звичайним new і статичної лінковке CRT,   деякі (не всі і не завжди, але цілком відтворено) блоки пам'яті з   функціями-шпигунами виявляються звільнені. При використанні CRT в dll цієї   проблеми не було, з чим це пов'язано, я не знаю.     

    Результат

    Yes! Воно працює!! :)        

    ПОПЕРЕДЖЕННЯ   

    Нормального тестування не проводилося,   крім того, у мене під рукою не виявилося Windows NT 4. Але на Windows 2000,   XP і 2003 Server перевірив, на перший погляд все добре ... І навіть XP SP2 НЕ   страшний:)     

    Для успішного старту треба покласти spyloader.exe і apispy.dll в один каталог, після чого запустити spyloader, передавши йому в командному рядку шлях до exe-файлу досліджуваного програми.

    Тільки приготуйтеся до того, що GetProcAddress -- досить популярна функція, і отримати сотню функцій-шпигунів (тобто викликів GetProcAddress) при дослідженні notepad.exe - не питання, досить спробувати відкрити який-небудь файл. А вже якщо ви запустите довідку і трохи по ній походіть ... У мене вийшло 530 функцій-шпигунів за дві хвилини:) Тому, якщо ви дійсно будете реалізовувати щось подібне, то краще фіксувати не все підряд, а фільтрувати виклики хоча б на ім'я модуля.

    Список літератури

    Тихомиров В.А. «Перехоплення API-функций в Windows NT/2000/XP ».

    Ігор Філімонов «Методи перехоплення API-викликів в Win32»

    Intel Corporation «IA-32 Intel Architecture Software Developer's Manual», частини 2A і 2B

    Максим М. Гумер «Завантажувач PE-файлів»

    Сергій холодиль «HOWTO: Виклик функції в іншому процесі »

    Для підготовки даної роботи були використані матеріали з сайту http://www.rsdn.ru/

         
     
         
    Реферат Банк
     
    Рефераты
     
    Бесплатные рефераты
     

     

     

     

     

     

     

     
     
     
      Все права защищены. Reff.net.ua - українські реферати ! DMCA.com Protection Status