Смекни!
smekni.com

Ладно, вы поняли свою ошибку и больше не меняете таблицу импорта своего модуля. Но дело в том, что для реализации 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<unsigned char c>struct one_byte_command{ one_byte_command() : code(c) {}unsigned char code;};// Класс, упрощающий работу с командой с однобайтным кодом // и 4-байтнымоперандом.template<unsigned char c>struct one_byte_value_command{ one_byte_value_command() : code(c) {} unsigned char code; unsigned long value;};// Класс, упрощающий работу с командой с однобайтным кодом // и относительным адресомtemplate<unsigned char c>struct one_byte_rel_address_command{ one_byte_rel_address_command() : code(c) {} unsigned char code; relative_address address;};

С их помощью можно определить классы для команд процессора, а из них уже собрать функцию. Например, так:

// Команда pushatypedef one_byte_command<0x60> pusha;// Команда pushftypedef one_byte_command<0x9C> pushf;// Команда push xxxtypedef one_byte_value_command<0x68> push_value;// Команда popatypedef one_byte_command<0x61> popa;// Команда popftypedef one_byte_command<0x9D> popf;// Команда call xxxtypedef one_byte_rel_address_command<0xE8> call_address;// Команда jmp xxxtypedef 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<int>(name) > 0x0000ffff) { // Копируемимя strcat(full_name, name);} else { // А некоторые функции экспортируются по ординалам...char ordinal[10]; strcat(full_name, "by ordinal: "); strcat(full_name, itoa(reinterpret_cast<int>(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<LPARAM>(&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<func_descrition> 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<COPYDATASTRUCT*>(lParam); // Получаемимя char* str = (char*)pcd->lpData; printf("New function: %s&bsol;n", 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 %s&bsol;n", functions[wParam].name.c_str());return 0; // Не удалось установиь перехватчик на функциюcase WM_CANNOTHOOK: // Уведомляемпользователя printf("Can not hook %s&bsol;n", functions[wParam].name.c_str()); return 0; } return DefWindowProc(hwnd, uMsg, wParam, lParam);}
ПРИМЕЧАНИЕДля простоты этот код не проверяет имя функции на уникальность, поэтому в functions может оказаться несколько записей для одной и той же функции.

Внедрение в приложение и перехват GetProcAddress