Fluke

From GZProject Wiki

Revision as of 04:40, 3 November 2007; view current revision
←Older revision | Newer revision→
Jump to: navigation, search
HOWTO-Fluke
FlukeScript
Hi-level API
IOFluke
Input/Output capture library
Fluke
Code Injection library

Contents

Fluke

Когда появляется задача подключиться к исполняемому процессу, то приходиться переписывать большую часть кода и изобретать велосипед. Данный проект стандартизует процесс подключения к любому локальному приложению и делает его более простым. Причем использовать эту наработку можно в любом проекте для любых задач, поскольку код является переносимым и отделенным в независимый модуль.

Какая цель ставится чаще всего, когда мы говорим о подключении к приложению? Естественно вариантов может быть много, но среди основных - управление и контроль над приложением. То есть задача скорей всего будет сводиться к перехвату вызова функций и передачу их либо на сторону главного приложения либо обработка непосредственно внутри приложения, к которому произведено подключение. В этой статье я опишу процесс, способы и возможные варианты подключения к приложению. Перехват функций описан в следующей статье IOFluke.

Виды подключения

Нам необходимо выбрать метод передачи управления в целевой процесс. Любой контроль над приложением сводиться к созданию исполняемого кода и передачу его рабочий процесс. Для этого определимся с формой в которой мы хотим передавать этот код. Это может быть простая функция написанная на С++ или Assembler. И аккуратно переброшенная из одной области памяти в другую, сложности с реализацией такого алгоритма не возникает, однако мы ищем универсальный механизм, который позволит встраиваться в любой процесс и будет удобным.

Перебрасывание функции

Вот исходная функция написанная на ассемблере, код для перебрасывания между процессами находиться в теле между комментариями function body. После вызова функции GetBody(), на выходе получается бинарный массив с ассемблерным кодом:

     xor eax,eax;
     nop;
     nop;
     nop;
     retn;

Вот пример функции:

 std::vector<unsigned char> GetBody()
 {
   unsigned char* start;
   unsigned char* end;
   __asm
   {
     call process;
     // function body start
     xor eax,eax;
     nop;
     nop;
     nop;
     retn;
     // function body end
 process:
     mov [end],offset process;
     pop eax;
     mov [start],eax;
   };
   return std::vector<unsigned char>(start,end);
 }

Пусть читателя не пугает простата примера, он в достаточной точности описывает механизм с использованием которого может быть реализован любой експлоит. Код вида xor eax,eax может быть заменен на код обработки и получения адреса системной библиотеки kernel32.dll, а так же всех адресов библиотечных функций. После чего, мы получаем динамическую, полно функциональную программу, для которой не имеет значения по каким адресам памяти быть запущенной. Этот прием использует регистр SEH.

Возвращаемся к нашим функциям, получив динамический массив с кодом на памяти, мы должны продумать варианты передачи этого, кода на сторону другого процесса.

Передача исполняемого кода

Передача бинарного массива возможна при непосредственной записи содержимого массива в исполняемый процесс. Сделать это можно несколькими вариантами.

  • Используя WINAPI WriteProcessMemory.
  • Через программирование драйвера.

Так как схема с драйверами очень сильно завязана на версию операционной системы, я не считаю ее перспективной. Второй метод, через использование WriteProcessMemory, требует от нас аккуратности на процедуру помещения бинарных данных в исполняемый процесс.

Другими словами нам нужно ответить на вопрос в какую область памяти мы хотим положить данные. Это в первую очередь зависит от цели нашего внедрения. Если нашей задачей есть конкретное преобразование кода исполняемого процесса, то нам необходимо определить адрес в пространстве исполняемого процесса и произвести запись. Конечно, предварительно, остановив исполнение потока внутри целевого процесса. Другой вариант, если нам не имеет значения область памяти в которой будет исполнен наш код - важен лишь факт исполнения. А для этого придется использовать основные точки входа или остановки, которые диктует нам сама операционная система.

Среди таких точек может быть:

  • Точка входа в программу
  • Точка вызова любой библиотечной функции
  • Точка обработки очереди сообщений

Думаю, что список можно продолжить.

Есть одно ограничение с которым мы можем столкнуться, это размер бинарного кода, который мы пытаемся положить в целевой процесс. К сожалению, может возникнуть ситуация в которой размер кода больше допустимого размера для встраивания. Например - тело функции обработки сообщений содержит всего лишь одну операцию перехода на основной код внутри процесса. И после изменения процесса мы не сможем гарантировать, что наш бинарный код начнет исполняться со своей стартовой позиции. Или наш код заместит данные необходимые для корректного функционирования приложения. Для того чтобы решить данную проблему необходимо использовать встраивание библиотеки в исполняемый процесс. Эту задачу с успехом выполняет данный проект Fluke.

Подключение библиотеки

Реализация бинарного кода в библиотеке очень популярный метод написания контрольного модуля. Это решает очень много вопросов. Во-первых, мы не обязаны использовать ассемблер, во-вторых мы не тратим сил на динамическую адресацию, и в-третьих используем отладочную информацию для нашей библиотеки. Так же мы уходим от ограничения связанного с объемом исполняемого кода. Давайте рассмотрим процесс подключения любой библиотеки, к любому исполняемому процессу.

Синхронизация с основным приложением

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

Давайте посмотрим на проблему ближе, и тогда нам станет понятно почему необходимо использовать объекты синхронизации в приложении. Предположим нам необходимо с помощью мастер приложения создать несколько дочерних процессов, работая независимо с каждым. Для этого мы запускаем первый процесс и даем команду на подключение библиотеки к ново созданному процессу. Как известно, что ни один из доступных методов подключения не дает гарантии, что библиотека подгрузиться. Поэтому мы не знаем в какой момент мы можем запускать следующий процесс и подключать вторую библиотеку. Такая ситуация безусловно решается через доступные в Windows IPC механизмы, но на мой взгляд модуль Fluke обязан гарантировать и сигнализировать об ошибочных ситуациях. То есть в случае не возможности по каким либо причинам произвести подключение библиотеки, разработчик об этом узнает через исключительное событие и сможет произвести адекватное действие для устранение, что не приведет к нарушению логики внутри мастер приложения.

Написание библиотеки

Создание новой библиотеки - произвольный процесс в который практически не вноситься никаких ограничений. Для примера создаем проект в любой среде, и подключаем к нему статическую библиотеку, доступным нам способом. Исходники, которой доступны по адресу svndir:trunk/library/fluke. Далее в коде библиотеки необходимо создать глобальный объект унаследованный от класса Fluke::CFlukeSlave. И в конструкторе этого класса сделать вызов метода Fluke::CFlukeSlave::Create();. Вот пример:

 class MyApp:public Fluke::CFlukeSlave
 {
 public:
   MyApp()
   {
     Create();
   }
 };
 MyApp g_app;

Предварительно к проекту необходимо подключить файл svnfile:trunk/library/fluke/flukeslave.h. Который так же доступен в репозитории с проектом. На этом этапе подготовка библиотеке завершена.

Мастер приложение

Следующий этап - разработка мастер приложения. В его задачу входит основная логика по выбору и поддержки связи с подчиненным приложением. А так же способу подключения к нему. Все эти действия уже запрограммированы и требуют только выбора соответствующей функции в интерфейсе библиотеки. Это означает, что разработчик мастер приложения определяет способ каким образом будет осуществляться встраивание в исполняемый процесс, а затем вызывает функцию flukemaster с соответствующими параметрами. Этой процедуры достаточно для нормального функционирования связки мастер приложения и целевого процесса.

Список всех функций которые поддерживает библиотека для подключения к процессу описаны здесь: svnfile:trunk/library/fluke/flukemaster.h.

Получение исходников

Для сборки библиотеки необходимо получить:

Ссылки